From b48f550eb32350dbebda12fdd7108cc8748b2d74 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 5 Sep 2024 15:29:40 +0200 Subject: [PATCH 001/474] sdk/: add init files to fix import errors and add LICENSE in the sdk folder because including the LICENSE in a parent directory leads to issues when building distributional files --- sdk/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 sdk/__init__.py diff --git a/sdk/__init__.py b/sdk/__init__.py new file mode 100644 index 0000000..e69de29 From 1f4fa69701be59d6fc8c905de79ff210b777dc11 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 5 Sep 2024 15:31:47 +0200 Subject: [PATCH 002/474] sdk/basyx/object_store.py: redesign functions in objectstore to be compatible with aas_core typings. Additionally fix a few codestyle errors. Rebase with main --- sdk/basyx/object_store.py | 10 +++++----- sdk/basyx/tutorial/tutorial_objectstore.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sdk/basyx/object_store.py b/sdk/basyx/object_store.py index ea64c87..aee4712 100644 --- a/sdk/basyx/object_store.py +++ b/sdk/basyx/object_store.py @@ -15,9 +15,8 @@ from aas_core3.types import Identifiable, Referable, Class -# We define types for :class:`aas_core3.types.Identifier` and :class:`aas_core3.types.Referable` for easier referencing. +# We define types for :class:`aas_core3.types.Identifier` for easier referencing. _IdentifiableType = TypeVar('_IdentifiableType', bound=Identifiable) -_ReferableType = TypeVar('_ReferableType', bound=Referable) class AbstractObjectProvider(metaclass=abc.ABCMeta): @@ -150,12 +149,13 @@ def get_referable(self, identifier: str, id_short: str) -> Referable: """ referable: Referable identifiable = self.get_identifiable(identifier) - for element in identifiable.descend(): + for referable in identifiable.descend(): if ( - isinstance(element, Referable) and id_short == element.id_short + issubclass(type(referable), Referable) + and id_short in referable.id_short ): - return element + return referable raise KeyError("Referable object with short_id {} does not exist for identifiable object with id {}" .format(id_short, identifier)) diff --git a/sdk/basyx/tutorial/tutorial_objectstore.py b/sdk/basyx/tutorial/tutorial_objectstore.py index 6f8bf0f..e037b58 100644 --- a/sdk/basyx/tutorial/tutorial_objectstore.py +++ b/sdk/basyx/tutorial/tutorial_objectstore.py @@ -5,7 +5,7 @@ # # SPDX-License-Identifier: MIT -from basyx.object_store import ObjectStore +from basyx.objectstore import ObjectStore from aas_core3.types import Identifiable, AssetAdministrationShell, AssetInformation, AssetKind import aas_core3.types as aas_types @@ -78,3 +78,4 @@ # Retrieve parent of list_element by id_short print(element_list == obj_store.get_parent_referable("list_1")) + From 00e9955b34d4bc13147cfa60ee2d5fcedbf4a245 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 5 Sep 2024 15:35:28 +0200 Subject: [PATCH 003/474] .github/: Add testconfig with previous history from basyx-python-sdk and edit test directories --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a6dd57..2136188 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,3 +99,23 @@ jobs: - name: Check documentation for errors run: | SPHINXOPTS="-a -E -n -W --keep-going" make -C docs html + + + package: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ env.X_PYTHON_VERSION }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env.X_PYTHON_VERSION }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -q build + - name: Create source and wheel dist + run: | + ls + cd ./sdk + python -m build \ No newline at end of file From 044059b8b87a8df115d4a1cd64a0b97845f8d35f Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 5 Sep 2024 16:45:15 +0200 Subject: [PATCH 004/474] sdk/basyx/object_store.py: get_parent_referable now also complies with type checks --- sdk/basyx/tutorial/tutorial_objectstore.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/basyx/tutorial/tutorial_objectstore.py b/sdk/basyx/tutorial/tutorial_objectstore.py index e037b58..f98a093 100644 --- a/sdk/basyx/tutorial/tutorial_objectstore.py +++ b/sdk/basyx/tutorial/tutorial_objectstore.py @@ -78,4 +78,3 @@ # Retrieve parent of list_element by id_short print(element_list == obj_store.get_parent_referable("list_1")) - From 592df03020636b3c419bda3015048954469ccbdb Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 12 Sep 2024 14:44:06 +0200 Subject: [PATCH 005/474] mypy.ini: add config file for mypy --- mypy.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..f881ed1 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +explicit_package_bases = True \ No newline at end of file From e90a19d721f9084a9d4b8306410c3a06d4c8f060 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 12 Sep 2024 14:45:32 +0200 Subject: [PATCH 006/474] sdk/__init__.py: remove unnecessary init --- sdk/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 sdk/__init__.py diff --git a/sdk/__init__.py b/sdk/__init__.py deleted file mode 100644 index e69de29..0000000 From 857c02e92c3d1122e446bae3897c6df207c57d6f Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Fri, 13 Sep 2024 10:58:27 +0200 Subject: [PATCH 007/474] mypy.ini: remove mypy.ini after fixing imports such that the config is no more needed. .github/workflows/ci.yml: add line. sdk/basyx/tutorial_objectstore.py: fix import. sdk/test/test_objectstore.py: remove type ignores. sdk/basyx/__init__.py: add init for tutorial folder --- .github/workflows/ci.yml | 2 +- mypy.ini | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 mypy.ini diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2136188..6445a32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,4 +118,4 @@ jobs: run: | ls cd ./sdk - python -m build \ No newline at end of file + python -m build diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index f881ed1..0000000 --- a/mypy.ini +++ /dev/null @@ -1,2 +0,0 @@ -[mypy] -explicit_package_bases = True \ No newline at end of file From a70e724124fbb8d190e4055d542a58efe04d76a5 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 15 Nov 2021 19:00:02 +0100 Subject: [PATCH 008/474] =?UTF-8?q?Move=20Python=20package=20aas=20?= =?UTF-8?q?=E2=86=92=20basyx.aas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- basyx/aas/adapter/aasx.py | 766 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 766 insertions(+) create mode 100644 basyx/aas/adapter/aasx.py diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py new file mode 100644 index 0000000..e540dd2 --- /dev/null +++ b/basyx/aas/adapter/aasx.py @@ -0,0 +1,766 @@ +# Copyright (c) 2020 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +""" +.. _adapter.aasx: + +Functionality for reading and writing AASX files according to "Details of the Asset Administration Shell Part 1 V2.0", +section 7. + +The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the `pyecma376_2` library +for low level OPC reading and writing. It currently supports all required features except for embedded digital +signatures. + +Writing and reading of AASX packages is performed through the :class:`~.AASXReader` and :class:`~.AASXWriter` classes. +Each instance of these classes wraps an existing AASX file resp. a file to be created and allows to read/write the +included AAS objects into/form :class:`ObjectStores `. +For handling of embedded supplementary files, this module provides the +:class:`~.AbstractSupplementaryFileContainer` class +interface and the :class:`~.DictSupplementaryFileContainer` implementation. +""" + +import abc +import hashlib +import io +import itertools +import logging +import os +import re +from typing import Dict, Tuple, IO, Union, List, Set, Optional, Iterable, Iterator + +from .xml import read_aas_xml_file, write_aas_xml_file +from .. import model +from .json import read_aas_json_file, write_aas_json_file +import pyecma376_2 +from ..util import traversal + +logger = logging.getLogger(__name__) + +RELATIONSHIP_TYPE_AASX_ORIGIN = "http://www.admin-shell.io/aasx/relationships/aasx-origin" +RELATIONSHIP_TYPE_AAS_SPEC = "http://www.admin-shell.io/aasx/relationships/aas-spec" +RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://www.admin-shell.io/aasx/relationships/aas-spec-split" +RELATIONSHIP_TYPE_AAS_SUPL = "http://www.admin-shell.io/aasx/relationships/aas-suppl" + + +class AASXReader: + """ + An AASXReader wraps an existing AASX package file to allow reading its contents and metadata. + + Basic usage: + + .. code-block:: python + + objects = DictObjectStore() + files = DictSupplementaryFileContainer() + with AASXReader("filename.aasx") as reader: + meta_data = reader.get_core_properties() + reader.read_into(objects, files) + """ + def __init__(self, file: Union[os.PathLike, str, IO]): + """ + Open an AASX reader for the given filename or file handle + + The given file is opened as OPC ZIP package. Make sure to call `AASXReader.close()` after reading the file + contents to close the underlying ZIP file reader. You may also use the AASXReader as a context manager to ensure + closing under any circumstances. + + :param file: A filename, file path or an open file-like object in binary mode + :raises ValueError: If the file is not a valid OPC zip package + """ + try: + logger.debug("Opening {} as AASX pacakge for reading ...".format(file)) + self.reader = pyecma376_2.ZipPackageReader(file) + except Exception as e: + raise ValueError("{} is not a valid ECMA376-2 (OPC) file".format(file)) from e + + def get_core_properties(self) -> pyecma376_2.OPCCoreProperties: + """ + Retrieve the OPC Core Properties (meta data) of the AASX package file. + + If no meta data is provided in the package file, an emtpy OPCCoreProperties object is returned. + + :return: The AASX package's meta data + """ + return self.reader.get_core_properties() + + def get_thumbnail(self) -> Optional[bytes]: + """ + Retrieve the packages thumbnail image + + The thumbnail image file is read into memory and returned as bytes object. You may use some python image library + for further processing or conversion, e.g. `pillow`: + + .. code-block:: python + + import io + from PIL import Image + thumbnail = Image.open(io.BytesIO(reader.get_thumbnail())) + + :return: The AASX package thumbnail's file contents or None if no thumbnail is provided + """ + try: + thumbnail_part = self.reader.get_related_parts_by_type()[pyecma376_2.RELATIONSHIP_TYPE_THUMBNAIL][0] + except IndexError: + return None + + with self.reader.open_part(thumbnail_part) as p: + return p.read() + + def read_into(self, object_store: model.AbstractObjectStore, + file_store: "AbstractSupplementaryFileContainer", + override_existing: bool = False) -> Set[model.Identifier]: + """ + Read the contents of the AASX package and add them into a given + :class:`ObjectStore ` + + This function does the main job of reading the AASX file's contents. It traverses the relationships within the + package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided + `object_store`. While doing so, it searches all parsed :class:`Submodels ` for + :class:`~aas.model.submodel.File` objects to extract the supplementary + files. The referenced supplementary files are added to the given `file_store` and the + :class:`~aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file + to allow for robust resolution the file within the + `file_store` later. + + :param object_store: An :class:`ObjectStore ` to add the AAS objects + from the AASX file to + :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the + embedded supplementary files to + :param override_existing: If `True`, existing objects in the object store are overridden with objects from the + AASX that have the same :class:`~aas.model.base.Identifier`. Default behavior is to skip those objects from + the AASX. + :return: A set of the :class:`Identifiers ` of all + :class:`~aas.model.base.Identifiable` objects parsed from the AASX file + """ + # Find AASX-Origin part + core_rels = self.reader.get_related_parts_by_type() + try: + aasx_origin_part = core_rels[RELATIONSHIP_TYPE_AASX_ORIGIN][0] + except IndexError as e: + raise ValueError("Not a valid AASX file: aasx-origin Relationship is missing.") from e + + read_identifiables: Set[model.Identifier] = set() + + # Iterate AAS files + for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ + RELATIONSHIP_TYPE_AAS_SPEC]: + self._read_aas_part_into(aas_part, object_store, file_store, read_identifiables, override_existing) + + # Iterate split parts of AAS file + for split_part in self.reader.get_related_parts_by_type(aas_part)[ + RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: + self._read_aas_part_into(split_part, object_store, file_store, read_identifiables, override_existing) + + return read_identifiables + + def close(self) -> None: + """ + Close the AASXReader and the underlying OPC / ZIP file readers. Must be called after reading the file. + """ + self.reader.close() + + def __enter__(self) -> "AASXReader": + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.close() + + def _read_aas_part_into(self, part_name: str, + object_store: model.AbstractObjectStore, + file_store: "AbstractSupplementaryFileContainer", + read_identifiables: Set[model.Identifier], + override_existing: bool) -> None: + """ + Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. + + This method primarily checks for duplicate objects. It uses `_parse_aas_parse()` to do the actual parsing and + `_collect_supplementary_files()` for supplementary file processing of non-duplicate objects. + + :param part_name: The OPC part name to read + :param object_store: An ObjectStore to add the AAS objects from the AASX file to + :param file_store: A SupplementaryFileContainer to add the embedded supplementary files to, which are reference + from a File object of this part + :param read_identifiables: A set of Identifiers of objects which have already been read. New objects' + Identifiers are added to this set. Objects with already known Identifiers are skipped silently. + :param override_existing: If True, existing objects in the object store are overridden with objects from the + AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. + """ + for obj in self._parse_aas_part(part_name): + if obj.identification in read_identifiables: + continue + if obj.identification in object_store: + if override_existing: + logger.info("Overriding existing object in ObjectStore with {} ...".format(obj)) + object_store.discard(obj) + else: + logger.warning("Skipping {}, since an object with the same id is already contained in the " + "ObjectStore".format(obj)) + continue + object_store.add(obj) + read_identifiables.add(obj.identification) + if isinstance(obj, model.Submodel): + self._collect_supplementary_files(part_name, obj, file_store) + + def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: + """ + Helper function to parse the AAS objects from a single JSON or XML part of the AASX package. + + This method chooses and calls the correct parser. + + :param part_name: The OPC part name of the part to be parsed + :return: A DictObjectStore containing the parsed AAS objects + """ + content_type = self.reader.get_content_type(part_name) + extension = part_name.split("/")[-1].split(".")[-1] + if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": + logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) + with self.reader.open_part(part_name) as p: + return read_aas_xml_file(p) + elif content_type.split(";")[0] in ("text/json", "application/json") \ + or content_type == "" and extension == "json": + logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) + with self.reader.open_part(part_name) as p: + return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig')) + else: + logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}" + .format(part_name, content_type, extension)) + return model.DictObjectStore() + + def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel, + file_store: "AbstractSupplementaryFileContainer") -> None: + """ + Helper function to search File objects within a single parsed Submodel, extract the referenced supplementary + files and update the File object's values with the absolute path. + + :param part_name: The OPC part name of the part the submodel has been parsed from. This is used to resolve + relative file paths. + :param submodel: The Submodel to process + :param file_store: The SupplementaryFileContainer to add the extracted supplementary files to + """ + for element in traversal.walk_submodel(submodel): + if isinstance(element, model.File): + if element.value is None: + continue + # Only absolute-path references and relative-path URI references (see RFC 3986, sec. 4.2) are considered + # to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute + # URIs and network-path references) + if element.value.startswith('//') or ':' in element.value.split('/')[0]: + logger.info("Skipping supplementary file %s, since it seems to be an absolute URI or network-path " + "URI reference", element.value) + continue + absolute_name = pyecma376_2.package_model.part_realpath(element.value, part_name) + logger.debug("Reading supplementary file {} from AASX package ...".format(absolute_name)) + with self.reader.open_part(absolute_name) as p: + final_name = file_store.add_file(absolute_name, p, self.reader.get_content_type(absolute_name)) + element.value = final_name + + +class AASXWriter: + """ + An AASXWriter wraps a new AASX package file to write its contents to it piece by piece. + + Basic usage: + + .. code-block:: python + + # object_store and file_store are expected to be given (e.g. some storage backend or previously created data) + cp = OPCCoreProperties() + cp.creator = "ACPLT" + cp.created = datetime.datetime.now() + + with AASXWriter("filename.aasx") as writer: + writer.write_aas(Identifier("https://acplt.org/AssetAdministrationShell", IdentifierType.IRI), + object_store, + file_store) + writer.write_aas(Identifier("https://acplt.org/AssetAdministrationShell2", IdentifierType.IRI), + object_store, + file_store) + writer.write_core_properties(cp) + + **Attention:** The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context + manager functionality (as shown above). Otherwise the resulting AASX file will lack important data structures + and will not be readable. + """ + AASX_ORIGIN_PART_NAME = "/aasx/aasx-origin" + + def __init__(self, file: Union[os.PathLike, str, IO]): + """ + Create a new AASX package in the given file and open the AASXWriter to add contents to the package. + + Make sure to call `AASXWriter.close()` after writing all contents to write the aas-spec relationships for all + AAS parts to the file and close the underlying ZIP file writer. You may also use the AASXWriter as a context + manager to ensure closing under any circumstances. + + :param file: filename, path, or binary file handle opened for writing + """ + # names of aas-spec parts, used by `_write_aasx_origin_relationships()` + self._aas_part_names: List[str] = [] + # name of the thumbnail part (if any) + self._thumbnail_part: Optional[str] = None + # name of the core properties part (if any) + self._properties_part: Optional[str] = None + # names and hashes of all supplementary file parts that have already been written + self._supplementary_part_names: Dict[str, Optional[bytes]] = {} + self._aas_name_friendlyfier = NameFriendlyfier() + + # Open OPC package writer + self.writer = pyecma376_2.ZipPackageWriter(file) + + # Create AASX origin part + logger.debug("Creating AASX origin part in AASX package ...") + p = self.writer.open_part(self.AASX_ORIGIN_PART_NAME, "text/plain") + p.close() + + def write_aas(self, + aas_id: model.Identifier, + object_store: model.AbstractObjectStore, + file_store: "AbstractSupplementaryFileContainer", + write_json: bool = False, + submodel_split_parts: bool = True) -> None: + """ + Convenience method to add an :class:`~aas.model.aas.AssetAdministrationShell` with all included and referenced + objects to the AASX package according to the part name conventions from DotAAS. + + This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given + object_store. :class:`References <~aas.model.base.Reference>` to + the :class:`~aas.model.aas.Asset`, :class:`ConceptDescriptions ` and + :class:`Submodels ` are also resolved using the object_store. All of these objects + are written to aas-spec parts in the AASX package, following the conventions presented in "Details of the Asset + Administration Shell". For each Submodel, a aas-spec-split part is used. Supplementary files which are + referenced by a File object in any of the Submodels, are also added to the AASX package. + + Internally, this method uses :meth:`aas.adapter.aasx.AASXWriter.write_aas_objects` to write the individual AASX + parts for the AAS and each submodel. + + :param aas_id: :class:`~aas.model.base.Identifier` of the :class:`~aas.model.aas.AssetAdministrationShell` to + be added to the AASX file + :param object_store: :class:`ObjectStore ` to retrieve the + :class:`~aas.model.base.Identifiable` AAS objects (:class:`~aas.model.aas.AssetAdministrationShell`, + :class:`~aas.model.aas.Asset`, :class:`~aas.model.concept.ConceptDescription` and + :class:`~aas.model.submodel.Submodel`) from + :param file_store: :class:`SupplementaryFileContainer ` to + retrieve supplementary files from, which are referenced by :class:`~aas.model.submodel.File` objects + :param write_json: If `True`, JSON parts are created for the AAS and each submodel in the AASX package file + instead of XML parts. Defaults to `False`. + :param submodel_split_parts: If `True` (default), submodels are written to separate AASX parts instead of being + included in the AAS part with in the AASX package. + """ + aas_friendly_name = self._aas_name_friendlyfier.get_friendly_name(aas_id) + aas_part_name = "/aasx/{0}/{0}.aas.{1}".format(aas_friendly_name, "json" if write_json else "xml") + + aas = object_store.get_identifiable(aas_id) + if not isinstance(aas, model.AssetAdministrationShell): + raise ValueError(f"Identifier does not belong to an AssetAdminstrationShell object but to {aas!r}") + + objects_to_be_written: Set[model.Identifier] = {aas.identification} + + # Add the Asset object to the objects in the AAS part + objects_to_be_written.add(aas.asset.get_identifier()) + + # Add referenced ConceptDescriptions to the AAS part + for dictionary in aas.concept_dictionary: + for concept_rescription_ref in dictionary.concept_description: + objects_to_be_written.add(concept_rescription_ref.get_identifier()) + + # Write submodels: Either create a split part for each of them or otherwise add them to objects_to_be_written + aas_split_part_names: List[str] = [] + if submodel_split_parts: + # Create a AAS split part for each (available) submodel of the AAS + aas_friendlyfier = NameFriendlyfier() + for submodel_ref in aas.submodel: + submodel_identification = submodel_ref.get_identifier() + submodel_friendly_name = aas_friendlyfier.get_friendly_name(submodel_identification) + submodel_part_name = "/aasx/{0}/{1}/{1}.submodel.{2}".format(aas_friendly_name, submodel_friendly_name, + "json" if write_json else "xml") + self.write_aas_objects(submodel_part_name, [submodel_identification], object_store, file_store, + write_json, split_part=True) + aas_split_part_names.append(submodel_part_name) + else: + for submodel_ref in aas.submodel: + objects_to_be_written.add(submodel_ref.get_identifier()) + + # Write AAS part + logger.debug("Writing AAS {} to part {} in AASX package ...".format(aas.identification, aas_part_name)) + self.write_aas_objects(aas_part_name, objects_to_be_written, object_store, file_store, write_json, + split_part=False, + additional_relationships=(pyecma376_2.OPCRelationship("r{}".format(i), + RELATIONSHIP_TYPE_AAS_SPEC_SPLIT, + submodel_part_name, + pyecma376_2.OPCTargetMode.INTERNAL) + for i, submodel_part_name in enumerate(aas_split_part_names))) + + def write_aas_objects(self, + part_name: str, + object_ids: Iterable[model.Identifier], + object_store: model.AbstractObjectStore, + file_store: "AbstractSupplementaryFileContainer", + write_json: bool = False, + split_part: bool = False, + additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: + """ + Write a defined list of AAS objects to an XML or JSON part in the AASX package and append the referenced + supplementary files to the package. + + This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given + object_store. If the list + of written objects includes :class:`aas.model.submodel.Submodel` objects, Supplementary files which are + referenced by :class:`~aas.model.submodel.File` objects within + those submodels, are also added to the AASX package. + + You must make sure to call this method only once per unique `part_name` on a single package instance. + + :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 + part name and unique within the package. The extension of the part should match the data format (i.e. + '.json' if `write_json` else '.xml'). + :param object_ids: A list of :class:`Identifiers ` of the objects to be written to + the AASX package. Only these :class:`~aas.model.base.Identifiable` objects (and included + :class:`~aas.model.base.Referable` objects) are written to the package. + :param object_store: The objects store to retrieve the :class:`~aas.model.base.Identifiable` objects from + :param file_store: The :class:`SupplementaryFileContainer ` + to retrieve supplementary files from (if there are any :class:`~aas.model.submodel.File` + objects within the written objects. + :param write_json: If `True`, the part is written as a JSON file instead of an XML file. Defaults to `False`. + :param split_part: If `True`, no aas-spec relationship is added from the aasx-origin to this part. You must make + sure to reference it via a aas-spec-split relationship from another aas-spec part + :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object + part to be written, in addition to the aas-suppl relationships which are created automatically. + """ + logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) + + objects: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + supplementary_files: List[str] = [] + + # Retrieve objects and scan for referenced supplementary files + for identifier in object_ids: + try: + the_object = object_store.get_identifiable(identifier) + except KeyError: + logger.error("Could not find object {} in ObjectStore".format(identifier)) + continue + objects.add(the_object) + if isinstance(the_object, model.Submodel): + for element in traversal.walk_submodel(the_object): + if isinstance(element, model.File): + file_name = element.value + # Skip File objects with empty value URI references that are considered to be no local file + # (absolute URIs or network-path URI references) + if file_name is None or file_name.startswith('//') or ':' in file_name.split('/')[0]: + continue + supplementary_files.append(file_name) + + # Add aas-spec relationship + if not split_part: + self._aas_part_names.append(part_name) + + # Write part + with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p: + if write_json: + write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects) + else: + write_aas_xml_file(p, objects) + + # Write submodel's supplementary files to AASX file + supplementary_file_names = [] + for file_name in supplementary_files: + try: + content_type = file_store.get_content_type(file_name) + hash = file_store.get_sha256(file_name) + except KeyError: + logger.warning("Could not find file {} in file store.".format(file_name)) + continue + # Check if this supplementary file has already been written to the AASX package or has a name conflict + if self._supplementary_part_names.get(file_name) == hash: + continue + elif file_name in self._supplementary_part_names: + logger.error("Trying to write supplementary file {} to AASX twice with different contents" + .format(file_name)) + logger.debug("Writing supplementary file {} to AASX package ...".format(file_name)) + with self.writer.open_part(file_name, content_type) as p: + file_store.write_file(file_name, p) + supplementary_file_names.append(pyecma376_2.package_model.normalize_part_name(file_name)) + self._supplementary_part_names[file_name] = hash + + # Add relationships from submodel to supplementary parts + logger.debug("Writing aas-suppl relationships for AAS object part {} to AASX package ...".format(part_name)) + self.writer.write_relationships( + itertools.chain( + (pyecma376_2.OPCRelationship("r{}".format(i), + RELATIONSHIP_TYPE_AAS_SUPL, + submodel_file_name, + pyecma376_2.OPCTargetMode.INTERNAL) + for i, submodel_file_name in enumerate(supplementary_file_names)), + additional_relationships), + part_name) + + def write_core_properties(self, core_properties: pyecma376_2.OPCCoreProperties): + """ + Write OPC Core Properties (meta data) to the AASX package file. + + .. Attention:: + This method may only be called once for each AASXWriter! + + :param core_properties: The OPCCoreProperties object with the meta data to be written to the package file + """ + if self._properties_part is not None: + raise RuntimeError("Core Properties have already been written.") + logger.debug("Writing core properties to AASX package ...") + with self.writer.open_part(pyecma376_2.DEFAULT_CORE_PROPERTIES_NAME, "application/xml") as p: + core_properties.write_xml(p) + self._properties_part = pyecma376_2.DEFAULT_CORE_PROPERTIES_NAME + + def write_thumbnail(self, name: str, data: bytearray, content_type: str): + """ + Write an image file as thumbnail image to the AASX package. + + .. Attention:: + This method may only be called once for each AASXWriter! + + :param name: The OPC part name of the thumbnail part. Should not contain '/' or URI-encoded '/' or '\'. + :param data: The image file's binary contents to be written + :param content_type: OPC content type (MIME type) of the image file + """ + if self._thumbnail_part is not None: + raise RuntimeError("package thumbnail has already been written to {}.".format(self._thumbnail_part)) + with self.writer.open_part(name, content_type) as p: + p.write(data) + self._thumbnail_part = name + + def close(self): + """ + Write relationships for all data files to package and close underlying OPC package and ZIP file. + """ + self._write_aasx_origin_relationships() + self._write_package_relationships() + self.writer.close() + + def __enter__(self) -> "AASXWriter": + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.close() + + def _write_aasx_origin_relationships(self): + """ + Helper function to write aas-spec relationships of the aasx-origin part. + + This method uses the list of aas-spec parts in `_aas_part_names`. It should be called just before closing the + file to make sure all aas-spec parts of the package have already been written. + """ + # Add relationships from AASX-origin part to AAS parts + logger.debug("Writing aas-spec relationships to AASX package ...") + self.writer.write_relationships( + (pyecma376_2.OPCRelationship("r{}".format(i), RELATIONSHIP_TYPE_AAS_SPEC, + aas_part_name, + pyecma376_2.OPCTargetMode.INTERNAL) + for i, aas_part_name in enumerate(self._aas_part_names)), + self.AASX_ORIGIN_PART_NAME) + + def _write_package_relationships(self): + """ + Helper function to write package (root) relationships to the OPC package. + + This method must be called just before closing the package file to make sure we write exactly the correct + relationships: + * aasx-origin (always) + * core-properties (if core properties have been added) + * thumbnail (if thumbnail part has been added) + """ + logger.debug("Writing package relationships to AASX package ...") + package_relationships: List[pyecma376_2.OPCRelationship] = [ + pyecma376_2.OPCRelationship("r1", RELATIONSHIP_TYPE_AASX_ORIGIN, + self.AASX_ORIGIN_PART_NAME, + pyecma376_2.OPCTargetMode.INTERNAL), + ] + if self._properties_part is not None: + package_relationships.append(pyecma376_2.OPCRelationship( + "r2", pyecma376_2.RELATIONSHIP_TYPE_CORE_PROPERTIES, self._properties_part, + pyecma376_2.OPCTargetMode.INTERNAL)) + if self._thumbnail_part is not None: + package_relationships.append(pyecma376_2.OPCRelationship( + "r3", pyecma376_2.RELATIONSHIP_TYPE_THUMBNAIL, self._thumbnail_part, + pyecma376_2.OPCTargetMode.INTERNAL)) + self.writer.write_relationships(package_relationships) + + +class NameFriendlyfier: + """ + A simple helper class to create unique "AAS friendly names" according to DotAAS, section 7.6. + + Objects of this class store the already created friendly names to avoid name collisions within one set of names. + """ + RE_NON_ALPHANUMERICAL = re.compile(r"[^a-zA-Z0-9]") + + def __init__(self) -> None: + self.issued_names: Set[str] = set() + + def get_friendly_name(self, identifier: model.Identifier): + """ + Generate a friendly name from an AAS identifier. + + According to section 7.6 of "Details of the Asset Administration Shell", all non-alphanumerical characters are + replaced with underscores. We also replace all non-ASCII characters to generate valid URIs as the result. + If this replacement results in a collision with a previously generated friendly name of this NameFriendlifier, + a number is appended with underscore to the friendly name. + + Example: + + .. code-block:: python + + friendlyfier = NameFriendlyfier() + friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS-a", model.IdentifierType.IRI)) + > "http___example_com_AAS_a" + + friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS+a", model.IdentifierType.IRI)) + > "http___example_com_AAS_a_1" + + """ + # friendlify name + raw_name = self.RE_NON_ALPHANUMERICAL.sub('_', identifier.id) + + # Unify name (avoid collisions) + amended_name = raw_name + i = 1 + while amended_name in self.issued_names: + amended_name = "{}_{}".format(raw_name, i) + i += 1 + + self.issued_names.add(amended_name) + return amended_name + + +class AbstractSupplementaryFileContainer(metaclass=abc.ABCMeta): + """ + Abstract interface for containers of supplementary files for AASs. + + Supplementary files may be PDF files or other binary or textual files, referenced in a File object of an AAS by + their name. They are used to provide associated documents without embedding their contents (as + :class:`~aas.model.submodel.Blob` object) in the AAS. + + A SupplementaryFileContainer keeps track of the name and content_type (MIME type) for each file. Additionally it + allows to resolve name conflicts by comparing the files' contents and providing an alternative name for a dissimilar + new file. It also provides each files sha256 hash sum to allow name conflict checking in other classes (e.g. when + writing AASX files). + """ + @abc.abstractmethod + def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: + """ + Add a new file to the SupplementaryFileContainer and resolve name conflicts. + + The file contents must be provided as a binary file-like object to be read by the SupplementaryFileContainer. + If the container already contains an equally named file, the content_type and file contents are compared (using + a hash sum). In case of dissimilar files, a new unique name for the new file is computed and returned. It should + be used to update in the File object of the AAS. + + :param name: The file's proposed name. Should start with a '/'. Should not contain URI-encoded '/' or '\' + :param file: A binary file-like opened for reading the file contents + :param content_type: The file's content_type + :return: The file name as stored in the SupplementaryFileContainer. Typically `name` or a modified version of + `name` to resolve conflicts. + """ + pass # pragma: no cover + + @abc.abstractmethod + def get_content_type(self, name: str) -> str: + """ + Get a stored file's content_type. + + :param name: file name of questioned file + :return: The file's content_type + :raises KeyError: If no file with this name is stored + """ + pass # pragma: no cover + + @abc.abstractmethod + def get_sha256(self, name: str) -> bytes: + """ + Get a stored file content's sha256 hash sum. + + This may be used by other classes (e.g. the AASXWriter) to check for name conflicts. + + :param name: file name of questioned file + :return: The file content's sha256 hash sum + :raises KeyError: If no file with this name is stored + """ + pass # pragma: no cover + + @abc.abstractmethod + def write_file(self, name: str, file: IO[bytes]) -> None: + """ + Retrieve a stored file's contents by writing them into a binary writable file-like object. + + :param name: file name of questioned file + :param file: A binary file-like object with write() method to write the file contents into + :raises KeyError: If no file with this name is stored + """ + pass # pragma: no cover + + @abc.abstractmethod + def __contains__(self, item: str) -> bool: + """ + Check if a file with the given name is stored in this SupplementaryFileContainer. + """ + pass # pragma: no cover + + @abc.abstractmethod + def __iter__(self) -> Iterator[str]: + """ + Return an iterator over all file names stored in this SupplementaryFileContainer. + """ + pass # pragma: no cover + + +class DictSupplementaryFileContainer(AbstractSupplementaryFileContainer): + """ + SupplementaryFileContainer implementation using a dict to store the file contents in-memory. + """ + def __init__(self): + # Stores the files' contents, identified by their sha256 hash + self._store: Dict[bytes, bytes] = {} + # Maps file names to (sha256, content_type) + self._name_map: Dict[str, Tuple[bytes, str]] = {} + + def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: + data = file.read() + hash = hashlib.sha256(data).digest() + if hash not in self._store: + self._store[hash] = data + name_map_data = (hash, content_type) + new_name = name + i = 1 + while True: + if new_name not in self._name_map: + self._name_map[new_name] = name_map_data + return new_name + elif self._name_map[new_name] == name_map_data: + return new_name + new_name = self._append_counter(name, i) + i += 1 + + @staticmethod + def _append_counter(name: str, i: int) -> str: + split1 = name.split('/') + split2 = split1[-1].split('.') + index = -2 if len(split2) > 1 else -1 + new_basename = "{}_{:04d}".format(split2[index], i) + split2[index] = new_basename + split1[-1] = ".".join(split2) + return "/".join(split1) + + def get_content_type(self, name: str) -> str: + return self._name_map[name][1] + + def get_sha256(self, name: str) -> bytes: + return self._name_map[name][0] + + def write_file(self, name: str, file: IO[bytes]) -> None: + file.write(self._store[self._name_map[name][0]]) + + def __contains__(self, item: object) -> bool: + return item in self._name_map + + def __iter__(self) -> Iterator[str]: + return iter(self._name_map) From b43702c2b3a63cb2acff6064814d334d324cb67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 19 Apr 2022 19:01:26 +0200 Subject: [PATCH 009/474] Update license headers for MIT license --- basyx/aas/adapter/aasx.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index e540dd2..5aa2080 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.aasx: From 9a4534d851503275130ba937fc4df2e686a44178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 25 Jun 2022 20:34:03 +0200 Subject: [PATCH 010/474] rename Identifiable/identification to Identifiable/id --- basyx/aas/adapter/aasx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 5aa2080..f5d797a 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -189,9 +189,9 @@ def _read_aas_part_into(self, part_name: str, AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. """ for obj in self._parse_aas_part(part_name): - if obj.identification in read_identifiables: + if obj.id in read_identifiables: continue - if obj.identification in object_store: + if obj.id in object_store: if override_existing: logger.info("Overriding existing object in ObjectStore with {} ...".format(obj)) object_store.discard(obj) @@ -200,7 +200,7 @@ def _read_aas_part_into(self, part_name: str, "ObjectStore".format(obj)) continue object_store.add(obj) - read_identifiables.add(obj.identification) + read_identifiables.add(obj.id) if isinstance(obj, model.Submodel): self._collect_supplementary_files(part_name, obj, file_store) From 27f8d8d1f720e8a0aa2681f04a3f84f5a820454a Mon Sep 17 00:00:00 2001 From: zrgt Date: Wed, 30 Nov 2022 22:06:04 +0100 Subject: [PATCH 011/474] Small typo fix --- basyx/aas/adapter/aasx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index f5d797a..e2a0c2c 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -362,8 +362,8 @@ def write_aas(self, # Add referenced ConceptDescriptions to the AAS part for dictionary in aas.concept_dictionary: - for concept_rescription_ref in dictionary.concept_description: - objects_to_be_written.add(concept_rescription_ref.get_identifier()) + for concept_description_ref in dictionary.concept_description: + objects_to_be_written.add(concept_description_ref.get_identifier()) # Write submodels: Either create a split part for each of them or otherwise add them to objects_to_be_written aas_split_part_names: List[str] = [] From 4145b8c0fa364e4555ea48c5003e45b6723853c4 Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:02:10 +0200 Subject: [PATCH 012/474] Add not failsafe mode to AASX Reader Currently, we can pass a `failsafe` parameter in deserializing methods of JSON- and XML-adapters. The parameter determines whether a document should be parsed in a failsafe way. However, I cannot pass the parameter in the deserializing method of the AASX Reader because it does not have `**kwargs`. This commit adds `**kwargs` to deserializing methods of `AASXReader`, such that it is possible to pass the parameter and documents will be deserializend in a not failsafe mode. --- basyx/aas/adapter/aasx.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index e2a0c2c..5e47119 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -111,7 +111,7 @@ def get_thumbnail(self) -> Optional[bytes]: def read_into(self, object_store: model.AbstractObjectStore, file_store: "AbstractSupplementaryFileContainer", - override_existing: bool = False) -> Set[model.Identifier]: + override_existing: bool = False, **kwargs) -> Set[model.Identifier]: """ Read the contents of the AASX package and add them into a given :class:`ObjectStore ` @@ -147,12 +147,14 @@ def read_into(self, object_store: model.AbstractObjectStore, # Iterate AAS files for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ RELATIONSHIP_TYPE_AAS_SPEC]: - self._read_aas_part_into(aas_part, object_store, file_store, read_identifiables, override_existing) + self._read_aas_part_into(aas_part, object_store, file_store, + read_identifiables, override_existing, **kwargs) # Iterate split parts of AAS file for split_part in self.reader.get_related_parts_by_type(aas_part)[ RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: - self._read_aas_part_into(split_part, object_store, file_store, read_identifiables, override_existing) + self._read_aas_part_into(split_part, object_store, file_store, + read_identifiables, override_existing, **kwargs) return read_identifiables @@ -172,7 +174,7 @@ def _read_aas_part_into(self, part_name: str, object_store: model.AbstractObjectStore, file_store: "AbstractSupplementaryFileContainer", read_identifiables: Set[model.Identifier], - override_existing: bool) -> None: + override_existing: bool, **kwargs) -> None: """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. @@ -188,10 +190,10 @@ def _read_aas_part_into(self, part_name: str, :param override_existing: If True, existing objects in the object store are overridden with objects from the AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. """ - for obj in self._parse_aas_part(part_name): - if obj.id in read_identifiables: + for obj in self._parse_aas_part(part_name, **kwargs): + if obj.identification in read_identifiables: continue - if obj.id in object_store: + if obj.identification in object_store: if override_existing: logger.info("Overriding existing object in ObjectStore with {} ...".format(obj)) object_store.discard(obj) @@ -200,11 +202,11 @@ def _read_aas_part_into(self, part_name: str, "ObjectStore".format(obj)) continue object_store.add(obj) - read_identifiables.add(obj.id) + read_identifiables.add(obj.identification) if isinstance(obj, model.Submodel): self._collect_supplementary_files(part_name, obj, file_store) - def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: + def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore: """ Helper function to parse the AAS objects from a single JSON or XML part of the AASX package. @@ -218,12 +220,12 @@ def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_xml_file(p) + return read_aas_xml_file(p, **kwargs) elif content_type.split(";")[0] in ("text/json", "application/json") \ or content_type == "" and extension == "json": logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig')) + return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs) else: logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}" .format(part_name, content_type, extension)) From eacc4dac014777294ae75a0ce3df4fdb26f932e3 Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:13:24 +0200 Subject: [PATCH 013/474] Remove the www. subdomain in AASX namespace (#122) Needed to be compliant to the spec V3.0. Fixes #96 --------- Co-authored-by: s-heppner --- basyx/aas/adapter/aasx.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 5e47119..90829fa 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -39,10 +39,10 @@ logger = logging.getLogger(__name__) -RELATIONSHIP_TYPE_AASX_ORIGIN = "http://www.admin-shell.io/aasx/relationships/aasx-origin" -RELATIONSHIP_TYPE_AAS_SPEC = "http://www.admin-shell.io/aasx/relationships/aas-spec" -RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://www.admin-shell.io/aasx/relationships/aas-spec-split" -RELATIONSHIP_TYPE_AAS_SUPL = "http://www.admin-shell.io/aasx/relationships/aas-suppl" +RELATIONSHIP_TYPE_AASX_ORIGIN = "http://admin-shell.io/aasx/relationships/aasx-origin" +RELATIONSHIP_TYPE_AAS_SPEC = "http://admin-shell.io/aasx/relationships/aas-spec" +RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://admin-shell.io/aasx/relationships/aas-spec-split" +RELATIONSHIP_TYPE_AAS_SUPL = "http://admin-shell.io/aasx/relationships/aas-suppl" class AASXReader: From ec07aeb90435f4f22659d6e0706b9e5007855ada Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Wed, 20 Dec 2023 18:59:14 +0100 Subject: [PATCH 014/474] fixed half the broken doc links --- basyx/aas/adapter/aasx.py | 261 ++++++++++++++++++++++++-------------- 1 file changed, 167 insertions(+), 94 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 90829fa..7b17bc0 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -58,6 +58,7 @@ class AASXReader: with AASXReader("filename.aasx") as reader: meta_data = reader.get_core_properties() reader.read_into(objects, files) + """ def __init__(self, file: Union[os.PathLike, str, IO]): """ @@ -118,10 +119,10 @@ def read_into(self, object_store: model.AbstractObjectStore, This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - `object_store`. While doing so, it searches all parsed :class:`Submodels ` for - :class:`~aas.model.submodel.File` objects to extract the supplementary + `object_store`. While doing so, it searches all parsed :class:`Submodels ` for + :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced supplementary files are added to the given `file_store` and the - :class:`~aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file + :class:`~basyx.aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file to allow for robust resolution the file within the `file_store` later. @@ -130,10 +131,10 @@ def read_into(self, object_store: model.AbstractObjectStore, :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the embedded supplementary files to :param override_existing: If `True`, existing objects in the object store are overridden with objects from the - AASX that have the same :class:`~aas.model.base.Identifier`. Default behavior is to skip those objects from + AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects from the AASX. - :return: A set of the :class:`Identifiers ` of all - :class:`~aas.model.base.Identifiable` objects parsed from the AASX file + :return: A set of the :class:`Identifiers ` of all + :class:`~basyx.aas.model.base.Identifiable` objects parsed from the AASX file """ # Find AASX-Origin part core_rels = self.reader.get_related_parts_by_type() @@ -191,9 +192,9 @@ def _read_aas_part_into(self, part_name: str, AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. """ for obj in self._parse_aas_part(part_name, **kwargs): - if obj.identification in read_identifiables: + if obj.id in read_identifiables: continue - if obj.identification in object_store: + if obj.id in object_store: if override_existing: logger.info("Overriding existing object in ObjectStore with {} ...".format(obj)) object_store.discard(obj) @@ -202,7 +203,7 @@ def _read_aas_part_into(self, part_name: str, "ObjectStore".format(obj)) continue object_store.add(obj) - read_identifiables.add(obj.identification) + read_identifiables.add(obj.id) if isinstance(obj, model.Submodel): self._collect_supplementary_files(part_name, obj, file_store) @@ -274,10 +275,10 @@ class AASXWriter: cp.created = datetime.datetime.now() with AASXWriter("filename.aasx") as writer: - writer.write_aas(Identifier("https://acplt.org/AssetAdministrationShell", IdentifierType.IRI), + writer.write_aas("https://acplt.org/AssetAdministrationShell", object_store, file_store) - writer.write_aas(Identifier("https://acplt.org/AssetAdministrationShell2", IdentifierType.IRI), + writer.write_aas("https://acplt.org/AssetAdministrationShell2", object_store, file_store) writer.write_core_properties(cp) @@ -306,7 +307,6 @@ def __init__(self, file: Union[os.PathLike, str, IO]): self._properties_part: Optional[str] = None # names and hashes of all supplementary file parts that have already been written self._supplementary_part_names: Dict[str, Optional[bytes]] = {} - self._aas_name_friendlyfier = NameFriendlyfier() # Open OPC package writer self.writer = pyecma376_2.ZipPackageWriter(file) @@ -317,83 +317,106 @@ def __init__(self, file: Union[os.PathLike, str, IO]): p.close() def write_aas(self, - aas_id: model.Identifier, + aas_ids: Union[model.Identifier, Iterable[model.Identifier]], object_store: model.AbstractObjectStore, file_store: "AbstractSupplementaryFileContainer", - write_json: bool = False, - submodel_split_parts: bool = True) -> None: + write_json: bool = False) -> None: """ - Convenience method to add an :class:`~aas.model.aas.AssetAdministrationShell` with all included and referenced + Convenience method to write one or more + :class:`AssetAdministrationShells ` with all included and referenced objects to the AASX package according to the part name conventions from DotAAS. - This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given - object_store. :class:`References <~aas.model.base.Reference>` to - the :class:`~aas.model.aas.Asset`, :class:`ConceptDescriptions ` and - :class:`Submodels ` are also resolved using the object_store. All of these objects - are written to aas-spec parts in the AASX package, following the conventions presented in "Details of the Asset - Administration Shell". For each Submodel, a aas-spec-split part is used. Supplementary files which are - referenced by a File object in any of the Submodels, are also added to the AASX package. + This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve the + AASs from the given object_store. + :class:`References ` to :class:`Submodels ` and + :class:`ConceptDescriptions ` (via semanticId attributes) are also + resolved using the + `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` or `/aasx/data.json` in + the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". + Supplementary files which are referenced by a :class:`~basyx.aas.model.submodel.File` object in any of the + :class:`Submodels ` are also added to the AASX + package. + + This method uses `write_all_aas_objects()` to write the AASX part. + + .. attention:: - Internally, this method uses :meth:`aas.adapter.aasx.AASXWriter.write_aas_objects` to write the individual AASX - parts for the AAS and each submodel. + This method **must only be used once** on a single AASX package. Otherwise, the `/aasx/data.json` + (or `...xml`) part would be written twice to the package, hiding the first part and possibly causing + problems when reading the package. - :param aas_id: :class:`~aas.model.base.Identifier` of the :class:`~aas.model.aas.AssetAdministrationShell` to - be added to the AASX file + To write multiple Asset Administration Shells to a single AASX package file, call this method once, passing + a list of AAS Identifiers to the `aas_ids` parameter. + + :param aas_ids: :class:`~basyx.aas.model.base.Identifier` or Iterable of + :class:`Identifiers ` of the AAS(s) to be written to the AASX file :param object_store: :class:`ObjectStore ` to retrieve the - :class:`~aas.model.base.Identifiable` AAS objects (:class:`~aas.model.aas.AssetAdministrationShell`, - :class:`~aas.model.aas.Asset`, :class:`~aas.model.concept.ConceptDescription` and - :class:`~aas.model.submodel.Submodel`) from - :param file_store: :class:`SupplementaryFileContainer ` to - retrieve supplementary files from, which are referenced by :class:`~aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each submodel in the AASX package file - instead of XML parts. Defaults to `False`. - :param submodel_split_parts: If `True` (default), submodels are written to separate AASX parts instead of being - included in the AAS part with in the AASX package. - """ - aas_friendly_name = self._aas_name_friendlyfier.get_friendly_name(aas_id) - aas_part_name = "/aasx/{0}/{0}.aas.{1}".format(aas_friendly_name, "json" if write_json else "xml") - - aas = object_store.get_identifiable(aas_id) - if not isinstance(aas, model.AssetAdministrationShell): - raise ValueError(f"Identifier does not belong to an AssetAdminstrationShell object but to {aas!r}") - - objects_to_be_written: Set[model.Identifier] = {aas.identification} - - # Add the Asset object to the objects in the AAS part - objects_to_be_written.add(aas.asset.get_identifier()) - - # Add referenced ConceptDescriptions to the AAS part - for dictionary in aas.concept_dictionary: - for concept_description_ref in dictionary.concept_description: - objects_to_be_written.add(concept_description_ref.get_identifier()) - - # Write submodels: Either create a split part for each of them or otherwise add them to objects_to_be_written - aas_split_part_names: List[str] = [] - if submodel_split_parts: - # Create a AAS split part for each (available) submodel of the AAS - aas_friendlyfier = NameFriendlyfier() - for submodel_ref in aas.submodel: - submodel_identification = submodel_ref.get_identifier() - submodel_friendly_name = aas_friendlyfier.get_friendly_name(submodel_identification) - submodel_part_name = "/aasx/{0}/{1}/{1}.submodel.{2}".format(aas_friendly_name, submodel_friendly_name, - "json" if write_json else "xml") - self.write_aas_objects(submodel_part_name, [submodel_identification], object_store, file_store, - write_json, split_part=True) - aas_split_part_names.append(submodel_part_name) - else: + :class:`~basyx.aas.model.base.Identifiable` AAS objects (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, + :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.SubmodelElement`) from + :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve + supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects + :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.SubmodelElement` + in the AASX package file instead of XML parts. Defaults to `False`. + :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable + :class:`Submodels ` and + :class:`ConceptDescriptions ` are skipped, logging a warning/info + message) + :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another + :class:`~basyx.aas.model.base.Identifiable` object) + """ + if isinstance(aas_ids, model.Identifier): + aas_ids = (aas_ids,) + + objects_to_be_written: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + for aas_id in aas_ids: + try: + aas = object_store.get_identifiable(aas_id) + # TODO add failsafe mode + except KeyError: + raise + if not isinstance(aas, model.AssetAdministrationShell): + raise TypeError(f"Identifier {aas_id} does not belong to an AssetAdminstrationShell object but to " + f"{aas!r}") + + # Add the AssetAdministrationShell object to the data part + objects_to_be_written.add(aas) + + # Add referenced Submodels to the data part for submodel_ref in aas.submodel: - objects_to_be_written.add(submodel_ref.get_identifier()) - - # Write AAS part - logger.debug("Writing AAS {} to part {} in AASX package ...".format(aas.identification, aas_part_name)) - self.write_aas_objects(aas_part_name, objects_to_be_written, object_store, file_store, write_json, - split_part=False, - additional_relationships=(pyecma376_2.OPCRelationship("r{}".format(i), - RELATIONSHIP_TYPE_AAS_SPEC_SPLIT, - submodel_part_name, - pyecma376_2.OPCTargetMode.INTERNAL) - for i, submodel_part_name in enumerate(aas_split_part_names))) + try: + submodel = submodel_ref.resolve(object_store) + except KeyError: + logger.warning("Could not find submodel %s. Skipping it.", str(submodel_ref)) + continue + objects_to_be_written.add(submodel) + + # Traverse object tree and check if semanticIds are referencing to existing ConceptDescriptions in the + # ObjectStore + concept_descriptions: List[model.ConceptDescription] = [] + for identifiable in objects_to_be_written: + for semantic_id in traversal.walk_semantic_ids_recursive(identifiable): + if not isinstance(semantic_id, model.ModelReference) \ + or semantic_id.type is not model.ConceptDescription: + logger.info("semanticId %s does not reference a ConceptDescription.", str(semantic_id)) + continue + try: + cd = semantic_id.resolve(object_store) + except KeyError: + logger.info("ConceptDescription for semantidId %s not found in object store.", str(semantic_id)) + continue + except model.UnexpectedTypeError as e: + logger.error("semantidId %s resolves to %s, which is not a ConceptDescription", + str(semantic_id), e.value) + continue + concept_descriptions.append(cd) + objects_to_be_written.update(concept_descriptions) + + # Write AAS data part + self.write_all_aas_objects("/aasx/data.{}".format("json" if write_json else "xml"), + objects_to_be_written, file_store, write_json) + # TODO remove `method` parameter in future version. + # Not actually required since you can always create a local dict def write_aas_objects(self, part_name: str, object_ids: Iterable[model.Identifier], @@ -403,26 +426,28 @@ def write_aas_objects(self, split_part: bool = False, additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: """ - Write a defined list of AAS objects to an XML or JSON part in the AASX package and append the referenced - supplementary files to the package. + A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given + This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given object_store. If the list of written objects includes :class:`aas.model.submodel.Submodel` objects, Supplementary files which are - referenced by :class:`~aas.model.submodel.File` objects within + referenced by :class:`~basyx.aas.model.submodel.File` objects within those submodels, are also added to the AASX package. - You must make sure to call this method only once per unique `part_name` on a single package instance. + .. attention:: + + You must make sure to call this method or `write_all_aas_objects` only once per unique `part_name` on a + single package instance. :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. '.json' if `write_json` else '.xml'). - :param object_ids: A list of :class:`Identifiers ` of the objects to be written to - the AASX package. Only these :class:`~aas.model.base.Identifiable` objects (and included - :class:`~aas.model.base.Referable` objects) are written to the package. - :param object_store: The objects store to retrieve the :class:`~aas.model.base.Identifiable` objects from + :param object_ids: A list of :class:`Identifiers ` of the objects to be written to + the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included + :class:`~basyx.aas.model.base.Referable` objects) are written to the package. + :param object_store: The objects store to retrieve the :class:`~basyx.aas.model.base.Identifiable` objects from :param file_store: The :class:`SupplementaryFileContainer ` - to retrieve supplementary files from (if there are any :class:`~aas.model.submodel.File` + to retrieve supplementary files from (if there are any :class:`~basyx.aas.model.submodel.File` objects within the written objects. :param write_json: If `True`, the part is written as a JSON file instead of an XML file. Defaults to `False`. :param split_part: If `True`, no aas-spec relationship is added from the aasx-origin to this part. You must make @@ -433,7 +458,6 @@ def write_aas_objects(self, logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) objects: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - supplementary_files: List[str] = [] # Retrieve objects and scan for referenced supplementary files for identifier in object_ids: @@ -443,6 +467,50 @@ def write_aas_objects(self, logger.error("Could not find object {} in ObjectStore".format(identifier)) continue objects.add(the_object) + + self.write_all_aas_objects(part_name, objects, file_store, write_json, split_part, additional_relationships) + + # TODO remove `split_part` parameter in future version. + # Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 + def write_all_aas_objects(self, + part_name: str, + objects: model.AbstractObjectStore[model.Identifiable], + file_store: "AbstractSupplementaryFileContainer", + write_json: bool = False, + split_part: bool = False, + additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: + """ + Write all AAS objects in a given :class:`ObjectStore ` to an XML or + JSON part in the AASX package and add the referenced supplementary files to the package. + + This method takes an :class:`ObjectStore ` and writes all contained + objects into an "aas_env" part in the AASX package. If + the ObjectStore includes :class:`~aas.model.submodel.Submodel` objects, supplementary files which are + referenced by :class:`~basyx.aas.model.submodel.File` objects + within those Submodels, are fetched from the `file_store` and added to the AASX package. + + .. attention:: + + You must make sure to call this method only once per unique `part_name` on a single package instance. + + :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 + part name and unique within the package. The extension of the part should match the data format (i.e. + '.json' if `write_json` else '.xml'). + :param objects: The objects to be written to the AASX package. Only these Identifiable objects (and included + Referable objects) are written to the package. + :param file_store: The SupplementaryFileContainer to retrieve supplementary files from (if there are any `File` + objects within the written objects. + :param write_json: If True, the part is written as a JSON file instead of an XML file. Defaults to False. + :param split_part: If True, no aas-spec relationship is added from the aasx-origin to this part. You must make + sure to reference it via a aas-spec-split relationship from another aas-spec part + :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object + part to be written, in addition to the aas-suppl relationships which are created automatically. + """ + logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) + supplementary_files: List[str] = [] + + # Retrieve objects and scan for referenced supplementary files + for the_object in objects: if isinstance(the_object, model.Submodel): for element in traversal.walk_submodel(the_object): if isinstance(element, model.File): @@ -458,6 +526,7 @@ def write_aas_objects(self, self._aas_part_names.append(part_name) # Write part + # TODO allow writing xml *and* JSON part with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p: if write_json: write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects) @@ -587,6 +656,8 @@ def _write_package_relationships(self): self.writer.write_relationships(package_relationships) +# TODO remove in future version. +# Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 class NameFriendlyfier: """ A simple helper class to create unique "AAS friendly names" according to DotAAS, section 7.6. @@ -602,6 +673,8 @@ def get_friendly_name(self, identifier: model.Identifier): """ Generate a friendly name from an AAS identifier. + TODO: This information is outdated. The whole class is no longer needed. + According to section 7.6 of "Details of the Asset Administration Shell", all non-alphanumerical characters are replaced with underscores. We also replace all non-ASCII characters to generate valid URIs as the result. If this replacement results in a collision with a previously generated friendly name of this NameFriendlifier, @@ -612,15 +685,15 @@ def get_friendly_name(self, identifier: model.Identifier): .. code-block:: python friendlyfier = NameFriendlyfier() - friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS-a", model.IdentifierType.IRI)) + friendlyfier.get_friendly_name("http://example.com/AAS-a") > "http___example_com_AAS_a" - friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS+a", model.IdentifierType.IRI)) + friendlyfier.get_friendly_name("http://example.com/AAS+a") > "http___example_com_AAS_a_1" """ # friendlify name - raw_name = self.RE_NON_ALPHANUMERICAL.sub('_', identifier.id) + raw_name = self.RE_NON_ALPHANUMERICAL.sub('_', identifier) # Unify name (avoid collisions) amended_name = raw_name @@ -639,7 +712,7 @@ class AbstractSupplementaryFileContainer(metaclass=abc.ABCMeta): Supplementary files may be PDF files or other binary or textual files, referenced in a File object of an AAS by their name. They are used to provide associated documents without embedding their contents (as - :class:`~aas.model.submodel.Blob` object) in the AAS. + :class:`~basyx.aas.model.submodel.Blob` object) in the AAS. A SupplementaryFileContainer keeps track of the name and content_type (MIME type) for each file. Additionally it allows to resolve name conflicts by comparing the files' contents and providing an alternative name for a dissimilar From e5f670239a0f758ce0495932fea69e89915fd33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 22 Dec 2023 04:35:11 +0100 Subject: [PATCH 015/474] fix incorrect `Submodel` -> `SubmodelElement` replacements See commit 6699deed1ea5b3555483460d4c62931cece4583b. --- basyx/aas/adapter/aasx.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 7b17bc0..76317c8 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -119,7 +119,7 @@ def read_into(self, object_store: model.AbstractObjectStore, This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - `object_store`. While doing so, it searches all parsed :class:`Submodels ` for + `object_store`. While doing so, it searches all parsed :class:`Submodels ` for :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced supplementary files are added to the given `file_store` and the :class:`~basyx.aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file @@ -328,13 +328,13 @@ def write_aas(self, This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve the AASs from the given object_store. - :class:`References ` to :class:`Submodels ` and + :class:`References ` to :class:`Submodels ` and :class:`ConceptDescriptions ` (via semanticId attributes) are also resolved using the `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` or `/aasx/data.json` in the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". Supplementary files which are referenced by a :class:`~basyx.aas.model.submodel.File` object in any of the - :class:`Submodels ` are also added to the AASX + :class:`Submodels ` are also added to the AASX package. This method uses `write_all_aas_objects()` to write the AASX part. @@ -352,13 +352,13 @@ def write_aas(self, :class:`Identifiers ` of the AAS(s) to be written to the AASX file :param object_store: :class:`ObjectStore ` to retrieve the :class:`~basyx.aas.model.base.Identifiable` AAS objects (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, - :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.SubmodelElement`) from + :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.Submodel`) from :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.SubmodelElement` + :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts. Defaults to `False`. :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable - :class:`Submodels ` and + :class:`Submodels ` and :class:`ConceptDescriptions ` are skipped, logging a warning/info message) :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another From 85c3dca8282336e9ac3e02a18b64a4d67a10cbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 22 Dec 2023 04:57:00 +0100 Subject: [PATCH 016/474] fix pycodestyle warnings --- basyx/aas/adapter/aasx.py | 63 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 76317c8..1f69046 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -119,20 +119,19 @@ def read_into(self, object_store: model.AbstractObjectStore, This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - `object_store`. While doing so, it searches all parsed :class:`Submodels ` for - :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary - files. The referenced supplementary files are added to the given `file_store` and the - :class:`~basyx.aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file - to allow for robust resolution the file within the - `file_store` later. + `object_store`. While doing so, it searches all parsed :class:`Submodels ` + for :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced + supplementary files are added to the given `file_store` and the :class:`~basyx.aas.model.submodel.File` + objects' values are updated with the absolute name of the supplementary file to allow for robust resolution the + file within the `file_store` later. :param object_store: An :class:`ObjectStore ` to add the AAS objects from the AASX file to :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the embedded supplementary files to :param override_existing: If `True`, existing objects in the object store are overridden with objects from the - AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects from - the AASX. + AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects + from the AASX. :return: A set of the :class:`Identifiers ` of all :class:`~basyx.aas.model.base.Identifiable` objects parsed from the AASX file """ @@ -323,19 +322,18 @@ def write_aas(self, write_json: bool = False) -> None: """ Convenience method to write one or more - :class:`AssetAdministrationShells ` with all included and referenced - objects to the AASX package according to the part name conventions from DotAAS. - - This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve the - AASs from the given object_store. - :class:`References ` to :class:`Submodels ` and - :class:`ConceptDescriptions ` (via semanticId attributes) are also - resolved using the - `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` or `/aasx/data.json` in - the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". - Supplementary files which are referenced by a :class:`~basyx.aas.model.submodel.File` object in any of the - :class:`Submodels ` are also added to the AASX - package. + :class:`AssetAdministrationShells ` with all included + and referenced objects to the AASX package according to the part name conventions from DotAAS. + + This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve + the AASs from the given object_store. + :class:`References ` to :class:`Submodels ` + and :class:`ConceptDescriptions ` (via semanticId attributes) are + also resolved using the `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` + or `/aasx/data.json` in the AASX package, compliant to the convention presented in + "Details of the Asset Administration Shell". Supplementary files which are referenced by a + :class:`~basyx.aas.model.submodel.File` object in any of the + :class:`Submodels ` are also added to the AASX package. This method uses `write_all_aas_objects()` to write the AASX part. @@ -351,16 +349,18 @@ def write_aas(self, :param aas_ids: :class:`~basyx.aas.model.base.Identifier` or Iterable of :class:`Identifiers ` of the AAS(s) to be written to the AASX file :param object_store: :class:`ObjectStore ` to retrieve the - :class:`~basyx.aas.model.base.Identifiable` AAS objects (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, + :class:`~basyx.aas.model.base.Identifiable` AAS objects + (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.Submodel`) from :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.Submodel` - in the AASX package file instead of XML parts. Defaults to `False`. + :param write_json: If `True`, JSON parts are created for the AAS and each + :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts. + Defaults to `False`. :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable :class:`Submodels ` and - :class:`ConceptDescriptions ` are skipped, logging a warning/info - message) + :class:`ConceptDescriptions ` are skipped, logging a + warning/info message) :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another :class:`~basyx.aas.model.base.Identifiable` object) """ @@ -428,10 +428,9 @@ def write_aas_objects(self, """ A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given - object_store. If the list - of written objects includes :class:`aas.model.submodel.Submodel` objects, Supplementary files which are - referenced by :class:`~basyx.aas.model.submodel.File` objects within + This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as `aas_id`) to retrieve it + from the given object_store. If the list of written objects includes :class:`aas.model.submodel.Submodel` + objects, Supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within those submodels, are also added to the AASX package. .. attention:: @@ -442,8 +441,8 @@ def write_aas_objects(self, :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. '.json' if `write_json` else '.xml'). - :param object_ids: A list of :class:`Identifiers ` of the objects to be written to - the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included + :param object_ids: A list of :class:`Identifiers ` of the objects to be written + to the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included :class:`~basyx.aas.model.base.Referable` objects) are written to the package. :param object_store: The objects store to retrieve the :class:`~basyx.aas.model.base.Identifiable` objects from :param file_store: The :class:`SupplementaryFileContainer ` From 76d7180e8c06316b4a79847ea6426d03d922f586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 23 Dec 2023 01:28:28 +0100 Subject: [PATCH 017/474] massive docstring overhaul This fixes missing references, improves the layout, removes outdated information in some places, and more. --- basyx/aas/adapter/aasx.py | 100 +++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 1f69046..37eee7b 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -10,13 +10,13 @@ Functionality for reading and writing AASX files according to "Details of the Asset Administration Shell Part 1 V2.0", section 7. -The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the `pyecma376_2` library +The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the ``pyecma376_2`` library for low level OPC reading and writing. It currently supports all required features except for embedded digital signatures. Writing and reading of AASX packages is performed through the :class:`~.AASXReader` and :class:`~.AASXWriter` classes. Each instance of these classes wraps an existing AASX file resp. a file to be created and allows to read/write the -included AAS objects into/form :class:`ObjectStores `. +included AAS objects into/form :class:`ObjectStores `. For handling of embedded supplementary files, this module provides the :class:`~.AbstractSupplementaryFileContainer` class interface and the :class:`~.DictSupplementaryFileContainer` implementation. @@ -64,7 +64,7 @@ def __init__(self, file: Union[os.PathLike, str, IO]): """ Open an AASX reader for the given filename or file handle - The given file is opened as OPC ZIP package. Make sure to call `AASXReader.close()` after reading the file + The given file is opened as OPC ZIP package. Make sure to call ``AASXReader.close()`` after reading the file contents to close the underlying ZIP file reader. You may also use the AASXReader as a context manager to ensure closing under any circumstances. @@ -92,7 +92,7 @@ def get_thumbnail(self) -> Optional[bytes]: Retrieve the packages thumbnail image The thumbnail image file is read into memory and returned as bytes object. You may use some python image library - for further processing or conversion, e.g. `pillow`: + for further processing or conversion, e.g. ``pillow``: .. code-block:: python @@ -115,21 +115,21 @@ def read_into(self, object_store: model.AbstractObjectStore, override_existing: bool = False, **kwargs) -> Set[model.Identifier]: """ Read the contents of the AASX package and add them into a given - :class:`ObjectStore ` + :class:`ObjectStore ` This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - `object_store`. While doing so, it searches all parsed :class:`Submodels ` + ``object_store``. While doing so, it searches all parsed :class:`Submodels ` for :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced - supplementary files are added to the given `file_store` and the :class:`~basyx.aas.model.submodel.File` + supplementary files are added to the given ``file_store`` and the :class:`~basyx.aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file to allow for robust resolution the - file within the `file_store` later. + file within the ``file_store`` later. - :param object_store: An :class:`ObjectStore ` to add the AAS objects - from the AASX file to + :param object_store: An :class:`ObjectStore ` to add the AAS + objects from the AASX file to :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the embedded supplementary files to - :param override_existing: If `True`, existing objects in the object store are overridden with objects from the + :param override_existing: If ``True``, existing objects in the object store are overridden with objects from the AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects from the AASX. :return: A set of the :class:`Identifiers ` of all @@ -178,8 +178,8 @@ def _read_aas_part_into(self, part_name: str, """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. - This method primarily checks for duplicate objects. It uses `_parse_aas_parse()` to do the actual parsing and - `_collect_supplementary_files()` for supplementary file processing of non-duplicate objects. + This method primarily checks for duplicate objects. It uses ``_parse_aas_parse()`` to do the actual parsing and + ``_collect_supplementary_files()`` for supplementary file processing of non-duplicate objects. :param part_name: The OPC part name to read :param object_store: An ObjectStore to add the AAS objects from the AASX file to @@ -292,7 +292,7 @@ def __init__(self, file: Union[os.PathLike, str, IO]): """ Create a new AASX package in the given file and open the AASXWriter to add contents to the package. - Make sure to call `AASXWriter.close()` after writing all contents to write the aas-spec relationships for all + Make sure to call ``AASXWriter.close()`` after writing all contents to write the aas-spec relationships for all AAS parts to the file and close the underlying ZIP file writer. You may also use the AASXWriter as a context manager to ensure closing under any circumstances. @@ -325,38 +325,38 @@ def write_aas(self, :class:`AssetAdministrationShells ` with all included and referenced objects to the AASX package according to the part name conventions from DotAAS. - This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve - the AASs from the given object_store. + This method takes the AASs' :class:`Identifiers ` (as ``aas_ids``) to retrieve + the AASs from the given ``object_store``. :class:`References ` to :class:`Submodels ` and :class:`ConceptDescriptions ` (via semanticId attributes) are - also resolved using the `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` - or `/aasx/data.json` in the AASX package, compliant to the convention presented in + also resolved using the ``object_store``. All of these objects are written to an aas-spec part + ``/aasx/data.xml`` or ``/aasx/data.json`` in the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". Supplementary files which are referenced by a :class:`~basyx.aas.model.submodel.File` object in any of the :class:`Submodels ` are also added to the AASX package. - This method uses `write_all_aas_objects()` to write the AASX part. + This method uses :meth:`write_all_aas_objects` to write the AASX part. .. attention:: - This method **must only be used once** on a single AASX package. Otherwise, the `/aasx/data.json` - (or `...xml`) part would be written twice to the package, hiding the first part and possibly causing + This method **must only be used once** on a single AASX package. Otherwise, the ``/aasx/data.json`` + (or ``...xml``) part would be written twice to the package, hiding the first part and possibly causing problems when reading the package. To write multiple Asset Administration Shells to a single AASX package file, call this method once, passing - a list of AAS Identifiers to the `aas_ids` parameter. + a list of AAS Identifiers to the ``aas_ids`` parameter. :param aas_ids: :class:`~basyx.aas.model.base.Identifier` or Iterable of :class:`Identifiers ` of the AAS(s) to be written to the AASX file - :param object_store: :class:`ObjectStore ` to retrieve the + :param object_store: :class:`ObjectStore ` to retrieve the :class:`~basyx.aas.model.base.Identifiable` AAS objects (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.Submodel`) from - :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve + :param file_store: :class:`SupplementaryFileContainer ` to retrieve supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each + :param write_json: If ``True``, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts. - Defaults to `False`. + Defaults to ``False``. :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable :class:`Submodels ` and :class:`ConceptDescriptions ` are skipped, logging a @@ -428,29 +428,31 @@ def write_aas_objects(self, """ A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as `aas_id`) to retrieve it - from the given object_store. If the list of written objects includes :class:`aas.model.submodel.Submodel` + This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as ``aas_id``) to retrieve it + from the given object_store. If the list of written objects includes :class:`~basyx.aas.model.submodel.Submodel` objects, Supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within those submodels, are also added to the AASX package. .. attention:: - You must make sure to call this method or `write_all_aas_objects` only once per unique `part_name` on a - single package instance. + You must make sure to call this method or :meth:`write_all_aas_objects` only once per unique ``part_name`` + on a single package instance. :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. - '.json' if `write_json` else '.xml'). + '.json' if ``write_json`` else '.xml'). :param object_ids: A list of :class:`Identifiers ` of the objects to be written to the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included :class:`~basyx.aas.model.base.Referable` objects) are written to the package. :param object_store: The objects store to retrieve the :class:`~basyx.aas.model.base.Identifiable` objects from - :param file_store: The :class:`SupplementaryFileContainer ` + :param file_store: The + :class:`SupplementaryFileContainer ` to retrieve supplementary files from (if there are any :class:`~basyx.aas.model.submodel.File` objects within the written objects. - :param write_json: If `True`, the part is written as a JSON file instead of an XML file. Defaults to `False`. - :param split_part: If `True`, no aas-spec relationship is added from the aasx-origin to this part. You must make - sure to reference it via a aas-spec-split relationship from another aas-spec part + :param write_json: If ``True``, the part is written as a JSON file instead of an XML file. Defaults to + ``False``. + :param split_part: If ``True``, no aas-spec relationship is added from the aasx-origin to this part. You must + make sure to reference it via a aas-spec-split relationship from another aas-spec part :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object part to be written, in addition to the aas-suppl relationships which are created automatically. """ @@ -479,26 +481,26 @@ def write_all_aas_objects(self, split_part: bool = False, additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: """ - Write all AAS objects in a given :class:`ObjectStore ` to an XML or - JSON part in the AASX package and add the referenced supplementary files to the package. + Write all AAS objects in a given :class:`ObjectStore ` to an XML + or JSON part in the AASX package and add the referenced supplementary files to the package. - This method takes an :class:`ObjectStore ` and writes all contained - objects into an "aas_env" part in the AASX package. If - the ObjectStore includes :class:`~aas.model.submodel.Submodel` objects, supplementary files which are - referenced by :class:`~basyx.aas.model.submodel.File` objects - within those Submodels, are fetched from the `file_store` and added to the AASX package. + This method takes an :class:`ObjectStore ` and writes all + contained objects into an ``aas_env`` part in the AASX package. If the ObjectStore includes + :class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by + :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store`` + and added to the AASX package. .. attention:: - You must make sure to call this method only once per unique `part_name` on a single package instance. + You must make sure to call this method only once per unique ``part_name`` on a single package instance. :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. - '.json' if `write_json` else '.xml'). + '.json' if ``write_json`` else '.xml'). :param objects: The objects to be written to the AASX package. Only these Identifiable objects (and included Referable objects) are written to the package. - :param file_store: The SupplementaryFileContainer to retrieve supplementary files from (if there are any `File` - objects within the written objects. + :param file_store: The SupplementaryFileContainer to retrieve supplementary files from (if there are any + ``File`` objects within the written objects. :param write_json: If True, the part is written as a JSON file instead of an XML file. Defaults to False. :param split_part: If True, no aas-spec relationship is added from the aasx-origin to this part. You must make sure to reference it via a aas-spec-split relationship from another aas-spec part @@ -616,7 +618,7 @@ def _write_aasx_origin_relationships(self): """ Helper function to write aas-spec relationships of the aasx-origin part. - This method uses the list of aas-spec parts in `_aas_part_names`. It should be called just before closing the + This method uses the list of aas-spec parts in ``_aas_part_names``. It should be called just before closing the file to make sure all aas-spec parts of the package have already been written. """ # Add relationships from AASX-origin part to AAS parts @@ -731,8 +733,8 @@ def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: :param name: The file's proposed name. Should start with a '/'. Should not contain URI-encoded '/' or '\' :param file: A binary file-like opened for reading the file contents :param content_type: The file's content_type - :return: The file name as stored in the SupplementaryFileContainer. Typically `name` or a modified version of - `name` to resolve conflicts. + :return: The file name as stored in the SupplementaryFileContainer. Typically ``name`` or a modified version of + ``name`` to resolve conflicts. """ pass # pragma: no cover From cbddff5c26a3d387df5c4859559fc8c30a249455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 14 Jan 2024 22:11:00 +0100 Subject: [PATCH 018/474] adapter.aasx: improve error messages A `FileNotFoundError` is no longer converted to a `ValueError`, but re-raised instead. Furthermore, the message of the `ValueError` now contains more information as to why a file is not a valid OPC package. Fix #221 --- basyx/aas/adapter/aasx.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 37eee7b..1c50feb 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -69,13 +69,16 @@ def __init__(self, file: Union[os.PathLike, str, IO]): closing under any circumstances. :param file: A filename, file path or an open file-like object in binary mode + :raises FileNotFoundError: If the file does not exist :raises ValueError: If the file is not a valid OPC zip package """ try: logger.debug("Opening {} as AASX pacakge for reading ...".format(file)) self.reader = pyecma376_2.ZipPackageReader(file) + except FileNotFoundError: + raise except Exception as e: - raise ValueError("{} is not a valid ECMA376-2 (OPC) file".format(file)) from e + raise ValueError("{} is not a valid ECMA376-2 (OPC) file: {}".format(file, e)) from e def get_core_properties(self) -> pyecma376_2.OPCCoreProperties: """ From 315a1fc1ae91164d59c93dc05dd0caa0d3a779c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 17 Feb 2024 17:44:16 +0100 Subject: [PATCH 019/474] adapter.aasx: improve `AASXWriter` docstring Replace a block of text by an `attention` admonition to highlight it properly. Furthermore, add a missing comma. --- basyx/aas/adapter/aasx.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 1c50feb..20eb41d 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -285,9 +285,11 @@ class AASXWriter: file_store) writer.write_core_properties(cp) - **Attention:** The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context - manager functionality (as shown above). Otherwise the resulting AASX file will lack important data structures - and will not be readable. + .. attention:: + + The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context manager + functionality (as shown above). Otherwise, the resulting AASX file will lack important data structures + and will not be readable. """ AASX_ORIGIN_PART_NAME = "/aasx/aasx-origin" From 2625723b199e25b5c33efa75cc946c2829cf8237 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Wed, 10 Apr 2024 16:50:36 +0200 Subject: [PATCH 020/474] Update Copyright Notices (#224) This PR fixes the outdated `NOTICE`. While doing that, I `notice`d, that the years in the copyright strings were outdated as well, so I updated them (using the `/etc/scripts/set_copyright_year.sh`) In the future, we should create a recurring task that makes us update the years at least once a year. Maybe it should also become a task before publishing a new release? Fixes #196 Depends on #235 --- basyx/aas/adapter/aasx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 20eb41d..6fa1ac1 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2024 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. From bef1f9c4605d1c73e15752062703f31d9e892cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 14:56:16 +0200 Subject: [PATCH 021/474] adapter.aasx: allow deleting files from `SupplementaryFileContainer` `AbstractSupplementaryFileContainer` and `DictSupplementaryFileContainer` are extended by a `delete_file()` method, that allows deleting files from them. Since different files may have the same content, references to the files contents in `DictSupplementaryFileContainer._store` are tracked via `_store_refcount`. A files contents are only deleted from `_store`, if all filenames referring to these these contents are deleted, i.e. if the refcount reaches 0. --- basyx/aas/adapter/aasx.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 6fa1ac1..30bb394 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -778,6 +778,13 @@ def write_file(self, name: str, file: IO[bytes]) -> None: """ pass # pragma: no cover + @abc.abstractmethod + def delete_file(self, name: str) -> None: + """ + Deletes a file from this SupplementaryFileContainer given its name. + """ + pass # pragma: no cover + @abc.abstractmethod def __contains__(self, item: str) -> bool: """ @@ -802,18 +809,23 @@ def __init__(self): self._store: Dict[bytes, bytes] = {} # Maps file names to (sha256, content_type) self._name_map: Dict[str, Tuple[bytes, str]] = {} + # Tracks the number of references to _store keys, + # i.e. the number of different filenames referring to the same file + self._store_refcount: Dict[bytes, int] = {} def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: data = file.read() hash = hashlib.sha256(data).digest() if hash not in self._store: self._store[hash] = data + self._store_refcount[hash] = 0 name_map_data = (hash, content_type) new_name = name i = 1 while True: if new_name not in self._name_map: self._name_map[new_name] = name_map_data + self._store_refcount[hash] += 1 return new_name elif self._name_map[new_name] == name_map_data: return new_name @@ -839,6 +851,16 @@ def get_sha256(self, name: str) -> bytes: def write_file(self, name: str, file: IO[bytes]) -> None: file.write(self._store[self._name_map[name][0]]) + def delete_file(self, name: str) -> None: + # The number of different files with the same content are kept track of via _store_refcount. + # The contents are only deleted, once the refcount reaches zero. + hash: bytes = self._name_map[name][0] + self._store_refcount[hash] -= 1 + if self._store_refcount[hash] == 0: + del self._store[hash] + del self._store_refcount[hash] + del self._name_map[name] + def __contains__(self, item: object) -> bool: return item in self._name_map From 54b423486f59c00af6b9985172150fca55fadf66 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 26 Sep 2024 12:17:55 +0200 Subject: [PATCH 022/474] sdk/pyproject.toml: added dependency. sdk/basyx/aasx.py: changed dir of file --- {basyx/aas/adapter => sdk/basyx}/aasx.py | 0 sdk/pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {basyx/aas/adapter => sdk/basyx}/aasx.py (100%) diff --git a/basyx/aas/adapter/aasx.py b/sdk/basyx/aasx.py similarity index 100% rename from basyx/aas/adapter/aasx.py rename to sdk/basyx/aasx.py diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 60db5ef..7b47198 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "basyx-python-framework-base" version = "0.1" -dependencies = ["aas-core3.0"] +dependencies = ["aas-core3.0", "pyecma376-2"] requires-python = ">=3.8, <3.13" authors = [ {name = "The Eclipse BaSyx Authors"} From f74afb433a2e0ca4fa0a3f0c27c64b263bb6667b Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 15 Nov 2021 19:00:02 +0100 Subject: [PATCH 023/474] =?UTF-8?q?Move=20Python=20package=20aas=20?= =?UTF-8?q?=E2=86=92=20basyx.aas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- basyx/aas/adapter/__init__.py | 9 + basyx/aas/adapter/_generic.py | 99 ++ basyx/aas/adapter/aasx.py | 766 +++++++++ basyx/aas/adapter/json/__init__.py | 24 + basyx/aas/adapter/json/aasJSONSchema.json | 1439 +++++++++++++++++ .../aas/adapter/json/json_deserialization.py | 830 ++++++++++ basyx/aas/adapter/json/json_serialization.py | 768 +++++++++ basyx/aas/adapter/xml/AAS.xsd | 550 +++++++ basyx/aas/adapter/xml/AAS_ABAC.xsd | 185 +++ basyx/aas/adapter/xml/IEC61360.xsd | 171 ++ basyx/aas/adapter/xml/__init__.py | 18 + basyx/aas/adapter/xml/xml_deserialization.py | 1409 ++++++++++++++++ basyx/aas/adapter/xml/xml_serialization.py | 898 ++++++++++ 13 files changed, 7166 insertions(+) create mode 100644 basyx/aas/adapter/__init__.py create mode 100644 basyx/aas/adapter/_generic.py create mode 100644 basyx/aas/adapter/aasx.py create mode 100644 basyx/aas/adapter/json/__init__.py create mode 100644 basyx/aas/adapter/json/aasJSONSchema.json create mode 100644 basyx/aas/adapter/json/json_deserialization.py create mode 100644 basyx/aas/adapter/json/json_serialization.py create mode 100644 basyx/aas/adapter/xml/AAS.xsd create mode 100644 basyx/aas/adapter/xml/AAS_ABAC.xsd create mode 100644 basyx/aas/adapter/xml/IEC61360.xsd create mode 100644 basyx/aas/adapter/xml/__init__.py create mode 100644 basyx/aas/adapter/xml/xml_deserialization.py create mode 100644 basyx/aas/adapter/xml/xml_serialization.py diff --git a/basyx/aas/adapter/__init__.py b/basyx/aas/adapter/__init__.py new file mode 100644 index 0000000..0ce34e1 --- /dev/null +++ b/basyx/aas/adapter/__init__.py @@ -0,0 +1,9 @@ +""" +This package contains different kinds of adapters. + +* :ref:`json `: This package offers an adapter for serialization and deserialization of BaSyx +Python SDK objects to/from JSON. +* :ref:`xml `: This package offers an adapter for serialization and deserialization of BaSyx Python +SDK objects to/from XML. +* :ref:`aasx `: This package offers functions for reading and writing AASX-files. +""" diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py new file mode 100644 index 0000000..998b9ac --- /dev/null +++ b/basyx/aas/adapter/_generic.py @@ -0,0 +1,99 @@ +# Copyright (c) 2020 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +""" +The dicts defined in this module are used in the json and xml modules to translate enum members of our +implementation to the respective string and vice versa. +""" +from typing import Dict, Type + +from basyx.aas import model + +MODELING_KIND: Dict[model.ModelingKind, str] = { + model.ModelingKind.TEMPLATE: 'Template', + model.ModelingKind.INSTANCE: 'Instance'} + +ASSET_KIND: Dict[model.AssetKind, str] = { + model.AssetKind.TYPE: 'Type', + model.AssetKind.INSTANCE: 'Instance'} + +KEY_ELEMENTS: Dict[model.KeyElements, str] = { + model.KeyElements.ASSET: 'Asset', + model.KeyElements.ASSET_ADMINISTRATION_SHELL: 'AssetAdministrationShell', + model.KeyElements.CONCEPT_DESCRIPTION: 'ConceptDescription', + model.KeyElements.SUBMODEL: 'Submodel', + model.KeyElements.ANNOTATED_RELATIONSHIP_ELEMENT: 'AnnotatedRelationshipElement', + model.KeyElements.BASIC_EVENT: 'BasicEvent', + model.KeyElements.BLOB: 'Blob', + model.KeyElements.CAPABILITY: 'Capability', + model.KeyElements.CONCEPT_DICTIONARY: 'ConceptDictionary', + model.KeyElements.DATA_ELEMENT: 'DataElement', + model.KeyElements.ENTITY: 'Entity', + model.KeyElements.EVENT: 'Event', + model.KeyElements.FILE: 'File', + model.KeyElements.MULTI_LANGUAGE_PROPERTY: 'MultiLanguageProperty', + model.KeyElements.OPERATION: 'Operation', + model.KeyElements.PROPERTY: 'Property', + model.KeyElements.RANGE: 'Range', + model.KeyElements.REFERENCE_ELEMENT: 'ReferenceElement', + model.KeyElements.RELATIONSHIP_ELEMENT: 'RelationshipElement', + model.KeyElements.SUBMODEL_ELEMENT: 'SubmodelElement', + model.KeyElements.SUBMODEL_ELEMENT_COLLECTION: 'SubmodelElementCollection', + model.KeyElements.VIEW: 'View', + model.KeyElements.GLOBAL_REFERENCE: 'GlobalReference', + model.KeyElements.FRAGMENT_REFERENCE: 'FragmentReference'} + +KEY_TYPES: Dict[model.KeyType, str] = { + model.KeyType.CUSTOM: 'Custom', + model.KeyType.IRDI: 'IRDI', + model.KeyType.IRI: 'IRI', + model.KeyType.IDSHORT: 'IdShort', + model.KeyType.FRAGMENT_ID: 'FragmentId'} + +IDENTIFIER_TYPES: Dict[model.IdentifierType, str] = { + model.IdentifierType.CUSTOM: 'Custom', + model.IdentifierType.IRDI: 'IRDI', + model.IdentifierType.IRI: 'IRI'} + +ENTITY_TYPES: Dict[model.EntityType, str] = { + model.EntityType.CO_MANAGED_ENTITY: 'CoManagedEntity', + model.EntityType.SELF_MANAGED_ENTITY: 'SelfManagedEntity'} + +IEC61360_DATA_TYPES: Dict[model.concept.IEC61360DataType, str] = { + model.concept.IEC61360DataType.DATE: 'DATE', + model.concept.IEC61360DataType.STRING: 'STRING', + model.concept.IEC61360DataType.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE', + model.concept.IEC61360DataType.REAL_MEASURE: 'REAL_MEASURE', + model.concept.IEC61360DataType.REAL_COUNT: 'REAL_COUNT', + model.concept.IEC61360DataType.REAL_CURRENCY: 'REAL_CURRENCY', + model.concept.IEC61360DataType.BOOLEAN: 'BOOLEAN', + model.concept.IEC61360DataType.URL: 'URL', + model.concept.IEC61360DataType.RATIONAL: 'RATIONAL', + model.concept.IEC61360DataType.RATIONAL_MEASURE: 'RATIONAL_MEASURE', + model.concept.IEC61360DataType.TIME: 'TIME', + model.concept.IEC61360DataType.TIMESTAMP: 'TIMESTAMP', +} + +IEC61360_LEVEL_TYPES: Dict[model.concept.IEC61360LevelType, str] = { + model.concept.IEC61360LevelType.MIN: 'Min', + model.concept.IEC61360LevelType.MAX: 'Max', + model.concept.IEC61360LevelType.NOM: 'Nom', + model.concept.IEC61360LevelType.TYP: 'Typ', +} + +MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} +ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} +KEY_ELEMENTS_INVERSE: Dict[str, model.KeyElements] = {v: k for k, v in KEY_ELEMENTS.items()} +KEY_TYPES_INVERSE: Dict[str, model.KeyType] = {v: k for k, v in KEY_TYPES.items()} +IDENTIFIER_TYPES_INVERSE: Dict[str, model.IdentifierType] = {v: k for k, v in IDENTIFIER_TYPES.items()} +ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()} +IEC61360_DATA_TYPES_INVERSE: Dict[str, model.concept.IEC61360DataType] = {v: k for k, v in IEC61360_DATA_TYPES.items()} +IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.concept.IEC61360LevelType] = \ + {v: k for k, v in IEC61360_LEVEL_TYPES.items()} + +KEY_ELEMENTS_CLASSES_INVERSE: Dict[model.KeyElements, Type[model.Referable]] = \ + {v: k for k, v in model.KEY_ELEMENTS_CLASSES.items()} diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py new file mode 100644 index 0000000..e540dd2 --- /dev/null +++ b/basyx/aas/adapter/aasx.py @@ -0,0 +1,766 @@ +# Copyright (c) 2020 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +""" +.. _adapter.aasx: + +Functionality for reading and writing AASX files according to "Details of the Asset Administration Shell Part 1 V2.0", +section 7. + +The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the `pyecma376_2` library +for low level OPC reading and writing. It currently supports all required features except for embedded digital +signatures. + +Writing and reading of AASX packages is performed through the :class:`~.AASXReader` and :class:`~.AASXWriter` classes. +Each instance of these classes wraps an existing AASX file resp. a file to be created and allows to read/write the +included AAS objects into/form :class:`ObjectStores `. +For handling of embedded supplementary files, this module provides the +:class:`~.AbstractSupplementaryFileContainer` class +interface and the :class:`~.DictSupplementaryFileContainer` implementation. +""" + +import abc +import hashlib +import io +import itertools +import logging +import os +import re +from typing import Dict, Tuple, IO, Union, List, Set, Optional, Iterable, Iterator + +from .xml import read_aas_xml_file, write_aas_xml_file +from .. import model +from .json import read_aas_json_file, write_aas_json_file +import pyecma376_2 +from ..util import traversal + +logger = logging.getLogger(__name__) + +RELATIONSHIP_TYPE_AASX_ORIGIN = "http://www.admin-shell.io/aasx/relationships/aasx-origin" +RELATIONSHIP_TYPE_AAS_SPEC = "http://www.admin-shell.io/aasx/relationships/aas-spec" +RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://www.admin-shell.io/aasx/relationships/aas-spec-split" +RELATIONSHIP_TYPE_AAS_SUPL = "http://www.admin-shell.io/aasx/relationships/aas-suppl" + + +class AASXReader: + """ + An AASXReader wraps an existing AASX package file to allow reading its contents and metadata. + + Basic usage: + + .. code-block:: python + + objects = DictObjectStore() + files = DictSupplementaryFileContainer() + with AASXReader("filename.aasx") as reader: + meta_data = reader.get_core_properties() + reader.read_into(objects, files) + """ + def __init__(self, file: Union[os.PathLike, str, IO]): + """ + Open an AASX reader for the given filename or file handle + + The given file is opened as OPC ZIP package. Make sure to call `AASXReader.close()` after reading the file + contents to close the underlying ZIP file reader. You may also use the AASXReader as a context manager to ensure + closing under any circumstances. + + :param file: A filename, file path or an open file-like object in binary mode + :raises ValueError: If the file is not a valid OPC zip package + """ + try: + logger.debug("Opening {} as AASX pacakge for reading ...".format(file)) + self.reader = pyecma376_2.ZipPackageReader(file) + except Exception as e: + raise ValueError("{} is not a valid ECMA376-2 (OPC) file".format(file)) from e + + def get_core_properties(self) -> pyecma376_2.OPCCoreProperties: + """ + Retrieve the OPC Core Properties (meta data) of the AASX package file. + + If no meta data is provided in the package file, an emtpy OPCCoreProperties object is returned. + + :return: The AASX package's meta data + """ + return self.reader.get_core_properties() + + def get_thumbnail(self) -> Optional[bytes]: + """ + Retrieve the packages thumbnail image + + The thumbnail image file is read into memory and returned as bytes object. You may use some python image library + for further processing or conversion, e.g. `pillow`: + + .. code-block:: python + + import io + from PIL import Image + thumbnail = Image.open(io.BytesIO(reader.get_thumbnail())) + + :return: The AASX package thumbnail's file contents or None if no thumbnail is provided + """ + try: + thumbnail_part = self.reader.get_related_parts_by_type()[pyecma376_2.RELATIONSHIP_TYPE_THUMBNAIL][0] + except IndexError: + return None + + with self.reader.open_part(thumbnail_part) as p: + return p.read() + + def read_into(self, object_store: model.AbstractObjectStore, + file_store: "AbstractSupplementaryFileContainer", + override_existing: bool = False) -> Set[model.Identifier]: + """ + Read the contents of the AASX package and add them into a given + :class:`ObjectStore ` + + This function does the main job of reading the AASX file's contents. It traverses the relationships within the + package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided + `object_store`. While doing so, it searches all parsed :class:`Submodels ` for + :class:`~aas.model.submodel.File` objects to extract the supplementary + files. The referenced supplementary files are added to the given `file_store` and the + :class:`~aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file + to allow for robust resolution the file within the + `file_store` later. + + :param object_store: An :class:`ObjectStore ` to add the AAS objects + from the AASX file to + :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the + embedded supplementary files to + :param override_existing: If `True`, existing objects in the object store are overridden with objects from the + AASX that have the same :class:`~aas.model.base.Identifier`. Default behavior is to skip those objects from + the AASX. + :return: A set of the :class:`Identifiers ` of all + :class:`~aas.model.base.Identifiable` objects parsed from the AASX file + """ + # Find AASX-Origin part + core_rels = self.reader.get_related_parts_by_type() + try: + aasx_origin_part = core_rels[RELATIONSHIP_TYPE_AASX_ORIGIN][0] + except IndexError as e: + raise ValueError("Not a valid AASX file: aasx-origin Relationship is missing.") from e + + read_identifiables: Set[model.Identifier] = set() + + # Iterate AAS files + for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ + RELATIONSHIP_TYPE_AAS_SPEC]: + self._read_aas_part_into(aas_part, object_store, file_store, read_identifiables, override_existing) + + # Iterate split parts of AAS file + for split_part in self.reader.get_related_parts_by_type(aas_part)[ + RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: + self._read_aas_part_into(split_part, object_store, file_store, read_identifiables, override_existing) + + return read_identifiables + + def close(self) -> None: + """ + Close the AASXReader and the underlying OPC / ZIP file readers. Must be called after reading the file. + """ + self.reader.close() + + def __enter__(self) -> "AASXReader": + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.close() + + def _read_aas_part_into(self, part_name: str, + object_store: model.AbstractObjectStore, + file_store: "AbstractSupplementaryFileContainer", + read_identifiables: Set[model.Identifier], + override_existing: bool) -> None: + """ + Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. + + This method primarily checks for duplicate objects. It uses `_parse_aas_parse()` to do the actual parsing and + `_collect_supplementary_files()` for supplementary file processing of non-duplicate objects. + + :param part_name: The OPC part name to read + :param object_store: An ObjectStore to add the AAS objects from the AASX file to + :param file_store: A SupplementaryFileContainer to add the embedded supplementary files to, which are reference + from a File object of this part + :param read_identifiables: A set of Identifiers of objects which have already been read. New objects' + Identifiers are added to this set. Objects with already known Identifiers are skipped silently. + :param override_existing: If True, existing objects in the object store are overridden with objects from the + AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. + """ + for obj in self._parse_aas_part(part_name): + if obj.identification in read_identifiables: + continue + if obj.identification in object_store: + if override_existing: + logger.info("Overriding existing object in ObjectStore with {} ...".format(obj)) + object_store.discard(obj) + else: + logger.warning("Skipping {}, since an object with the same id is already contained in the " + "ObjectStore".format(obj)) + continue + object_store.add(obj) + read_identifiables.add(obj.identification) + if isinstance(obj, model.Submodel): + self._collect_supplementary_files(part_name, obj, file_store) + + def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: + """ + Helper function to parse the AAS objects from a single JSON or XML part of the AASX package. + + This method chooses and calls the correct parser. + + :param part_name: The OPC part name of the part to be parsed + :return: A DictObjectStore containing the parsed AAS objects + """ + content_type = self.reader.get_content_type(part_name) + extension = part_name.split("/")[-1].split(".")[-1] + if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": + logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) + with self.reader.open_part(part_name) as p: + return read_aas_xml_file(p) + elif content_type.split(";")[0] in ("text/json", "application/json") \ + or content_type == "" and extension == "json": + logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) + with self.reader.open_part(part_name) as p: + return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig')) + else: + logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}" + .format(part_name, content_type, extension)) + return model.DictObjectStore() + + def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel, + file_store: "AbstractSupplementaryFileContainer") -> None: + """ + Helper function to search File objects within a single parsed Submodel, extract the referenced supplementary + files and update the File object's values with the absolute path. + + :param part_name: The OPC part name of the part the submodel has been parsed from. This is used to resolve + relative file paths. + :param submodel: The Submodel to process + :param file_store: The SupplementaryFileContainer to add the extracted supplementary files to + """ + for element in traversal.walk_submodel(submodel): + if isinstance(element, model.File): + if element.value is None: + continue + # Only absolute-path references and relative-path URI references (see RFC 3986, sec. 4.2) are considered + # to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute + # URIs and network-path references) + if element.value.startswith('//') or ':' in element.value.split('/')[0]: + logger.info("Skipping supplementary file %s, since it seems to be an absolute URI or network-path " + "URI reference", element.value) + continue + absolute_name = pyecma376_2.package_model.part_realpath(element.value, part_name) + logger.debug("Reading supplementary file {} from AASX package ...".format(absolute_name)) + with self.reader.open_part(absolute_name) as p: + final_name = file_store.add_file(absolute_name, p, self.reader.get_content_type(absolute_name)) + element.value = final_name + + +class AASXWriter: + """ + An AASXWriter wraps a new AASX package file to write its contents to it piece by piece. + + Basic usage: + + .. code-block:: python + + # object_store and file_store are expected to be given (e.g. some storage backend or previously created data) + cp = OPCCoreProperties() + cp.creator = "ACPLT" + cp.created = datetime.datetime.now() + + with AASXWriter("filename.aasx") as writer: + writer.write_aas(Identifier("https://acplt.org/AssetAdministrationShell", IdentifierType.IRI), + object_store, + file_store) + writer.write_aas(Identifier("https://acplt.org/AssetAdministrationShell2", IdentifierType.IRI), + object_store, + file_store) + writer.write_core_properties(cp) + + **Attention:** The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context + manager functionality (as shown above). Otherwise the resulting AASX file will lack important data structures + and will not be readable. + """ + AASX_ORIGIN_PART_NAME = "/aasx/aasx-origin" + + def __init__(self, file: Union[os.PathLike, str, IO]): + """ + Create a new AASX package in the given file and open the AASXWriter to add contents to the package. + + Make sure to call `AASXWriter.close()` after writing all contents to write the aas-spec relationships for all + AAS parts to the file and close the underlying ZIP file writer. You may also use the AASXWriter as a context + manager to ensure closing under any circumstances. + + :param file: filename, path, or binary file handle opened for writing + """ + # names of aas-spec parts, used by `_write_aasx_origin_relationships()` + self._aas_part_names: List[str] = [] + # name of the thumbnail part (if any) + self._thumbnail_part: Optional[str] = None + # name of the core properties part (if any) + self._properties_part: Optional[str] = None + # names and hashes of all supplementary file parts that have already been written + self._supplementary_part_names: Dict[str, Optional[bytes]] = {} + self._aas_name_friendlyfier = NameFriendlyfier() + + # Open OPC package writer + self.writer = pyecma376_2.ZipPackageWriter(file) + + # Create AASX origin part + logger.debug("Creating AASX origin part in AASX package ...") + p = self.writer.open_part(self.AASX_ORIGIN_PART_NAME, "text/plain") + p.close() + + def write_aas(self, + aas_id: model.Identifier, + object_store: model.AbstractObjectStore, + file_store: "AbstractSupplementaryFileContainer", + write_json: bool = False, + submodel_split_parts: bool = True) -> None: + """ + Convenience method to add an :class:`~aas.model.aas.AssetAdministrationShell` with all included and referenced + objects to the AASX package according to the part name conventions from DotAAS. + + This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given + object_store. :class:`References <~aas.model.base.Reference>` to + the :class:`~aas.model.aas.Asset`, :class:`ConceptDescriptions ` and + :class:`Submodels ` are also resolved using the object_store. All of these objects + are written to aas-spec parts in the AASX package, following the conventions presented in "Details of the Asset + Administration Shell". For each Submodel, a aas-spec-split part is used. Supplementary files which are + referenced by a File object in any of the Submodels, are also added to the AASX package. + + Internally, this method uses :meth:`aas.adapter.aasx.AASXWriter.write_aas_objects` to write the individual AASX + parts for the AAS and each submodel. + + :param aas_id: :class:`~aas.model.base.Identifier` of the :class:`~aas.model.aas.AssetAdministrationShell` to + be added to the AASX file + :param object_store: :class:`ObjectStore ` to retrieve the + :class:`~aas.model.base.Identifiable` AAS objects (:class:`~aas.model.aas.AssetAdministrationShell`, + :class:`~aas.model.aas.Asset`, :class:`~aas.model.concept.ConceptDescription` and + :class:`~aas.model.submodel.Submodel`) from + :param file_store: :class:`SupplementaryFileContainer ` to + retrieve supplementary files from, which are referenced by :class:`~aas.model.submodel.File` objects + :param write_json: If `True`, JSON parts are created for the AAS and each submodel in the AASX package file + instead of XML parts. Defaults to `False`. + :param submodel_split_parts: If `True` (default), submodels are written to separate AASX parts instead of being + included in the AAS part with in the AASX package. + """ + aas_friendly_name = self._aas_name_friendlyfier.get_friendly_name(aas_id) + aas_part_name = "/aasx/{0}/{0}.aas.{1}".format(aas_friendly_name, "json" if write_json else "xml") + + aas = object_store.get_identifiable(aas_id) + if not isinstance(aas, model.AssetAdministrationShell): + raise ValueError(f"Identifier does not belong to an AssetAdminstrationShell object but to {aas!r}") + + objects_to_be_written: Set[model.Identifier] = {aas.identification} + + # Add the Asset object to the objects in the AAS part + objects_to_be_written.add(aas.asset.get_identifier()) + + # Add referenced ConceptDescriptions to the AAS part + for dictionary in aas.concept_dictionary: + for concept_rescription_ref in dictionary.concept_description: + objects_to_be_written.add(concept_rescription_ref.get_identifier()) + + # Write submodels: Either create a split part for each of them or otherwise add them to objects_to_be_written + aas_split_part_names: List[str] = [] + if submodel_split_parts: + # Create a AAS split part for each (available) submodel of the AAS + aas_friendlyfier = NameFriendlyfier() + for submodel_ref in aas.submodel: + submodel_identification = submodel_ref.get_identifier() + submodel_friendly_name = aas_friendlyfier.get_friendly_name(submodel_identification) + submodel_part_name = "/aasx/{0}/{1}/{1}.submodel.{2}".format(aas_friendly_name, submodel_friendly_name, + "json" if write_json else "xml") + self.write_aas_objects(submodel_part_name, [submodel_identification], object_store, file_store, + write_json, split_part=True) + aas_split_part_names.append(submodel_part_name) + else: + for submodel_ref in aas.submodel: + objects_to_be_written.add(submodel_ref.get_identifier()) + + # Write AAS part + logger.debug("Writing AAS {} to part {} in AASX package ...".format(aas.identification, aas_part_name)) + self.write_aas_objects(aas_part_name, objects_to_be_written, object_store, file_store, write_json, + split_part=False, + additional_relationships=(pyecma376_2.OPCRelationship("r{}".format(i), + RELATIONSHIP_TYPE_AAS_SPEC_SPLIT, + submodel_part_name, + pyecma376_2.OPCTargetMode.INTERNAL) + for i, submodel_part_name in enumerate(aas_split_part_names))) + + def write_aas_objects(self, + part_name: str, + object_ids: Iterable[model.Identifier], + object_store: model.AbstractObjectStore, + file_store: "AbstractSupplementaryFileContainer", + write_json: bool = False, + split_part: bool = False, + additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: + """ + Write a defined list of AAS objects to an XML or JSON part in the AASX package and append the referenced + supplementary files to the package. + + This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given + object_store. If the list + of written objects includes :class:`aas.model.submodel.Submodel` objects, Supplementary files which are + referenced by :class:`~aas.model.submodel.File` objects within + those submodels, are also added to the AASX package. + + You must make sure to call this method only once per unique `part_name` on a single package instance. + + :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 + part name and unique within the package. The extension of the part should match the data format (i.e. + '.json' if `write_json` else '.xml'). + :param object_ids: A list of :class:`Identifiers ` of the objects to be written to + the AASX package. Only these :class:`~aas.model.base.Identifiable` objects (and included + :class:`~aas.model.base.Referable` objects) are written to the package. + :param object_store: The objects store to retrieve the :class:`~aas.model.base.Identifiable` objects from + :param file_store: The :class:`SupplementaryFileContainer ` + to retrieve supplementary files from (if there are any :class:`~aas.model.submodel.File` + objects within the written objects. + :param write_json: If `True`, the part is written as a JSON file instead of an XML file. Defaults to `False`. + :param split_part: If `True`, no aas-spec relationship is added from the aasx-origin to this part. You must make + sure to reference it via a aas-spec-split relationship from another aas-spec part + :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object + part to be written, in addition to the aas-suppl relationships which are created automatically. + """ + logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) + + objects: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + supplementary_files: List[str] = [] + + # Retrieve objects and scan for referenced supplementary files + for identifier in object_ids: + try: + the_object = object_store.get_identifiable(identifier) + except KeyError: + logger.error("Could not find object {} in ObjectStore".format(identifier)) + continue + objects.add(the_object) + if isinstance(the_object, model.Submodel): + for element in traversal.walk_submodel(the_object): + if isinstance(element, model.File): + file_name = element.value + # Skip File objects with empty value URI references that are considered to be no local file + # (absolute URIs or network-path URI references) + if file_name is None or file_name.startswith('//') or ':' in file_name.split('/')[0]: + continue + supplementary_files.append(file_name) + + # Add aas-spec relationship + if not split_part: + self._aas_part_names.append(part_name) + + # Write part + with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p: + if write_json: + write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects) + else: + write_aas_xml_file(p, objects) + + # Write submodel's supplementary files to AASX file + supplementary_file_names = [] + for file_name in supplementary_files: + try: + content_type = file_store.get_content_type(file_name) + hash = file_store.get_sha256(file_name) + except KeyError: + logger.warning("Could not find file {} in file store.".format(file_name)) + continue + # Check if this supplementary file has already been written to the AASX package or has a name conflict + if self._supplementary_part_names.get(file_name) == hash: + continue + elif file_name in self._supplementary_part_names: + logger.error("Trying to write supplementary file {} to AASX twice with different contents" + .format(file_name)) + logger.debug("Writing supplementary file {} to AASX package ...".format(file_name)) + with self.writer.open_part(file_name, content_type) as p: + file_store.write_file(file_name, p) + supplementary_file_names.append(pyecma376_2.package_model.normalize_part_name(file_name)) + self._supplementary_part_names[file_name] = hash + + # Add relationships from submodel to supplementary parts + logger.debug("Writing aas-suppl relationships for AAS object part {} to AASX package ...".format(part_name)) + self.writer.write_relationships( + itertools.chain( + (pyecma376_2.OPCRelationship("r{}".format(i), + RELATIONSHIP_TYPE_AAS_SUPL, + submodel_file_name, + pyecma376_2.OPCTargetMode.INTERNAL) + for i, submodel_file_name in enumerate(supplementary_file_names)), + additional_relationships), + part_name) + + def write_core_properties(self, core_properties: pyecma376_2.OPCCoreProperties): + """ + Write OPC Core Properties (meta data) to the AASX package file. + + .. Attention:: + This method may only be called once for each AASXWriter! + + :param core_properties: The OPCCoreProperties object with the meta data to be written to the package file + """ + if self._properties_part is not None: + raise RuntimeError("Core Properties have already been written.") + logger.debug("Writing core properties to AASX package ...") + with self.writer.open_part(pyecma376_2.DEFAULT_CORE_PROPERTIES_NAME, "application/xml") as p: + core_properties.write_xml(p) + self._properties_part = pyecma376_2.DEFAULT_CORE_PROPERTIES_NAME + + def write_thumbnail(self, name: str, data: bytearray, content_type: str): + """ + Write an image file as thumbnail image to the AASX package. + + .. Attention:: + This method may only be called once for each AASXWriter! + + :param name: The OPC part name of the thumbnail part. Should not contain '/' or URI-encoded '/' or '\'. + :param data: The image file's binary contents to be written + :param content_type: OPC content type (MIME type) of the image file + """ + if self._thumbnail_part is not None: + raise RuntimeError("package thumbnail has already been written to {}.".format(self._thumbnail_part)) + with self.writer.open_part(name, content_type) as p: + p.write(data) + self._thumbnail_part = name + + def close(self): + """ + Write relationships for all data files to package and close underlying OPC package and ZIP file. + """ + self._write_aasx_origin_relationships() + self._write_package_relationships() + self.writer.close() + + def __enter__(self) -> "AASXWriter": + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.close() + + def _write_aasx_origin_relationships(self): + """ + Helper function to write aas-spec relationships of the aasx-origin part. + + This method uses the list of aas-spec parts in `_aas_part_names`. It should be called just before closing the + file to make sure all aas-spec parts of the package have already been written. + """ + # Add relationships from AASX-origin part to AAS parts + logger.debug("Writing aas-spec relationships to AASX package ...") + self.writer.write_relationships( + (pyecma376_2.OPCRelationship("r{}".format(i), RELATIONSHIP_TYPE_AAS_SPEC, + aas_part_name, + pyecma376_2.OPCTargetMode.INTERNAL) + for i, aas_part_name in enumerate(self._aas_part_names)), + self.AASX_ORIGIN_PART_NAME) + + def _write_package_relationships(self): + """ + Helper function to write package (root) relationships to the OPC package. + + This method must be called just before closing the package file to make sure we write exactly the correct + relationships: + * aasx-origin (always) + * core-properties (if core properties have been added) + * thumbnail (if thumbnail part has been added) + """ + logger.debug("Writing package relationships to AASX package ...") + package_relationships: List[pyecma376_2.OPCRelationship] = [ + pyecma376_2.OPCRelationship("r1", RELATIONSHIP_TYPE_AASX_ORIGIN, + self.AASX_ORIGIN_PART_NAME, + pyecma376_2.OPCTargetMode.INTERNAL), + ] + if self._properties_part is not None: + package_relationships.append(pyecma376_2.OPCRelationship( + "r2", pyecma376_2.RELATIONSHIP_TYPE_CORE_PROPERTIES, self._properties_part, + pyecma376_2.OPCTargetMode.INTERNAL)) + if self._thumbnail_part is not None: + package_relationships.append(pyecma376_2.OPCRelationship( + "r3", pyecma376_2.RELATIONSHIP_TYPE_THUMBNAIL, self._thumbnail_part, + pyecma376_2.OPCTargetMode.INTERNAL)) + self.writer.write_relationships(package_relationships) + + +class NameFriendlyfier: + """ + A simple helper class to create unique "AAS friendly names" according to DotAAS, section 7.6. + + Objects of this class store the already created friendly names to avoid name collisions within one set of names. + """ + RE_NON_ALPHANUMERICAL = re.compile(r"[^a-zA-Z0-9]") + + def __init__(self) -> None: + self.issued_names: Set[str] = set() + + def get_friendly_name(self, identifier: model.Identifier): + """ + Generate a friendly name from an AAS identifier. + + According to section 7.6 of "Details of the Asset Administration Shell", all non-alphanumerical characters are + replaced with underscores. We also replace all non-ASCII characters to generate valid URIs as the result. + If this replacement results in a collision with a previously generated friendly name of this NameFriendlifier, + a number is appended with underscore to the friendly name. + + Example: + + .. code-block:: python + + friendlyfier = NameFriendlyfier() + friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS-a", model.IdentifierType.IRI)) + > "http___example_com_AAS_a" + + friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS+a", model.IdentifierType.IRI)) + > "http___example_com_AAS_a_1" + + """ + # friendlify name + raw_name = self.RE_NON_ALPHANUMERICAL.sub('_', identifier.id) + + # Unify name (avoid collisions) + amended_name = raw_name + i = 1 + while amended_name in self.issued_names: + amended_name = "{}_{}".format(raw_name, i) + i += 1 + + self.issued_names.add(amended_name) + return amended_name + + +class AbstractSupplementaryFileContainer(metaclass=abc.ABCMeta): + """ + Abstract interface for containers of supplementary files for AASs. + + Supplementary files may be PDF files or other binary or textual files, referenced in a File object of an AAS by + their name. They are used to provide associated documents without embedding their contents (as + :class:`~aas.model.submodel.Blob` object) in the AAS. + + A SupplementaryFileContainer keeps track of the name and content_type (MIME type) for each file. Additionally it + allows to resolve name conflicts by comparing the files' contents and providing an alternative name for a dissimilar + new file. It also provides each files sha256 hash sum to allow name conflict checking in other classes (e.g. when + writing AASX files). + """ + @abc.abstractmethod + def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: + """ + Add a new file to the SupplementaryFileContainer and resolve name conflicts. + + The file contents must be provided as a binary file-like object to be read by the SupplementaryFileContainer. + If the container already contains an equally named file, the content_type and file contents are compared (using + a hash sum). In case of dissimilar files, a new unique name for the new file is computed and returned. It should + be used to update in the File object of the AAS. + + :param name: The file's proposed name. Should start with a '/'. Should not contain URI-encoded '/' or '\' + :param file: A binary file-like opened for reading the file contents + :param content_type: The file's content_type + :return: The file name as stored in the SupplementaryFileContainer. Typically `name` or a modified version of + `name` to resolve conflicts. + """ + pass # pragma: no cover + + @abc.abstractmethod + def get_content_type(self, name: str) -> str: + """ + Get a stored file's content_type. + + :param name: file name of questioned file + :return: The file's content_type + :raises KeyError: If no file with this name is stored + """ + pass # pragma: no cover + + @abc.abstractmethod + def get_sha256(self, name: str) -> bytes: + """ + Get a stored file content's sha256 hash sum. + + This may be used by other classes (e.g. the AASXWriter) to check for name conflicts. + + :param name: file name of questioned file + :return: The file content's sha256 hash sum + :raises KeyError: If no file with this name is stored + """ + pass # pragma: no cover + + @abc.abstractmethod + def write_file(self, name: str, file: IO[bytes]) -> None: + """ + Retrieve a stored file's contents by writing them into a binary writable file-like object. + + :param name: file name of questioned file + :param file: A binary file-like object with write() method to write the file contents into + :raises KeyError: If no file with this name is stored + """ + pass # pragma: no cover + + @abc.abstractmethod + def __contains__(self, item: str) -> bool: + """ + Check if a file with the given name is stored in this SupplementaryFileContainer. + """ + pass # pragma: no cover + + @abc.abstractmethod + def __iter__(self) -> Iterator[str]: + """ + Return an iterator over all file names stored in this SupplementaryFileContainer. + """ + pass # pragma: no cover + + +class DictSupplementaryFileContainer(AbstractSupplementaryFileContainer): + """ + SupplementaryFileContainer implementation using a dict to store the file contents in-memory. + """ + def __init__(self): + # Stores the files' contents, identified by their sha256 hash + self._store: Dict[bytes, bytes] = {} + # Maps file names to (sha256, content_type) + self._name_map: Dict[str, Tuple[bytes, str]] = {} + + def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: + data = file.read() + hash = hashlib.sha256(data).digest() + if hash not in self._store: + self._store[hash] = data + name_map_data = (hash, content_type) + new_name = name + i = 1 + while True: + if new_name not in self._name_map: + self._name_map[new_name] = name_map_data + return new_name + elif self._name_map[new_name] == name_map_data: + return new_name + new_name = self._append_counter(name, i) + i += 1 + + @staticmethod + def _append_counter(name: str, i: int) -> str: + split1 = name.split('/') + split2 = split1[-1].split('.') + index = -2 if len(split2) > 1 else -1 + new_basename = "{}_{:04d}".format(split2[index], i) + split2[index] = new_basename + split1[-1] = ".".join(split2) + return "/".join(split1) + + def get_content_type(self, name: str) -> str: + return self._name_map[name][1] + + def get_sha256(self, name: str) -> bytes: + return self._name_map[name][0] + + def write_file(self, name: str, file: IO[bytes]) -> None: + file.write(self._store[self._name_map[name][0]]) + + def __contains__(self, item: object) -> bool: + return item in self._name_map + + def __iter__(self) -> Iterator[str]: + return iter(self._name_map) diff --git a/basyx/aas/adapter/json/__init__.py b/basyx/aas/adapter/json/__init__.py new file mode 100644 index 0000000..d469468 --- /dev/null +++ b/basyx/aas/adapter/json/__init__.py @@ -0,0 +1,24 @@ +""" +.. _adapter.json.__init__: + +This package contains functionality for serialization and deserialization of BaSyx Python SDK objects into/from JSON. + +:ref:`json_serialization `: The module offers a function to write an ObjectStore to a +given file and therefore defines the custom JSONEncoder :class:`~.aas.adapter.json.json_serialization.AASToJsonEncoder` +which handles encoding of all BaSyx Python SDK objects and their attributes by converting them into standard python +objects. + +:ref:`json_deserialization `: The module implements custom JSONDecoder classes +:class:`~aas.adapter.json.json_deserialization.AASFromJsonDecoder` and +:class:`~aas.adapter.json.json_deserialization.StrictAASFromJsonDecoder`, that — when used with Python's `json` +module — detect AAS objects in the parsed JSON and convert them into the corresponding BaSyx Python SDK object. +A function :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file` is provided to read all AAS objects +within a JSON file and return them as BaSyx Python SDK objectstore. +""" +import os.path + +from .json_serialization import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, object_store_to_json +from .json_deserialization import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrippedAASFromJsonDecoder, \ + StrictStrippedAASFromJsonDecoder, read_aas_json_file, read_aas_json_file_into + +JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json new file mode 100644 index 0000000..1c7020a --- /dev/null +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -0,0 +1,1439 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "AssetAdministrationShellEnvironment", + "$id": "http://www.admin-shell.io/schema/json/v2.0.1", + "type": "object", + "required": [ + "assetAdministrationShells", + "submodels", + "assets", + "conceptDescriptions" + ], + "properties": { + "assetAdministrationShells": { + "type": "array", + "items": { + "$ref": "#/definitions/AssetAdministrationShell" + } + }, + "submodels": { + "type": "array", + "items": { + "$ref": "#/definitions/Submodel" + } + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "conceptDescriptions": { + "type": "array", + "items": { + "$ref": "#/definitions/ConceptDescription" + } + } + }, + "definitions": { + "Referable": { + "type": "object", + "properties": { + "idShort": { + "type": "string" + }, + "category": { + "type": "string" + }, + "description": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "parent": { + "$ref": "#/definitions/Reference" + }, + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": [ + "idShort", + "modelType" + ] + }, + "Identifiable": { + "allOf": [ + { + "$ref": "#/definitions/Referable" + }, + { + "properties": { + "identification": { + "$ref": "#/definitions/Identifier" + }, + "administration": { + "$ref": "#/definitions/AdministrativeInformation" + } + }, + "required": [ + "identification" + ] + } + ] + }, + "Qualifiable": { + "type": "object", + "properties": { + "qualifiers": { + "type": "array", + "items": { + "$ref": "#/definitions/Constraint" + } + } + } + }, + "HasSemantics": { + "type": "object", + "properties": { + "semanticId": { + "$ref": "#/definitions/Reference" + } + } + }, + "HasDataSpecification": { + "type": "object", + "properties": { + "embeddedDataSpecifications": { + "type": "array", + "items": { + "$ref": "#/definitions/EmbeddedDataSpecification" + } + } + } + }, + "AssetAdministrationShell": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "derivedFrom": { + "$ref": "#/definitions/Reference" + }, + "asset": { + "$ref": "#/definitions/Reference" + }, + "submodels": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + }, + "views": { + "type": "array", + "items": { + "$ref": "#/definitions/View" + } + }, + "conceptDictionaries": { + "type": "array", + "items": { + "$ref": "#/definitions/ConceptDictionary" + } + }, + "security": { + "$ref": "#/definitions/Security" + } + }, + "required": [ + "asset" + ] + } + ] + }, + "Identifier": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "idType": { + "$ref": "#/definitions/KeyType" + } + }, + "required": [ + "id", + "idType" + ] + }, + "KeyType": { + "type": "string", + "enum": [ + "Custom", + "IRDI", + "IRI", + "IdShort", + "FragmentId" + ] + }, + "AdministrativeInformation": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "revision": { + "type": "string" + } + } + }, + "LangString": { + "type": "object", + "properties": { + "language": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "language", + "text" + ] + }, + "Reference": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/Key" + } + } + }, + "required": [ + "keys" + ] + }, + "Key": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/KeyElements" + }, + "idType": { + "$ref": "#/definitions/KeyType" + }, + "value": { + "type": "string" + }, + "local": { + "type": "boolean" + } + }, + "required": [ + "type", + "idType", + "value", + "local" + ] + }, + "KeyElements": { + "type": "string", + "enum": [ + "Asset", + "AssetAdministrationShell", + "ConceptDescription", + "Submodel", + "AccessPermissionRule", + "AnnotatedRelationshipElement", + "BasicEvent", + "Blob", + "Capability", + "ConceptDictionary", + "DataElement", + "File", + "Entity", + "Event", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "SubmodelElement", + "SubmodelElementCollection", + "View", + "GlobalReference", + "FragmentReference" + ] + }, + "ModelTypes": { + "type": "string", + "enum": [ + "Asset", + "AssetAdministrationShell", + "ConceptDescription", + "Submodel", + "AccessPermissionRule", + "AnnotatedRelationshipElement", + "BasicEvent", + "Blob", + "Capability", + "ConceptDictionary", + "DataElement", + "File", + "Entity", + "Event", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "SubmodelElement", + "SubmodelElementCollection", + "View", + "GlobalReference", + "FragmentReference", + "Constraint", + "Formula", + "Qualifier" + ] + }, + "ModelType": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/ModelTypes" + } + }, + "required": [ + "name" + ] + }, + "EmbeddedDataSpecification": { + "type": "object", + "properties": { + "dataSpecification": { + "$ref": "#/definitions/Reference" + }, + "dataSpecificationContent": { + "$ref": "#/definitions/DataSpecificationContent" + } + }, + "required": [ + "dataSpecification", + "dataSpecificationContent" + ] + }, + "DataSpecificationContent": { + "oneOf": [ + { + "$ref": "#/definitions/DataSpecificationIEC61360Content" + }, + { + "$ref": "#/definitions/DataSpecificationPhysicalUnitContent" + } + ] + }, + "DataSpecificationPhysicalUnitContent": { + "type": "object", + "properties": { + "unitName": { + "type": "string" + }, + "unitSymbol": { + "type": "string" + }, + "definition": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "siNotation": { + "type": "string" + }, + "siName": { + "type": "string" + }, + "dinNotation": { + "type": "string" + }, + "eceName": { + "type": "string" + }, + "eceCode": { + "type": "string" + }, + "nistName": { + "type": "string" + }, + "sourceOfDefinition": { + "type": "string" + }, + "conversionFactor": { + "type": "string" + }, + "registrationAuthorityId": { + "type": "string" + }, + "supplier": { + "type": "string" + } + }, + "required": [ + "unitName", + "unitSymbol", + "definition" + ] + }, + "DataSpecificationIEC61360Content": { + "allOf": [ + { + "$ref": "#/definitions/ValueObject" + }, + { + "type": "object", + "properties": { + "dataType": { + "enum": [ + "DATE", + "STRING", + "STRING_TRANSLATABLE", + "REAL_MEASURE", + "REAL_COUNT", + "REAL_CURRENCY", + "BOOLEAN", + "URL", + "RATIONAL", + "RATIONAL_MEASURE", + "TIME", + "TIMESTAMP", + "INTEGER_COUNT", + "INTEGER_MEASURE", + "INTEGER_CURRENCY" + ] + }, + "definition": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "preferredName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "shortName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "sourceOfDefinition": { + "type": "string" + }, + "symbol": { + "type": "string" + }, + "unit": { + "type": "string" + }, + "unitId": { + "$ref": "#/definitions/Reference" + }, + "valueFormat": { + "type": "string" + }, + "valueList": { + "$ref": "#/definitions/ValueList" + }, + "levelType": { + "type": "array", + "items": { + "$ref": "#/definitions/LevelType" + } + } + }, + "required": [ + "preferredName" + ] + } + ] + }, + "LevelType": { + "type": "string", + "enum": [ + "Min", + "Max", + "Nom", + "Typ" + ] + }, + "ValueList": { + "type": "object", + "properties": { + "valueReferencePairTypes": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/ValueReferencePairType" + } + } + }, + "required": [ + "valueReferencePairTypes" + ] + }, + "ValueReferencePairType": { + "allOf": [ + { + "$ref": "#/definitions/ValueObject" + } + ] + }, + "ValueObject": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "valueId": { + "$ref": "#/definitions/Reference" + }, + "valueType": { + "type": "string", + "enum": [ + "anyUri", + "base64Binary", + "boolean", + "date", + "dateTime", + "dateTimeStamp", + "decimal", + "integer", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "positiveInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "nonPositiveInteger", + "negativeInteger", + "double", + "duration", + "dayTimeDuration", + "yearMonthDuration", + "float", + "gDay", + "gMonth", + "gMonthDay", + "gYear", + "gYearMonth", + "hexBinary", + "NOTATION", + "QName", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ENTITY", + "ID", + "IDREF", + "NMTOKEN", + "time" + ] + } + } + }, + "Asset": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "kind": { + "$ref": "#/definitions/AssetKind" + }, + "assetIdentificationModel": { + "$ref": "#/definitions/Reference" + }, + "billOfMaterial": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "kind" + ] + } + ] + }, + "AssetKind": { + "type": "string", + "enum": [ + "Type", + "Instance" + ] + }, + "ModelingKind": { + "type": "string", + "enum": [ + "Template", + "Instance" + ] + }, + "Submodel": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "$ref": "#/definitions/Qualifiable" + }, + { + "$ref": "#/definitions/HasSemantics" + }, + { + "properties": { + "kind": { + "$ref": "#/definitions/ModelingKind" + }, + "submodelElements": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement" + } + } + } + } + ] + }, + "Constraint": { + "type": "object", + "properties": { + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": [ + "modelType" + ] + }, + "Operation": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "inputVariable": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + } + }, + "outputVariable": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + } + }, + "inoutputVariable": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + } + } + } + } + ] + }, + "OperationVariable": { + "type": "object", + "properties": { + "value": { + "oneOf": [ + { + "$ref": "#/definitions/Blob" + }, + { + "$ref": "#/definitions/File" + }, + { + "$ref": "#/definitions/Capability" + }, + { + "$ref": "#/definitions/Entity" + }, + { + "$ref": "#/definitions/Event" + }, + { + "$ref": "#/definitions/BasicEvent" + }, + { + "$ref": "#/definitions/MultiLanguageProperty" + }, + { + "$ref": "#/definitions/Operation" + }, + { + "$ref": "#/definitions/Property" + }, + { + "$ref": "#/definitions/Range" + }, + { + "$ref": "#/definitions/ReferenceElement" + }, + { + "$ref": "#/definitions/RelationshipElement" + }, + { + "$ref": "#/definitions/SubmodelElementCollection" + } + ] + } + }, + "required": [ + "value" + ] + }, + "SubmodelElement": { + "allOf": [ + { + "$ref": "#/definitions/Referable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "$ref": "#/definitions/HasSemantics" + }, + { + "$ref": "#/definitions/Qualifiable" + }, + { + "properties": { + "kind": { + "$ref": "#/definitions/ModelingKind" + } + } + } + ] + }, + "Event": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + } + ] + }, + "BasicEvent": { + "allOf": [ + { + "$ref": "#/definitions/Event" + }, + { + "properties": { + "observed": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "observed" + ] + } + ] + }, + "EntityType": { + "type": "string", + "enum": [ + "CoManagedEntity", + "SelfManagedEntity" + ] + }, + "Entity": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "statements": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement" + } + }, + "entityType": { + "$ref": "#/definitions/EntityType" + }, + "asset": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "entityType" + ] + } + ] + }, + "View": { + "allOf": [ + { + "$ref": "#/definitions/Referable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "$ref": "#/definitions/HasSemantics" + }, + { + "properties": { + "containedElements": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + } + } + } + ] + }, + "ConceptDictionary": { + "allOf": [ + { + "$ref": "#/definitions/Referable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "conceptDescriptions": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + } + } + } + ] + }, + "ConceptDescription": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "isCaseOf": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + } + } + } + ] + }, + "Capability": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + } + ] + }, + "Property": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "$ref": "#/definitions/ValueObject" + } + ] + }, + "Range": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "valueType": { + "type": "string", + "enum": [ + "anyUri", + "base64Binary", + "boolean", + "date", + "dateTime", + "dateTimeStamp", + "decimal", + "integer", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "positiveInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "nonPositiveInteger", + "negativeInteger", + "double", + "duration", + "dayTimeDuration", + "yearMonthDuration", + "float", + "gDay", + "gMonth", + "gMonthDay", + "gYear", + "gYearMonth", + "hexBinary", + "NOTATION", + "QName", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ENTITY", + "ID", + "IDREF", + "NMTOKEN", + "time" + ] + }, + "min": { + "type": "string" + }, + "max": { + "type": "string" + } + }, + "required": [ + "valueType" + ] + } + ] + }, + "MultiLanguageProperty": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "valueId": { + "$ref": "#/definitions/Reference" + } + } + } + ] + }, + "File": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "value": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "mimeType" + ] + } + ] + }, + "Blob": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "value": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "mimeType" + ] + } + ] + }, + "ReferenceElement": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "value": { + "$ref": "#/definitions/Reference" + } + } + } + ] + }, + "SubmodelElementCollection": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "value": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Blob" + }, + { + "$ref": "#/definitions/File" + }, + { + "$ref": "#/definitions/Capability" + }, + { + "$ref": "#/definitions/Entity" + }, + { + "$ref": "#/definitions/Event" + }, + { + "$ref": "#/definitions/BasicEvent" + }, + { + "$ref": "#/definitions/MultiLanguageProperty" + }, + { + "$ref": "#/definitions/Operation" + }, + { + "$ref": "#/definitions/Property" + }, + { + "$ref": "#/definitions/Range" + }, + { + "$ref": "#/definitions/ReferenceElement" + }, + { + "$ref": "#/definitions/RelationshipElement" + }, + { + "$ref": "#/definitions/SubmodelElementCollection" + } + ] + } + }, + "allowDuplicates": { + "type": "boolean" + }, + "ordered": { + "type": "boolean" + } + } + } + ] + }, + "RelationshipElement": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "first": { + "$ref": "#/definitions/Reference" + }, + "second": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "first", + "second" + ] + } + ] + }, + "AnnotatedRelationshipElement": { + "allOf": [ + { + "$ref": "#/definitions/RelationshipElement" + }, + { + "properties": { + "annotation": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Blob" + }, + { + "$ref": "#/definitions/File" + }, + { + "$ref": "#/definitions/MultiLanguageProperty" + }, + { + "$ref": "#/definitions/Property" + }, + { + "$ref": "#/definitions/Range" + }, + { + "$ref": "#/definitions/ReferenceElement" + } + ] + } + } + } + } + ] + }, + "Qualifier": { + "allOf": [ + { + "$ref": "#/definitions/Constraint" + }, + { + "$ref": "#/definitions/HasSemantics" + }, + { + "$ref": "#/definitions/ValueObject" + }, + { + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ] + } + ] + }, + "Formula": { + "allOf": [ + { + "$ref": "#/definitions/Constraint" + }, + { + "properties": { + "dependsOn": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + } + } + } + ] + }, + "Security": { + "type": "object", + "properties": { + "accessControlPolicyPoints": { + "$ref": "#/definitions/AccessControlPolicyPoints" + }, + "certificate": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/BlobCertificate" + } + ] + } + }, + "requiredCertificateExtension": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + } + }, + "required": [ + "accessControlPolicyPoints" + ] + }, + "Certificate": { + "type": "object" + }, + "BlobCertificate": { + "allOf": [ + { + "$ref": "#/definitions/Certificate" + }, + { + "properties": { + "blobCertificate": { + "$ref": "#/definitions/Blob" + }, + "containedExtension": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + }, + "lastCertificate": { + "type": "boolean" + } + } + } + ] + }, + "AccessControlPolicyPoints": { + "type": "object", + "properties": { + "policyAdministrationPoint": { + "$ref": "#/definitions/PolicyAdministrationPoint" + }, + "policyDecisionPoint": { + "$ref": "#/definitions/PolicyDecisionPoint" + }, + "policyEnforcementPoint": { + "$ref": "#/definitions/PolicyEnforcementPoint" + }, + "policyInformationPoints": { + "$ref": "#/definitions/PolicyInformationPoints" + } + }, + "required": [ + "policyAdministrationPoint", + "policyDecisionPoint", + "policyEnforcementPoint" + ] + }, + "PolicyAdministrationPoint": { + "type": "object", + "properties": { + "localAccessControl": { + "$ref": "#/definitions/AccessControl" + }, + "externalAccessControl": { + "type": "boolean" + } + }, + "required": [ + "externalAccessControl" + ] + }, + "PolicyInformationPoints": { + "type": "object", + "properties": { + "internalInformationPoint": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + }, + "externalInformationPoint": { + "type": "boolean" + } + }, + "required": [ + "externalInformationPoint" + ] + }, + "PolicyEnforcementPoint": { + "type": "object", + "properties": { + "externalPolicyEnforcementPoint": { + "type": "boolean" + } + }, + "required": [ + "externalPolicyEnforcementPoint" + ] + }, + "PolicyDecisionPoint": { + "type": "object", + "properties": { + "externalPolicyDecisionPoints": { + "type": "boolean" + } + }, + "required": [ + "externalPolicyDecisionPoints" + ] + }, + "AccessControl": { + "type": "object", + "properties": { + "selectableSubjectAttributes": { + "$ref": "#/definitions/Reference" + }, + "defaultSubjectAttributes": { + "$ref": "#/definitions/Reference" + }, + "selectablePermissions": { + "$ref": "#/definitions/Reference" + }, + "defaultPermissions": { + "$ref": "#/definitions/Reference" + }, + "selectableEnvironmentAttributes": { + "$ref": "#/definitions/Reference" + }, + "defaultEnvironmentAttributes": { + "$ref": "#/definitions/Reference" + }, + "accessPermissionRule": { + "type": "array", + "items": { + "$ref": "#/definitions/AccessPermissionRule" + } + } + } + }, + "AccessPermissionRule": { + "allOf": [ + { + "$ref": "#/definitions/Referable" + }, + { + "$ref": "#/definitions/Qualifiable" + }, + { + "properties": { + "targetSubjectAttributes": { + "type": "array", + "items": { + "$ref": "#/definitions/SubjectAttributes" + }, + "minItems": 1 + }, + "permissionsPerObject": { + "type": "array", + "items": { + "$ref": "#/definitions/PermissionsPerObject" + } + } + }, + "required": [ + "targetSubjectAttributes" + ] + } + ] + }, + "SubjectAttributes": { + "type": "object", + "properties": { + "subjectAttributes": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + } + } + }, + "PermissionsPerObject": { + "type": "object", + "properties": { + "object": { + "$ref": "#/definitions/Reference" + }, + "targetObjectAttributes": { + "$ref": "#/definitions/ObjectAttributes" + }, + "permission": { + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + } + }, + "ObjectAttributes": { + "type": "object", + "properties": { + "objectAttribute": { + "type": "array", + "items": { + "$ref": "#/definitions/Property" + }, + "minItems": 1 + } + } + }, + "Permission": { + "type": "object", + "properties": { + "permission": { + "$ref": "#/definitions/Reference" + }, + "kindOfPermission": { + "type": "string", + "enum": [ + "Allow", + "Deny", + "NotApplicable", + "Undefined" + ] + } + }, + "required": [ + "permission", + "kindOfPermission" + ] + } + } +} \ No newline at end of file diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py new file mode 100644 index 0000000..250b681 --- /dev/null +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -0,0 +1,830 @@ +# Copyright (c) 2020 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +""" +.. _adapter.json.json_deserialization: + +Module for deserializing Asset Administration Shell data from the official JSON format + +The module provides custom JSONDecoder classes :class:`~.AASFromJsonDecoder` and :class:`~.StrictAASFromJsonDecoder` to +be used with the Python standard `json` module. + +Furthermore it provides two classes :class:`~aas.adapter.json.json_deserialization.StrippedAASFromJsonDecoder` and +:class:`~aas.adapter.json.json_deserialization.StrictStrippedAASFromJsonDecoder` for parsing stripped JSON objects, +which are used in the http adapter (see https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91). +The classes contain a custom :meth:`~aas.adapter.json.json_deserialization.AASFromJsonDecoder.object_hook` function +to detect encoded AAS objects within the JSON data and convert them to BaSyx Python SDK objects while parsing. +Additionally, there's the :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into` function, that takes a +complete AAS JSON file, reads its contents and stores the objects in the provided +:class:`~aas.model.provider.AbstractObjectStore`. :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file` is +a wrapper for this function. Instead of storing the objects in a given :class:`~aas.model.provider.AbstractObjectStore`, +it returns a :class:`~aas.model.provider.DictObjectStore` containing parsed objects. + +The deserialization is performed in a bottom-up approach: The `object_hook()` method gets called for every parsed JSON +object (as dict) and checks for existence of the `modelType` attribute. If it is present, the `AAS_CLASS_PARSERS` dict +defines, which of the constructor methods of the class is to be used for converting the dict into an object. Embedded +objects that should have a `modelType` themselves are expected to be converted already. Other embedded objects are +converted using a number of helper constructor methods. +""" +import base64 +import json +import logging +import pprint +from typing import Dict, Callable, TypeVar, Type, List, IO, Optional, Set + +from basyx.aas import model +from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ + IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ + KEY_ELEMENTS_CLASSES_INVERSE + +logger = logging.getLogger(__name__) + + +# ############################################################################# +# Helper functions (for simplifying implementation of constructor functions) +# ############################################################################# + +T = TypeVar('T') + + +def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: + """ + Helper function for getting an item from a (str→object) dict in a typesafe way. + + The type of the object is checked at runtime and a TypeError is raised, if the object has not the expected type. + + :param dct: The dict + :param key: The key of the item to retrieve + :param type_: The expected type of the item + :return: The item + :raises TypeError: If the item has an unexpected type + :raises KeyError: If the key is not found in the dict (just as usual) + """ + val = dct[key] + if not isinstance(val, type_): + raise TypeError("Dict entry '{}' has unexpected type {}".format(key, type(val).__name__)) + return val + + +def _expect_type(object_: object, type_: Type, context: str, failsafe: bool) -> bool: + """ + Helper function to check type of an embedded object. + + This function may be used in any constructor function for an AAS object that expects to find already constructed + AAS objects of a certain type within its data dict. In this case, we want to ensure that the object has this kind + and raise a TypeError if not. In failsafe mode, we want to log the error and prevent the object from being added + to the parent object. A typical use of this function would look like this: + + if _expect_type(element, model.SubmodelElement, str(submodel), failsafe): + submodel.submodel_element.add(element) + + :param object_: The object to by type-checked + :param type_: The expected type + :param context: A string to add to the exception message / log message, that describes the context in that the + object has been found + :param failsafe: Log error and return false instead of raising a TypeError + :return: True if the + :raises TypeError: If the object is not of the expected type and the failsafe mode is not active + """ + if isinstance(object_, type_): + return True + if failsafe: + logger.error("Expected a %s in %s, but found %s", type_.__name__, context, repr(object_)) + else: + raise TypeError("Expected a %s in %s, but found %s" % (type_.__name__, context, repr(object_))) + return False + + +class AASFromJsonDecoder(json.JSONDecoder): + """ + Custom JSONDecoder class to use the `json` module for deserializing Asset Administration Shell data from the + official JSON format + + The class contains a custom :meth:`~.AASFromJsonDecoder.object_hook` function to detect encoded AAS objects within + the JSON data and convert them to BaSyx Python SDK objects while parsing. + + Typical usage: + + .. code-block:: python + + data = json.loads(json_string, cls=AASFromJsonDecoder) + + The `object_hook` function uses a set of `_construct_*()` methods, one for each + AAS object type to transform the JSON objects in to BaSyx Python SDK objects. These constructor methods are divided + into two parts: "Helper Constructor Methods", that are used to construct AAS object types without a `modelType` + attribute as embedded objects within other AAS objects, and "Direct Constructor Methods" for AAS object types *with* + `modelType` attribute. The former are called from other constructor methods or utility methods based on the expected + type of an attribute, the latter are called directly from the `object_hook()` function based on the `modelType` + attribute. + + This class may be subclassed to override some of the constructor functions, e.g. to construct objects of specialized + subclasses of the BaSyx Python SDK object classes instead of these normal classes from the `model` package. To + simplify this tasks, (nearly) all the constructor methods take a parameter `object_type` defaulting to the normal + BaSyx Python SDK object class, that can be overridden in a derived function: + + .. code-block:: python + + class EnhancedAsset(model.Asset): + pass + + class EnhancedAASDecoder(AASFromJsonDecoder): + @classmethod + def _construct_asset(cls, dct): + return super()._construct_asset(dct, object_class=EnhancedAsset) + + + :cvar failsafe: If `True` (the default), don't raise Exceptions for missing attributes and wrong types, but instead + skip defective objects and use logger to output warnings. Use StrictAASFromJsonDecoder for a + non-failsafe version. + :cvar stripped: If `True`, the JSON objects will be parsed in a stripped manner, excluding some attributes. + Defaults to `False`. + See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 + """ + failsafe = True + stripped = False + + 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 + + # 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]] = { + 'Asset': cls._construct_asset, + 'AssetAdministrationShell': cls._construct_asset_administration_shell, + 'View': cls._construct_view, + 'ConceptDescription': cls._construct_concept_description, + 'Qualifier': cls._construct_qualifier, + 'Formula': cls._construct_formula, + 'Submodel': cls._construct_submodel, + 'ConceptDictionary': cls._construct_concept_dictionary, + 'Capability': cls._construct_capability, + 'Entity': cls._construct_entity, + 'BasicEvent': cls._construct_basic_event, + 'Operation': cls._construct_operation, + 'RelationshipElement': cls._construct_relationship_element, + 'AnnotatedRelationshipElement': cls._construct_annotated_relationship_element, + 'SubmodelElementCollection': cls._construct_submodel_element_collection, + 'Blob': cls._construct_blob, + 'File': cls._construct_file, + 'MultiLanguageProperty': cls._construct_multi_language_property, + 'Property': cls._construct_property, + 'Range': cls._construct_range, + 'ReferenceElement': cls._construct_reference_element, + } + + # Get modelType and constructor function + if not isinstance(dct['modelType'], dict) or 'name' not in dct['modelType']: + logger.warning("JSON object has unexpected format of modelType: %s", dct['modelType']) + # Even in strict mode, we consider 'modelType' attributes of wrong type as non-AAS objects instead of + # raising an exception. However, the object's type will probably checked later by read_json_aas_file() or + # _expect_type() + return dct + model_type = dct['modelType']['name'] + if model_type not in AAS_CLASS_PARSERS: + if not cls.failsafe: + raise TypeError("Found JSON object with modelType=\"%s\", which is not a known AAS class" % model_type) + logger.error("Found JSON object with modelType=\"%s\", which is not a known AAS class", model_type) + return dct + + # Use constructor function to transform JSON representation into BaSyx Python SDK model object + try: + return AAS_CLASS_PARSERS[model_type](dct) + except (KeyError, TypeError) as e: + error_message = "Error while trying to convert JSON object into {}: {} >>> {}".format( + model_type, e, pprint.pformat(dct, depth=2, width=2**14, compact=True)) + if cls.failsafe: + logger.error(error_message, exc_info=e) + # In failsafe mode, we return the raw JSON object dict, if there were errors while parsing an object, so + # a client application is able to handle this data. The read_json_aas_file() function and all + # constructors for complex objects will skip those items by using _expect_type(). + return dct + else: + raise type(e)(error_message) from e + + # ################################################################################################## + # Utility Methods used in constructor methods to add general attributes (from abstract base classes) + # ################################################################################################## + + @classmethod + def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None: + """ + Utility method to add the optional attributes of the abstract meta classes Referable, Identifiable, + HasSemantics, HasKind and Qualifiable to an object inheriting from any of these classes, if present + + :param obj: The object to amend its attributes + :param dct: The object's dict representation from JSON + """ + if isinstance(obj, model.Referable): + if 'category' in dct: + obj.category = _get_ts(dct, 'category', str) + if 'description' in dct: + obj.description = cls._construct_lang_string_set(_get_ts(dct, 'description', list)) + if isinstance(obj, model.Identifiable): + if 'idShort' in dct: + obj.id_short = _get_ts(dct, 'idShort', str) + if 'administration' in dct: + obj.administration = cls._construct_administrative_information(_get_ts(dct, 'administration', dict)) + if isinstance(obj, model.HasSemantics): + if 'semanticId' in dct: + obj.semantic_id = cls._construct_reference(_get_ts(dct, 'semanticId', dict)) + # `HasKind` provides only mandatory, immutable attributes; so we cannot do anything here, after object creation. + # However, the `cls._get_kind()` function may assist by retrieving them from the JSON object + if isinstance(obj, model.Qualifiable) and not cls.stripped: + if 'qualifiers' in dct: + for constraint in _get_ts(dct, 'qualifiers', list): + if _expect_type(constraint, model.Constraint, str(obj), cls.failsafe): + obj.qualifier.add(constraint) + + @classmethod + def _get_kind(cls, dct: Dict[str, object]) -> model.ModelingKind: + """ + Utility method to get the kind of an HasKind object from its JSON representation. + + :param dct: The object's dict representation from JSON + :return: The object's `kind` value + """ + return MODELING_KIND_INVERSE[_get_ts(dct, "kind", str)] if 'kind' in dct else model.ModelingKind.INSTANCE + + # ############################################################################# + # Helper Constructor Methods starting from here + # ############################################################################# + + # These constructor methods create objects that are not identified by a 'modelType' JSON attribute, so they can not + # be called from the object_hook() method. Instead, they are called by other constructor functions to transform + # embedded JSON data into the expected type at their location in the outer JSON object. + + @classmethod + def _construct_key(cls, dct: Dict[str, object], object_class=model.Key) -> model.Key: + return object_class(type_=KEY_ELEMENTS_INVERSE[_get_ts(dct, 'type', str)], + id_type=KEY_TYPES_INVERSE[_get_ts(dct, 'idType', str)], + value=_get_ts(dct, 'value', str), + local=_get_ts(dct, 'local', bool)) + + @classmethod + def _construct_reference(cls, dct: Dict[str, object], object_class=model.Reference) -> model.Reference: + keys = [cls._construct_key(key_data) for key_data in _get_ts(dct, "keys", list)] + return object_class(tuple(keys)) + + @classmethod + def _construct_aas_reference(cls, dct: Dict[str, object], type_: Type[T], object_class=model.AASReference)\ + -> model.AASReference: + keys = [cls._construct_key(key_data) for key_data in _get_ts(dct, "keys", list)] + if keys and not issubclass(KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): + logger.warning("type %s of last key of reference to %s does not match reference type %s", + keys[-1].type.name, " / ".join(str(k) for k in keys), type_.__name__) + return object_class(tuple(keys), type_) + + @classmethod + def _construct_identifier(cls, dct: Dict[str, object], object_class=model.Identifier) -> model.Identifier: + return object_class(_get_ts(dct, 'id', str), + IDENTIFIER_TYPES_INVERSE[_get_ts(dct, 'idType', str)]) + + @classmethod + def _construct_administrative_information( + cls, dct: Dict[str, object], object_class=model.AdministrativeInformation)\ + -> model.AdministrativeInformation: + ret = object_class() + if 'version' in dct: + ret.version = _get_ts(dct, 'version', str) + if 'revision' in dct: + ret.revision = _get_ts(dct, 'revision', str) + elif 'revision' in dct: + logger.warning("Ignoring 'revision' attribute of AdministrativeInformation object due to missing 'version'") + return ret + + @classmethod + def _construct_security(cls, _dct: Dict[str, object], object_class=model.Security) -> model.Security: + return object_class() + + @classmethod + def _construct_operation_variable( + cls, dct: Dict[str, object], object_class=model.OperationVariable) -> model.OperationVariable: + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + ret = object_class(value=_get_ts(dct, 'value', model.SubmodelElement)) # type: ignore + return ret + + @classmethod + def _construct_lang_string_set(cls, lst: List[Dict[str, object]]) -> Optional[model.LangStringSet]: + ret = {} + for desc in lst: + try: + ret[_get_ts(desc, 'language', str)] = _get_ts(desc, 'text', str) + except (KeyError, TypeError) as e: + error_message = "Error while trying to convert JSON object into LangString: {} >>> {}".format( + e, pprint.pformat(desc, depth=2, width=2 ** 14, compact=True)) + if cls.failsafe: + logger.error(error_message, exc_info=e) + else: + raise type(e)(error_message) from e + return ret + + @classmethod + def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList: + ret: model.ValueList = set() + for element in _get_ts(dct, 'valueReferencePairTypes', list): + try: + ret.add(cls._construct_value_reference_pair(element)) + except (KeyError, TypeError) as e: + error_message = "Error while trying to convert JSON object into ValueReferencePair: {} >>> {}".format( + e, pprint.pformat(element, depth=2, width=2 ** 14, compact=True)) + if cls.failsafe: + logger.error(error_message, exc_info=e) + else: + raise type(e)(error_message) from e + return ret + + @classmethod + def _construct_value_reference_pair(cls, dct: Dict[str, object], object_class=model.ValueReferencePair) -> \ + model.ValueReferencePair: + value_type = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)] + return object_class(value_type=value_type, + value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_type), + value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict))) + + # ############################################################################# + # Direct Constructor Methods (for classes with `modelType`) starting from here + # ############################################################################# + + # These constructor methods create objects that *are* identified by a 'modelType' JSON attribute, so they can be + # be called from the object_hook() method directly. + + @classmethod + def _construct_asset(cls, dct: Dict[str, object], object_class=model.Asset) -> model.Asset: + ret = object_class(kind=ASSET_KIND_INVERSE[_get_ts(dct, 'kind', str)], + identification=cls._construct_identifier(_get_ts(dct, "identification", dict))) + cls._amend_abstract_attributes(ret, dct) + if 'assetIdentificationModel' in dct: + ret.asset_identification_model = cls._construct_aas_reference( + _get_ts(dct, 'assetIdentificationModel', dict), model.Submodel) + if 'billOfMaterial' in dct: + ret.bill_of_material = cls._construct_aas_reference(_get_ts(dct, 'billOfMaterial', dict), model.Submodel) + return ret + + @classmethod + def _construct_asset_administration_shell( + cls, dct: Dict[str, object], object_class=model.AssetAdministrationShell) -> model.AssetAdministrationShell: + ret = object_class( + asset=cls._construct_aas_reference(_get_ts(dct, 'asset', dict), model.Asset), + identification=cls._construct_identifier(_get_ts(dct, 'identification', dict))) + cls._amend_abstract_attributes(ret, dct) + if not cls.stripped and 'submodels' in dct: + for sm_data in _get_ts(dct, 'submodels', list): + ret.submodel.add(cls._construct_aas_reference(sm_data, model.Submodel)) + if not cls.stripped and 'views' in dct: + for view in _get_ts(dct, 'views', list): + if _expect_type(view, model.View, str(ret), cls.failsafe): + ret.view.add(view) + if 'conceptDictionaries' in dct: + for concept_dictionary in _get_ts(dct, 'conceptDictionaries', list): + if _expect_type(concept_dictionary, model.ConceptDictionary, str(ret), cls.failsafe): + ret.concept_dictionary.add(concept_dictionary) + if 'security' in dct: + ret.security = cls._construct_security(_get_ts(dct, 'security', dict)) + if 'derivedFrom' in dct: + ret.derived_from = cls._construct_aas_reference(_get_ts(dct, 'derivedFrom', dict), + model.AssetAdministrationShell) + return ret + + @classmethod + def _construct_view(cls, dct: Dict[str, object], object_class=model.View) -> model.View: + ret = object_class(_get_ts(dct, 'idShort', str)) + cls._amend_abstract_attributes(ret, dct) + if 'containedElements' in dct: + for element_data in _get_ts(dct, 'containedElements', list): + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + ret.contained_element.add(cls._construct_aas_reference(element_data, model.Referable)) # type: ignore + return ret + + @classmethod + def _construct_concept_description(cls, dct: Dict[str, object], object_class=model.ConceptDescription)\ + -> model.ConceptDescription: + # Hack to detect IEC61360ConceptDescriptions, which are represented using dataSpecification according to DotAAS + ret = None + if 'embeddedDataSpecifications' in dct: + for dspec in _get_ts(dct, 'embeddedDataSpecifications', list): + dspec_ref = cls._construct_reference(_get_ts(dspec, 'dataSpecification', dict)) + if dspec_ref.key and (dspec_ref.key[0].value == + "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0"): + ret = cls._construct_iec61360_concept_description( + dct, _get_ts(dspec, 'dataSpecificationContent', dict)) + # If this is not a special ConceptDescription, just construct one of the default object_class + if ret is None: + ret = object_class(identification=cls._construct_identifier(_get_ts(dct, 'identification', dict))) + cls._amend_abstract_attributes(ret, dct) + if 'isCaseOf' in dct: + for case_data in _get_ts(dct, "isCaseOf", list): + ret.is_case_of.add(cls._construct_reference(case_data)) + return ret + + @classmethod + def _construct_iec61360_concept_description(cls, dct: Dict[str, object], data_spec: Dict[str, object], + object_class=model.concept.IEC61360ConceptDescription)\ + -> model.concept.IEC61360ConceptDescription: + ret = object_class(identification=cls._construct_identifier(_get_ts(dct, 'identification', dict)), + preferred_name=cls._construct_lang_string_set(_get_ts(data_spec, 'preferredName', list))) + if 'dataType' in data_spec: + ret.data_type = IEC61360_DATA_TYPES_INVERSE[_get_ts(data_spec, 'dataType', str)] + if 'definition' in data_spec: + ret.definition = cls._construct_lang_string_set(_get_ts(data_spec, 'definition', list)) + if 'shortName' in data_spec: + ret.short_name = cls._construct_lang_string_set(_get_ts(data_spec, 'shortName', list)) + if 'unit' in data_spec: + ret.unit = _get_ts(data_spec, 'unit', str) + if 'unitId' in data_spec: + ret.unit_id = cls._construct_reference(_get_ts(data_spec, 'unitId', dict)) + if 'sourceOfDefinition' in data_spec: + ret.source_of_definition = _get_ts(data_spec, 'sourceOfDefinition', str) + if 'symbol' in data_spec: + ret.symbol = _get_ts(data_spec, 'symbol', str) + if 'valueFormat' in data_spec: + ret.value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(data_spec, 'valueFormat', str)] + if 'valueList' in data_spec: + ret.value_list = cls._construct_value_list(_get_ts(data_spec, 'valueList', dict)) + if 'value' in data_spec: + ret.value = model.datatypes.from_xsd(_get_ts(data_spec, 'value', str), ret.value_format) + if 'valueId' in data_spec: + ret.value_id = cls._construct_reference(_get_ts(data_spec, 'valueId', dict)) + if 'levelType' in data_spec: + ret.level_types = set(IEC61360_LEVEL_TYPES_INVERSE[level_type] + for level_type in _get_ts(data_spec, 'levelType', list)) + return ret + + @classmethod + def _construct_concept_dictionary(cls, dct: Dict[str, object], object_class=model.ConceptDictionary)\ + -> model.ConceptDictionary: + ret = object_class(_get_ts(dct, "idShort", str)) + cls._amend_abstract_attributes(ret, dct) + if 'conceptDescriptions' in dct: + for desc_data in _get_ts(dct, "conceptDescriptions", list): + ret.concept_description.add(cls._construct_aas_reference(desc_data, model.ConceptDescription)) + return ret + + @classmethod + def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> model.Entity: + ret = object_class(id_short=_get_ts(dct, "idShort", str), + entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], + asset=(cls._construct_aas_reference(_get_ts(dct, 'asset', dict), model.Asset) + if 'asset' in dct else None), + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if not cls.stripped and 'statements' in dct: + for element in _get_ts(dct, "statements", list): + if _expect_type(element, model.SubmodelElement, str(ret), cls.failsafe): + ret.statement.add(element) + return ret + + @classmethod + def _construct_qualifier(cls, dct: Dict[str, object], object_class=model.Qualifier) -> model.Qualifier: + ret = object_class(type_=_get_ts(dct, 'type', str), + value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)]) + cls._amend_abstract_attributes(ret, dct) + if 'value' in dct: + ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) + if 'valueId' in dct: + ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) + return ret + + @classmethod + def _construct_formula(cls, dct: Dict[str, object], object_class=model.Formula) -> model.Formula: + ret = object_class() + cls._amend_abstract_attributes(ret, dct) + if 'dependsOn' in dct: + for dependency_data in _get_ts(dct, 'dependsOn', list): + try: + ret.depends_on.add(cls._construct_reference(dependency_data)) + except (KeyError, TypeError) as e: + error_message = \ + "Error while trying to convert JSON object into dependency Reference for {}: {} >>> {}".format( + ret, e, pprint.pformat(dct, depth=2, width=2 ** 14, compact=True)) + if cls.failsafe: + logger.error(error_message, exc_info=e) + else: + raise type(e)(error_message) from e + return ret + + @classmethod + def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel) -> model.Submodel: + ret = object_class(identification=cls._construct_identifier(_get_ts(dct, 'identification', dict)), + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if not cls.stripped and 'submodelElements' in dct: + for element in _get_ts(dct, "submodelElements", list): + if _expect_type(element, model.SubmodelElement, str(ret), cls.failsafe): + ret.submodel_element.add(element) + return ret + + @classmethod + def _construct_capability(cls, dct: Dict[str, object], object_class=model.Capability) -> model.Capability: + ret = object_class(id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + return ret + + @classmethod + def _construct_basic_event(cls, dct: Dict[str, object], object_class=model.BasicEvent) -> model.BasicEvent: + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + ret = object_class(id_short=_get_ts(dct, "idShort", str), + observed=cls._construct_aas_reference(_get_ts(dct, 'observed', dict), + model.Referable), # type: ignore + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + return ret + + @classmethod + def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operation) -> model.Operation: + ret = object_class(_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + + # Deserialize variables (they are not Referable, thus we don't + for json_name, target in (('inputVariable', ret.input_variable), + ('outputVariable', ret.output_variable), + ('inoutputVariable', ret.in_output_variable)): + if json_name in dct: + for variable_data in _get_ts(dct, json_name, list): + try: + target.append(cls._construct_operation_variable(variable_data)) + except (KeyError, TypeError) as e: + error_message = "Error while trying to convert JSON object into {} of {}: {}".format( + json_name, ret, pprint.pformat(variable_data, depth=2, width=2 ** 14, compact=True)) + if cls.failsafe: + logger.error(error_message, exc_info=e) + else: + raise type(e)(error_message) from e + return ret + + @classmethod + def _construct_relationship_element( + cls, dct: Dict[str, object], object_class=model.RelationshipElement) -> model.RelationshipElement: + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + ret = object_class(id_short=_get_ts(dct, "idShort", str), + first=cls._construct_aas_reference(_get_ts(dct, 'first', dict), + model.Referable), # type: ignore + second=cls._construct_aas_reference(_get_ts(dct, 'second', dict), + model.Referable), # type: ignore + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + return ret + + @classmethod + def _construct_annotated_relationship_element( + cls, dct: Dict[str, object], object_class=model.AnnotatedRelationshipElement)\ + -> model.AnnotatedRelationshipElement: + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + ret = object_class( + id_short=_get_ts(dct, "idShort", str), + first=cls._construct_aas_reference(_get_ts(dct, 'first', dict), model.Referable), # type: ignore + second=cls._construct_aas_reference(_get_ts(dct, 'second', dict), model.Referable), # type: ignore + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if not cls.stripped and 'annotation' in dct: + for element in _get_ts(dct, "annotation", list): + if _expect_type(element, model.DataElement, str(ret), cls.failsafe): + ret.annotation.add(element) + return ret + + @classmethod + def _construct_submodel_element_collection( + cls, + dct: Dict[str, object], + object_class_ordered=model.SubmodelElementCollectionOrdered, + object_class_unordered=model.SubmodelElementCollectionUnordered)\ + -> model.SubmodelElementCollection: + ret: model.SubmodelElementCollection + if 'ordered' in dct and _get_ts(dct, 'ordered', bool): + ret = object_class_ordered( + id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + else: + ret = object_class_unordered( + id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if not cls.stripped and 'value' in dct: + for element in _get_ts(dct, "value", list): + if _expect_type(element, model.SubmodelElement, str(ret), cls.failsafe): + ret.value.add(element) + return ret + + @classmethod + def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> model.Blob: + ret = object_class(id_short=_get_ts(dct, "idShort", str), + mime_type=_get_ts(dct, "mimeType", str), + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if 'value' in dct: + ret.value = base64.b64decode(_get_ts(dct, 'value', str)) + return ret + + @classmethod + def _construct_file(cls, dct: Dict[str, object], object_class=model.File) -> model.File: + ret = object_class(id_short=_get_ts(dct, "idShort", str), + value=None, + mime_type=_get_ts(dct, "mimeType", str), + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if 'value' in dct and dct['value'] is not None: + ret.value = _get_ts(dct, 'value', str) + return ret + + @classmethod + def _construct_multi_language_property( + cls, dct: Dict[str, object], object_class=model.MultiLanguageProperty) -> model.MultiLanguageProperty: + ret = object_class(id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if 'value' in dct and dct['value'] is not None: + ret.value = cls._construct_lang_string_set(_get_ts(dct, 'value', list)) + if 'valueId' in dct: + ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) + return ret + + @classmethod + def _construct_property(cls, dct: Dict[str, object], object_class=model.Property) -> model.Property: + ret = object_class(id_short=_get_ts(dct, "idShort", str), + value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)], + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if 'value' in dct and dct['value'] is not None: + ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) + if 'valueId' in dct: + ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) + return ret + + @classmethod + def _construct_range(cls, dct: Dict[str, object], object_class=model.Range) -> model.Range: + ret = object_class(id_short=_get_ts(dct, "idShort", str), + value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)], + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if 'min' in dct and dct['min'] is not None: + ret.min = model.datatypes.from_xsd(_get_ts(dct, 'min', str), ret.value_type) + if 'max' in dct and dct['max'] is not None: + ret.max = model.datatypes.from_xsd(_get_ts(dct, 'max', str), ret.value_type) + return ret + + @classmethod + def _construct_reference_element( + cls, dct: Dict[str, object], object_class=model.ReferenceElement) -> model.ReferenceElement: + ret = object_class(id_short=_get_ts(dct, "idShort", str), + value=None, + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if 'value' in dct and dct['value'] is not None: + ret.value = cls._construct_reference(_get_ts(dct, 'value', dict)) + return ret + + +class StrictAASFromJsonDecoder(AASFromJsonDecoder): + """ + A strict version of the AASFromJsonDecoder class for deserializing Asset Administration Shell data from the + official JSON format + + This version has set `failsafe = False`, which will lead to Exceptions raised for every missing attribute or wrong + object type. + """ + failsafe = False + + +class StrippedAASFromJsonDecoder(AASFromJsonDecoder): + """ + Decoder for stripped JSON objects. Used in the HTTP adapter. + """ + stripped = True + + +class StrictStrippedAASFromJsonDecoder(StrictAASFromJsonDecoder, StrippedAASFromJsonDecoder): + """ + Non-failsafe decoder for stripped JSON objects. + """ + pass + + +def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFromJsonDecoder]]) \ + -> Type[AASFromJsonDecoder]: + """ + Returns the correct decoder based on the parameters failsafe and stripped. If a decoder class is given, failsafe + and stripped are ignored. + + :param failsafe: If `True`, a failsafe decoder is selected. Ignored if a decoder class is specified. + :param stripped: If `True`, a decoder for parsing stripped JSON objects is selected. Ignored if a decoder class is + specified. + :param decoder: Is returned, if specified. + :return: An :class:`~.AASFromJsonDecoder` (sub)class. + """ + if decoder is not None: + return decoder + if failsafe: + if stripped: + return StrippedAASFromJsonDecoder + return AASFromJsonDecoder + else: + if stripped: + return StrictStrippedAASFromJsonDecoder + return StrictAASFromJsonDecoder + + +def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, replace_existing: bool = False, + ignore_existing: bool = False, failsafe: bool = True, stripped: bool = False, + decoder: Optional[Type[AASFromJsonDecoder]] = None) -> 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. + + :param object_store: The :class:`ObjectStore ` in which the identifiable + objects should be stored + :param file: A file-like object to read the JSON-serialized data from + :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not + :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. + This parameter is ignored if replace_existing is `True`. + :param failsafe: If `True`, the document is parsed in a failsafe way: Missing attributes and elements are logged + instead of causing exceptions. Defect objects are skipped. + This parameter is ignored if a decoder class is specified. + :param stripped: If `True`, stripped JSON objects are parsed. + 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 + :return: A set of :class:`Identifiers ` that were added to object_store + """ + ret: Set[model.Identifier] = set() + decoder_ = _select_decoder(failsafe, stripped, decoder) + + # read, parse and convert JSON file + data = json.load(file, cls=decoder_) + + for name, expected_type in (('assetAdministrationShells', model.AssetAdministrationShell), + ('assets', model.Asset), + ('submodels', model.Submodel), + ('conceptDescriptions', model.ConceptDescription)): + try: + lst = _get_ts(data, name, list) + except (KeyError, TypeError) as e: + error_message = "Could not find list '{}' in AAS JSON file".format(name) + if decoder_.failsafe: + logger.warning(error_message) + continue + else: + raise type(e)(error_message) from e + + for item in lst: + error_message = "Expected a {} in list '{}', but found {}".format( + expected_type.__name__, name, 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 item.identification in ret: + error_message = 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...") + continue + existing_element = object_store.get(item.identification) + if existing_element is not None: + if not replace_existing: + error_message = f"object with identifier {item.identification} already exists " \ + f"in the object 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}...") + continue + object_store.discard(existing_element) + object_store.add(item) + ret.add(item.identification) + elif decoder_.failsafe: + logger.error(error_message) + else: + raise TypeError(error_message) + return ret + + +def read_aas_json_file(file: IO, **kwargs) -> model.DictObjectStore[model.Identifiable]: + """ + A wrapper of :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects in an + empty :class:`~aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as + :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into`. + + :param file: A filename or file-like object to read the JSON-serialized data from + :param kwargs: Keyword arguments passed to :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into` + :return: A :class:`~aas.model.provider.DictObjectStore` containing all AAS objects from the JSON file + """ + object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + read_aas_json_file_into(object_store, file, **kwargs) + return object_store diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py new file mode 100644 index 0000000..a8ff5ba --- /dev/null +++ b/basyx/aas/adapter/json/json_serialization.py @@ -0,0 +1,768 @@ +# Copyright (c) 2020 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +""" +.. _adapter.json.json_serialization: + +Module for serializing Asset Administration Shell objects to the official JSON format + +The module provides an custom JSONEncoder classes :class:`~.AASToJsonEncoder` and :class:`~.StrippedAASToJsonEncoder` +to be used with the Python standard `json` module. While the former serializes objects as defined in the specification, +the latter serializes stripped objects, excluding some attributes +(see https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91). +Each class contains a custom :meth:`~.AASToJsonEncoder.default` function which converts BaSyx Python SDK objects to +simple python types for an automatic JSON serialization. +To simplify the usage of this module, the :meth:`~aas.adapter.json.json_serialization.write_aas_json_file` and +:meth:`~aas.adapter.json.json_serialization.object_store_to_json` are provided. +The former is used to serialize a given :class:`~aas.model.provider.AbstractObjectStore` to a file, while the latter +serializes the object store to a string and returns it. + +The serialization is performed in an iterative approach: The :meth:`~.AASToJsonEncoder.default` function gets called for +every object and checks if an object is an BaSyx Python SDK object. In this case, it calls a special function for the +respective BaSyx Python SDK class which converts the object (but not the contained objects) into a simple Python dict, +which is serializable. Any contained BaSyx Python SDK objects are included into the dict as they are to be converted +later on. The special helper function :meth:`~.AASToJsonEncoder._abstract_classes_to_json` is called by most of the +conversion functions to handle all the attributes of abstract base classes. +""" +import base64 +import inspect +from typing import List, Dict, IO, Optional, Type +import json + +from basyx.aas import model +from .. import _generic + + +class AASToJsonEncoder(json.JSONEncoder): + """ + Custom JSONDecoder class to use the `json` module for serializing Asset Administration Shell data into the + official JSON format + + The class overrides the `default()` method to transform BaSyx Python SDK objects into dicts that may be serialized + by the standard encode method. + + Typical usage: + + .. code-block:: python + + json_string = json.dumps(data, cls=AASToJsonEncoder) + + :cvar stripped: If True, the JSON objects will be serialized in a stripped manner, excluding some attributes. + Defaults to `False`. + See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 + """ + stripped = False + + def default(self, obj: object) -> object: + """ + The overwritten `default` method for `json.JSONEncoder` + + :param obj: The object to serialize to json + :return: The serialized object + """ + if isinstance(obj, model.AssetAdministrationShell): + return self._asset_administration_shell_to_json(obj) + if isinstance(obj, model.Identifier): + return self._identifier_to_json(obj) + if isinstance(obj, model.AdministrativeInformation): + return self._administrative_information_to_json(obj) + if isinstance(obj, model.Reference): + return self._reference_to_json(obj) + if isinstance(obj, model.Key): + return self._key_to_json(obj) + if isinstance(obj, model.ValueReferencePair): + return self._value_reference_pair_to_json(obj) + if isinstance(obj, model.Asset): + return self._asset_to_json(obj) + if isinstance(obj, model.Submodel): + return self._submodel_to_json(obj) + if isinstance(obj, model.Operation): + return self._operation_to_json(obj) + if isinstance(obj, model.OperationVariable): + return self._operation_variable_to_json(obj) + if isinstance(obj, model.Capability): + return self._capability_to_json(obj) + if isinstance(obj, model.BasicEvent): + return self._basic_event_to_json(obj) + if isinstance(obj, model.Entity): + return self._entity_to_json(obj) + if isinstance(obj, model.View): + return self._view_to_json(obj) + if isinstance(obj, model.ConceptDictionary): + return self._concept_dictionary_to_json(obj) + if isinstance(obj, model.ConceptDescription): + return self._concept_description_to_json(obj) + if isinstance(obj, model.Property): + return self._property_to_json(obj) + if isinstance(obj, model.Range): + return self._range_to_json(obj) + if isinstance(obj, model.MultiLanguageProperty): + return self._multi_language_property_to_json(obj) + if isinstance(obj, model.File): + return self._file_to_json(obj) + if isinstance(obj, model.Blob): + return self._blob_to_json(obj) + if isinstance(obj, model.ReferenceElement): + return self._reference_element_to_json(obj) + if isinstance(obj, model.SubmodelElementCollection): + return self._submodel_element_collection_to_json(obj) + if isinstance(obj, model.AnnotatedRelationshipElement): + return self._annotated_relationship_element_to_json(obj) + if isinstance(obj, model.RelationshipElement): + return self._relationship_element_to_json(obj) + if isinstance(obj, model.Qualifier): + return self._qualifier_to_json(obj) + if isinstance(obj, model.Formula): + return self._formula_to_json(obj) + return super().default(obj) + + @classmethod + def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: + """ + transformation function to serialize abstract classes from model.base which are inherited by many classes + + :param obj: object which must be serialized + :return: dict with the serialized attributes of the abstract classes this object inherits from + """ + data = {} + if isinstance(obj, model.Referable): + data['idShort'] = obj.id_short + if obj.category: + data['category'] = obj.category + if obj.description: + data['description'] = cls._lang_string_set_to_json(obj.description) + try: + ref_type = next(iter(t for t in inspect.getmro(type(obj)) if t in model.KEY_ELEMENTS_CLASSES)) + except StopIteration as e: + raise TypeError("Object of type {} is Referable but does not inherit from a known AAS type" + .format(obj.__class__.__name__)) from e + data['modelType'] = {'name': ref_type.__name__} + if isinstance(obj, model.Identifiable): + data['identification'] = obj.identification + if obj.administration: + data['administration'] = obj.administration + if isinstance(obj, model.HasSemantics): + if obj.semantic_id: + data['semanticId'] = obj.semantic_id + if isinstance(obj, model.HasKind): + if obj.kind is model.ModelingKind.TEMPLATE: + data['kind'] = _generic.MODELING_KIND[obj.kind] + if isinstance(obj, model.Qualifiable) and not cls.stripped: + if obj.qualifier: + data['qualifiers'] = list(obj.qualifier) + return data + + # ############################################################# + # transformation functions to serialize classes from model.base + # ############################################################# + + @classmethod + def _lang_string_set_to_json(cls, obj: model.LangStringSet) -> List[Dict[str, object]]: + return [{'language': k, 'text': v} + for k, v in obj.items()] + + @classmethod + def _key_to_json(cls, obj: model.Key) -> Dict[str, object]: + """ + serialization of an object from class Key to json + + :param obj: object of class Key + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update({'type': _generic.KEY_ELEMENTS[obj.type], + 'idType': _generic.KEY_TYPES[obj.id_type], + 'value': obj.value, + 'local': obj.local}) + return data + + @classmethod + def _administrative_information_to_json(cls, obj: model.AdministrativeInformation) -> Dict[str, object]: + """ + serialization of an object from class AdministrativeInformation to json + + :param obj: object of class AdministrativeInformation + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if obj.version: + data['version'] = obj.version + if obj.revision: + data['revision'] = obj.revision + return data + + @classmethod + def _identifier_to_json(cls, obj: model.Identifier) -> Dict[str, object]: + """ + serialization of an object from class Identifier to json + + :param obj: object of class Identifier + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['id'] = obj.id + data['idType'] = _generic.IDENTIFIER_TYPES[obj.id_type] + return data + + @classmethod + def _reference_to_json(cls, obj: model.Reference) -> Dict[str, object]: + """ + serialization of an object from class Reference to json + + :param obj: object of class Reference + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['keys'] = list(obj.key) + return data + + @classmethod + def _constraint_to_json(cls, obj: model.Constraint) -> Dict[str, object]: # TODO check if correct for each class + """ + serialization of an object from class Constraint to json + + :param obj: object of class Constraint + :return: dict with the serialized attributes of this object + """ + CONSTRAINT_CLASSES = [model.Qualifier, model.Formula] + try: + const_type = next(iter(t for t in inspect.getmro(type(obj)) if t in CONSTRAINT_CLASSES)) + except StopIteration as e: + raise TypeError("Object of type {} is a Constraint but does not inherit from a known AAS Constraint type" + .format(obj.__class__.__name__)) from e + return {'modelType': {'name': const_type.__name__}} + + @classmethod + def _namespace_to_json(cls, obj): # not in specification yet + """ + serialization of an object from class Namespace to json + + :param obj: object of class Namespace + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + return data + + @classmethod + def _formula_to_json(cls, obj: model.Formula) -> Dict[str, object]: + """ + serialization of an object from class Formula to json + + :param obj: object of class Formula + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update(cls._constraint_to_json(obj)) + if obj.depends_on: + data['dependsOn'] = list(obj.depends_on) + return data + + @classmethod + def _qualifier_to_json(cls, obj: model.Qualifier) -> Dict[str, object]: + """ + serialization of an object from class Qualifier to json + + :param obj: object of class Qualifier + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update(cls._constraint_to_json(obj)) + if obj.value: + data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None + if obj.value_id: + data['valueId'] = obj.value_id + data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] + data['type'] = obj.type + return data + + @classmethod + def _value_reference_pair_to_json(cls, obj: model.ValueReferencePair) -> Dict[str, object]: + """ + serialization of an object from class ValueReferencePair to json + + :param obj: object of class ValueReferencePair + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update({'value': model.datatypes.xsd_repr(obj.value), + 'valueId': obj.value_id, + 'valueType': model.datatypes.XSD_TYPE_NAMES[obj.value_type]}) + return data + + @classmethod + def _value_list_to_json(cls, obj: model.ValueList) -> Dict[str, object]: + """ + serialization of an object from class ValueList to json + + :param obj: object of class ValueList + :return: dict with the serialized attributes of this object + """ + return {'valueReferencePairTypes': list(obj)} + + # ############################################################ + # transformation functions to serialize classes from model.aas + # ############################################################ + + @classmethod + def _view_to_json(cls, obj: model.View) -> Dict[str, object]: + """ + serialization of an object from class View to json + + :param obj: object of class View + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if obj.contained_element: + data['containedElements'] = list(obj.contained_element) + return data + + @classmethod + def _asset_to_json(cls, obj: model.Asset) -> Dict[str, object]: + """ + serialization of an object from class Asset to json + + :param obj: object of class Asset + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['kind'] = _generic.ASSET_KIND[obj.kind] + if obj.asset_identification_model: + data['assetIdentificationModel'] = obj.asset_identification_model + if obj.bill_of_material: + data['billOfMaterial'] = obj.bill_of_material + return data + + @classmethod + def _concept_description_to_json(cls, obj: model.ConceptDescription) -> Dict[str, object]: + """ + serialization of an object from class ConceptDescription to json + + :param obj: object of class ConceptDescription + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if obj.is_case_of: + data['isCaseOf'] = list(obj.is_case_of) + + if isinstance(obj, model.concept.IEC61360ConceptDescription): + cls._append_iec61360_concept_description_attrs(obj, data) + + return data + + @classmethod + def _append_iec61360_concept_description_attrs(cls, obj: model.concept.IEC61360ConceptDescription, + data: Dict[str, object]) -> None: + """ + Add the 'embeddedDataSpecifications' attribute to IEC61360ConceptDescription's JSON representation. + + `IEC61360ConceptDescription` is not a distinct class according DotAAS, but instead is built by referencing + "DataSpecificationIEC61360" as dataSpecification. However, we implemented it as an explicit class, inheriting + from ConceptDescription, but we want to generate compliant JSON documents. So, we fake the JSON structure of an + object with dataSpecifications. + """ + data_spec: Dict[str, object] = { + 'preferredName': cls._lang_string_set_to_json(obj.preferred_name) + } + if obj.data_type is not None: + data_spec['dataType'] = _generic.IEC61360_DATA_TYPES[obj.data_type] + if obj.definition is not None: + data_spec['definition'] = cls._lang_string_set_to_json(obj.definition) + if obj.short_name is not None: + data_spec['shortName'] = cls._lang_string_set_to_json(obj.short_name) + if obj.unit is not None: + data_spec['unit'] = obj.unit + if obj.unit_id is not None: + data_spec['unitId'] = obj.unit_id + if obj.source_of_definition is not None: + data_spec['sourceOfDefinition'] = obj.source_of_definition + if obj.symbol is not None: + data_spec['symbol'] = obj.symbol + if obj.value_format is not None: + data_spec['valueFormat'] = model.datatypes.XSD_TYPE_NAMES[obj.value_format] + if obj.value_list is not None: + data_spec['valueList'] = cls._value_list_to_json(obj.value_list) + if obj.value is not None: + data_spec['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None + if obj.value_id is not None: + data_spec['valueId'] = obj.value_id + if obj.level_types: + data_spec['levelType'] = [_generic.IEC61360_LEVEL_TYPES[lt] for lt in obj.level_types] + data['embeddedDataSpecifications'] = [ + {'dataSpecification': model.Reference(( + model.Key(model.KeyElements.GLOBAL_REFERENCE, False, + "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0", + model.KeyType.IRI),)), + 'dataSpecificationContent': data_spec} + ] + + @classmethod + def _concept_dictionary_to_json(cls, obj: model.ConceptDictionary) -> Dict[str, object]: + """ + serialization of an object from class ConceptDictionary to json + + :param obj: object of class ConceptDictionary + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if obj.concept_description: + data['conceptDescriptions'] = list(obj.concept_description) + return data + + @classmethod + def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell) -> Dict[str, object]: + """ + serialization of an object from class AssetAdministrationShell to json + + :param obj: object of class AssetAdministrationShell + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update(cls._namespace_to_json(obj)) + if obj.derived_from: + data["derivedFrom"] = obj.derived_from + data["asset"] = obj.asset + if not cls.stripped and obj.submodel: + data["submodels"] = list(obj.submodel) + if not cls.stripped and obj.view: + data["views"] = list(obj.view) + if obj.concept_dictionary: + data["conceptDictionaries"] = list(obj.concept_dictionary) + if obj.security: + data["security"] = obj.security + return data + + # ################################################################# + # transformation functions to serialize classes from model.security + # ################################################################# + + @classmethod + def _security_to_json(cls, obj: model.Security) -> Dict[str, object]: # has no attributes in our implementation + """ + serialization of an object from class Security to json + + :param obj: object of class Security + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + return data + + # ################################################################# + # transformation functions to serialize classes from model.submodel + # ################################################################# + + @classmethod + def _submodel_to_json(cls, obj: model.Submodel) -> Dict[str, object]: # TODO make kind optional + """ + serialization of an object from class Submodel to json + + :param obj: object of class Submodel + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if not cls.stripped and obj.submodel_element != set(): + data['submodelElements'] = list(obj.submodel_element) + return data + + @classmethod + def _data_element_to_json(cls, obj: model.DataElement) -> Dict[str, object]: # no attributes in specification yet + """ + serialization of an object from class DataElement to json + + :param obj: object of class DataElement + :return: dict with the serialized attributes of this object + """ + return {} + + @classmethod + def _property_to_json(cls, obj: model.Property) -> Dict[str, object]: + """ + serialization of an object from class Property to json + + :param obj: object of class Property + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None + if obj.value_id: + data['valueId'] = obj.value_id + data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] + return data + + @classmethod + def _multi_language_property_to_json(cls, obj: model.MultiLanguageProperty) -> Dict[str, object]: + """ + serialization of an object from class MultiLanguageProperty to json + + :param obj: object of class MultiLanguageProperty + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if obj.value: + data['value'] = cls._lang_string_set_to_json(obj.value) + if obj.value_id: + data['valueId'] = obj.value_id + return data + + @classmethod + def _range_to_json(cls, obj: model.Range) -> Dict[str, object]: + """ + serialization of an object from class Range to json + + :param obj: object of class Range + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update({'valueType': model.datatypes.XSD_TYPE_NAMES[obj.value_type], + 'min': model.datatypes.xsd_repr(obj.min) if obj.min is not None else None, + 'max': model.datatypes.xsd_repr(obj.max) if obj.max is not None else None}) + return data + + @classmethod + def _blob_to_json(cls, obj: model.Blob) -> Dict[str, object]: + """ + serialization of an object from class Blob to json + + :param obj: object of class Blob + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['mimeType'] = obj.mime_type + if obj.value is not None: + data['value'] = base64.b64encode(obj.value).decode() + return data + + @classmethod + def _file_to_json(cls, obj: model.File) -> Dict[str, object]: + """ + serialization of an object from class File to json + + :param obj: object of class File + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update({'value': obj.value, 'mimeType': obj.mime_type}) + return data + + @classmethod + def _reference_element_to_json(cls, obj: model.ReferenceElement) -> Dict[str, object]: + """ + serialization of an object from class Reference to json + + :param obj: object of class Reference + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if obj.value: + data['value'] = obj.value + return data + + @classmethod + def _submodel_element_collection_to_json(cls, obj: model.SubmodelElementCollection) -> Dict[str, object]: + """ + serialization of an object from class SubmodelElementCollectionOrdered and SubmodelElementCollectionUnordered to + json + + :param obj: object of class SubmodelElementCollectionOrdered and SubmodelElementCollectionUnordered + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if not cls.stripped and obj.value: + data['value'] = list(obj.value) + data['ordered'] = obj.ordered + return data + + @classmethod + def _relationship_element_to_json(cls, obj: model.RelationshipElement) -> Dict[str, object]: + """ + serialization of an object from class RelationshipElement to json + + :param obj: object of class RelationshipElement + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update({'first': obj.first, 'second': obj.second}) + return data + + @classmethod + def _annotated_relationship_element_to_json(cls, obj: model.AnnotatedRelationshipElement) -> Dict[str, object]: + """ + serialization of an object from class AnnotatedRelationshipElement to json + + :param obj: object of class AnnotatedRelationshipElement + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data.update({'first': obj.first, 'second': obj.second}) + if not cls.stripped and obj.annotation: + data['annotation'] = list(obj.annotation) + return data + + @classmethod + def _operation_variable_to_json(cls, obj: model.OperationVariable) -> Dict[str, object]: + """ + serialization of an object from class OperationVariable to json + + :param obj: object of class OperationVariable + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['value'] = obj.value + return data + + @classmethod + def _operation_to_json(cls, obj: model.Operation) -> Dict[str, object]: + """ + serialization of an object from class Operation to json + + :param obj: object of class Operation + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if obj.input_variable: + data['inputVariable'] = list(obj.input_variable) + if obj.output_variable: + data['outputVariable'] = list(obj.output_variable) + if obj.in_output_variable: + data['inoutputVariable'] = list(obj.in_output_variable) + return data + + @classmethod + def _capability_to_json(cls, obj: model.Capability) -> Dict[str, object]: + """ + serialization of an object from class Capability to json + + :param obj: object of class Capability + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + # no attributes in specification yet + return data + + @classmethod + def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]: + """ + serialization of an object from class Entity to json + + :param obj: object of class Entity + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + if not cls.stripped and obj.statement: + data['statements'] = list(obj.statement) + data['entityType'] = _generic.ENTITY_TYPES[obj.entity_type] + if obj.asset: + data['asset'] = obj.asset + return data + + @classmethod + def _event_to_json(cls, obj: model.Event) -> Dict[str, object]: # no attributes in specification yet + """ + serialization of an object from class Event to json + + :param obj: object of class Event + :return: dict with the serialized attributes of this object + """ + return {} + + @classmethod + def _basic_event_to_json(cls, obj: model.BasicEvent) -> Dict[str, object]: + """ + serialization of an object from class BasicEvent to json + + :param obj: object of class BasicEvent + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['observed'] = obj.observed + return data + + +class StrippedAASToJsonEncoder(AASToJsonEncoder): + """ + AASToJsonEncoder for stripped objects. Used in the HTTP API. + See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 + """ + stripped = True + + +def _select_encoder(stripped: bool, encoder: Optional[Type[AASToJsonEncoder]] = None) -> Type[AASToJsonEncoder]: + """ + Returns the correct encoder based on the stripped parameter. If an encoder class is given, stripped is ignored. + + :param stripped: If true, an encoder for parsing stripped JSON objects is selected. Ignored if an encoder class is + specified. + :param encoder: Is returned, if specified. + :return: A AASToJsonEncoder (sub)class. + """ + if encoder is not None: + return encoder + return AASToJsonEncoder if not stripped else StrippedAASToJsonEncoder + + +def _create_dict(data: model.AbstractObjectStore) -> dict: + # separate different kind of objects + assets = [] + asset_administration_shells = [] + submodels = [] + concept_descriptions = [] + for obj in data: + if isinstance(obj, model.Asset): + assets.append(obj) + if isinstance(obj, model.AssetAdministrationShell): + asset_administration_shells.append(obj) + if isinstance(obj, model.Submodel): + submodels.append(obj) + if isinstance(obj, model.ConceptDescription): + concept_descriptions.append(obj) + dict_ = { + 'assetAdministrationShells': asset_administration_shells, + 'submodels': submodels, + 'assets': assets, + 'conceptDescriptions': concept_descriptions, + } + return dict_ + + +def object_store_to_json(data: model.AbstractObjectStore, stripped: bool = False, + encoder: Optional[Type[AASToJsonEncoder]] = None, **kwargs) -> str: + """ + Create a json serialization of a set of AAS objects according to 'Details of the Asset Administration Shell', + chapter 5.5 + + :param data: :class:`ObjectStore ` which contains different objects of the + AAS meta model which should be serialized to a + JSON file + :param stripped: If true, objects are serialized to stripped json objects.. + See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 + This parameter is ignored if an encoder class is specified. + :param encoder: The encoder class used to encoder the JSON objects + :param kwargs: Additional keyword arguments to be passed to `json.dumps()` + """ + encoder_ = _select_encoder(stripped, encoder) + # serialize object to json + return json.dumps(_create_dict(data), cls=encoder_, **kwargs) + + +def write_aas_json_file(file: IO, data: model.AbstractObjectStore, stripped: bool = False, + encoder: Optional[Type[AASToJsonEncoder]] = None, **kwargs) -> None: + """ + Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset + Administration Shell', chapter 5.5 + + :param file: A file-like object to write the JSON-serialized data to + :param data: :class:`ObjectStore ` which contains different objects of the + AAS meta model which should be serialized to a + JSON file + :param stripped: If true, objects are serialized to stripped json objects.. + See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 + This parameter is ignored if an encoder class is specified. + :param encoder: The encoder class used to encoder the JSON objects + :param kwargs: Additional keyword arguments to be passed to json.dumps() + """ + encoder_ = _select_encoder(stripped, encoder) + # serialize object to json + json.dump(_create_dict(data), file, cls=encoder_, **kwargs) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd new file mode 100644 index 0000000..0e2fbca --- /dev/null +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -0,0 +1,550 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/basyx/aas/adapter/xml/AAS_ABAC.xsd b/basyx/aas/adapter/xml/AAS_ABAC.xsd new file mode 100644 index 0000000..a735cce --- /dev/null +++ b/basyx/aas/adapter/xml/AAS_ABAC.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/basyx/aas/adapter/xml/IEC61360.xsd b/basyx/aas/adapter/xml/IEC61360.xsd new file mode 100644 index 0000000..bdaee9d --- /dev/null +++ b/basyx/aas/adapter/xml/IEC61360.xsd @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/basyx/aas/adapter/xml/__init__.py b/basyx/aas/adapter/xml/__init__.py new file mode 100644 index 0000000..43e333c --- /dev/null +++ b/basyx/aas/adapter/xml/__init__.py @@ -0,0 +1,18 @@ +""" +.. _adapter.xml.__init__: + +This package contains functionality for serialization and deserialization of BaSyx Python SDK objects into/from XML. + +:ref:`xml_serialization `: The module offers a function to write an +:class:`ObjectStore ` to a given file. + +:ref:`xml_deserialization `: The module offers a function to create an +:class:`ObjectStore ` from a given xml document. +""" +import os.path + +from .xml_serialization import write_aas_xml_file +from .xml_deserialization import AASFromXmlDecoder, StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder, \ + StrictStrippedAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element + +XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'AAS.xsd') diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py new file mode 100644 index 0000000..447f50e --- /dev/null +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -0,0 +1,1409 @@ +# Copyright (c) 2020 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +""" +.. _adapter.xml.xml_deserialization: + +Module for deserializing Asset Administration Shell data from the official XML format + +This module provides the following functions for parsing XML documents: + +- :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_element` constructs a single object from an XML document + containing a single element +- :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` constructs all elements of an XML document and + stores them in a given :class:`ObjectStore ` +- :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file` constructs all elements of an XML document and returns + them in a :class:`~aas.model.provider.DictObjectStore` + +These functions take a decoder class as keyword argument, which allows parsing in failsafe (default) or non-failsafe +mode. Parsing stripped elements - used in the HTTP adapter - is also possible. It is also possible to subclass the +default decoder class and provide an own decoder. + +In failsafe mode errors regarding missing attributes and elements or invalid values are caught and logged. +In non-failsafe mode any error would abort parsing. +Error handling is done only by `_failsafe_construct()` in this module. Nearly all constructor functions are called +by other constructor functions via `_failsafe_construct()`, so an error chain is constructed in the error case, +which allows printing stacktrace-like error messages like the following in the error case (in failsafe mode of course): + + +.. code-block:: + + KeyError: aas:identification on line 252 has no attribute with name idType! + -> Failed to construct aas:identification on line 252 using construct_identifier! + -> Failed to construct aas:conceptDescription on line 247 using construct_concept_description! + + +Unlike the JSON deserialization, parsing is done top-down. Elements with a specific tag are searched on the level +directly below the level of the current xml element (in terms of parent and child relation) and parsed when +found. Constructor functions of these elements will then again search for mandatory and optional child elements +and construct them if available, and so on. +""" + +from ... import model +from lxml import etree # type: ignore +import logging +import base64 +import enum + +from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar +from .xml_serialization import NS_AAS, NS_ABAC, NS_IEC +from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE, \ + IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, \ + KEY_ELEMENTS_CLASSES_INVERSE + +logger = logging.getLogger(__name__) + +T = TypeVar("T") +RE = TypeVar("RE", bound=model.RelationshipElement) + + +def _str_to_bool(string: str) -> bool: + """ + XML only allows "false" and "true" (case-sensitive) as valid values for a boolean. + + This function checks the string and raises a ValueError if the string is neither "true" nor "false". + + :param string: String representation of a boolean. ("true" or "false") + :return: The respective boolean value. + :raises ValueError: If string is neither "true" nor "false". + """ + if string not in ("true", "false"): + raise ValueError(f"{string} is not a valid boolean! Only true and false are allowed.") + return string == "true" + + +def _tag_replace_namespace(tag: str, nsmap: Dict[str, str]) -> str: + """ + Attempts to replace the namespace in front of a tag with the prefix used in the xml document. + + :param tag: The tag of an xml element. + :param nsmap: A dict mapping prefixes to namespaces. + :return: The modified element tag. If the namespace wasn't found in nsmap, the unmodified tag is returned. + """ + split = tag.split("}") + for prefix, namespace in nsmap.items(): + if namespace == split[0][1:]: + return prefix + ":" + split[1] + return tag + + +def _element_pretty_identifier(element: etree.Element) -> str: + """ + Returns a pretty element identifier for a given XML element. + + If the prefix is known, the namespace in the element tag is replaced by the prefix. + If additionally also the sourceline is known, is is added as a suffix to name. + For example, instead of "{http://www.admin-shell.io/aas/2/0}assetAdministrationShell" this function would return + "aas:assetAdministrationShell on line $line", if both, prefix and sourceline, are known. + + :param element: The xml element. + :return: The pretty element identifier. + """ + identifier = element.tag + if element.prefix is not None: + identifier = element.prefix + ":" + element.tag.split("}")[1] + if element.sourceline is not None: + identifier += f" on line {element.sourceline}" + return identifier + + +def _exception_to_str(exception: BaseException) -> str: + """ + A helper function used to stringify exceptions. + + It removes the quotation marks '' that are put around str(KeyError), otherwise it's just calls str(exception). + + :param exception: The exception to stringify. + :return: The stringified exception. + """ + string = str(exception) + return string[1:-1] if isinstance(exception, KeyError) else string + + +def _get_child_mandatory(parent: etree.Element, child_tag: str) -> etree.Element: + """ + A helper function for getting a mandatory child element. + + :param parent: The parent element. + :param child_tag: The tag of the child element to return. + :return: The child element. + :raises KeyError: If the parent element has no child element with the given tag. + """ + child = parent.find(child_tag) + if child is None: + raise KeyError(_element_pretty_identifier(parent) + + f" has no child {_tag_replace_namespace(child_tag, parent.nsmap)}!") + return child + + +def _get_all_children_expect_tag(parent: etree.Element, expected_tag: str, failsafe: bool) -> Iterable[etree.Element]: + """ + Iterates over all children, matching the tag. + + not failsafe: Throws an error if a child element doesn't match. + failsafe: Logs a warning if a child element doesn't match. + + :param parent: The parent element. + :param expected_tag: The tag of the children. + :return: An iterator over all child elements that match child_tag. + :raises KeyError: If the tag of a child element doesn't match and failsafe is true. + """ + for child in parent: + if child.tag != expected_tag: + error_message = f"{_element_pretty_identifier(child)}, child of {_element_pretty_identifier(parent)}, " \ + f"doesn't match the expected tag {_tag_replace_namespace(expected_tag, child.nsmap)}!" + if not failsafe: + raise KeyError(error_message) + logger.warning(error_message) + continue + yield child + + +def _get_attrib_mandatory(element: etree.Element, attrib: str) -> str: + """ + A helper function for getting a mandatory attribute of an element. + + :param element: The xml element. + :param attrib: The name of the attribute. + :return: The value of the attribute. + :raises KeyError: If the attribute does not exist. + """ + if attrib not in element.attrib: + raise KeyError(f"{_element_pretty_identifier(element)} has no attribute with name {attrib}!") + return element.attrib[attrib] + + +def _get_attrib_mandatory_mapped(element: etree.Element, attrib: str, dct: Dict[str, T]) -> T: + """ + A helper function for getting a mapped mandatory attribute of an xml element. + + It first gets the attribute value using _get_attrib_mandatory(), which raises a KeyError if the attribute + does not exist. + Then it returns dct[] and raises a ValueError, if the attribute value does not exist in the dict. + + :param element: The xml element. + :param attrib: The name of the attribute. + :param dct: The dictionary that is used to map the attribute value. + :return: The mapped value of the attribute. + :raises ValueError: If the value of the attribute does not exist in dct. + """ + attrib_value = _get_attrib_mandatory(element, attrib) + if attrib_value not in dct: + raise ValueError(f"Attribute {attrib} of {_element_pretty_identifier(element)} " + f"has invalid value: {attrib_value}") + return dct[attrib_value] + + +def _get_text_or_none(element: Optional[etree.Element]) -> Optional[str]: + """ + A helper function for getting the text of an element, when it's not clear whether the element exists or not. + + This function is useful whenever the text of an optional child element is needed. + Then the text can be get with: text = _get_text_or_none(element.find("childElement") + element.find() returns either the element or None, if it doesn't exist. This is why this function accepts + an optional element, to reduce the amount of code in the constructor functions below. + + :param element: The xml element or None. + :return: The text of the xml element if the xml element is not None and if the xml element has a text. + None otherwise. + """ + return element.text if element is not None else None + + +def _get_text_mapped_or_none(element: Optional[etree.Element], dct: Dict[str, T]) -> Optional[T]: + """ + Returns dct[element.text] or None, if the element is None, has no text or the text is not in dct. + + :param element: The xml element or None. + :param dct: The dictionary that is used to map the text. + :return: The mapped text or None. + """ + text = _get_text_or_none(element) + if text is None or text not in dct: + return None + return dct[text] + + +def _get_text_mandatory(element: etree.Element) -> str: + """ + A helper function for getting the mandatory text of an element. + + :param element: The xml element. + :return: The text of the xml element. + :raises KeyError: If the xml element has no text. + """ + text = element.text + if text is None: + raise KeyError(_element_pretty_identifier(element) + " has no text!") + return text + + +def _get_text_mandatory_mapped(element: etree.Element, dct: Dict[str, T]) -> T: + """ + A helper function for getting the mapped mandatory text of an element. + + It first gets the text of the element using _get_text_mandatory(), + which raises a KeyError if the element has no text. + Then it returns dct[] and raises a ValueError, if the text of the element does not exist in the dict. + + :param element: The xml element. + :param dct: The dictionary that is used to map the text. + :return: The mapped text of the element. + :raises ValueError: If the text of the xml element does not exist in dct. + """ + text = _get_text_mandatory(element) + if text not in dct: + raise ValueError(_element_pretty_identifier(element) + f" has invalid text: {text}") + return dct[text] + + +def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[..., T], failsafe: bool, + **kwargs: Any) -> Optional[T]: + """ + A wrapper function that is used to handle exceptions raised in constructor functions. + + This is the only function of this module where exceptions are caught. + This is why constructor functions should (in almost all cases) call other constructor functions using this function, + so errors can be caught and logged in failsafe mode. + The functions accepts None as a valid value for element for the same reason _get_text_or_none() does, so it can be + called like _failsafe_construct(element.find("childElement"), ...), since element.find() can return None. + This function will also return None in this case. + + :param element: The xml element or None. + :param constructor: The constructor function to apply on the element. + :param failsafe: Indicates whether errors should be caught or re-raised. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: The constructed class instance, if construction was successful. + None if the element was None or if the construction failed. + """ + if element is None: + return None + try: + return constructor(element, **kwargs) + except (KeyError, ValueError) as e: + error_message = f"Failed to construct {_element_pretty_identifier(element)} using {constructor.__name__}!" + if not failsafe: + raise type(e)(error_message) from e + error_type = type(e).__name__ + cause: Optional[BaseException] = e + while cause is not None: + error_message = _exception_to_str(cause) + "\n -> " + error_message + cause = cause.__cause__ + logger.error(error_type + ": " + error_message) + return None + + +def _failsafe_construct_mandatory(element: etree.Element, constructor: Callable[..., T], **kwargs: Any) -> T: + """ + _failsafe_construct() but not failsafe and it returns T instead of Optional[T] + + :param element: The xml element. + :param constructor: The constructor function to apply on the xml element. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: The constructed child element. + :raises TypeError: If the result of _failsafe_construct() in non-failsafe mode was None. + This shouldn't be possible and if it happens, indicates a bug in _failsafe_construct(). + """ + constructed = _failsafe_construct(element, constructor, False, **kwargs) + if constructed is None: + raise TypeError("The result of a non-failsafe _failsafe_construct() call was None! " + "This is a bug in the Eclipse BaSyx Python SDK XML deserialization, please report it!") + return constructed + + +def _failsafe_construct_multiple(elements: Iterable[etree.Element], constructor: Callable[..., T], failsafe: bool, + **kwargs: Any) -> Iterable[T]: + """ + A generator function that applies _failsafe_construct() to multiple elements. + + :param elements: Any iterable containing any number of xml elements. + :param constructor: The constructor function to apply on the xml elements. + :param failsafe: Indicates whether errors should be caught or re-raised. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: An iterator over the successfully constructed elements. + If an error occurred while constructing an element and while in failsafe mode, + the respective element will be skipped. + """ + for element in elements: + parsed = _failsafe_construct(element, constructor, failsafe, **kwargs) + if parsed is not None: + yield parsed + + +def _child_construct_mandatory(parent: etree.Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any) \ + -> T: + """ + Shorthand for _failsafe_construct_mandatory() in combination with _get_child_mandatory(). + + :param parent: The xml element where the child element is searched. + :param child_tag: The tag of the child element to construct. + :param constructor: The constructor function for the child element. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: The constructed child element. + """ + return _failsafe_construct_mandatory(_get_child_mandatory(parent, child_tag), constructor, **kwargs) + + +def _child_construct_multiple(parent: etree.Element, expected_tag: str, constructor: Callable[..., T], + failsafe: bool, **kwargs: Any) -> Iterable[T]: + """ + Shorthand for _failsafe_construct_multiple() in combination with _get_child_multiple(). + + :param parent: The xml element where child elements are searched. + :param expected_tag: The expected tag of the child elements. + :param constructor: The constructor function for the child element. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: An iterator over successfully constructed child elements. + If an error occurred while constructing an element and while in failsafe mode, + the respective element will be skipped. + """ + return _failsafe_construct_multiple(_get_all_children_expect_tag(parent, expected_tag, failsafe), constructor, + failsafe, **kwargs) + + +def _child_text_mandatory(parent: etree.Element, child_tag: str) -> str: + """ + Shorthand for _get_text_mandatory() in combination with _get_child_mandatory(). + + :param parent: The xml element where the child element is searched. + :param child_tag: The tag of the child element to get the text from. + :return: The text of the child element. + """ + return _get_text_mandatory(_get_child_mandatory(parent, child_tag)) + + +def _child_text_mandatory_mapped(parent: etree.Element, child_tag: str, dct: Dict[str, T]) -> T: + """ + Shorthand for _get_text_mandatory_mapped() in combination with _get_child_mandatory(). + + :param parent: The xml element where the child element is searched. + :param child_tag: The tag of the child element to get the text from. + :param dct: The dictionary that is used to map the text of the child element. + :return: The mapped text of the child element. + """ + return _get_text_mandatory_mapped(_get_child_mandatory(parent, child_tag), dct) + + +def _get_modeling_kind(element: etree.Element) -> model.ModelingKind: + """ + Returns the modeling kind of an element with the default value INSTANCE, if none specified. + + :param element: The xml element. + :return: The modeling kind of the element. + """ + modeling_kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), MODELING_KIND_INVERSE) + return modeling_kind if modeling_kind is not None else model.ModelingKind.INSTANCE + + +class AASFromXmlDecoder: + """ + The default XML decoder class. + + It parses XML documents in a failsafe manner, meaning any errors encountered will be logged and invalid XML elements + will be skipped. + Most member functions support the `object_class` parameter. It was introduced so they can be overwritten + in subclasses, which allows constructing instances of subtypes. + """ + failsafe = True + stripped = False + + @classmethod + def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None: + """ + A helper function that amends optional attributes to already constructed class instances, if they inherit + from an abstract class like Referable, Identifiable, HasSemantics or Qualifiable. + + :param obj: The constructed class instance. + :param element: The respective xml element. + :return: None + """ + if isinstance(obj, model.Referable): + category = _get_text_or_none(element.find(NS_AAS + "category")) + if category is not None: + obj.category = category + description = _failsafe_construct(element.find(NS_AAS + "description"), cls.construct_lang_string_set, + cls.failsafe) + if description is not None: + obj.description = description + if isinstance(obj, model.Identifiable): + id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) + if id_short is not None: + obj.id_short = id_short + administration = _failsafe_construct(element.find(NS_AAS + "administration"), + cls.construct_administrative_information, cls.failsafe) + if administration: + obj.administration = administration + if isinstance(obj, model.HasSemantics): + semantic_id = _failsafe_construct(element.find(NS_AAS + "semanticId"), cls.construct_reference, + cls.failsafe) + if semantic_id is not None: + obj.semantic_id = semantic_id + if isinstance(obj, model.Qualifiable) and not cls.stripped: + # TODO: simplify this should our suggestion regarding the XML schema get accepted + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + for constraint in element.findall(NS_AAS + "qualifier"): + if len(constraint) == 0: + continue + if len(constraint) > 1: + logger.warning(f"{_element_pretty_identifier(constraint)} has more than one constraint, " + "using the first one...") + constructed = _failsafe_construct(constraint[0], cls.construct_constraint, cls.failsafe) + if constructed is not None: + obj.qualifier.add(constructed) + + @classmethod + def _construct_relationship_element_internal(cls, element: etree.Element, object_class: Type[RE], **_kwargs: Any) \ + -> RE: + """ + Helper function used by construct_relationship_element() and construct_annotated_relationship_element() + to reduce duplicate code + """ + relationship_element = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_construct_mandatory(element, NS_AAS + "first", cls._construct_referable_reference), + _child_construct_mandatory(element, NS_AAS + "second", cls._construct_referable_reference), + kind=_get_modeling_kind(element) + ) + cls._amend_abstract_attributes(relationship_element, element) + return relationship_element + + @classmethod + def _construct_key_tuple(cls, element: etree.Element, namespace: str = NS_AAS, **_kwargs: Any) \ + -> Tuple[model.Key, ...]: + """ + Helper function used by construct_reference() and construct_aas_reference() to reduce duplicate code + """ + keys = _get_child_mandatory(element, namespace + "keys") + return tuple(_child_construct_multiple(keys, namespace + "key", cls.construct_key, cls.failsafe)) + + @classmethod + def _construct_submodel_reference(cls, element: etree.Element, **kwargs: Any) -> model.AASReference[model.Submodel]: + """ + Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. + """ + return cls.construct_aas_reference_expect_type(element, model.Submodel, **kwargs) + + @classmethod + def _construct_asset_reference(cls, element: etree.Element, **kwargs: Any) \ + -> model.AASReference[model.Asset]: + """ + Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. + """ + return cls.construct_aas_reference_expect_type(element, model.Asset, **kwargs) + + @classmethod + def _construct_asset_administration_shell_reference(cls, element: etree.Element, **kwargs: Any) \ + -> model.AASReference[model.AssetAdministrationShell]: + """ + Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. + """ + return cls.construct_aas_reference_expect_type(element, model.AssetAdministrationShell, **kwargs) + + @classmethod + def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ + -> model.AASReference[model.Referable]: + """ + Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. + """ + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + return cls.construct_aas_reference_expect_type(element, model.Referable, **kwargs) # type: ignore + + @classmethod + def _construct_concept_description_reference(cls, element: etree.Element, **kwargs: Any) \ + -> model.AASReference[model.ConceptDescription]: + """ + Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. + """ + return cls.construct_aas_reference_expect_type(element, model.ConceptDescription, **kwargs) + + @classmethod + def construct_key(cls, element: etree.Element, object_class=model.Key, **_kwargs: Any) \ + -> model.Key: + return object_class( + _get_attrib_mandatory_mapped(element, "type", KEY_ELEMENTS_INVERSE), + _str_to_bool(_get_attrib_mandatory(element, "local")), + _get_text_mandatory(element), + _get_attrib_mandatory_mapped(element, "idType", KEY_TYPES_INVERSE) + ) + + @classmethod + def construct_reference(cls, element: etree.Element, namespace: str = NS_AAS, object_class=model.Reference, + **_kwargs: Any) -> model.Reference: + return object_class(cls._construct_key_tuple(element, namespace=namespace)) + + @classmethod + def construct_aas_reference(cls, element: etree.Element, object_class=model.AASReference, **_kwargs: Any) \ + -> model.AASReference: + """ + This constructor for AASReference determines the type of the AASReference by its keys. If no keys are present, + it will default to the type Referable. This behaviour is wanted in read_aas_xml_element(). + """ + keys = cls._construct_key_tuple(element) + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + type_: Type[model.Referable] = model.Referable # type: ignore + if len(keys) > 0: + type_ = KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, model.Referable) # type: ignore + return object_class(keys, type_) + + @classmethod + def construct_aas_reference_expect_type(cls, element: etree.Element, type_: Type[model.base._RT], + object_class=model.AASReference, **_kwargs: Any) \ + -> model.AASReference[model.base._RT]: + """ + This constructor for AASReference allows passing an expected type, which is checked against the type of the last + key of the reference. This constructor function is used by other constructor functions, since all expect a + specific target type. + """ + keys = cls._construct_key_tuple(element) + if keys and not issubclass(KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): + logger.warning("type %s of last key of reference to %s does not match reference type %s", + keys[-1].type.name, " / ".join(str(k) for k in keys), type_.__name__) + return object_class(keys, type_) + + @classmethod + def construct_administrative_information(cls, element: etree.Element, object_class=model.AdministrativeInformation, + **_kwargs: Any) -> model.AdministrativeInformation: + return object_class( + _get_text_or_none(element.find(NS_AAS + "version")), + _get_text_or_none(element.find(NS_AAS + "revision")) + ) + + @classmethod + def construct_lang_string_set(cls, element: etree.Element, namespace: str = NS_AAS, **_kwargs: Any) \ + -> model.LangStringSet: + """ + This function doesn't support the object_class parameter, because LangStringSet is just a generic type alias. + """ + lss: model.LangStringSet = {} + for lang_string in _get_all_children_expect_tag(element, namespace + "langString", cls.failsafe): + lss[_get_attrib_mandatory(lang_string, "lang")] = _get_text_mandatory(lang_string) + return lss + + @classmethod + def construct_qualifier(cls, element: etree.Element, object_class=model.Qualifier, **_kwargs: Any) \ + -> model.Qualifier: + qualifier = object_class( + _child_text_mandatory(element, NS_AAS + "type"), + _child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) + ) + value = _get_text_or_none(element.find(NS_AAS + "value")) + if value is not None: + qualifier.value = model.datatypes.from_xsd(value, qualifier.value_type) + value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) + if value_id is not None: + qualifier.value_id = value_id + cls._amend_abstract_attributes(qualifier, element) + return qualifier + + @classmethod + def construct_formula(cls, element: etree.Element, object_class=model.Formula, **_kwargs: Any) -> model.Formula: + formula = object_class() + depends_on_refs = element.find(NS_AAS + "dependsOnRefs") + if depends_on_refs is not None: + for ref in _failsafe_construct_multiple(depends_on_refs.findall(NS_AAS + "reference"), + cls.construct_reference, cls.failsafe): + formula.depends_on.add(ref) + return formula + + @classmethod + def construct_identifier(cls, element: etree.Element, object_class=model.Identifier, **_kwargs: Any) \ + -> model.Identifier: + return object_class( + _get_text_mandatory(element), + _get_attrib_mandatory_mapped(element, "idType", IDENTIFIER_TYPES_INVERSE) + ) + + @classmethod + def construct_security(cls, _element: etree.Element, object_class=model.Security, **_kwargs: Any) -> model.Security: + """ + TODO: this is just a stub implementation + """ + return object_class() + + @classmethod + def construct_view(cls, element: etree.Element, object_class=model.View, **_kwargs: Any) -> model.View: + view = object_class(_child_text_mandatory(element, NS_AAS + "idShort")) + contained_elements = element.find(NS_AAS + "containedElements") + if contained_elements is not None: + for ref in _failsafe_construct_multiple(contained_elements.findall(NS_AAS + "containedElementRef"), + cls._construct_referable_reference, cls.failsafe): + view.contained_element.add(ref) + cls._amend_abstract_attributes(view, element) + return view + + @classmethod + def construct_concept_dictionary(cls, element: etree.Element, object_class=model.ConceptDictionary, + **_kwargs: Any) -> model.ConceptDictionary: + concept_dictionary = object_class(_child_text_mandatory(element, NS_AAS + "idShort")) + concept_description = element.find(NS_AAS + "conceptDescriptionRefs") + if concept_description is not None: + for ref in _failsafe_construct_multiple(concept_description.findall(NS_AAS + "conceptDescriptionRef"), + cls._construct_concept_description_reference, cls.failsafe): + concept_dictionary.concept_description.add(ref) + cls._amend_abstract_attributes(concept_dictionary, element) + return concept_dictionary + + @classmethod + def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement: + """ + This function doesn't support the object_class parameter. + Overwrite each individual SubmodelElement/DataElement constructor function instead. + """ + # unlike in construct_data_elements, we have to declare a submodel_elements dict without namespace here first + # because mypy doesn't automatically infer Callable[..., model.SubmodelElement] for the functions, because + # construct_submodel_element_collection doesn't have the object_class parameter, but object_class_ordered and + # object_class_unordered + submodel_elements: Dict[str, Callable[..., model.SubmodelElement]] = { + "annotatedRelationshipElement": cls.construct_annotated_relationship_element, + "basicEvent": cls.construct_basic_event, + "capability": cls.construct_capability, + "entity": cls.construct_entity, + "operation": cls.construct_operation, + "relationshipElement": cls.construct_relationship_element, + "submodelElementCollection": cls.construct_submodel_element_collection + } + submodel_elements = {NS_AAS + k: v for k, v in submodel_elements.items()} + if element.tag not in submodel_elements: + return cls.construct_data_element(element, abstract_class_name="SubmodelElement", **kwargs) + return submodel_elements[element.tag](element, **kwargs) + + @classmethod + def construct_data_element(cls, element: etree.Element, abstract_class_name: str = "DataElement", **kwargs: Any) \ + -> model.DataElement: + """ + This function does not support the object_class parameter. + Overwrite each individual DataElement constructor function instead. + """ + data_elements: Dict[str, Callable[..., model.DataElement]] = {NS_AAS + k: v for k, v in { + "blob": cls.construct_blob, + "file": cls.construct_file, + "multiLanguageProperty": cls.construct_multi_language_property, + "property": cls.construct_property, + "range": cls.construct_range, + "referenceElement": cls.construct_reference_element, + }.items()} + if element.tag not in data_elements: + raise KeyError(_element_pretty_identifier(element) + f" is not a valid {abstract_class_name}!") + return data_elements[element.tag](element, **kwargs) + + @classmethod + def construct_constraint(cls, element: etree.Element, **kwargs: Any) -> model.Constraint: + """ + This function does not support the object_class parameter. + Overwrite construct_formula or construct_qualifier instead. + """ + constraints: Dict[str, Callable[..., model.Constraint]] = {NS_AAS + k: v for k, v in { + "formula": cls.construct_formula, + "qualifier": cls.construct_qualifier + }.items()} + if element.tag not in constraints: + raise KeyError(_element_pretty_identifier(element) + " is not a valid Constraint!") + return constraints[element.tag](element, **kwargs) + + @classmethod + def construct_operation_variable(cls, element: etree.Element, object_class=model.OperationVariable, + **_kwargs: Any) -> model.OperationVariable: + value = _get_child_mandatory(element, NS_AAS + "value") + if len(value) == 0: + raise KeyError(f"{_element_pretty_identifier(value)} has no submodel element!") + if len(value) > 1: + logger.warning(f"{_element_pretty_identifier(value)} has more than one submodel element, " + "using the first one...") + return object_class( + _failsafe_construct_mandatory(value[0], cls.construct_submodel_element) + ) + + @classmethod + def construct_annotated_relationship_element(cls, element: etree.Element, + object_class=model.AnnotatedRelationshipElement, **_kwargs: Any) \ + -> model.AnnotatedRelationshipElement: + annotated_relationship_element = cls._construct_relationship_element_internal(element, object_class) + if not cls.stripped: + for data_element in _get_child_mandatory(element, NS_AAS + "annotations"): + if len(data_element) == 0: + raise KeyError(f"{_element_pretty_identifier(data_element)} has no data element!") + if len(data_element) > 1: + logger.warning(f"{_element_pretty_identifier(data_element)} has more than one data element, " + "using the first one...") + constructed = _failsafe_construct(data_element[0], cls.construct_data_element, cls.failsafe) + if constructed is not None: + annotated_relationship_element.annotation.add(constructed) + return annotated_relationship_element + + @classmethod + def construct_basic_event(cls, element: etree.Element, object_class=model.BasicEvent, **_kwargs: Any) \ + -> model.BasicEvent: + basic_event = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_construct_mandatory(element, NS_AAS + "observed", cls._construct_referable_reference), + kind=_get_modeling_kind(element) + ) + cls._amend_abstract_attributes(basic_event, element) + return basic_event + + @classmethod + def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: + blob = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_text_mandatory(element, NS_AAS + "mimeType"), + kind=_get_modeling_kind(element) + ) + value = _get_text_or_none(element.find(NS_AAS + "value")) + if value is not None: + blob.value = base64.b64decode(value) + cls._amend_abstract_attributes(blob, element) + return blob + + @classmethod + def construct_capability(cls, element: etree.Element, object_class=model.Capability, **_kwargs: Any) \ + -> model.Capability: + capability = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + kind=_get_modeling_kind(element) + ) + cls._amend_abstract_attributes(capability, element) + return capability + + @classmethod + def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_kwargs: Any) -> model.Entity: + entity = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), + # pass the asset to the constructor, because self managed entities need asset references + asset=_failsafe_construct(element.find(NS_AAS + "assetRef"), cls._construct_asset_reference, cls.failsafe), + kind=_get_modeling_kind(element) + ) + if not cls.stripped: + # TODO: remove wrapping submodelElement, in accordance to future schemas + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + statements = _get_child_mandatory(element, NS_AAS + "statements") + for submodel_element in _get_all_children_expect_tag(statements, NS_AAS + "submodelElement", cls.failsafe): + if len(submodel_element) == 0: + raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") + if len(submodel_element) > 1: + logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," + " using the first one...") + constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) + if constructed is not None: + entity.statement.add(constructed) + cls._amend_abstract_attributes(entity, element) + return entity + + @classmethod + def construct_file(cls, element: etree.Element, object_class=model.File, **_kwargs: Any) -> model.File: + file = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_text_mandatory(element, NS_AAS + "mimeType"), + kind=_get_modeling_kind(element) + ) + value = _get_text_or_none(element.find(NS_AAS + "value")) + if value is not None: + file.value = value + cls._amend_abstract_attributes(file, element) + return file + + @classmethod + def construct_multi_language_property(cls, element: etree.Element, object_class=model.MultiLanguageProperty, + **_kwargs: Any) -> model.MultiLanguageProperty: + multi_language_property = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + kind=_get_modeling_kind(element) + ) + value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_lang_string_set, cls.failsafe) + if value is not None: + multi_language_property.value = value + value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) + if value_id is not None: + multi_language_property.value_id = value_id + cls._amend_abstract_attributes(multi_language_property, element) + return multi_language_property + + @classmethod + def construct_operation(cls, element: etree.Element, object_class=model.Operation, **_kwargs: Any) \ + -> model.Operation: + operation = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + kind=_get_modeling_kind(element) + ) + for input_variable in _failsafe_construct_multiple(element.findall(NS_AAS + "inputVariable"), + cls.construct_operation_variable, cls.failsafe): + operation.input_variable.append(input_variable) + for output_variable in _failsafe_construct_multiple(element.findall(NS_AAS + "outputVariable"), + cls.construct_operation_variable, cls.failsafe): + operation.output_variable.append(output_variable) + for in_output_variable in _failsafe_construct_multiple(element.findall(NS_AAS + "inoutputVariable"), + cls.construct_operation_variable, cls.failsafe): + operation.in_output_variable.append(in_output_variable) + cls._amend_abstract_attributes(operation, element) + return operation + + @classmethod + def construct_property(cls, element: etree.Element, object_class=model.Property, **_kwargs: Any) -> model.Property: + property_ = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), + kind=_get_modeling_kind(element) + ) + value = _get_text_or_none(element.find(NS_AAS + "value")) + if value is not None: + property_.value = model.datatypes.from_xsd(value, property_.value_type) + value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) + if value_id is not None: + property_.value_id = value_id + cls._amend_abstract_attributes(property_, element) + return property_ + + @classmethod + def construct_range(cls, element: etree.Element, object_class=model.Range, **_kwargs: Any) -> model.Range: + range_ = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), + kind=_get_modeling_kind(element) + ) + max_ = _get_text_or_none(element.find(NS_AAS + "max")) + if max_ is not None: + range_.max = model.datatypes.from_xsd(max_, range_.value_type) + min_ = _get_text_or_none(element.find(NS_AAS + "min")) + if min_ is not None: + range_.min = model.datatypes.from_xsd(min_, range_.value_type) + cls._amend_abstract_attributes(range_, element) + return range_ + + @classmethod + def construct_reference_element(cls, element: etree.Element, object_class=model.ReferenceElement, **_kwargs: Any) \ + -> model.ReferenceElement: + reference_element = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + kind=_get_modeling_kind(element) + ) + value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_reference, cls.failsafe) + if value is not None: + reference_element.value = value + cls._amend_abstract_attributes(reference_element, element) + return reference_element + + @classmethod + def construct_relationship_element(cls, element: etree.Element, object_class=model.RelationshipElement, + **_kwargs: Any) -> model.RelationshipElement: + return cls._construct_relationship_element_internal(element, object_class=object_class, **_kwargs) + + @classmethod + def construct_submodel_element_collection(cls, element: etree.Element, + object_class_ordered=model.SubmodelElementCollectionOrdered, + object_class_unordered=model.SubmodelElementCollectionUnordered, + **_kwargs: Any) -> model.SubmodelElementCollection: + ordered = _str_to_bool(_child_text_mandatory(element, NS_AAS + "ordered")) + collection_type = object_class_ordered if ordered else object_class_unordered + collection = collection_type( + _child_text_mandatory(element, NS_AAS + "idShort"), + kind=_get_modeling_kind(element) + ) + if not cls.stripped: + value = _get_child_mandatory(element, NS_AAS + "value") + # TODO: simplify this should our suggestion regarding the XML schema get accepted + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + for submodel_element in _get_all_children_expect_tag(value, NS_AAS + "submodelElement", cls.failsafe): + if len(submodel_element) == 0: + raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") + if len(submodel_element) > 1: + logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," + " using the first one...") + constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) + if constructed is not None: + collection.value.add(constructed) + cls._amend_abstract_attributes(collection, element) + return collection + + @classmethod + def construct_asset_administration_shell(cls, element: etree.Element, object_class=model.AssetAdministrationShell, + **_kwargs: Any) -> model.AssetAdministrationShell: + aas = object_class( + _child_construct_mandatory(element, NS_AAS + "assetRef", cls._construct_asset_reference), + _child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier) + ) + security = _failsafe_construct(element.find(NS_ABAC + "security"), cls.construct_security, cls.failsafe) + if security is not None: + aas.security = security + if not cls.stripped: + submodels = element.find(NS_AAS + "submodelRefs") + if submodels is not None: + for ref in _child_construct_multiple(submodels, NS_AAS + "submodelRef", + cls._construct_submodel_reference, cls.failsafe): + aas.submodel.add(ref) + views = element.find(NS_AAS + "views") + if views is not None: + for view in _child_construct_multiple(views, NS_AAS + "view", cls.construct_view, cls.failsafe): + aas.view.add(view) + concept_dictionaries = element.find(NS_AAS + "conceptDictionaries") + if concept_dictionaries is not None: + for cd in _child_construct_multiple(concept_dictionaries, NS_AAS + "conceptDictionary", + cls.construct_concept_dictionary, cls.failsafe): + aas.concept_dictionary.add(cd) + derived_from = _failsafe_construct(element.find(NS_AAS + "derivedFrom"), + cls._construct_asset_administration_shell_reference, cls.failsafe) + if derived_from is not None: + aas.derived_from = derived_from + cls._amend_abstract_attributes(aas, element) + return aas + + @classmethod + def construct_asset(cls, element: etree.Element, object_class=model.Asset, **_kwargs: Any) -> model.Asset: + asset = object_class( + _child_text_mandatory_mapped(element, NS_AAS + "kind", ASSET_KIND_INVERSE), + _child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier) + ) + asset_identification_model = _failsafe_construct(element.find(NS_AAS + "assetIdentificationModelRef"), + cls._construct_submodel_reference, cls.failsafe) + if asset_identification_model is not None: + asset.asset_identification_model = asset_identification_model + bill_of_material = _failsafe_construct(element.find(NS_AAS + "billOfMaterialRef"), + cls._construct_submodel_reference, cls.failsafe) + if bill_of_material is not None: + asset.bill_of_material = bill_of_material + cls._amend_abstract_attributes(asset, element) + return asset + + @classmethod + def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **_kwargs: Any) \ + -> model.Submodel: + submodel = object_class( + _child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier), + kind=_get_modeling_kind(element) + ) + if not cls.stripped: + # TODO: simplify this should our suggestion regarding the XML schema get accepted + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + for submodel_element in _get_all_children_expect_tag( + _get_child_mandatory(element, NS_AAS + "submodelElements"), NS_AAS + "submodelElement", + cls.failsafe): + if len(submodel_element) == 0: + raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") + if len(submodel_element) > 1: + logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," + " using the first one...") + constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) + if constructed is not None: + submodel.submodel_element.add(constructed) + cls._amend_abstract_attributes(submodel, element) + return submodel + + @classmethod + def construct_value_reference_pair(cls, element: etree.Element, value_format: Optional[model.DataTypeDef] = None, + object_class=model.ValueReferencePair, **_kwargs: Any) \ + -> model.ValueReferencePair: + if value_format is None: + raise ValueError("No value format given!") + return object_class( + value_format, + model.datatypes.from_xsd(_child_text_mandatory(element, NS_IEC + "value"), value_format), + _child_construct_mandatory(element, NS_IEC + "valueId", cls.construct_reference, namespace=NS_IEC) + ) + + @classmethod + def construct_value_list(cls, element: etree.Element, value_format: Optional[model.DataTypeDef] = None, + **_kwargs: Any) -> model.ValueList: + """ + This function doesn't support the object_class parameter, because ValueList is just a generic type alias. + """ + return set( + _child_construct_multiple(element, NS_IEC + "valueReferencePair", cls.construct_value_reference_pair, + cls.failsafe, value_format=value_format) + ) + + @classmethod + def construct_iec61360_concept_description(cls, element: etree.Element, + identifier: Optional[model.Identifier] = None, + object_class=model.IEC61360ConceptDescription, **_kwargs: Any) \ + -> model.IEC61360ConceptDescription: + if identifier is None: + raise ValueError("No identifier given!") + cd = object_class( + identifier, + _child_construct_mandatory(element, NS_IEC + "preferredName", cls.construct_lang_string_set, + namespace=NS_IEC) + ) + data_type = _get_text_mapped_or_none(element.find(NS_IEC + "dataType"), IEC61360_DATA_TYPES_INVERSE) + if data_type is not None: + cd.data_type = data_type + definition = _failsafe_construct(element.find(NS_IEC + "definition"), cls.construct_lang_string_set, + cls.failsafe, namespace=NS_IEC) + if definition is not None: + cd.definition = definition + short_name = _failsafe_construct(element.find(NS_IEC + "shortName"), cls.construct_lang_string_set, + cls.failsafe, namespace=NS_IEC) + if short_name is not None: + cd.short_name = short_name + unit = _get_text_or_none(element.find(NS_IEC + "unit")) + if unit is not None: + cd.unit = unit + unit_id = _failsafe_construct(element.find(NS_IEC + "unitId"), cls.construct_reference, cls.failsafe, + namespace=NS_IEC) + if unit_id is not None: + cd.unit_id = unit_id + source_of_definition = _get_text_or_none(element.find(NS_IEC + "sourceOfDefinition")) + if source_of_definition is not None: + cd.source_of_definition = source_of_definition + symbol = _get_text_or_none(element.find(NS_IEC + "symbol")) + if symbol is not None: + cd.symbol = symbol + value_format = _get_text_mapped_or_none(element.find(NS_IEC + "valueFormat"), + model.datatypes.XSD_TYPE_CLASSES) + if value_format is not None: + cd.value_format = value_format + value_list = _failsafe_construct(element.find(NS_IEC + "valueList"), cls.construct_value_list, cls.failsafe, + value_format=value_format) + if value_list is not None: + cd.value_list = value_list + value = _get_text_or_none(element.find(NS_IEC + "value")) + if value is not None and value_format is not None: + cd.value = model.datatypes.from_xsd(value, value_format) + value_id = _failsafe_construct(element.find(NS_IEC + "valueId"), cls.construct_reference, cls.failsafe, + namespace=NS_IEC) + if value_id is not None: + cd.value_id = value_id + for level_type_element in element.findall(NS_IEC + "levelType"): + level_type = _get_text_mapped_or_none(level_type_element, IEC61360_LEVEL_TYPES_INVERSE) + if level_type is None: + error_message = f"{_element_pretty_identifier(level_type_element)} has invalid value: " \ + + str(level_type_element.text) + if not cls.failsafe: + raise ValueError(error_message) + logger.warning(error_message) + continue + cd.level_types.add(level_type) + return cd + + @classmethod + def construct_concept_description(cls, element: etree.Element, object_class=model.ConceptDescription, + **_kwargs: Any) -> model.ConceptDescription: + cd: Optional[model.ConceptDescription] = None + identifier = _child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier) + # Hack to detect IEC61360ConceptDescriptions, which are represented using dataSpecification according to DotAAS + dspec_tag = NS_AAS + "embeddedDataSpecification" + dspecs = element.findall(dspec_tag) + if len(dspecs) > 1: + logger.warning(f"{_element_pretty_identifier(element)} has more than one " + f"{_tag_replace_namespace(dspec_tag, element.nsmap)}. This model currently supports only one" + f" per {_tag_replace_namespace(element.tag, element.nsmap)}!") + if len(dspecs) > 0: + dspec = dspecs[0] + dspec_content = dspec.find(NS_AAS + "dataSpecificationContent") + if dspec_content is not None: + dspec_ref = _failsafe_construct(dspec.find(NS_AAS + "dataSpecification"), cls.construct_reference, + cls.failsafe) + if dspec_ref is not None and len(dspec_ref.key) > 0 and dspec_ref.key[0].value == \ + "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0": + cd = _failsafe_construct(dspec_content.find(NS_AAS + "dataSpecificationIEC61360"), + cls.construct_iec61360_concept_description, cls.failsafe, + identifier=identifier) + if cd is None: + cd = object_class(identifier) + for ref in _failsafe_construct_multiple(element.findall(NS_AAS + "isCaseOf"), cls.construct_reference, + cls.failsafe): + cd.is_case_of.add(ref) + cls._amend_abstract_attributes(cd, element) + return cd + + +class StrictAASFromXmlDecoder(AASFromXmlDecoder): + """ + Non-failsafe XML decoder. Encountered errors won't be caught and abort parsing. + """ + failsafe = False + + +class StrippedAASFromXmlDecoder(AASFromXmlDecoder): + """ + Decoder for stripped XML elements. Used in the HTTP adapter. + """ + stripped = True + + +class StrictStrippedAASFromXmlDecoder(StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder): + """ + Non-failsafe decoder for stripped XML elements. + """ + pass + + +def _parse_xml_document(file: IO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree.Element]: + """ + Parse an XML document into an element tree + + :param file: A filename or file-like object to read the XML-serialized data from + :param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception if the document + is malformed, parsing is aborted, an error is logged and None is returned + :param parser_kwargs: Keyword arguments passed to the XMLParser constructor + :return: The root element of the element tree + """ + + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, **parser_kwargs) + + try: + return etree.parse(file, parser).getroot() + except etree.XMLSyntaxError as e: + if failsafe: + logger.error(e) + return None + raise e + + +def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFromXmlDecoder]]) \ + -> Type[AASFromXmlDecoder]: + """ + Returns the correct decoder based on the parameters failsafe and stripped. If a decoder class is given, failsafe + and stripped are ignored. + + :param failsafe: If true, a failsafe decoder is selected. Ignored if a decoder class is specified. + :param stripped: If true, a decoder for parsing stripped XML elements is selected. Ignored if a decoder class is + specified. + :param decoder: Is returned, if specified. + :return: A AASFromXmlDecoder (sub)class. + """ + if decoder is not None: + return decoder + if failsafe: + if stripped: + return StrippedAASFromXmlDecoder + return AASFromXmlDecoder + else: + if stripped: + return StrictStrippedAASFromXmlDecoder + return StrictAASFromXmlDecoder + + +@enum.unique +class XMLConstructables(enum.Enum): + """ + This enum is used to specify which type to construct in read_aas_xml_element(). + """ + KEY = enum.auto() + REFERENCE = enum.auto() + AAS_REFERENCE = enum.auto() + ADMINISTRATIVE_INFORMATION = enum.auto() + QUALIFIER = enum.auto() + FORMULA = enum.auto() + IDENTIFIER = enum.auto() + SECURITY = enum.auto() + VIEW = enum.auto() + CONCEPT_DICTIONARY = enum.auto() + OPERATION_VARIABLE = enum.auto() + ANNOTATED_RELATIONSHIP_ELEMENT = enum.auto() + BASIC_EVENT = enum.auto() + BLOB = enum.auto() + CAPABILITY = enum.auto() + ENTITY = enum.auto() + FILE = enum.auto() + MULTI_LANGUAGE_PROPERTY = enum.auto() + OPERATION = enum.auto() + PROPERTY = enum.auto() + RANGE = enum.auto() + REFERENCE_ELEMENT = enum.auto() + RELATIONSHIP_ELEMENT = enum.auto() + SUBMODEL_ELEMENT_COLLECTION = enum.auto() + ASSET_ADMINISTRATION_SHELL = enum.auto() + ASSET = enum.auto() + SUBMODEL = enum.auto() + VALUE_REFERENCE_PAIR = enum.auto() + IEC61360_CONCEPT_DESCRIPTION = enum.auto() + CONCEPT_DESCRIPTION = enum.auto() + CONSTRAINT = enum.auto() + DATA_ELEMENT = enum.auto() + SUBMODEL_ELEMENT = enum.auto() + VALUE_LIST = enum.auto() + LANG_STRING_SET = enum.auto() + + +def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool = True, stripped: bool = False, + decoder: Optional[Type[AASFromXmlDecoder]] = None, **constructor_kwargs) -> Optional[object]: + """ + Construct a single object from an XML string. The namespaces have to be declared on the object itself, since there + is no surrounding aasenv element. + + :param file: A filename or file-like object to read the XML-serialized data from + :param construct: A member of the enum :class:`~.XMLConstructables`, specifying which type to construct. + :param failsafe: If true, the document is parsed in a failsafe way: missing attributes and elements are logged + instead of causing exceptions. Defect objects are skipped. + This parameter is ignored if a decoder class is specified. + :param stripped: If true, stripped XML elements are parsed. + 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 XML elements + :param constructor_kwargs: Keyword arguments passed to the constructor function + :return: The constructed object or None, if an error occurred in failsafe mode. + """ + decoder_ = _select_decoder(failsafe, stripped, decoder) + constructor: Callable[..., object] + + if construct == XMLConstructables.KEY: + constructor = decoder_.construct_key + elif construct == XMLConstructables.REFERENCE: + constructor = decoder_.construct_reference + elif construct == XMLConstructables.AAS_REFERENCE: + constructor = decoder_.construct_aas_reference + elif construct == XMLConstructables.ADMINISTRATIVE_INFORMATION: + constructor = decoder_.construct_administrative_information + elif construct == XMLConstructables.QUALIFIER: + constructor = decoder_.construct_qualifier + elif construct == XMLConstructables.FORMULA: + constructor = decoder_.construct_formula + elif construct == XMLConstructables.IDENTIFIER: + constructor = decoder_.construct_identifier + elif construct == XMLConstructables.SECURITY: + constructor = decoder_.construct_security + elif construct == XMLConstructables.VIEW: + constructor = decoder_.construct_view + elif construct == XMLConstructables.CONCEPT_DICTIONARY: + constructor = decoder_.construct_concept_dictionary + elif construct == XMLConstructables.OPERATION_VARIABLE: + constructor = decoder_.construct_operation_variable + elif construct == XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT: + constructor = decoder_.construct_annotated_relationship_element + elif construct == XMLConstructables.BASIC_EVENT: + constructor = decoder_.construct_basic_event + elif construct == XMLConstructables.BLOB: + constructor = decoder_.construct_blob + elif construct == XMLConstructables.CAPABILITY: + constructor = decoder_.construct_capability + elif construct == XMLConstructables.ENTITY: + constructor = decoder_.construct_entity + elif construct == XMLConstructables.FILE: + constructor = decoder_.construct_file + elif construct == XMLConstructables.MULTI_LANGUAGE_PROPERTY: + constructor = decoder_.construct_multi_language_property + elif construct == XMLConstructables.OPERATION: + constructor = decoder_.construct_operation + elif construct == XMLConstructables.PROPERTY: + constructor = decoder_.construct_property + elif construct == XMLConstructables.RANGE: + constructor = decoder_.construct_range + elif construct == XMLConstructables.REFERENCE_ELEMENT: + constructor = decoder_.construct_reference_element + elif construct == XMLConstructables.RELATIONSHIP_ELEMENT: + constructor = decoder_.construct_relationship_element + elif construct == XMLConstructables.SUBMODEL_ELEMENT_COLLECTION: + constructor = decoder_.construct_submodel_element_collection + elif construct == XMLConstructables.ASSET_ADMINISTRATION_SHELL: + constructor = decoder_.construct_asset_administration_shell + elif construct == XMLConstructables.ASSET: + constructor = decoder_.construct_asset + elif construct == XMLConstructables.SUBMODEL: + constructor = decoder_.construct_submodel + elif construct == XMLConstructables.VALUE_REFERENCE_PAIR: + constructor = decoder_.construct_value_reference_pair + elif construct == XMLConstructables.IEC61360_CONCEPT_DESCRIPTION: + constructor = decoder_.construct_iec61360_concept_description + elif construct == XMLConstructables.CONCEPT_DESCRIPTION: + constructor = decoder_.construct_concept_description + # the following constructors decide which constructor to call based on the elements tag + elif construct == XMLConstructables.CONSTRAINT: + constructor = decoder_.construct_constraint + elif construct == XMLConstructables.DATA_ELEMENT: + constructor = decoder_.construct_data_element + elif construct == XMLConstructables.SUBMODEL_ELEMENT: + constructor = decoder_.construct_submodel_element + # type aliases + elif construct == XMLConstructables.VALUE_LIST: + constructor = decoder_.construct_value_list + elif construct == XMLConstructables.LANG_STRING_SET: + constructor = decoder_.construct_lang_string_set + else: + raise ValueError(f"{construct.name} cannot be constructed!") + + element = _parse_xml_document(file, failsafe=decoder_.failsafe) + return _failsafe_construct(element, constructor, decoder_.failsafe, **constructor_kwargs) + + +def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identifiable], file: IO, + replace_existing: bool = False, ignore_existing: bool = False, failsafe: bool = True, + stripped: bool = False, decoder: Optional[Type[AASFromXmlDecoder]] = None, + **parser_kwargs: Any) -> Set[model.Identifier]: + """ + Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 + into a given :class:`ObjectStore `. + + :param object_store: The :class:`ObjectStore ` in which the + :class:`~aas.model.base.Identifiable` objects should be stored + :param file: A filename or file-like object to read the XML-serialized data from + :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not + :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. + This parameter is ignored if replace_existing is True. + :param failsafe: If `True`, the document is parsed in a failsafe way: missing attributes and elements are logged + instead of causing exceptions. Defect objects are skipped. + This parameter is ignored if a decoder class is specified. + :param stripped: If `True`, stripped XML elements are parsed. + 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 XML elements + :param parser_kwargs: Keyword arguments passed to the XMLParser constructor + :return: A set of :class:`Identifiers ` that were added to object_store + """ + ret: Set[model.Identifier] = set() + + decoder_ = _select_decoder(failsafe, stripped, decoder) + + element_constructors: Dict[str, Callable[..., model.Identifiable]] = { + "assetAdministrationShell": decoder_.construct_asset_administration_shell, + "asset": decoder_.construct_asset, + "submodel": decoder_.construct_submodel, + "conceptDescription": decoder_.construct_concept_description + } + + element_constructors = {NS_AAS + k: v for k, v in element_constructors.items()} + + root = _parse_xml_document(file, failsafe=decoder_.failsafe, **parser_kwargs) + + if root is None: + return ret + + # Add AAS objects to ObjectStore + for list_ in root: + element_tag = list_.tag[:-1] + if list_.tag[-1] != "s" or element_tag not in element_constructors: + error_message = f"Unexpected top-level list {_element_pretty_identifier(list_)}!" + if not decoder_.failsafe: + raise TypeError(error_message) + logger.warning(error_message) + continue + constructor = element_constructors[element_tag] + for element in _child_construct_multiple(list_, element_tag, constructor, decoder_.failsafe): + if element.identification in ret: + error_message = f"{element} has a duplicate identifier already parsed in the document!" + if not decoder_.failsafe: + raise KeyError(error_message) + logger.error(error_message + " skipping it...") + continue + existing_element = object_store.get(element.identification) + if existing_element is not None: + if not replace_existing: + error_message = f"object with identifier {element.identification} already exists " \ + f"in the object store: {existing_element}!" + if not ignore_existing: + raise KeyError(error_message + f" failed to insert {element}!") + logger.info(error_message + f" skipping insertion of {element}...") + continue + object_store.discard(existing_element) + object_store.add(element) + ret.add(element.identification) + return ret + + +def read_aas_xml_file(file: IO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]: + """ + A wrapper of :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an + empty :class:`~aas.model.provider.DictObjectStore`. This function supports + the same keyword arguments as :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`. + + :param file: A filename or file-like object to read the XML-serialized data from + :param kwargs: Keyword arguments passed to :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` + :return: A :class:`~aas.model.provider.DictObjectStore` containing all AAS objects from the XML file + """ + object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + read_aas_xml_file_into(object_store, file, **kwargs) + return object_store diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py new file mode 100644 index 0000000..f2707f2 --- /dev/null +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -0,0 +1,898 @@ +# Copyright (c) 2020 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +""" +.. _adapter.xml.xml_serialization: + +Module for serializing Asset Administration Shell data to the official XML format + +How to use: + +- For generating an XML-File from a :class:`~aas.model.provider.AbstractObjectStore`, check out the function + :meth:`~aas.adapter.xml.xml_serialization.write_aas_xml_file`. +- For serializing any object to an XML fragment, that fits the XML specification from 'Details of the + Asset Administration Shell', chapter 5.4, check out `_to_xml()`. These functions return + an :class:`xml.etree.ElementTree.Element` object to be serialized into XML. +""" + +from lxml import etree # type: ignore +from typing import Dict, IO, Optional +import base64 + +from basyx.aas import model +from .. import _generic + + +# ############################################################## +# functions to manipulate etree.Elements more effectively +# ############################################################## + +# Namespace definition +NS_AAS = "{http://www.admin-shell.io/aas/2/0}" +NS_ABAC = "{http://www.admin-shell.io/aas/abac/2/0}" +NS_AAS_COMMON = "{http://www.admin-shell.io/aas_common/2/0}" +NS_XSI = "{http://www.w3.org/2001/XMLSchema-instance}" +NS_XS = "{http://www.w3.org/2001/XMLSchema}" +NS_IEC = "{http://www.admin-shell.io/IEC61360/2/0}" +NS_MAP = {"aas": "http://www.admin-shell.io/aas/2/0", + "abac": "http://www.admin-shell.io/aas/abac/2/0", + "aas_common": "http://www.admin-shell.io/aas_common/2/0", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", + "IEC": "http://www.admin-shell.io/IEC61360/2/0", + "xs": "http://www.w3.org/2001/XMLSchema"} + + +def _generate_element(name: str, + text: Optional[str] = None, + attributes: Optional[Dict] = None) -> etree.Element: + """ + generate an ElementTree.Element object + + :param name: namespace+tag_name of the element + :param text: Text of the element. Default is None + :param attributes: Attributes of the elements in form of a dict {"attribute_name": "attribute_content"} + :return: ElementTree.Element object + """ + et_element = etree.Element(name) + if text: + et_element.text = text + if attributes: + for key, value in attributes.items(): + et_element.set(key, value) + return et_element + + +def boolean_to_xml(obj: bool) -> str: + """ + serialize a boolean to XML + + :param obj: boolean + :return: string in the XML accepted form + """ + if obj: + return "true" + else: + return "false" + + +# ############################################################## +# transformation functions to serialize abstract classes from model.base +# ############################################################## + + +def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: + """ + Generates an XML element and adds attributes of abstract base classes of `obj`. + + If the object obj is inheriting from any abstract AAS class, this function adds all the serialized information of + those abstract classes to the generated element. + + :param tag: tag of the element + :param obj: an object of the AAS + :return: parent element with the serialized information from the abstract classes + """ + elm = _generate_element(tag) + if isinstance(obj, model.Referable): + elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) + if obj.category: + elm.append(_generate_element(name=NS_AAS + "category", text=obj.category)) + if obj.description: + elm.append(lang_string_set_to_xml(obj.description, tag=NS_AAS + "description")) + if isinstance(obj, model.Identifiable): + elm.append(_generate_element(name=NS_AAS + "identification", + text=obj.identification.id, + attributes={"idType": _generic.IDENTIFIER_TYPES[obj.identification.id_type]})) + if obj.administration: + elm.append(administrative_information_to_xml(obj.administration)) + if isinstance(obj, model.HasKind): + if obj.kind is model.ModelingKind.TEMPLATE: + elm.append(_generate_element(name=NS_AAS + "kind", text="Template")) + else: + # then modeling-kind is Instance + elm.append(_generate_element(name=NS_AAS + "kind", text="Instance")) + if isinstance(obj, model.HasSemantics): + if obj.semantic_id: + elm.append(reference_to_xml(obj.semantic_id, tag=NS_AAS+"semanticId")) + if isinstance(obj, model.Qualifiable): + if obj.qualifier: + for qualifier in obj.qualifier: + et_qualifier = _generate_element(NS_AAS+"qualifier") + if isinstance(qualifier, model.Qualifier): + et_qualifier.append(qualifier_to_xml(qualifier, tag=NS_AAS+"qualifier")) + if isinstance(qualifier, model.Formula): + et_qualifier.append(formula_to_xml(qualifier, tag=NS_AAS+"formula")) + elm.append(et_qualifier) + return elm + + +# ############################################################## +# transformation functions to serialize classes from model.base +# ############################################################## + + +def _value_to_xml(value: model.ValueDataType, + value_type: model.DataTypeDef, + tag: str = NS_AAS+"value") -> etree.Element: + """ + Serialization of objects of class ValueDataType to XML + + :param value: model.ValueDataType object + :param value_type: Corresponding model.DataTypeDef + :param tag: tag of the serialized ValueDataType object + :return: Serialized ElementTree.Element object + """ + # todo: add "{NS_XSI+"type": "xs:"+model.datatypes.XSD_TYPE_NAMES[value_type]}" as attribute, if the schema allows + # it + return _generate_element(tag, + text=model.datatypes.xsd_repr(value)) + + +def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: + """ + serialization of objects of class LangStringSet to XML + + :param obj: object of class LangStringSet + :param tag: tag name of the returned XML element (incl. namespace) + :return: serialized ElementTree object + """ + et_lss = _generate_element(name=tag) + for language in obj: + et_lss.append(_generate_element(name=NS_AAS + "langString", + text=obj[language], + attributes={"lang": language})) + return et_lss + + +def administrative_information_to_xml(obj: model.AdministrativeInformation, + tag: str = NS_AAS+"administration") -> etree.Element: + """ + serialization of objects of class AdministrativeInformation to XML + + :param obj: object of class AdministrativeInformation + :param tag: tag of the serialized element. default is "administration" + :return: serialized ElementTree object + """ + et_administration = _generate_element(tag) + if obj.version: + et_administration.append(_generate_element(name=NS_AAS + "version", text=obj.version)) + if obj.revision: + et_administration.append(_generate_element(name=NS_AAS + "revision", text=obj.revision)) + return et_administration + + +def data_element_to_xml(obj: model.DataElement) -> etree.Element: + """ + serialization of objects of class DataElement to XML + + :param obj: Object of class DataElement + :return: serialized ElementTree element + """ + if isinstance(obj, model.MultiLanguageProperty): + return multi_language_property_to_xml(obj) + if isinstance(obj, model.Property): + return property_to_xml(obj) + if isinstance(obj, model.Range): + return range_to_xml(obj) + if isinstance(obj, model.Blob): + return blob_to_xml(obj) + if isinstance(obj, model.File): + return file_to_xml(obj) + if isinstance(obj, model.ReferenceElement): + return reference_element_to_xml(obj) + + +def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etree.Element: + """ + serialization of objects of class Reference to XML + + :param obj: object of class Reference + :param tag: tag of the returned element + :return: serialized ElementTree + """ + et_reference = _generate_element(tag) + et_keys = _generate_element(name=NS_AAS + "keys") + for aas_key in obj.key: + et_keys.append(_generate_element(name=NS_AAS + "key", + text=aas_key.value, + attributes={"idType": _generic.KEY_TYPES[aas_key.id_type], + "local": boolean_to_xml(aas_key.local), + "type": _generic.KEY_ELEMENTS[aas_key.type]})) + et_reference.append(et_keys) + return et_reference + + +def formula_to_xml(obj: model.Formula, tag: str = NS_AAS+"formula") -> etree.Element: + """ + serialization of objects of class Formula to XML + + :param obj: object of class Formula + :param tag: tag of the ElementTree object, default is "formula" + :return: serialized ElementTree object + """ + et_formula = abstract_classes_to_xml(tag, obj) + if obj.depends_on: + et_depends_on = _generate_element(name=NS_AAS + "dependsOnRefs", text=None) + for aas_reference in obj.depends_on: + et_depends_on.append(reference_to_xml(aas_reference, NS_AAS+"reference")) + et_formula.append(et_depends_on) + return et_formula + + +def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etree.Element: + """ + serialization of objects of class Qualifier to XML + + :param obj: object of class Qualifier + :param tag: tag of the serialized ElementTree object, default is "qualifier" + :return: serialized ElementTreeObject + """ + et_qualifier = abstract_classes_to_xml(tag, obj) + et_qualifier.append(_generate_element(NS_AAS + "type", text=obj.type)) + et_qualifier.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) + if obj.value_id: + et_qualifier.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) + if obj.value: + et_qualifier.append(_value_to_xml(obj.value, obj.value_type)) + return et_qualifier + + +def value_reference_pair_to_xml(obj: model.ValueReferencePair, + tag: str = NS_AAS+"valueReferencePair") -> etree.Element: + """ + serialization of objects of class ValueReferencePair to XML + + todo: couldn't find it in the official schema, so guessing how to implement serialization + check namespace, tag and correct serialization + + :param obj: object of class ValueReferencePair + :param tag: tag of the serialized element, default is "valueReferencePair" + :return: serialized ElementTree object + """ + et_vrp = _generate_element(tag) + et_vrp.append(_value_to_xml(obj.value, obj.value_type)) + et_vrp.append(reference_to_xml(obj.value_id, "valueId")) + return et_vrp + + +def value_list_to_xml(obj: model.ValueList, + tag: str = NS_AAS+"valueList") -> etree.Element: + """ + serialization of objects of class ValueList to XML + + todo: couldn't find it in the official schema, so guessing how to implement serialization + + :param obj: object of class ValueList + :param tag: tag of the serialized element, default is "valueList" + :return: serialized ElementTree object + """ + et_value_list = _generate_element(tag) + for aas_reference_pair in obj: + et_value_list.append(value_reference_pair_to_xml(aas_reference_pair, "valueReferencePair")) + return et_value_list + + +# ############################################################## +# transformation functions to serialize classes from model.aas +# ############################################################## + + +def view_to_xml(obj: model.View, tag: str = NS_AAS+"view") -> etree.Element: + """ + serialization of objects of class View to XML + + :param obj: object of class View + :param tag: namespace+tag of the ElementTree object. default is "view" + :return: serialized ElementTree object + """ + et_view = abstract_classes_to_xml(tag, obj) + et_contained_elements = _generate_element(name=NS_AAS + "containedElements") + if obj.contained_element: + for contained_element in obj.contained_element: + et_contained_elements.append(reference_to_xml(contained_element, NS_AAS+"containedElementRef")) + et_view.append(et_contained_elements) + return et_view + + +def asset_to_xml(obj: model.Asset, tag: str = NS_AAS+"asset") -> etree.Element: + """ + serialization of objects of class Asset to XML + + :param obj: object of class Asset + :param tag: namespace+tag of the ElementTree object. default is "asset" + :return: serialized ElementTree object + """ + et_asset = abstract_classes_to_xml(tag, obj) + if obj.asset_identification_model: + et_asset.append(reference_to_xml(obj.asset_identification_model, NS_AAS+"assetIdentificationModelRef")) + if obj.bill_of_material: + et_asset.append(reference_to_xml(obj.bill_of_material, NS_AAS+"billOfMaterialRef")) + et_asset.append(_generate_element(name=NS_AAS + "kind", text=_generic.ASSET_KIND[obj.kind])) + return et_asset + + +def concept_description_to_xml(obj: model.ConceptDescription, + tag: str = NS_AAS+"conceptDescription") -> etree.Element: + """ + serialization of objects of class ConceptDescription to XML + + :param obj: object of class ConceptDescription + :param tag: tag of the ElementTree object. default is "conceptDescription" + :return: serialized ElementTree object + """ + et_concept_description = abstract_classes_to_xml(tag, obj) + if isinstance(obj, model.concept.IEC61360ConceptDescription): + et_embedded_data_specification = _generate_element(NS_AAS+"embeddedDataSpecification") + et_data_spec_content = _generate_element(NS_AAS+"dataSpecificationContent") + et_data_spec_content.append(_iec61360_concept_description_to_xml(obj)) + et_embedded_data_specification.append(et_data_spec_content) + et_concept_description.append(et_embedded_data_specification) + et_embedded_data_specification.append(reference_to_xml(model.Reference(tuple([model.Key( + model.KeyElements.GLOBAL_REFERENCE, + False, + "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0", + model.KeyType.IRI + )])), NS_AAS+"dataSpecification")) + if obj.is_case_of: + for reference in obj.is_case_of: + et_concept_description.append(reference_to_xml(reference, NS_AAS+"isCaseOf")) + return et_concept_description + + +def _iec61360_concept_description_to_xml(obj: model.concept.IEC61360ConceptDescription, + tag: str = NS_AAS+"dataSpecificationIEC61360") -> etree.Element: + """ + Add the 'embeddedDataSpecifications' attribute to IEC61360ConceptDescription's JSON representation. + + `IEC61360ConceptDescription` is not a distinct class according DotAAS, but instead is built by referencing + "DataSpecificationIEC61360" as dataSpecification. However, we implemented it as an explicit class, inheriting from + ConceptDescription, but we want to generate compliant XML documents. So, we fake the XML structure of an object + with dataSpecifications. + + :param obj: model.concept.IEC61360ConceptDescription object + :param tag: name of the serialized lss_tag + :return: serialized ElementTree object + """ + + def _iec_lang_string_set_to_xml(lss: model.LangStringSet, lss_tag: str) -> etree.Element: + """ + serialization of objects of class LangStringSet to XML + + :param lss: object of class LangStringSet + :param lss_tag: lss_tag name of the returned XML element (incl. namespace) + :return: serialized ElementTree object + """ + et_lss = _generate_element(name=lss_tag) + for language in lss: + et_lss.append(_generate_element(name=NS_IEC + "langString", + text=lss[language], + attributes={"lang": language})) + return et_lss + + def _iec_reference_to_xml(ref: model.Reference, ref_tag: str = NS_AAS + "reference") -> etree.Element: + """ + serialization of objects of class Reference to XML + + :param ref: object of class Reference + :param ref_tag: ref_tag of the returned element + :return: serialized ElementTree + """ + et_reference = _generate_element(ref_tag) + et_keys = _generate_element(name=NS_IEC + "keys") + for aas_key in ref.key: + et_keys.append(_generate_element(name=NS_IEC + "key", + text=aas_key.value, + attributes={"idType": _generic.KEY_TYPES[aas_key.id_type], + "local": boolean_to_xml(aas_key.local), + "type": _generic.KEY_ELEMENTS[aas_key.type]})) + et_reference.append(et_keys) + return et_reference + + def _iec_value_reference_pair_to_xml(vrp: model.ValueReferencePair, + vrp_tag: str = NS_IEC + "valueReferencePair") -> etree.Element: + """ + serialization of objects of class ValueReferencePair to XML + + :param vrp: object of class ValueReferencePair + :param vrp_tag: vl_tag of the serialized element, default is "valueReferencePair" + :return: serialized ElementTree object + """ + et_vrp = _generate_element(vrp_tag) + et_vrp.append(_iec_reference_to_xml(vrp.value_id, NS_IEC + "valueId")) + et_vrp.append(_value_to_xml(vrp.value, vrp.value_type, tag=NS_IEC+"value")) + return et_vrp + + def _iec_value_list_to_xml(vl: model.ValueList, + vl_tag: str = NS_IEC + "valueList") -> etree.Element: + """ + serialization of objects of class ValueList to XML + + :param vl: object of class ValueList + :param vl_tag: vl_tag of the serialized element, default is "valueList" + :return: serialized ElementTree object + """ + et_value_list = _generate_element(vl_tag) + for aas_reference_pair in vl: + et_value_list.append(_iec_value_reference_pair_to_xml(aas_reference_pair, NS_IEC+"valueReferencePair")) + return et_value_list + + et_iec = _generate_element(tag) + et_iec.append(_iec_lang_string_set_to_xml(obj.preferred_name, NS_IEC + "preferredName")) + if obj.short_name: + et_iec.append(_iec_lang_string_set_to_xml(obj.short_name, NS_IEC + "shortName")) + if obj.unit: + et_iec.append(_generate_element(NS_IEC+"unit", text=obj.unit)) + if obj.unit_id: + et_iec.append(_iec_reference_to_xml(obj.unit_id, NS_IEC+"unitId")) + if obj.source_of_definition: + et_iec.append(_generate_element(NS_IEC+"sourceOfDefinition", text=obj.source_of_definition)) + if obj.symbol: + et_iec.append(_generate_element(NS_IEC+"symbol", text=obj.symbol)) + if obj.data_type: + et_iec.append(_generate_element(NS_IEC+"dataType", text=_generic.IEC61360_DATA_TYPES[obj.data_type])) + if obj.definition: + et_iec.append(_iec_lang_string_set_to_xml(obj.definition, NS_IEC + "definition")) + if obj.value_format: + et_iec.append(_generate_element(NS_IEC+"valueFormat", text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) + if obj.value_list: + et_iec.append(_iec_value_list_to_xml(obj.value_list, NS_IEC+"valueList")) + if obj.value: + et_iec.append(_generate_element(NS_IEC+"value", text=model.datatypes.xsd_repr(obj.value))) + if obj.value_id: + et_iec.append(_iec_reference_to_xml(obj.value_id, NS_IEC+"valueId")) + if obj.level_types: + for level_type in obj.level_types: + et_iec.append(_generate_element(NS_IEC+"levelType", text=_generic.IEC61360_LEVEL_TYPES[level_type])) + return et_iec + + +def concept_dictionary_to_xml(obj: model.ConceptDictionary, + tag: str = NS_AAS+"conceptDictionary") -> etree.Element: + """ + serialization of objects of class ConceptDictionary to XML + + :param obj: object of class ConceptDictionary + :param tag: tag of the ElementTree object. default is "conceptDictionary" + :return: serialized ElementTree object + """ + et_concept_dictionary = abstract_classes_to_xml(tag, obj) + et_concept_descriptions_refs = _generate_element(NS_AAS + "conceptDescriptionRefs") + if obj.concept_description: + for reference in obj.concept_description: + et_concept_descriptions_refs.append(reference_to_xml(reference, NS_AAS+"conceptDescriptionRef")) + et_concept_dictionary.append(et_concept_descriptions_refs) + return et_concept_dictionary + + +def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, + tag: str = NS_AAS+"assetAdministrationShell") -> etree.Element: + """ + serialization of objects of class AssetAdministrationShell to XML + + :param obj: object of class AssetAdministrationShell + :param tag: tag of the ElementTree object. default is "assetAdministrationShell" + :return: serialized ElementTree object + """ + et_aas = abstract_classes_to_xml(tag, obj) + if obj.derived_from: + et_aas.append(reference_to_xml(obj.derived_from, tag=NS_AAS+"derivedFrom")) + et_aas.append(reference_to_xml(obj.asset, tag=NS_AAS+"assetRef")) + if obj.submodel: + et_submodels = _generate_element(NS_AAS + "submodelRefs") + for reference in obj.submodel: + et_submodels.append(reference_to_xml(reference, tag=NS_AAS+"submodelRef")) + et_aas.append(et_submodels) + if obj.view: + et_views = _generate_element(NS_AAS + "views") + for view in obj.view: + et_views.append(view_to_xml(view, NS_AAS+"view")) + et_aas.append(et_views) + if obj.concept_dictionary: + et_concept_dictionaries = _generate_element(NS_AAS + "conceptDictionaries") + for concept_dictionary in obj.concept_dictionary: + et_concept_dictionaries.append(concept_dictionary_to_xml(concept_dictionary, + NS_AAS+"conceptDictionary")) + et_aas.append(et_concept_dictionaries) + if obj.security: + et_aas.append(security_to_xml(obj.security, tag=NS_ABAC+"security")) + return et_aas + + +# ############################################################## +# transformation functions to serialize classes from model.security +# ############################################################## + + +def security_to_xml(obj: model.Security, + tag: str = NS_ABAC+"security") -> etree.Element: + """ + serialization of objects of class Security to XML + + todo: This is not yet implemented + + :param obj: object of class Security + :param tag: tag of the serialized element (optional). Default is "security" + :return: serialized ElementTree object + """ + return abstract_classes_to_xml(tag, obj) + + +# ############################################################## +# transformation functions to serialize classes from model.submodel +# ############################################################## + + +def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: + """ + serialization of objects of class SubmodelElement to XML + + :param obj: object of class SubmodelElement + :return: serialized ElementTree object + """ + if isinstance(obj, model.DataElement): + return data_element_to_xml(obj) + if isinstance(obj, model.BasicEvent): + return basic_event_to_xml(obj) + if isinstance(obj, model.Capability): + return capability_to_xml(obj) + if isinstance(obj, model.Entity): + return entity_to_xml(obj) + if isinstance(obj, model.Operation): + return operation_to_xml(obj) + if isinstance(obj, model.AnnotatedRelationshipElement): + return annotated_relationship_element_to_xml(obj) + if isinstance(obj, model.RelationshipElement): + return relationship_element_to_xml(obj) + if isinstance(obj, model.SubmodelElementCollection): + return submodel_element_collection_to_xml(obj) + + +def submodel_to_xml(obj: model.Submodel, + tag: str = NS_AAS+"submodel") -> etree.Element: + """ + serialization of objects of class Submodel to XML + + :param obj: object of class Submodel + :param tag: tag of the serialized element (optional). Default is "submodel" + :return: serialized ElementTree object + """ + et_submodel = abstract_classes_to_xml(tag, obj) + et_submodel_elements = _generate_element(NS_AAS + "submodelElements") + if obj.submodel_element: + for submodel_element in obj.submodel_element: + # TODO: simplify this should our suggestion regarding the XML schema get accepted + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + et_submodel_element = _generate_element(NS_AAS+"submodelElement") + et_submodel_element.append(submodel_element_to_xml(submodel_element)) + et_submodel_elements.append(et_submodel_element) + et_submodel.append(et_submodel_elements) + return et_submodel + + +def property_to_xml(obj: model.Property, + tag: str = NS_AAS+"property") -> etree.Element: + """ + serialization of objects of class Property to XML + + :param obj: object of class Property + :param tag: tag of the serialized element (optional), default is "property" + :return: serialized ElementTree object + """ + et_property = abstract_classes_to_xml(tag, obj) + et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) + if obj.value: + et_property.append(_value_to_xml(obj.value, obj.value_type)) + if obj.value_id: + et_property.append(reference_to_xml(obj.value_id, NS_AAS + "valueId")) + return et_property + + +def multi_language_property_to_xml(obj: model.MultiLanguageProperty, + tag: str = NS_AAS+"multiLanguageProperty") -> etree.Element: + """ + serialization of objects of class MultiLanguageProperty to XML + + :param obj: object of class MultiLanguageProperty + :param tag: tag of the serialized element (optional), default is "multiLanguageProperty" + :return: serialized ElementTree object + """ + et_multi_language_property = abstract_classes_to_xml(tag, obj) + if obj.value_id: + et_multi_language_property.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) + if obj.value: + et_multi_language_property.append(lang_string_set_to_xml(obj.value, tag=NS_AAS + "value")) + return et_multi_language_property + + +def range_to_xml(obj: model.Range, + tag: str = NS_AAS+"range") -> etree.Element: + """ + serialization of objects of class Range to XML + + :param obj: object of class Range + :param tag: namespace+tag of the serialized element (optional), default is "range + :return: serialized ElementTree object + """ + et_range = abstract_classes_to_xml(tag, obj) + et_range.append(_generate_element(name=NS_AAS + "valueType", + text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) + if obj.min is not None: + et_range.append(_value_to_xml(obj.min, obj.value_type, tag=NS_AAS+"min")) + if obj.max is not None: + et_range.append(_value_to_xml(obj.max, obj.value_type, tag=NS_AAS+"max")) + return et_range + + +def blob_to_xml(obj: model.Blob, + tag: str = NS_AAS+"blob") -> etree.Element: + """ + serialization of objects of class Blob to XML + + :param obj: object of class Blob + :param tag: tag of the serialized element, default is "blob" + :return: serialized ElementTree object + """ + et_blob = abstract_classes_to_xml(tag, obj) + et_value = etree.Element(NS_AAS + "value") + if obj.value is not None: + et_value.text = base64.b64encode(obj.value).decode() + et_blob.append(et_value) + et_blob.append(_generate_element(NS_AAS + "mimeType", text=obj.mime_type)) + return et_blob + + +def file_to_xml(obj: model.File, + tag: str = NS_AAS+"file") -> etree.Element: + """ + serialization of objects of class File to XML + + :param obj: object of class File + :param tag: tag of the serialized element, default is "file" + :return: serialized ElementTree object + """ + et_file = abstract_classes_to_xml(tag, obj) + et_file.append(_generate_element(NS_AAS + "mimeType", text=obj.mime_type)) + if obj.value: + et_file.append(_generate_element(NS_AAS + "value", text=obj.value)) + return et_file + + +def reference_element_to_xml(obj: model.ReferenceElement, + tag: str = NS_AAS+"referenceElement") -> etree.Element: + """ + serialization of objects of class ReferenceElement to XMl + + :param obj: object of class ReferenceElement + :param tag: namespace+tag of the serialized element (optional), default is "referenceElement" + :return: serialized ElementTree object + """ + et_reference_element = abstract_classes_to_xml(tag, obj) + if obj.value: + et_reference_element.append(reference_to_xml(obj.value, NS_AAS+"value")) + return et_reference_element + + +def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, + tag: str = NS_AAS+"submodelElementCollection") -> etree.Element: + """ + serialization of objects of class SubmodelElementCollection to XML + + Note that we do not have parameter "allowDuplicates" in out implementation + + :param obj: object of class SubmodelElementCollection + :param tag: namespace+tag of the serialized element (optional), default is "submodelElementCollection" + :return: serialized ElementTree object + """ + et_submodel_element_collection = abstract_classes_to_xml(tag, obj) + # todo: remove wrapping submodelElement-tag, in accordance to future schema + et_value = _generate_element(NS_AAS + "value") + if obj.value: + for submodel_element in obj.value: + et_submodel_element = _generate_element(NS_AAS+"submodelElement") + et_submodel_element.append(submodel_element_to_xml(submodel_element)) + et_value.append(et_submodel_element) + et_submodel_element_collection.append(et_value) + et_submodel_element_collection.append(_generate_element(NS_AAS + "ordered", text=boolean_to_xml(obj.ordered))) + et_submodel_element_collection.append(_generate_element(NS_AAS + "allowDuplicates", text="false")) + return et_submodel_element_collection + + +def relationship_element_to_xml(obj: model.RelationshipElement, + tag: str = NS_AAS+"relationshipElement") -> etree.Element: + """ + serialization of objects of class RelationshipElement to XML + + :param obj: object of class RelationshipElement + :param tag: tag of the serialized element (optional), default is "relationshipElement" + :return: serialized ELementTree object + """ + et_relationship_element = abstract_classes_to_xml(tag, obj) + et_relationship_element.append(reference_to_xml(obj.first, NS_AAS+"first")) + et_relationship_element.append(reference_to_xml(obj.second, NS_AAS+"second")) + return et_relationship_element + + +def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElement, + tag: str = NS_AAS+"annotatedRelationshipElement") -> etree.Element: + """ + serialization of objects of class AnnotatedRelationshipElement to XML + + :param obj: object of class AnnotatedRelationshipElement + :param tag: tag of the serialized element (optional), default is "annotatedRelationshipElement + :return: serialized ElementTree object + """ + et_annotated_relationship_element = relationship_element_to_xml(obj, tag) + et_annotations = _generate_element(name=NS_AAS+"annotations") + if obj.annotation: + for data_element in obj.annotation: + et_data_element = _generate_element(name=NS_AAS+"dataElement") + et_data_element.append(data_element_to_xml(data_element)) + et_annotations.append(et_data_element) + et_annotated_relationship_element.append(et_annotations) + return et_annotated_relationship_element + + +def operation_variable_to_xml(obj: model.OperationVariable, + tag: str = NS_AAS+"operationVariable") -> etree.Element: + """ + serialization of objects of class OperationVariable to XML + + :param obj: object of class OperationVariable + :param tag: tag of the serialized element (optional), default is "operationVariable" + :return: serialized ElementTree object + """ + et_operation_variable = _generate_element(tag) + et_value = _generate_element(NS_AAS+"value") + et_value.append(submodel_element_to_xml(obj.value)) + et_operation_variable.append(et_value) + return et_operation_variable + + +def operation_to_xml(obj: model.Operation, + tag: str = NS_AAS+"operation") -> etree.Element: + """ + serialization of objects of class Operation to XML + + :param obj: object of class Operation + :param tag: namespace+tag of the serialized element (optional), default is "operation" + :return: serialized ElementTree object + """ + et_operation = abstract_classes_to_xml(tag, obj) + if obj.input_variable: + for input_ov in obj.input_variable: + et_operation.append(operation_variable_to_xml(input_ov, NS_AAS+"inputVariable")) + if obj.output_variable: + for output_ov in obj.output_variable: + et_operation.append(operation_variable_to_xml(output_ov, NS_AAS+"outputVariable")) + if obj.in_output_variable: + for in_out_ov in obj.in_output_variable: + et_operation.append(operation_variable_to_xml(in_out_ov, NS_AAS+"inoutputVariable")) + return et_operation + + +def capability_to_xml(obj: model.Capability, + tag: str = NS_AAS+"capability") -> etree.Element: + """ + serialization of objects of class Capability to XML + + :param obj: object of class Capability + :param tag: tag of the serialized element, default is "capability" + :return: serialized ElementTree object + """ + return abstract_classes_to_xml(tag, obj) + + +def entity_to_xml(obj: model.Entity, + tag: str = NS_AAS+"entity") -> etree.Element: + """ + serialization of objects of class Entity to XML + + :param obj: object of class Entity + :param tag: tag of the serialized element (optional), default is "entity" + :return: serialized ElementTree object + """ + # todo: remove wrapping submodelElement, in accordance to future schemas + et_entity = abstract_classes_to_xml(tag, obj) + et_statements = _generate_element(NS_AAS + "statements") + for statement in obj.statement: + # todo: remove the once the proposed changes get accepted + et_submodel_element = _generate_element(NS_AAS+"submodelElement") + et_submodel_element.append(submodel_element_to_xml(statement)) + et_statements.append(et_submodel_element) + et_entity.append(et_statements) + et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type])) + if obj.asset: + et_entity.append(reference_to_xml(obj.asset, NS_AAS+"assetRef")) + return et_entity + + +def basic_event_to_xml(obj: model.BasicEvent, + tag: str = NS_AAS+"basicEvent") -> etree.Element: + """ + serialization of objects of class BasicEvent to XML + + :param obj: object of class BasicEvent + :param tag: tag of the serialized element (optional), default is "basicEvent" + :return: serialized ElementTree object + """ + et_basic_event = abstract_classes_to_xml(tag, obj) + et_basic_event.append(reference_to_xml(obj.observed, NS_AAS+"observed")) + return et_basic_event + + +# ############################################################## +# general functions +# ############################################################## + + +def write_aas_xml_file(file: IO, + data: model.AbstractObjectStore, + **kwargs) -> None: + """ + Write a set of AAS objects to an Asset Administration Shell XML file according to 'Details of the Asset + Administration Shell', chapter 5.4 + + :param file: A file-like object to write the XML-serialized data to + :param data: :class:`ObjectStore ` which contains different objects of the + AAS meta model which should be serialized to an XML file + :param kwargs: Additional keyword arguments to be passed to `tree.write()` + """ + # separate different kind of objects + assets = [] + asset_administration_shells = [] + submodels = [] + concept_descriptions = [] + for obj in data: + if isinstance(obj, model.Asset): + assets.append(obj) + if isinstance(obj, model.AssetAdministrationShell): + asset_administration_shells.append(obj) + if isinstance(obj, model.Submodel): + submodels.append(obj) + if isinstance(obj, model.ConceptDescription): + concept_descriptions.append(obj) + + # serialize objects to XML + root = etree.Element(NS_AAS + "aasenv", nsmap=NS_MAP) + et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") + for aas_obj in asset_administration_shells: + et_asset_administration_shells.append(asset_administration_shell_to_xml(aas_obj)) + et_assets = _generate_element(NS_AAS + "assets") + for ass_obj in assets: + et_assets.append(asset_to_xml(ass_obj)) + et_submodels = etree.Element(NS_AAS + "submodels") + for sub_obj in submodels: + et_submodels.append(submodel_to_xml(sub_obj)) + et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") + for con_obj in concept_descriptions: + et_concept_descriptions.append(concept_description_to_xml(con_obj)) + root.insert(0, et_concept_descriptions) + root.insert(0, et_submodels) + root.insert(0, et_assets) + root.insert(0, et_asset_administration_shells) + + tree = etree.ElementTree(root) + tree.write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) From e483c92a4dd07641d1a1e60e4377a77e5f3e8454 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 15 Nov 2021 19:00:43 +0100 Subject: [PATCH 024/474] Fix codestyle issue from rebranding --- basyx/aas/adapter/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/__init__.py b/basyx/aas/adapter/__init__.py index 0ce34e1..0e3b40d 100644 --- a/basyx/aas/adapter/__init__.py +++ b/basyx/aas/adapter/__init__.py @@ -3,7 +3,7 @@ * :ref:`json `: This package offers an adapter for serialization and deserialization of BaSyx Python SDK objects to/from JSON. -* :ref:`xml `: This package offers an adapter for serialization and deserialization of BaSyx Python -SDK objects to/from XML. +* :ref:`xml `: This package offers an adapter for serialization and deserialization of BaSyx +Python SDK objects to/from XML. * :ref:`aasx `: This package offers functions for reading and writing AASX-files. """ From 32532c9b5ef9018e0365939adaa9c7d2631e00a7 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 2 Feb 2022 12:14:16 +0100 Subject: [PATCH 025/474] docs: Fix minor formatting syntax issues --- basyx/aas/adapter/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/__init__.py b/basyx/aas/adapter/__init__.py index 0e3b40d..7f96702 100644 --- a/basyx/aas/adapter/__init__.py +++ b/basyx/aas/adapter/__init__.py @@ -2,8 +2,8 @@ This package contains different kinds of adapters. * :ref:`json `: This package offers an adapter for serialization and deserialization of BaSyx -Python SDK objects to/from JSON. + Python SDK objects to/from JSON. * :ref:`xml `: This package offers an adapter for serialization and deserialization of BaSyx -Python SDK objects to/from XML. + Python SDK objects to/from XML. * :ref:`aasx `: This package offers functions for reading and writing AASX-files. """ From 7fe438e95c6b5e25776044529c2999fa62f65346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 28 Oct 2021 19:43:43 +0200 Subject: [PATCH 026/474] minor codestyle improvements --- basyx/aas/adapter/json/json_serialization.py | 4 ++-- basyx/aas/adapter/xml/xml_serialization.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index a8ff5ba..e120083 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -714,9 +714,9 @@ def _create_dict(data: model.AbstractObjectStore) -> dict: assets.append(obj) if isinstance(obj, model.AssetAdministrationShell): asset_administration_shells.append(obj) - if isinstance(obj, model.Submodel): + elif isinstance(obj, model.Submodel): submodels.append(obj) - if isinstance(obj, model.ConceptDescription): + elif isinstance(obj, model.ConceptDescription): concept_descriptions.append(obj) dict_ = { 'assetAdministrationShells': asset_administration_shells, diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index f2707f2..16e65b0 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -870,9 +870,9 @@ def write_aas_xml_file(file: IO, assets.append(obj) if isinstance(obj, model.AssetAdministrationShell): asset_administration_shells.append(obj) - if isinstance(obj, model.Submodel): + elif isinstance(obj, model.Submodel): submodels.append(obj) - if isinstance(obj, model.ConceptDescription): + elif isinstance(obj, model.ConceptDescription): concept_descriptions.append(obj) # serialize objects to XML From 1753854962b496347dfc14fc11d786c82b768de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 19 Apr 2022 19:01:26 +0200 Subject: [PATCH 027/474] Update license headers for MIT license --- basyx/aas/adapter/_generic.py | 7 +++---- basyx/aas/adapter/aasx.py | 7 +++---- basyx/aas/adapter/json/json_deserialization.py | 7 +++---- basyx/aas/adapter/json/json_serialization.py | 7 +++---- basyx/aas/adapter/xml/xml_deserialization.py | 7 +++---- basyx/aas/adapter/xml/xml_serialization.py | 7 +++---- 6 files changed, 18 insertions(+), 24 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 998b9ac..6ae76a6 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ The dicts defined in this module are used in the json and xml modules to translate enum members of our implementation to the respective string and vice versa. diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index e540dd2..5aa2080 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.aasx: diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 250b681..c764932 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.json.json_deserialization: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index e120083..6629ea4 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.json.json_serialization: diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 447f50e..26ac82c 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.xml.xml_deserialization: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 16e65b0..ce4edbb 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.xml.xml_serialization: From cdba873a7cd97a88351616c0a087215238f05416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 19 Jul 2022 19:34:47 +0200 Subject: [PATCH 028/474] adapter: fix an oversight in 8bb8b1f996c58b7a698ed7dd014866c9f44d673f --- basyx/aas/adapter/json/json_serialization.py | 2 +- basyx/aas/adapter/xml/xml_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 6629ea4..cd217f5 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -711,7 +711,7 @@ def _create_dict(data: model.AbstractObjectStore) -> dict: for obj in data: if isinstance(obj, model.Asset): assets.append(obj) - if isinstance(obj, model.AssetAdministrationShell): + elif isinstance(obj, model.AssetAdministrationShell): asset_administration_shells.append(obj) elif isinstance(obj, model.Submodel): submodels.append(obj) diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index ce4edbb..e85d65e 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -867,7 +867,7 @@ def write_aas_xml_file(file: IO, for obj in data: if isinstance(obj, model.Asset): assets.append(obj) - if isinstance(obj, model.AssetAdministrationShell): + elif isinstance(obj, model.AssetAdministrationShell): asset_administration_shells.append(obj) elif isinstance(obj, model.Submodel): submodels.append(obj) From b5bbd41cb474fa72802d9383d20e83f7e617de56 Mon Sep 17 00:00:00 2001 From: zrgt Date: Wed, 30 Nov 2022 22:06:04 +0100 Subject: [PATCH 029/474] Small typo fix --- basyx/aas/adapter/aasx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 5aa2080..677603a 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -362,8 +362,8 @@ def write_aas(self, # Add referenced ConceptDescriptions to the AAS part for dictionary in aas.concept_dictionary: - for concept_rescription_ref in dictionary.concept_description: - objects_to_be_written.add(concept_rescription_ref.get_identifier()) + for concept_description_ref in dictionary.concept_description: + objects_to_be_written.add(concept_description_ref.get_identifier()) # Write submodels: Either create a split part for each of them or otherwise add them to objects_to_be_written aas_split_part_names: List[str] = [] From 7414dbee6f01bf56718cb671f7b63de88168c23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 1 Mar 2023 17:38:04 +0100 Subject: [PATCH 030/474] adapter.xml: fix serialization of 0 values in Property objects Fix #46 --- basyx/aas/adapter/xml/xml_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index e85d65e..d734fb9 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -602,7 +602,7 @@ def property_to_xml(obj: model.Property, """ et_property = abstract_classes_to_xml(tag, obj) et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) - if obj.value: + if obj.value is not None: et_property.append(_value_to_xml(obj.value, obj.value_type)) if obj.value_id: et_property.append(reference_to_xml(obj.value_id, NS_AAS + "valueId")) From 877655f81b66ae3017d3efd0eafa44ae0e1bfb44 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 15:37:02 +0100 Subject: [PATCH 031/474] Fix json serialisation for `File` 'value' is not required in Schema --- basyx/aas/adapter/json/json_serialization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index cd217f5..a2ec130 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -543,7 +543,9 @@ def _file_to_json(cls, obj: model.File) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data.update({'value': obj.value, 'mimeType': obj.mime_type}) + data['mimeType'] = obj.mime_type + if obj.value is not None: + data['value'] = obj.value return data @classmethod From 834b83a962681f49dae16df41ad498fd84b79786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 4 May 2023 17:02:47 +0200 Subject: [PATCH 032/474] adapter.xml.xml_deserialization: be more forgiving with invalid input When deserializing XML we would previously only have a look at the first child of an unnecessary wrapper element and ignore additional elements. However, since some implemenetations like the AASX Package Explorer generate schema-incompatible XML, this behavior is changed in a way such that additional elements are no longer ignored, but also parsed instead. Fix #71 --- basyx/aas/adapter/xml/xml_deserialization.py | 28 +++++++++----------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 26ac82c..4ca7bc2 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -449,9 +449,8 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None continue if len(constraint) > 1: logger.warning(f"{_element_pretty_identifier(constraint)} has more than one constraint, " - "using the first one...") - constructed = _failsafe_construct(constraint[0], cls.construct_constraint, cls.failsafe) - if constructed is not None: + "which is invalid! Deserializing all constraints anyways...") + for constructed in _failsafe_construct_multiple(constraint, cls.construct_constraint, cls.failsafe): obj.qualifier.add(constructed) @classmethod @@ -729,9 +728,8 @@ def construct_annotated_relationship_element(cls, element: etree.Element, raise KeyError(f"{_element_pretty_identifier(data_element)} has no data element!") if len(data_element) > 1: logger.warning(f"{_element_pretty_identifier(data_element)} has more than one data element, " - "using the first one...") - constructed = _failsafe_construct(data_element[0], cls.construct_data_element, cls.failsafe) - if constructed is not None: + "which is invalid! Deserializing all data elements anyways...") + for constructed in _failsafe_construct_multiple(data_element, cls.construct_data_element, cls.failsafe): annotated_relationship_element.annotation.add(constructed) return annotated_relationship_element @@ -787,9 +785,9 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") if len(submodel_element) > 1: logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " using the first one...") - constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) - if constructed is not None: + " which is invalid! Deserializing all submodel elements anyways...") + for constructed in _failsafe_construct_multiple(submodel_element, cls.construct_submodel_element, + cls.failsafe): entity.statement.add(constructed) cls._amend_abstract_attributes(entity, element) return entity @@ -912,9 +910,9 @@ def construct_submodel_element_collection(cls, element: etree.Element, raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") if len(submodel_element) > 1: logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " using the first one...") - constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) - if constructed is not None: + " which is invalid! Deserializing all submodel elements anyways...") + for constructed in _failsafe_construct_multiple(submodel_element, cls.construct_submodel_element, + cls.failsafe): collection.value.add(constructed) cls._amend_abstract_attributes(collection, element) return collection @@ -985,9 +983,9 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") if len(submodel_element) > 1: logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " using the first one...") - constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) - if constructed is not None: + " which is invalid! Deserializing all submodel elements anyways...") + for constructed in _failsafe_construct_multiple(submodel_element, cls.construct_submodel_element, + cls.failsafe): submodel.submodel_element.add(constructed) cls._amend_abstract_attributes(submodel, element) return submodel From 7f65947cc190cd27ce6eca943986171761e0390c Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:02:10 +0200 Subject: [PATCH 033/474] Add not failsafe mode to AASX Reader Currently, we can pass a `failsafe` parameter in deserializing methods of JSON- and XML-adapters. The parameter determines whether a document should be parsed in a failsafe way. However, I cannot pass the parameter in the deserializing method of the AASX Reader because it does not have `**kwargs`. This commit adds `**kwargs` to deserializing methods of `AASXReader`, such that it is possible to pass the parameter and documents will be deserializend in a not failsafe mode. --- basyx/aas/adapter/aasx.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 677603a..5e47119 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -111,7 +111,7 @@ def get_thumbnail(self) -> Optional[bytes]: def read_into(self, object_store: model.AbstractObjectStore, file_store: "AbstractSupplementaryFileContainer", - override_existing: bool = False) -> Set[model.Identifier]: + override_existing: bool = False, **kwargs) -> Set[model.Identifier]: """ Read the contents of the AASX package and add them into a given :class:`ObjectStore ` @@ -147,12 +147,14 @@ def read_into(self, object_store: model.AbstractObjectStore, # Iterate AAS files for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ RELATIONSHIP_TYPE_AAS_SPEC]: - self._read_aas_part_into(aas_part, object_store, file_store, read_identifiables, override_existing) + self._read_aas_part_into(aas_part, object_store, file_store, + read_identifiables, override_existing, **kwargs) # Iterate split parts of AAS file for split_part in self.reader.get_related_parts_by_type(aas_part)[ RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: - self._read_aas_part_into(split_part, object_store, file_store, read_identifiables, override_existing) + self._read_aas_part_into(split_part, object_store, file_store, + read_identifiables, override_existing, **kwargs) return read_identifiables @@ -172,7 +174,7 @@ def _read_aas_part_into(self, part_name: str, object_store: model.AbstractObjectStore, file_store: "AbstractSupplementaryFileContainer", read_identifiables: Set[model.Identifier], - override_existing: bool) -> None: + override_existing: bool, **kwargs) -> None: """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. @@ -188,7 +190,7 @@ def _read_aas_part_into(self, part_name: str, :param override_existing: If True, existing objects in the object store are overridden with objects from the AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. """ - for obj in self._parse_aas_part(part_name): + for obj in self._parse_aas_part(part_name, **kwargs): if obj.identification in read_identifiables: continue if obj.identification in object_store: @@ -204,7 +206,7 @@ def _read_aas_part_into(self, part_name: str, if isinstance(obj, model.Submodel): self._collect_supplementary_files(part_name, obj, file_store) - def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: + def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore: """ Helper function to parse the AAS objects from a single JSON or XML part of the AASX package. @@ -218,12 +220,12 @@ def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_xml_file(p) + return read_aas_xml_file(p, **kwargs) elif content_type.split(";")[0] in ("text/json", "application/json") \ or content_type == "" and extension == "json": logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig')) + return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs) else: logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}" .format(part_name, content_type, extension)) From c66ff2e679eb5a0fcdef865f62b1cd970d7a7e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Aug 2023 11:22:49 +0200 Subject: [PATCH 034/474] adapter.json.json_deserialization: fix pycodestyle warnings Since a recent update, pycodestyle requires whitespaces between the last comma and the backslash at the end of a line, where it is broken. --- basyx/aas/adapter/json/json_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index c764932..e5639e5 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -36,8 +36,8 @@ from typing import Dict, Callable, TypeVar, Type, List, IO, Optional, Set from basyx.aas import model -from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ - IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ +from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE, \ + IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, \ KEY_ELEMENTS_CLASSES_INVERSE logger = logging.getLogger(__name__) From 8f43d00267b0bb97ecbc4126c2f3c8f0eef43b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Wed, 7 Sep 2022 10:24:32 +0200 Subject: [PATCH 035/474] Fixed xml deserialization for missing prefixes --- basyx/aas/adapter/xml/xml_deserialization.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 4ca7bc2..551bde7 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -85,8 +85,9 @@ def _tag_replace_namespace(tag: str, nsmap: Dict[str, str]) -> str: """ split = tag.split("}") for prefix, namespace in nsmap.items(): - if namespace == split[0][1:]: - return prefix + ":" + split[1] + if prefix: + if namespace == split[0][1:]: + return prefix + ":" + split[1] return tag From 63b64fe295e015e90d43b1c708306cabd61fd888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Thu, 28 Sep 2023 11:16:35 +0200 Subject: [PATCH 036/474] Simplify condition --- basyx/aas/adapter/xml/xml_deserialization.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 551bde7..5d40306 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -85,9 +85,8 @@ def _tag_replace_namespace(tag: str, nsmap: Dict[str, str]) -> str: """ split = tag.split("}") for prefix, namespace in nsmap.items(): - if prefix: - if namespace == split[0][1:]: - return prefix + ":" + split[1] + if prefix and namespace == split[0][1:]: + return prefix + ":" + split[1] return tag From 5e9fccf98fa91944dcf8b0ad6a2713d1fbb5ff8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 2 Apr 2022 18:12:43 +0200 Subject: [PATCH 037/474] remove AssetInformation/billOfMaterial --- basyx/aas/adapter/json/aasJSONSchema.json | 2600 ++++++++--------- .../aas/adapter/json/json_deserialization.py | 149 +- basyx/aas/adapter/json/json_serialization.py | 157 +- basyx/aas/adapter/xml/AAS.xsd | 211 +- basyx/aas/adapter/xml/xml_deserialization.py | 234 +- basyx/aas/adapter/xml/xml_serialization.py | 451 ++- 6 files changed, 1750 insertions(+), 2052 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 1c7020a..de1e6dd 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -1,1439 +1,1199 @@ { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "title": "AssetAdministrationShellEnvironment", - "$id": "http://www.admin-shell.io/schema/json/v2.0.1", - "type": "object", - "required": [ - "assetAdministrationShells", - "submodels", - "assets", - "conceptDescriptions" - ], - "properties": { - "assetAdministrationShells": { - "type": "array", - "items": { - "$ref": "#/definitions/AssetAdministrationShell" - } - }, - "submodels": { - "type": "array", - "items": { - "$ref": "#/definitions/Submodel" - } - }, - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "conceptDescriptions": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "AssetAdministrationShellEnvironment", + "$id": "http://www.admin-shell.io/schema/json/V3.0RC01", + "type": "object", + "required": ["assetAdministrationShells", "submodels", "conceptDescriptions"], + "properties": { + "assetAdministrationShells": { + "type": "array", + "items": { + "$ref": "#/definitions/AssetAdministrationShell" + } + }, + "submodels": { + "type": "array", + "items": { + "$ref": "#/definitions/Submodel" + } + }, + "conceptDescriptions": { + "type": "array", + "items": { + "$ref": "#/definitions/ConceptDescription" + } + } + }, + "definitions": { + "Referable": { + "allOf":[ + {"$ref": "#/definitions/HasExtensions"}, + {"properties": { + "idShort": { + "type": "string" + }, + "category": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": ["modelType" ] + }] +}, + "Identifiable": { + "allOf": [ + { "$ref": "#/definitions/Referable" }, + { "properties": { + "identification": { + "$ref": "#/definitions/Identifier" + }, + "administration": { + "$ref": "#/definitions/AdministrativeInformation" + } + }, + "required": [ "identification" ] + } + ] + }, + "Qualifiable": { + "type": "object", + "properties": { + "qualifiers": { + "type": "array", + "items": { + "$ref": "#/definitions/Constraint" + } + } + } + }, + "HasSemantics": { + "type": "object", + "properties": { + "semanticId": { + "$ref": "#/definitions/Reference" + } + } + }, + "HasDataSpecification": { + "type": "object", + "properties": { + "embeddedDataSpecifications": { + "type": "array", + "items": { + "$ref": "#/definitions/EmbeddedDataSpecification" + } + } + } + }, + "HasExtensions": { + "type": "object", + "properties": { + "extensions": { + "type": "array", + "items": { + "$ref": "#/definitions/Extension" + } + } + } + }, + "Extension": { + "allOf": [ + { + "$ref": "#/definitions/HasSemantics" + }, + { "properties": { + "name": { + "type": "string" + }, + "valueType":{ + "type": "string", + "enum": [ + "anyUri", + "base64Binary", + "boolean", + "date", + "dateTime", + "dateTimeStamp", + "decimal", + "integer", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "positiveInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "nonPositiveInteger", + "negativeInteger", + "double", + "duration", + "dayTimeDuration", + "yearMonthDuration", + "float", + "gDay", + "gMonth", + "gMonthDay", + "gYear", + "gYearMonth", + "hexBinary", + "NOTATION", + "QName", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ENTITY", + "ID", + "IDREF", + "NMTOKEN", + "time" + ]}, + "value":{ + "type": "string" + }, + "refersTo":{ + "$ref": "#/definitions/Reference" + } + }, + "required": [ "name" ] + } + ] +}, + "AssetAdministrationShell": { + "allOf": [ + { "$ref": "#/definitions/Identifiable" }, + { "$ref": "#/definitions/HasDataSpecification" }, + { "properties": { + "derivedFrom": { + "$ref": "#/definitions/Reference" + }, + "assetInformation": { + "$ref": "#/definitions/AssetInformation" + }, + "submodels": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + }, + "security": { + "$ref": "#/definitions/Security" + } + }, + "required": [ "assetInformation" ] + } + ] + }, + "Identifier": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "idType": { + "$ref": "#/definitions/KeyType" + } + }, + "required": [ "id", "idType" ] + }, + "KeyType": { + "type": "string", + "enum": ["Custom", "IRDI", "IRI", "IdShort", "FragmentId"] + }, + "AdministrativeInformation": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "revision": { + "type": "string" + } + } + }, + "LangString": { + "type": "object", + "properties": { + "language": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ "language", "text" ] + }, + "Reference": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/Key" + } + } + }, + "required": [ "keys" ] + }, + "Key": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/KeyElements" + }, + "idType": { + "$ref": "#/definitions/KeyType" + }, + "value": { + "type": "string" + } + }, + "required": [ "type", "idType", "value"] + }, + "KeyElements": { + "type": "string", + "enum": [ + "AssetAdministrationShell", + "ConceptDescription", + "Submodel", + "AccessPermissionRule", + "AnnotatedRelationshipElement", + "BasicEvent", + "Blob", + "Capability", + "DataElement", + "File", + "Entity", + "Event", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "SubmodelElement", + "SubmodelElementCollection", + "GlobalReference", + "FragmentReference" + ] + }, + "ModelTypes": { + "type": "string", + "enum": [ + "AssetAdministrationShell", + "ConceptDescription", + "Submodel", + "AccessPermissionRule", + "AnnotatedRelationshipElement", + "BasicEvent", + "Blob", + "Capability", + "DataElement", + "File", + "Entity", + "Event", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "SubmodelElement", + "SubmodelElementCollection", + "GlobalReference", + "FragmentReference", + "Constraint", + "Formula", + "Qualifier" + ] + }, + "ModelType": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/ModelTypes" + } + }, + "required": [ "name" ] + }, + "EmbeddedDataSpecification": { + "type": "object", + "properties": { + "dataSpecification": { + "$ref": "#/definitions/Reference" + }, + "dataSpecificationContent": { + "$ref": "#/definitions/DataSpecificationContent" + } + }, + "required": [ "dataSpecification", "dataSpecificationContent" ] + }, + "DataSpecificationContent": { + "oneOf": [ + { "$ref": "#/definitions/DataSpecificationIEC61360Content" }, + { "$ref": "#/definitions/DataSpecificationPhysicalUnitContent" } + ] + }, + "DataSpecificationPhysicalUnitContent": { + "type": "object", + "properties": { + "unitName": { + "type": "string" + }, + "unitSymbol": { + "type": "string" + }, + "definition": { "type": "array", "items": { - "$ref": "#/definitions/ConceptDescription" - } - } - }, - "definitions": { - "Referable": { - "type": "object", - "properties": { - "idShort": { - "type": "string" - }, - "category": { - "type": "string" - }, - "description": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "parent": { - "$ref": "#/definitions/Reference" - }, - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ - "idShort", - "modelType" - ] - }, - "Identifiable": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "properties": { - "identification": { - "$ref": "#/definitions/Identifier" - }, - "administration": { - "$ref": "#/definitions/AdministrativeInformation" - } - }, - "required": [ - "identification" - ] - } - ] - }, - "Qualifiable": { - "type": "object", - "properties": { - "qualifiers": { - "type": "array", - "items": { - "$ref": "#/definitions/Constraint" - } - } - } - }, - "HasSemantics": { - "type": "object", - "properties": { - "semanticId": { - "$ref": "#/definitions/Reference" - } - } - }, - "HasDataSpecification": { - "type": "object", - "properties": { - "embeddedDataSpecifications": { - "type": "array", - "items": { - "$ref": "#/definitions/EmbeddedDataSpecification" - } - } - } - }, - "AssetAdministrationShell": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "derivedFrom": { - "$ref": "#/definitions/Reference" - }, - "asset": { - "$ref": "#/definitions/Reference" - }, - "submodels": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - }, - "views": { - "type": "array", - "items": { - "$ref": "#/definitions/View" - } - }, - "conceptDictionaries": { - "type": "array", - "items": { - "$ref": "#/definitions/ConceptDictionary" - } - }, - "security": { - "$ref": "#/definitions/Security" - } - }, - "required": [ - "asset" - ] - } - ] - }, - "Identifier": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "idType": { - "$ref": "#/definitions/KeyType" - } - }, - "required": [ - "id", - "idType" - ] - }, - "KeyType": { - "type": "string", - "enum": [ - "Custom", - "IRDI", - "IRI", - "IdShort", - "FragmentId" - ] - }, - "AdministrativeInformation": { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "revision": { - "type": "string" - } - } - }, - "LangString": { - "type": "object", - "properties": { - "language": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": [ - "language", - "text" - ] - }, - "Reference": { - "type": "object", - "properties": { - "keys": { - "type": "array", - "items": { - "$ref": "#/definitions/Key" - } - } - }, - "required": [ - "keys" - ] - }, - "Key": { - "type": "object", - "properties": { - "type": { - "$ref": "#/definitions/KeyElements" - }, - "idType": { - "$ref": "#/definitions/KeyType" - }, - "value": { - "type": "string" - }, - "local": { - "type": "boolean" - } - }, - "required": [ - "type", - "idType", - "value", - "local" - ] - }, - "KeyElements": { - "type": "string", - "enum": [ - "Asset", - "AssetAdministrationShell", - "ConceptDescription", - "Submodel", - "AccessPermissionRule", - "AnnotatedRelationshipElement", - "BasicEvent", - "Blob", - "Capability", - "ConceptDictionary", - "DataElement", - "File", - "Entity", - "Event", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "SubmodelElement", - "SubmodelElementCollection", - "View", - "GlobalReference", - "FragmentReference" - ] - }, - "ModelTypes": { - "type": "string", - "enum": [ - "Asset", - "AssetAdministrationShell", - "ConceptDescription", - "Submodel", - "AccessPermissionRule", - "AnnotatedRelationshipElement", - "BasicEvent", - "Blob", - "Capability", - "ConceptDictionary", - "DataElement", - "File", - "Entity", - "Event", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "SubmodelElement", - "SubmodelElementCollection", - "View", - "GlobalReference", - "FragmentReference", - "Constraint", - "Formula", - "Qualifier" - ] - }, - "ModelType": { - "type": "object", - "properties": { - "name": { - "$ref": "#/definitions/ModelTypes" - } - }, - "required": [ - "name" - ] - }, - "EmbeddedDataSpecification": { - "type": "object", - "properties": { - "dataSpecification": { - "$ref": "#/definitions/Reference" - }, - "dataSpecificationContent": { - "$ref": "#/definitions/DataSpecificationContent" - } - }, - "required": [ - "dataSpecification", - "dataSpecificationContent" - ] - }, - "DataSpecificationContent": { - "oneOf": [ - { - "$ref": "#/definitions/DataSpecificationIEC61360Content" - }, - { - "$ref": "#/definitions/DataSpecificationPhysicalUnitContent" - } - ] - }, - "DataSpecificationPhysicalUnitContent": { - "type": "object", - "properties": { - "unitName": { - "type": "string" - }, - "unitSymbol": { - "type": "string" - }, - "definition": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "siNotation": { - "type": "string" - }, - "siName": { - "type": "string" - }, - "dinNotation": { - "type": "string" - }, - "eceName": { - "type": "string" - }, - "eceCode": { - "type": "string" - }, - "nistName": { - "type": "string" - }, - "sourceOfDefinition": { - "type": "string" - }, - "conversionFactor": { - "type": "string" - }, - "registrationAuthorityId": { - "type": "string" - }, - "supplier": { - "type": "string" - } - }, - "required": [ - "unitName", - "unitSymbol", - "definition" - ] - }, - "DataSpecificationIEC61360Content": { - "allOf": [ - { - "$ref": "#/definitions/ValueObject" - }, - { - "type": "object", - "properties": { - "dataType": { - "enum": [ - "DATE", - "STRING", - "STRING_TRANSLATABLE", - "REAL_MEASURE", - "REAL_COUNT", - "REAL_CURRENCY", - "BOOLEAN", - "URL", - "RATIONAL", - "RATIONAL_MEASURE", - "TIME", - "TIMESTAMP", - "INTEGER_COUNT", - "INTEGER_MEASURE", - "INTEGER_CURRENCY" - ] - }, - "definition": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "preferredName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "shortName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "sourceOfDefinition": { - "type": "string" - }, - "symbol": { - "type": "string" - }, - "unit": { - "type": "string" - }, - "unitId": { - "$ref": "#/definitions/Reference" - }, - "valueFormat": { - "type": "string" - }, - "valueList": { - "$ref": "#/definitions/ValueList" - }, - "levelType": { - "type": "array", - "items": { - "$ref": "#/definitions/LevelType" - } - } - }, - "required": [ - "preferredName" - ] - } - ] - }, - "LevelType": { - "type": "string", - "enum": [ - "Min", - "Max", - "Nom", - "Typ" - ] - }, - "ValueList": { - "type": "object", - "properties": { - "valueReferencePairTypes": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/ValueReferencePairType" - } - } - }, - "required": [ - "valueReferencePairTypes" - ] - }, - "ValueReferencePairType": { - "allOf": [ - { - "$ref": "#/definitions/ValueObject" - } - ] - }, - "ValueObject": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "valueId": { - "$ref": "#/definitions/Reference" - }, - "valueType": { - "type": "string", - "enum": [ - "anyUri", - "base64Binary", - "boolean", - "date", - "dateTime", - "dateTimeStamp", - "decimal", - "integer", - "long", - "int", - "short", - "byte", - "nonNegativeInteger", - "positiveInteger", - "unsignedLong", - "unsignedInt", - "unsignedShort", - "unsignedByte", - "nonPositiveInteger", - "negativeInteger", - "double", - "duration", - "dayTimeDuration", - "yearMonthDuration", - "float", - "gDay", - "gMonth", - "gMonthDay", - "gYear", - "gYearMonth", - "hexBinary", - "NOTATION", - "QName", - "string", - "normalizedString", - "token", - "language", - "Name", - "NCName", - "ENTITY", - "ID", - "IDREF", - "NMTOKEN", - "time" - ] - } + "$ref": "#/definitions/LangString" } }, - "Asset": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "kind": { - "$ref": "#/definitions/AssetKind" - }, - "assetIdentificationModel": { - "$ref": "#/definitions/Reference" - }, - "billOfMaterial": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "kind" - ] - } - ] - }, - "AssetKind": { - "type": "string", - "enum": [ - "Type", - "Instance" - ] - }, - "ModelingKind": { - "type": "string", - "enum": [ - "Template", - "Instance" - ] - }, - "Submodel": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "$ref": "#/definitions/Qualifiable" - }, - { - "$ref": "#/definitions/HasSemantics" - }, - { - "properties": { - "kind": { - "$ref": "#/definitions/ModelingKind" - }, - "submodelElements": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement" - } - } - } - } - ] - }, - "Constraint": { - "type": "object", - "properties": { - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ - "modelType" - ] - }, - "Operation": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "inputVariable": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - } - }, - "outputVariable": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - } - }, - "inoutputVariable": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - } - } - } - } - ] - }, - "OperationVariable": { - "type": "object", - "properties": { - "value": { - "oneOf": [ - { - "$ref": "#/definitions/Blob" - }, - { - "$ref": "#/definitions/File" - }, - { - "$ref": "#/definitions/Capability" - }, - { - "$ref": "#/definitions/Entity" - }, - { - "$ref": "#/definitions/Event" - }, - { - "$ref": "#/definitions/BasicEvent" - }, - { - "$ref": "#/definitions/MultiLanguageProperty" - }, - { - "$ref": "#/definitions/Operation" - }, - { - "$ref": "#/definitions/Property" - }, - { - "$ref": "#/definitions/Range" - }, - { - "$ref": "#/definitions/ReferenceElement" - }, - { - "$ref": "#/definitions/RelationshipElement" - }, - { - "$ref": "#/definitions/SubmodelElementCollection" - } - ] - } - }, - "required": [ - "value" - ] - }, - "SubmodelElement": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "$ref": "#/definitions/HasSemantics" - }, - { - "$ref": "#/definitions/Qualifiable" - }, - { - "properties": { - "kind": { - "$ref": "#/definitions/ModelingKind" - } - } - } - ] - }, - "Event": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - } - ] - }, - "BasicEvent": { - "allOf": [ - { - "$ref": "#/definitions/Event" - }, - { - "properties": { - "observed": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "observed" - ] - } - ] - }, - "EntityType": { - "type": "string", - "enum": [ - "CoManagedEntity", - "SelfManagedEntity" - ] - }, - "Entity": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "statements": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement" - } - }, - "entityType": { - "$ref": "#/definitions/EntityType" - }, - "asset": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "entityType" - ] - } - ] + "siNotation": { + "type": "string" }, - "View": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "$ref": "#/definitions/HasSemantics" - }, - { - "properties": { - "containedElements": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - } - } - } - ] + "siName": { + "type": "string" }, - "ConceptDictionary": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "conceptDescriptions": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - } - } - } - ] + "dinNotation": { + "type": "string" }, - "ConceptDescription": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "isCaseOf": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - } - } - } - ] + "eceName": { + "type": "string" }, - "Capability": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - } - ] + "eceCode": { + "type": "string" }, - "Property": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "$ref": "#/definitions/ValueObject" - } - ] + "nistName": { + "type": "string" }, - "Range": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "valueType": { - "type": "string", - "enum": [ - "anyUri", - "base64Binary", - "boolean", - "date", - "dateTime", - "dateTimeStamp", - "decimal", - "integer", - "long", - "int", - "short", - "byte", - "nonNegativeInteger", - "positiveInteger", - "unsignedLong", - "unsignedInt", - "unsignedShort", - "unsignedByte", - "nonPositiveInteger", - "negativeInteger", - "double", - "duration", - "dayTimeDuration", - "yearMonthDuration", - "float", - "gDay", - "gMonth", - "gMonthDay", - "gYear", - "gYearMonth", - "hexBinary", - "NOTATION", - "QName", - "string", - "normalizedString", - "token", - "language", - "Name", - "NCName", - "ENTITY", - "ID", - "IDREF", - "NMTOKEN", - "time" - ] - }, - "min": { - "type": "string" - }, - "max": { - "type": "string" - } - }, - "required": [ - "valueType" - ] - } - ] + "sourceOfDefinition": { + "type": "string" }, - "MultiLanguageProperty": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "valueId": { - "$ref": "#/definitions/Reference" - } - } - } - ] + "conversionFactor": { + "type": "string" }, - "File": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "value": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "mimeType" - ] - } - ] - }, - "Blob": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "value": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "mimeType" - ] - } - ] - }, - "ReferenceElement": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "value": { - "$ref": "#/definitions/Reference" - } - } - } - ] - }, - "SubmodelElementCollection": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "value": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Blob" - }, - { - "$ref": "#/definitions/File" - }, - { - "$ref": "#/definitions/Capability" - }, - { - "$ref": "#/definitions/Entity" - }, - { - "$ref": "#/definitions/Event" - }, - { - "$ref": "#/definitions/BasicEvent" - }, - { - "$ref": "#/definitions/MultiLanguageProperty" - }, - { - "$ref": "#/definitions/Operation" - }, - { - "$ref": "#/definitions/Property" - }, - { - "$ref": "#/definitions/Range" - }, - { - "$ref": "#/definitions/ReferenceElement" - }, - { - "$ref": "#/definitions/RelationshipElement" - }, - { - "$ref": "#/definitions/SubmodelElementCollection" - } - ] - } - }, - "allowDuplicates": { - "type": "boolean" - }, - "ordered": { - "type": "boolean" - } - } - } - ] - }, - "RelationshipElement": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "first": { - "$ref": "#/definitions/Reference" - }, - "second": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "first", - "second" - ] - } - ] - }, - "AnnotatedRelationshipElement": { - "allOf": [ - { - "$ref": "#/definitions/RelationshipElement" - }, - { - "properties": { - "annotation": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Blob" - }, - { - "$ref": "#/definitions/File" - }, - { - "$ref": "#/definitions/MultiLanguageProperty" - }, - { - "$ref": "#/definitions/Property" - }, - { - "$ref": "#/definitions/Range" - }, - { - "$ref": "#/definitions/ReferenceElement" - } - ] - } - } - } - } - ] - }, - "Qualifier": { - "allOf": [ - { - "$ref": "#/definitions/Constraint" - }, - { - "$ref": "#/definitions/HasSemantics" - }, - { - "$ref": "#/definitions/ValueObject" - }, - { - "properties": { - "type": { - "type": "string" - } - }, - "required": [ - "type" - ] - } - ] - }, - "Formula": { - "allOf": [ - { - "$ref": "#/definitions/Constraint" - }, - { - "properties": { - "dependsOn": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - } - } - } - ] - }, - "Security": { - "type": "object", - "properties": { - "accessControlPolicyPoints": { - "$ref": "#/definitions/AccessControlPolicyPoints" - }, - "certificate": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/BlobCertificate" - } - ] - } - }, - "requiredCertificateExtension": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - } - }, - "required": [ - "accessControlPolicyPoints" - ] - }, - "Certificate": { - "type": "object" - }, - "BlobCertificate": { - "allOf": [ - { - "$ref": "#/definitions/Certificate" - }, - { - "properties": { - "blobCertificate": { - "$ref": "#/definitions/Blob" - }, - "containedExtension": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - }, - "lastCertificate": { - "type": "boolean" - } - } - } - ] - }, - "AccessControlPolicyPoints": { - "type": "object", - "properties": { - "policyAdministrationPoint": { - "$ref": "#/definitions/PolicyAdministrationPoint" - }, - "policyDecisionPoint": { - "$ref": "#/definitions/PolicyDecisionPoint" - }, - "policyEnforcementPoint": { - "$ref": "#/definitions/PolicyEnforcementPoint" - }, - "policyInformationPoints": { - "$ref": "#/definitions/PolicyInformationPoints" - } - }, - "required": [ - "policyAdministrationPoint", - "policyDecisionPoint", - "policyEnforcementPoint" - ] + "registrationAuthorityId": { + "type": "string" }, - "PolicyAdministrationPoint": { - "type": "object", - "properties": { - "localAccessControl": { - "$ref": "#/definitions/AccessControl" - }, - "externalAccessControl": { - "type": "boolean" - } - }, - "required": [ - "externalAccessControl" - ] - }, - "PolicyInformationPoints": { - "type": "object", - "properties": { - "internalInformationPoint": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - }, - "externalInformationPoint": { - "type": "boolean" - } - }, - "required": [ - "externalInformationPoint" - ] - }, - "PolicyEnforcementPoint": { - "type": "object", - "properties": { - "externalPolicyEnforcementPoint": { - "type": "boolean" - } - }, - "required": [ - "externalPolicyEnforcementPoint" - ] - }, - "PolicyDecisionPoint": { - "type": "object", - "properties": { - "externalPolicyDecisionPoints": { - "type": "boolean" - } + "supplier": { + "type": "string" + } + }, + "required": [ "unitName", "unitSymbol", "definition" ] + }, + "DataSpecificationIEC61360Content": { + "allOf": [ + { "$ref": "#/definitions/ValueObject" }, + { + "type": "object", + "properties": { + "dataType": { + "enum": [ + "DATE", + "STRING", + "STRING_TRANSLATABLE", + "REAL_MEASURE", + "REAL_COUNT", + "REAL_CURRENCY", + "BOOLEAN", + "URL", + "RATIONAL", + "RATIONAL_MEASURE", + "TIME", + "TIMESTAMP", + "INTEGER_COUNT", + "INTEGER_MEASURE", + "INTEGER_CURRENCY" + ] + }, + "definition": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "preferredName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "shortName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "sourceOfDefinition": { + "type": "string" + }, + "symbol": { + "type": "string" + }, + "unit": { + "type": "string" + }, + "unitId": { + "$ref": "#/definitions/Reference" + }, + "valueFormat": { + "type": "string" + }, + "valueList": { + "$ref": "#/definitions/ValueList" + }, + "levelType": { + "type": "array", + "items": { + "$ref": "#/definitions/LevelType" + } + } + }, + "required": [ "preferredName" ] + } + ] + }, + "LevelType": { + "type": "string", + "enum": [ "Min", "Max", "Nom", "Typ" ] + }, + "ValueList": { + "type": "object", + "properties": { + "valueReferencePairTypes": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/ValueReferencePairType" + } + } + }, + "required": [ "valueReferencePairTypes" ] + }, + "ValueReferencePairType": { + "allOf": [ + { "$ref": "#/definitions/ValueObject" } + ] + }, + "ValueObject": { + "type": "object", + "properties": { + "value": { "type": "string" }, + "valueId": { + "$ref": "#/definitions/Reference" + }, + "valueType": { + "type": "string", + "enum": [ + "anyUri", + "base64Binary", + "boolean", + "date", + "dateTime", + "dateTimeStamp", + "decimal", + "integer", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "positiveInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "nonPositiveInteger", + "negativeInteger", + "double", + "duration", + "dayTimeDuration", + "yearMonthDuration", + "float", + "gDay", + "gMonth", + "gMonthDay", + "gYear", + "gYearMonth", + "hexBinary", + "NOTATION", + "QName", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ENTITY", + "ID", + "IDREF", + "NMTOKEN", + "time" + ]} + } + }, + "AssetInformation": { + "allOf": [ + { "properties": { + "assetKind": { + "$ref": "#/definitions/AssetKind" + }, + "globalAssetId":{ + "$ref": "#/definitions/Reference" + }, + "externalAssetIds":{ + "type": "array", + "items": { + "$ref": "#/definitions/IdentifierKeyValuePair" + } + }, + "thumbnail":{ + "$ref": "#/definitions/File" + } + }, + "required": [ "assetKind" ] + } + ] + }, + "IdentifierKeyValuePair":{ + "allOf": [{ "$ref": "#/definitions/HasSemantics"}, + { "properties": { + "key": { + "dataType":"string" + }, + "value": { + "dataType":"string" + }, + + "subjectId":{ + "$ref": "#/definitions/Reference" + } + }, + "required": [ "key","value","subjectId" ] + } + ] +}, + "AssetKind": { + "type": "string", + "enum": ["Type", "Instance"] + }, + "ModelingKind": { + "type": "string", + "enum": ["Template", "Instance"] + }, + "Submodel": { + "allOf": [ + { "$ref": "#/definitions/Identifiable" }, + { "$ref": "#/definitions/HasDataSpecification" }, + { "$ref": "#/definitions/Qualifiable" }, + { "$ref": "#/definitions/HasSemantics" }, + { "properties": { + "kind": { + "$ref": "#/definitions/ModelingKind" + }, + "submodelElements": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement" + } + } + } + } + ] + }, + "Constraint": { + "type": "object", + "properties": { + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": [ "modelType" ] + }, + "Operation": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "inputVariable": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + } + }, + "outputVariable": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + } + }, + "inoutputVariable": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + } + } + } + } + ] + }, + "OperationVariable": { + "type": "object", + "properties": { + "value": { + "oneOf": [ + { "$ref": "#/definitions/Blob" }, + { "$ref": "#/definitions/File" }, + { "$ref": "#/definitions/Capability" }, + { "$ref": "#/definitions/Entity" }, + { "$ref": "#/definitions/Event" }, + { "$ref": "#/definitions/BasicEvent" }, + { "$ref": "#/definitions/MultiLanguageProperty" }, + { "$ref": "#/definitions/Operation" }, + { "$ref": "#/definitions/Property" }, + { "$ref": "#/definitions/Range" }, + { "$ref": "#/definitions/ReferenceElement" }, + { "$ref": "#/definitions/RelationshipElement" }, + { "$ref": "#/definitions/SubmodelElementCollection" } + ] + } + }, + "required": [ "value" ] + }, + "SubmodelElement": { + "allOf": [ + { "$ref": "#/definitions/Referable" }, + { "$ref": "#/definitions/HasDataSpecification" }, + { "$ref": "#/definitions/HasSemantics" }, + { "$ref": "#/definitions/Qualifiable" }, + { "properties": { + "kind": { + "$ref": "#/definitions/ModelingKind" + }, + "idShort":{ + "dataType": "string" + } + },"required":["idShort"] + } + ] + }, + "Event": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" } + ] + }, + "BasicEvent": { + "allOf": [ + { "$ref": "#/definitions/Event" }, + { "properties": { + "observed": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ "observed" ] + } + ] + }, + "EntityType": { + "type": "string", + "enum": ["CoManagedEntity", "SelfManagedEntity"] + }, + "Entity": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "statements": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement" + } + }, + "entityType": { + "$ref": "#/definitions/EntityType" + }, + "globalAssetId":{ + "$ref": "#/definitions/Reference" + }, + "specificAssetIds":{ + "$ref": "#/definitions/IdentifierKeyValuePair" + } + }, + "required": [ "entityType" ] + } + ] + }, + "ConceptDescription": { + "allOf": [ + { "$ref": "#/definitions/Identifiable" }, + { "$ref": "#/definitions/HasDataSpecification" }, + { "properties": { + "isCaseOf": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + } + } + } + ] + }, + "Capability": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" } + ] + }, + "Property": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "$ref": "#/definitions/ValueObject" } + ] + }, + "Range": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "valueType": { + "type": "string", + "enum": [ + "anyUri", + "base64Binary", + "boolean", + "date", + "dateTime", + "dateTimeStamp", + "decimal", + "integer", + "long", + "int", + "short", + "byte", + "nonNegativeInteger", + "positiveInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + "nonPositiveInteger", + "negativeInteger", + "double", + "duration", + "dayTimeDuration", + "yearMonthDuration", + "float", + "gDay", + "gMonth", + "gMonthDay", + "gYear", + "gYearMonth", + "hexBinary", + "NOTATION", + "QName", + "string", + "normalizedString", + "token", + "language", + "Name", + "NCName", + "ENTITY", + "ID", + "IDREF", + "NMTOKEN", + "time" + ] }, - "required": [ - "externalPolicyDecisionPoints" - ] - }, - "AccessControl": { - "type": "object", - "properties": { - "selectableSubjectAttributes": { - "$ref": "#/definitions/Reference" - }, - "defaultSubjectAttributes": { - "$ref": "#/definitions/Reference" - }, - "selectablePermissions": { - "$ref": "#/definitions/Reference" - }, - "defaultPermissions": { - "$ref": "#/definitions/Reference" - }, - "selectableEnvironmentAttributes": { - "$ref": "#/definitions/Reference" - }, - "defaultEnvironmentAttributes": { - "$ref": "#/definitions/Reference" - }, - "accessPermissionRule": { - "type": "array", - "items": { - "$ref": "#/definitions/AccessPermissionRule" - } - } - } - }, - "AccessPermissionRule": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "$ref": "#/definitions/Qualifiable" - }, - { - "properties": { - "targetSubjectAttributes": { - "type": "array", - "items": { - "$ref": "#/definitions/SubjectAttributes" - }, - "minItems": 1 - }, - "permissionsPerObject": { - "type": "array", - "items": { - "$ref": "#/definitions/PermissionsPerObject" - } - } - }, - "required": [ - "targetSubjectAttributes" - ] - } - ] - }, - "SubjectAttributes": { - "type": "object", - "properties": { - "subjectAttributes": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - } - } + "min": { "type": "string" }, + "max": { "type": "string" } + }, + "required": [ "valueType"] + } + ] + }, + "MultiLanguageProperty": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + } + }, + "valueId": { + "$ref": "#/definitions/Reference" + } + } + } + ] + }, + "File": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "value": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ "mimeType" ] + } + ] + }, + "Blob": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "value": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ "mimeType" ] + } + ] + }, + "ReferenceElement": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "value": { + "$ref": "#/definitions/Reference" + } + } + } + ] + }, + "SubmodelElementCollection": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "value": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/definitions/Blob" }, + { "$ref": "#/definitions/File" }, + { "$ref": "#/definitions/Capability" }, + { "$ref": "#/definitions/Entity" }, + { "$ref": "#/definitions/Event" }, + { "$ref": "#/definitions/BasicEvent" }, + { "$ref": "#/definitions/MultiLanguageProperty" }, + { "$ref": "#/definitions/Operation" }, + { "$ref": "#/definitions/Property" }, + { "$ref": "#/definitions/Range" }, + { "$ref": "#/definitions/ReferenceElement" }, + { "$ref": "#/definitions/RelationshipElement" }, + { "$ref": "#/definitions/SubmodelElementCollection" } + ] + } + }, + "allowDuplicates": { + "type": "boolean" + }, + "ordered": { + "type": "boolean" + } + } + } + ] + }, + "RelationshipElement": { + "allOf": [ + { "$ref": "#/definitions/SubmodelElement" }, + { "properties": { + "first": { + "$ref": "#/definitions/Reference" + }, + "second": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ "first", "second" ] + } + ] + }, + "AnnotatedRelationshipElement": { + "allOf": [ + { "$ref": "#/definitions/RelationshipElement" }, + { "properties": { + "annotation": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/definitions/Blob" }, + { "$ref": "#/definitions/File" }, + { "$ref": "#/definitions/MultiLanguageProperty" }, + { "$ref": "#/definitions/Property" }, + { "$ref": "#/definitions/Range" }, + { "$ref": "#/definitions/ReferenceElement" } + ] + } + } + } + } + ] + }, + "Qualifier": { + "allOf": [ + { "$ref": "#/definitions/Constraint" }, + { "$ref": "#/definitions/HasSemantics" }, + { "$ref": "#/definitions/ValueObject" }, + { "properties": { + "type": { + "type": "string" + } + }, + "required": [ "type" ] + } + ] + }, + "Formula": { + "allOf": [ + { "$ref": "#/definitions/Constraint" }, + { "properties": { + "dependsOn": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + } + } + } + ] + }, + "Security": { + "type": "object", + "properties": { + "accessControlPolicyPoints": { + "$ref": "#/definitions/AccessControlPolicyPoints" + }, + "certificate": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/definitions/BlobCertificate" } + ] + } + }, + "requiredCertificateExtension": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + } + }, + "required": [ "accessControlPolicyPoints" ] + }, + "Certificate": { + "type": "object" + }, + "BlobCertificate": { + "allOf": [ + { "$ref": "#/definitions/Certificate" }, + { "properties": { + "blobCertificate": { + "$ref": "#/definitions/Blob" + }, + "containedExtension": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + }, + "lastCertificate": { + "type": "boolean" + } + } + } + ] + }, + "AccessControlPolicyPoints": { + "type": "object", + "properties": { + "policyAdministrationPoint": { + "$ref": "#/definitions/PolicyAdministrationPoint" }, - "PermissionsPerObject": { - "type": "object", - "properties": { - "object": { - "$ref": "#/definitions/Reference" - }, - "targetObjectAttributes": { - "$ref": "#/definitions/ObjectAttributes" - }, - "permission": { - "type": "array", - "items": { - "$ref": "#/definitions/Permission" - } - } - } + "policyDecisionPoint": { + "$ref": "#/definitions/PolicyDecisionPoint" }, - "ObjectAttributes": { - "type": "object", - "properties": { - "objectAttribute": { - "type": "array", - "items": { - "$ref": "#/definitions/Property" - }, - "minItems": 1 - } - } + "policyEnforcementPoint": { + "$ref": "#/definitions/PolicyEnforcementPoint" }, - "Permission": { - "type": "object", - "properties": { - "permission": { - "$ref": "#/definitions/Reference" - }, - "kindOfPermission": { - "type": "string", - "enum": [ - "Allow", - "Deny", - "NotApplicable", - "Undefined" - ] - } - }, - "required": [ - "permission", - "kindOfPermission" - ] + "policyInformationPoints": { + "$ref": "#/definitions/PolicyInformationPoints" } - } -} \ No newline at end of file + }, + "required": [ "policyAdministrationPoint", "policyDecisionPoint", "policyEnforcementPoint" ] + }, + "PolicyAdministrationPoint": { + "type": "object", + "properties": { + "localAccessControl": { + "$ref": "#/definitions/AccessControl" + }, + "externalAccessControl": { + "type": "boolean" + } + }, + "required": [ "externalAccessControl" ] + }, + "PolicyInformationPoints": { + "type": "object", + "properties": { + "internalInformationPoint": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + } + }, + "externalInformationPoint": { + "type": "boolean" + } + }, + "required": [ "externalInformationPoint" ] + }, + "PolicyEnforcementPoint": { + "type": "object", + "properties": { + "externalPolicyEnforcementPoint": { + "type": "boolean" + } + }, + "required": [ "externalPolicyEnforcementPoint" ] + }, + "PolicyDecisionPoint": { + "type": "object", + "properties": { + "externalPolicyDecisionPoints": { + "type": "boolean" + } + }, + "required": [ "externalPolicyDecisionPoints" ] + }, + "AccessControl": { + "type": "object", + "properties": { + "selectableSubjectAttributes": { + "$ref": "#/definitions/Reference" + }, + "defaultSubjectAttributes": { + "$ref": "#/definitions/Reference" + }, + "selectablePermissions": { + "$ref": "#/definitions/Reference" + }, + "defaultPermissions": { + "$ref": "#/definitions/Reference" + }, + "selectableEnvironmentAttributes": { + "$ref": "#/definitions/Reference" + }, + "defaultEnvironmentAttributes": { + "$ref": "#/definitions/Reference" + }, + "accessPermissionRule": { + "type": "array", + "items": { + "$ref": "#/definitions/AccessPermissionRule" + } + } + } + }, + "AccessPermissionRule": { + "allOf": [ + { "$ref": "#/definitions/Referable" }, + { "$ref": "#/definitions/Qualifiable" }, + { "properties": { + "targetSubjectAttributes": { + "type": "array", + "items": { + "$ref": "#/definitions/SubjectAttributes" + }, + "minItems": 1 + }, + "permissionsPerObject": { + "type": "array", + "items": { + "$ref": "#/definitions/PermissionsPerObject" + } + } + }, + "required": [ "targetSubjectAttributes" ] + } + ] + }, + "SubjectAttributes": { + "type": "object", + "properties": { + "subjectAttributes": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + } + } + }, + "PermissionsPerObject": { + "type": "object", + "properties": { + "object": { + "$ref": "#/definitions/Reference" + }, + "targetObjectAttributes": { + "$ref": "#/definitions/ObjectAttributes" + }, + "permission": { + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + } + }, + "ObjectAttributes": { + "type": "object", + "properties": { + "objectAttribute": { + "type": "array", + "items": { + "$ref": "#/definitions/Property" + }, + "minItems": 1 + } + } + }, + "Permission": { + "type": "object", + "properties": { + "permission": { + "$ref": "#/definitions/Reference" + }, + "kindOfPermission": { + "type": "string", + "enum": ["Allow", "Deny", "NotApplicable", "Undefined"] + } + }, + "required": [ "permission", "kindOfPermission" ] + } + } +} diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index e5639e5..97dd02d 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -1,9 +1,10 @@ # Copyright (c) 2020 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. +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 """ .. _adapter.json.json_deserialization: @@ -36,8 +37,8 @@ from typing import Dict, Callable, TypeVar, Type, List, IO, Optional, Set from basyx.aas import model -from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE, \ - IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, \ +from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ + IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ KEY_ELEMENTS_CLASSES_INVERSE logger = logging.getLogger(__name__) @@ -127,13 +128,15 @@ class AASFromJsonDecoder(json.JSONDecoder): .. code-block:: python - class EnhancedAsset(model.Asset): + .. code-block:: python + + class EnhancedSubmodel(model.Submodel): pass - class EnhancedAASDecoder(AASFromJsonDecoder): + class EnhancedAASDecoder(StrictAASFromJsonDecoder): @classmethod - def _construct_asset(cls, dct): - return super()._construct_asset(dct, object_class=EnhancedAsset) + def _construct_submodel(cls, dct, object_class=EnhancedSubmodel): + return super()._construct_submodel(dct, object_class=object_class) :cvar failsafe: If `True` (the default), don't raise Exceptions for missing attributes and wrong types, but instead @@ -163,14 +166,13 @@ def object_hook(cls, dct: Dict[str, object]) -> object: # 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]] = { - 'Asset': cls._construct_asset, 'AssetAdministrationShell': cls._construct_asset_administration_shell, - 'View': cls._construct_view, + 'AssetInformation': cls._construct_asset_information, + 'IdentifierKeyValuePair': cls._construct_identifier_key_value_pair, 'ConceptDescription': cls._construct_concept_description, 'Qualifier': cls._construct_qualifier, - 'Formula': cls._construct_formula, + 'Extension': cls._construct_extension, 'Submodel': cls._construct_submodel, - 'ConceptDictionary': cls._construct_concept_dictionary, 'Capability': cls._construct_capability, 'Entity': cls._construct_entity, 'BasicEvent': cls._construct_basic_event, @@ -231,6 +233,8 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if isinstance(obj, model.Referable): if 'category' in dct: obj.category = _get_ts(dct, 'category', str) + if 'displayName' in dct: + obj.display_name = cls._construct_lang_string_set(_get_ts(dct, 'displayName', list)) if 'description' in dct: obj.description = cls._construct_lang_string_set(_get_ts(dct, 'description', list)) if isinstance(obj, model.Identifiable): @@ -249,6 +253,11 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if _expect_type(constraint, model.Constraint, str(obj), cls.failsafe): obj.qualifier.add(constraint) + if isinstance(obj, model.HasExtension) and not cls.stripped: + if 'extensions' in dct: + for extension in _get_ts(dct, 'extensions', list): + obj.extension.add(cls._construct_extension(extension)) + @classmethod def _get_kind(cls, dct: Dict[str, object]) -> model.ModelingKind: """ @@ -271,8 +280,14 @@ def _get_kind(cls, dct: Dict[str, object]) -> model.ModelingKind: def _construct_key(cls, dct: Dict[str, object], object_class=model.Key) -> model.Key: return object_class(type_=KEY_ELEMENTS_INVERSE[_get_ts(dct, 'type', str)], id_type=KEY_TYPES_INVERSE[_get_ts(dct, 'idType', str)], + value=_get_ts(dct, 'value', str)) + + @classmethod + def _construct_identifier_key_value_pair(cls, dct: Dict[str, object], object_class=model.IdentifierKeyValuePair) \ + -> model.IdentifierKeyValuePair: + return object_class(key=_get_ts(dct, 'key', str), value=_get_ts(dct, 'value', str), - local=_get_ts(dct, 'local', bool)) + external_subject_id=cls._construct_reference(_get_ts(dct, 'subjectId', dict))) @classmethod def _construct_reference(cls, dct: Dict[str, object], object_class=model.Reference) -> model.Reference: @@ -364,35 +379,31 @@ def _construct_value_reference_pair(cls, dct: Dict[str, object], object_class=mo # be called from the object_hook() method directly. @classmethod - def _construct_asset(cls, dct: Dict[str, object], object_class=model.Asset) -> model.Asset: - ret = object_class(kind=ASSET_KIND_INVERSE[_get_ts(dct, 'kind', str)], - identification=cls._construct_identifier(_get_ts(dct, "identification", dict))) + def _construct_asset_information(cls, dct: Dict[str, object], object_class=model.AssetInformation)\ + -> model.AssetInformation: + ret = object_class(asset_kind=ASSET_KIND_INVERSE[_get_ts(dct, 'assetKind', str)]) cls._amend_abstract_attributes(ret, dct) - if 'assetIdentificationModel' in dct: - ret.asset_identification_model = cls._construct_aas_reference( - _get_ts(dct, 'assetIdentificationModel', dict), model.Submodel) - if 'billOfMaterial' in dct: - ret.bill_of_material = cls._construct_aas_reference(_get_ts(dct, 'billOfMaterial', dict), model.Submodel) + if 'globalAssetId' in dct: + ret.global_asset_id = cls._construct_reference(_get_ts(dct, 'globalAssetId', dict)) + if 'externalAssetIds' in dct: + for desc_data in _get_ts(dct, "externalAssetIds", list): + ret.specific_asset_id.add(cls._construct_identifier_key_value_pair(desc_data, + model.IdentifierKeyValuePair)) + if 'thumbnail' in dct: + ret.default_thumbnail = _get_ts(dct, 'thumbnail', model.File) return ret @classmethod def _construct_asset_administration_shell( cls, dct: Dict[str, object], object_class=model.AssetAdministrationShell) -> model.AssetAdministrationShell: ret = object_class( - asset=cls._construct_aas_reference(_get_ts(dct, 'asset', dict), model.Asset), + asset_information=cls._construct_asset_information(_get_ts(dct, 'assetInformation', dict), + model.AssetInformation), identification=cls._construct_identifier(_get_ts(dct, 'identification', dict))) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'submodels' in dct: for sm_data in _get_ts(dct, 'submodels', list): ret.submodel.add(cls._construct_aas_reference(sm_data, model.Submodel)) - if not cls.stripped and 'views' in dct: - for view in _get_ts(dct, 'views', list): - if _expect_type(view, model.View, str(ret), cls.failsafe): - ret.view.add(view) - if 'conceptDictionaries' in dct: - for concept_dictionary in _get_ts(dct, 'conceptDictionaries', list): - if _expect_type(concept_dictionary, model.ConceptDictionary, str(ret), cls.failsafe): - ret.concept_dictionary.add(concept_dictionary) if 'security' in dct: ret.security = cls._construct_security(_get_ts(dct, 'security', dict)) if 'derivedFrom' in dct: @@ -400,17 +411,6 @@ def _construct_asset_administration_shell( model.AssetAdministrationShell) return ret - @classmethod - def _construct_view(cls, dct: Dict[str, object], object_class=model.View) -> model.View: - ret = object_class(_get_ts(dct, 'idShort', str)) - cls._amend_abstract_attributes(ret, dct) - if 'containedElements' in dct: - for element_data in _get_ts(dct, 'containedElements', list): - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - ret.contained_element.add(cls._construct_aas_reference(element_data, model.Referable)) # type: ignore - return ret - @classmethod def _construct_concept_description(cls, dct: Dict[str, object], object_class=model.ConceptDescription)\ -> model.ConceptDescription: @@ -465,23 +465,19 @@ def _construct_iec61360_concept_description(cls, dct: Dict[str, object], data_sp for level_type in _get_ts(data_spec, 'levelType', list)) return ret - @classmethod - def _construct_concept_dictionary(cls, dct: Dict[str, object], object_class=model.ConceptDictionary)\ - -> model.ConceptDictionary: - ret = object_class(_get_ts(dct, "idShort", str)) - cls._amend_abstract_attributes(ret, dct) - if 'conceptDescriptions' in dct: - for desc_data in _get_ts(dct, "conceptDescriptions", list): - ret.concept_description.add(cls._construct_aas_reference(desc_data, model.ConceptDescription)) - return ret - @classmethod def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> model.Entity: + global_asset_id = None + if 'globalAssetId' in dct: + global_asset_id = cls._construct_reference(_get_ts(dct, 'globalAssetId', dict)) + specific_asset_id = None + if 'externalAssetId' in dct: + specific_asset_id = cls._construct_identifier_key_value_pair(_get_ts(dct, 'externalAssetId', dict)) + ret = object_class(id_short=_get_ts(dct, "idShort", str), entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], - asset=(cls._construct_aas_reference(_get_ts(dct, 'asset', dict), model.Asset) - if 'asset' in dct else None), - kind=cls._get_kind(dct)) + global_asset_id=global_asset_id, + specific_asset_id=specific_asset_id) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'statements' in dct: for element in _get_ts(dct, "statements", list): @@ -501,21 +497,15 @@ def _construct_qualifier(cls, dct: Dict[str, object], object_class=model.Qualifi return ret @classmethod - def _construct_formula(cls, dct: Dict[str, object], object_class=model.Formula) -> model.Formula: - ret = object_class() + def _construct_extension(cls, dct: Dict[str, object], object_class=model.Extension) -> model.Extension: + ret = object_class(name=_get_ts(dct, 'name', str)) cls._amend_abstract_attributes(ret, dct) - if 'dependsOn' in dct: - for dependency_data in _get_ts(dct, 'dependsOn', list): - try: - ret.depends_on.add(cls._construct_reference(dependency_data)) - except (KeyError, TypeError) as e: - error_message = \ - "Error while trying to convert JSON object into dependency Reference for {}: {} >>> {}".format( - ret, e, pprint.pformat(dct, depth=2, width=2 ** 14, compact=True)) - if cls.failsafe: - logger.error(error_message, exc_info=e) - else: - raise type(e)(error_message) from e + if 'valueType' in dct: + ret.value_type = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)] + if 'value' in dct: + ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) + if 'refersTo' in dct: + ret.refers_to = cls._construct_reference(_get_ts(dct, 'refersTo', dict)) return ret @classmethod @@ -603,17 +593,19 @@ def _construct_annotated_relationship_element( @classmethod def _construct_submodel_element_collection( cls, - dct: Dict[str, object], - object_class_ordered=model.SubmodelElementCollectionOrdered, - object_class_unordered=model.SubmodelElementCollectionUnordered)\ + dct: Dict[str, object])\ -> model.SubmodelElementCollection: ret: model.SubmodelElementCollection - if 'ordered' in dct and _get_ts(dct, 'ordered', bool): - ret = object_class_ordered( - id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) - else: - ret = object_class_unordered( - id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + ordered = False + allow_duplicates = False + if 'ordered' in dct: + ordered = _get_ts(dct, "ordered", bool) + if 'allowDuplicates' in dct: + allow_duplicates = _get_ts(dct, "allowDuplicates", bool) + ret = model.SubmodelElementCollection.create(id_short=_get_ts(dct, "idShort", str), + kind=cls._get_kind(dct), + ordered=ordered, + allow_duplicates=allow_duplicates) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'value' in dct: for element in _get_ts(dct, "value", list): @@ -767,7 +759,6 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r data = json.load(file, cls=decoder_) for name, expected_type in (('assetAdministrationShells', model.AssetAdministrationShell), - ('assets', model.Asset), ('submodels', model.Submodel), ('conceptDescriptions', model.ConceptDescription)): try: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index a2ec130..cc3ccf3 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -1,22 +1,22 @@ # Copyright (c) 2020 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. +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 """ .. _adapter.json.json_serialization: Module for serializing Asset Administration Shell objects to the official JSON format -The module provides an custom JSONEncoder classes :class:`~.AASToJsonEncoder` and :class:`~.StrippedAASToJsonEncoder` +The module provides an custom JSONEncoder classes :class:`~.AASToJsonEncoder` and :class:`~.AASToJsonEncoderStripped` to be used with the Python standard `json` module. While the former serializes objects as defined in the specification, the latter serializes stripped objects, excluding some attributes (see https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91). Each class contains a custom :meth:`~.AASToJsonEncoder.default` function which converts BaSyx Python SDK objects to simple python types for an automatic JSON serialization. -To simplify the usage of this module, the :meth:`~aas.adapter.json.json_serialization.write_aas_json_file` and -:meth:`~aas.adapter.json.json_serialization.object_store_to_json` are provided. +To simplify the usage of this module, the :meth:`~.write_aas_json_file` and :meth:`~.object_store_to_json` are provided. The former is used to serialize a given :class:`~aas.model.provider.AbstractObjectStore` to a file, while the latter serializes the object store to a string and returns it. @@ -38,7 +38,7 @@ class AASToJsonEncoder(json.JSONEncoder): """ - Custom JSONDecoder class to use the `json` module for serializing Asset Administration Shell data into the + Custom JSON Encoder class to use the `json` module for serializing Asset Administration Shell data into the official JSON format The class overrides the `default()` method to transform BaSyx Python SDK objects into dicts that may be serialized @@ -75,8 +75,10 @@ def default(self, obj: object) -> object: return self._key_to_json(obj) if isinstance(obj, model.ValueReferencePair): return self._value_reference_pair_to_json(obj) - if isinstance(obj, model.Asset): - return self._asset_to_json(obj) + if isinstance(obj, model.AssetInformation): + return self._asset_information_to_json(obj) + if isinstance(obj, model.IdentifierKeyValuePair): + return self._identifier_key_value_pair_to_json(obj) if isinstance(obj, model.Submodel): return self._submodel_to_json(obj) if isinstance(obj, model.Operation): @@ -89,10 +91,6 @@ def default(self, obj: object) -> object: return self._basic_event_to_json(obj) if isinstance(obj, model.Entity): return self._entity_to_json(obj) - if isinstance(obj, model.View): - return self._view_to_json(obj) - if isinstance(obj, model.ConceptDictionary): - return self._concept_dictionary_to_json(obj) if isinstance(obj, model.ConceptDescription): return self._concept_description_to_json(obj) if isinstance(obj, model.Property): @@ -115,8 +113,8 @@ def default(self, obj: object) -> object: return self._relationship_element_to_json(obj) if isinstance(obj, model.Qualifier): return self._qualifier_to_json(obj) - if isinstance(obj, model.Formula): - return self._formula_to_json(obj) + if isinstance(obj, model.Extension): + return self._extension_to_json(obj) return super().default(obj) @classmethod @@ -127,9 +125,15 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: :param obj: object which must be serialized :return: dict with the serialized attributes of the abstract classes this object inherits from """ - data = {} + data: Dict[str, object] = {} + if isinstance(obj, model.HasExtension) and not cls.stripped: + if obj.extension: + data['extensions'] = list(obj.extension) if isinstance(obj, model.Referable): - data['idShort'] = obj.id_short + if obj.id_short: + data['idShort'] = obj.id_short + if obj.display_name: + data['displayName'] = cls._lang_string_set_to_json(obj.display_name) if obj.category: data['category'] = obj.category if obj.description: @@ -175,8 +179,7 @@ def _key_to_json(cls, obj: model.Key) -> Dict[str, object]: data = cls._abstract_classes_to_json(obj) data.update({'type': _generic.KEY_ELEMENTS[obj.type], 'idType': _generic.KEY_TYPES[obj.id_type], - 'value': obj.value, - 'local': obj.local}) + 'value': obj.value}) return data @classmethod @@ -227,7 +230,7 @@ def _constraint_to_json(cls, obj: model.Constraint) -> Dict[str, object]: # TOD :param obj: object of class Constraint :return: dict with the serialized attributes of this object """ - CONSTRAINT_CLASSES = [model.Qualifier, model.Formula] + CONSTRAINT_CLASSES = [model.Qualifier] try: const_type = next(iter(t for t in inspect.getmro(type(obj)) if t in CONSTRAINT_CLASSES)) except StopIteration as e: @@ -247,35 +250,39 @@ def _namespace_to_json(cls, obj): # not in specification yet return data @classmethod - def _formula_to_json(cls, obj: model.Formula) -> Dict[str, object]: + def _qualifier_to_json(cls, obj: model.Qualifier) -> Dict[str, object]: """ - serialization of an object from class Formula to json + serialization of an object from class Qualifier to json - :param obj: object of class Formula + :param obj: object of class Qualifier :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) data.update(cls._constraint_to_json(obj)) - if obj.depends_on: - data['dependsOn'] = list(obj.depends_on) + if obj.value: + data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None + if obj.value_id: + data['valueId'] = obj.value_id + data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] + data['type'] = obj.type return data @classmethod - def _qualifier_to_json(cls, obj: model.Qualifier) -> Dict[str, object]: + def _extension_to_json(cls, obj: model.Extension) -> Dict[str, object]: """ - serialization of an object from class Qualifier to json + serialization of an object from class Extension to json - :param obj: object of class Qualifier + :param obj: object of class Extension :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data.update(cls._constraint_to_json(obj)) if obj.value: data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None - if obj.value_id: - data['valueId'] = obj.value_id - data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] - data['type'] = obj.type + if obj.refers_to: + data['refersTo'] = obj.refers_to + if obj.value_type: + data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] + data['name'] = obj.name return data @classmethod @@ -307,32 +314,35 @@ def _value_list_to_json(cls, obj: model.ValueList) -> Dict[str, object]: # ############################################################ @classmethod - def _view_to_json(cls, obj: model.View) -> Dict[str, object]: + def _identifier_key_value_pair_to_json(cls, obj: model.IdentifierKeyValuePair) -> Dict[str, object]: """ - serialization of an object from class View to json + serialization of an object from class IdentifierKeyValuePair to json - :param obj: object of class View + :param obj: object of class IdentifierKeyValuePair :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - if obj.contained_element: - data['containedElements'] = list(obj.contained_element) + data['key'] = obj.key + data['value'] = obj.value + data['subjectId'] = obj.external_subject_id return data @classmethod - def _asset_to_json(cls, obj: model.Asset) -> Dict[str, object]: + def _asset_information_to_json(cls, obj: model.AssetInformation) -> Dict[str, object]: """ - serialization of an object from class Asset to json + serialization of an object from class AssetInformation to json - :param obj: object of class Asset + :param obj: object of class AssetInformation :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data['kind'] = _generic.ASSET_KIND[obj.kind] - if obj.asset_identification_model: - data['assetIdentificationModel'] = obj.asset_identification_model - if obj.bill_of_material: - data['billOfMaterial'] = obj.bill_of_material + data['assetKind'] = _generic.ASSET_KIND[obj.asset_kind] + if obj.global_asset_id: + data['globalAssetId'] = obj.global_asset_id + if obj.specific_asset_id: + data['externalAssetIds'] = list(obj.specific_asset_id) + if obj.default_thumbnail: + data['thumbnail'] = obj.default_thumbnail return data @classmethod @@ -392,25 +402,12 @@ def _append_iec61360_concept_description_attrs(cls, obj: model.concept.IEC61360C data_spec['levelType'] = [_generic.IEC61360_LEVEL_TYPES[lt] for lt in obj.level_types] data['embeddedDataSpecifications'] = [ {'dataSpecification': model.Reference(( - model.Key(model.KeyElements.GLOBAL_REFERENCE, False, + model.Key(model.KeyElements.GLOBAL_REFERENCE, "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0", model.KeyType.IRI),)), 'dataSpecificationContent': data_spec} ] - @classmethod - def _concept_dictionary_to_json(cls, obj: model.ConceptDictionary) -> Dict[str, object]: - """ - serialization of an object from class ConceptDictionary to json - - :param obj: object of class ConceptDictionary - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if obj.concept_description: - data['conceptDescriptions'] = list(obj.concept_description) - return data - @classmethod def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell) -> Dict[str, object]: """ @@ -423,13 +420,10 @@ def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell data.update(cls._namespace_to_json(obj)) if obj.derived_from: data["derivedFrom"] = obj.derived_from - data["asset"] = obj.asset + if obj.asset_information: + data["assetInformation"] = obj.asset_information if not cls.stripped and obj.submodel: data["submodels"] = list(obj.submodel) - if not cls.stripped and obj.view: - data["views"] = list(obj.view) - if obj.concept_dictionary: - data["conceptDictionaries"] = list(obj.concept_dictionary) if obj.security: data["security"] = obj.security return data @@ -543,9 +537,7 @@ def _file_to_json(cls, obj: model.File) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data['mimeType'] = obj.mime_type - if obj.value is not None: - data['value'] = obj.value + data.update({'value': obj.value, 'mimeType': obj.mime_type}) return data @classmethod @@ -574,6 +566,7 @@ def _submodel_element_collection_to_json(cls, obj: model.SubmodelElementCollecti if not cls.stripped and obj.value: data['value'] = list(obj.value) data['ordered'] = obj.ordered + data['allowDuplicates'] = obj.allow_duplicates return data @classmethod @@ -655,8 +648,10 @@ def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]: if not cls.stripped and obj.statement: data['statements'] = list(obj.statement) data['entityType'] = _generic.ENTITY_TYPES[obj.entity_type] - if obj.asset: - data['asset'] = obj.asset + if obj.global_asset_id: + data['globalAssetId'] = obj.global_asset_id + if obj.specific_asset_id: + data['externalAssetId'] = obj.specific_asset_id return data @classmethod @@ -706,14 +701,11 @@ def _select_encoder(stripped: bool, encoder: Optional[Type[AASToJsonEncoder]] = def _create_dict(data: model.AbstractObjectStore) -> dict: # separate different kind of objects - assets = [] asset_administration_shells = [] submodels = [] concept_descriptions = [] for obj in data: - if isinstance(obj, model.Asset): - assets.append(obj) - elif isinstance(obj, model.AssetAdministrationShell): + if isinstance(obj, model.AssetAdministrationShell): asset_administration_shells.append(obj) elif isinstance(obj, model.Submodel): submodels.append(obj) @@ -722,7 +714,6 @@ def _create_dict(data: model.AbstractObjectStore) -> dict: dict_ = { 'assetAdministrationShells': asset_administration_shells, 'submodels': submodels, - 'assets': assets, 'conceptDescriptions': concept_descriptions, } return dict_ @@ -735,12 +726,11 @@ def object_store_to_json(data: model.AbstractObjectStore, stripped: bool = False chapter 5.5 :param data: :class:`ObjectStore ` which contains different objects of the - AAS meta model which should be serialized to a - JSON file - :param stripped: If true, objects are serialized to stripped json objects.. + AAS meta model which should be serialized to a JSON file + :param stripped: If true, objects are serialized to stripped json objects. See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 This parameter is ignored if an encoder class is specified. - :param encoder: The encoder class used to encoder the JSON objects + :param encoder: The encoder class used to encode the JSON objects :param kwargs: Additional keyword arguments to be passed to `json.dumps()` """ encoder_ = _select_encoder(stripped, encoder) @@ -756,13 +746,12 @@ def write_aas_json_file(file: IO, data: model.AbstractObjectStore, stripped: boo :param file: A file-like object to write the JSON-serialized data to :param data: :class:`ObjectStore ` which contains different objects of the - AAS meta model which should be serialized to a - JSON file - :param stripped: If true, objects are serialized to stripped json objects.. + AAS meta model which should be serialized to a JSON file + :param stripped: If `True`, objects are serialized to stripped json objects. See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 This parameter is ignored if an encoder class is specified. - :param encoder: The encoder class used to encoder the JSON objects - :param kwargs: Additional keyword arguments to be passed to json.dumps() + :param encoder: The encoder class used to encode the JSON objects + :param kwargs: Additional keyword arguments to be passed to `json.dump()` """ encoder_ = _select_encoder(stripped, encoder) # serialize object to json diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 0e2fbca..39bf2b3 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -1,8 +1,9 @@ - - - + + + + + @@ -24,47 +25,51 @@ xmlns="http://www.w3.org/2001/XMLSchema" xmlns:aas="http://www.admin-shell.io/aa - - + - + - - - - - - - - + + + + + + + + + - - - - + - + + + + + + + + + - + - + @@ -108,21 +113,10 @@ type="aas:assetAdministrationShell_t"/> - - - - - - - - - - - - + - - + + @@ -130,6 +124,14 @@ type="aas:assetAdministrationShell_t"/> + + + + + + + + @@ -150,7 +152,8 @@ type="aas:assetAdministrationShell_t"/> - + + @@ -159,7 +162,7 @@ type="aas:assetAdministrationShell_t"/> - + @@ -169,12 +172,26 @@ type="aas:assetAdministrationShell_t"/> + + + + + + + + + + + + + + + - @@ -205,6 +222,14 @@ type="aas:assetAdministrationShell_t"/> + + + + + + + + @@ -231,13 +256,11 @@ type="aas:assetAdministrationShell_t"/> - - @@ -259,7 +282,6 @@ type="aas:assetAdministrationShell_t"/> - @@ -297,10 +319,9 @@ type="aas:assetAdministrationShell_t"/> + - @@ -319,20 +340,20 @@ type="aas:operationVariable_t"/> - - + + - - + - + + @@ -344,9 +365,9 @@ type="aas:operationVariable_t"/> - - + + @@ -360,7 +381,7 @@ type="aas:operationVariable_t"/> - + @@ -397,7 +418,6 @@ type="aas:operationVariable_t"/> - @@ -405,6 +425,7 @@ type="aas:operationVariable_t"/> + @@ -420,9 +441,9 @@ type="aas:operationVariable_t"/> - - + + @@ -447,25 +468,26 @@ type="aas:operationVariable_t"/> - - - - - - - - - - - - - + + + + + + + + + + - + + + + + + @@ -481,27 +503,29 @@ type="aas:embeddedDataSpecification_t"/> - + + + + + + + + - - - - - - + - + @@ -520,31 +544,4 @@ type="aas:embeddedDataSpecification_t"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 5d40306..8eae53d 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1,9 +1,10 @@ # Copyright (c) 2020 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. +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 """ .. _adapter.xml.xml_deserialization: @@ -85,7 +86,7 @@ def _tag_replace_namespace(tag: str, nsmap: Dict[str, str]) -> str: """ split = tag.split("}") for prefix, namespace in nsmap.items(): - if prefix and namespace == split[0][1:]: + if namespace == split[0][1:]: return prefix + ":" + split[1] return tag @@ -96,7 +97,7 @@ def _element_pretty_identifier(element: etree.Element) -> str: If the prefix is known, the namespace in the element tag is replaced by the prefix. If additionally also the sourceline is known, is is added as a suffix to name. - For example, instead of "{http://www.admin-shell.io/aas/2/0}assetAdministrationShell" this function would return + For example, instead of "{http://www.admin-shell.io/aas/3/0}assetAdministrationShell" this function would return "aas:assetAdministrationShell on line $line", if both, prefix and sourceline, are known. :param element: The xml element. @@ -422,6 +423,10 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None """ if isinstance(obj, model.Referable): category = _get_text_or_none(element.find(NS_AAS + "category")) + display_name = _failsafe_construct(element.find(NS_AAS + "displayName"), cls.construct_lang_string_set, + cls.failsafe) + if display_name is not None: + obj.display_name = display_name if category is not None: obj.category = category description = _failsafe_construct(element.find(NS_AAS + "description"), cls.construct_lang_string_set, @@ -442,16 +447,15 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None if semantic_id is not None: obj.semantic_id = semantic_id if isinstance(obj, model.Qualifiable) and not cls.stripped: - # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 - for constraint in element.findall(NS_AAS + "qualifier"): - if len(constraint) == 0: - continue - if len(constraint) > 1: - logger.warning(f"{_element_pretty_identifier(constraint)} has more than one constraint, " - "which is invalid! Deserializing all constraints anyways...") - for constructed in _failsafe_construct_multiple(constraint, cls.construct_constraint, cls.failsafe): - obj.qualifier.add(constructed) + qualifiers_elem = element.find(NS_AAS + "qualifiers") + if qualifiers_elem is not None and len(qualifiers_elem) > 0: + for constraint in _failsafe_construct_multiple(qualifiers_elem, cls.construct_constraint, cls.failsafe): + obj.qualifier.add(constraint) + if isinstance(obj, model.HasExtension) and not cls.stripped: + extension_elem = element.find(NS_AAS + "extension") + if extension_elem is not None: + for extension in _failsafe_construct_multiple(extension_elem, cls.construct_extension, cls.failsafe): + obj.extension.add(extension) @classmethod def _construct_relationship_element_internal(cls, element: etree.Element, object_class: Type[RE], **_kwargs: Any) \ @@ -485,14 +489,6 @@ def _construct_submodel_reference(cls, element: etree.Element, **kwargs: Any) -> """ return cls.construct_aas_reference_expect_type(element, model.Submodel, **kwargs) - @classmethod - def _construct_asset_reference(cls, element: etree.Element, **kwargs: Any) \ - -> model.AASReference[model.Asset]: - """ - Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. - """ - return cls.construct_aas_reference_expect_type(element, model.Asset, **kwargs) - @classmethod def _construct_asset_administration_shell_reference(cls, element: etree.Element, **kwargs: Any) \ -> model.AASReference[model.AssetAdministrationShell]: @@ -511,20 +507,11 @@ def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ # see https://github.com/python/mypy/issues/5374 return cls.construct_aas_reference_expect_type(element, model.Referable, **kwargs) # type: ignore - @classmethod - def _construct_concept_description_reference(cls, element: etree.Element, **kwargs: Any) \ - -> model.AASReference[model.ConceptDescription]: - """ - Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. - """ - return cls.construct_aas_reference_expect_type(element, model.ConceptDescription, **kwargs) - @classmethod def construct_key(cls, element: etree.Element, object_class=model.Key, **_kwargs: Any) \ -> model.Key: return object_class( _get_attrib_mandatory_mapped(element, "type", KEY_ELEMENTS_INVERSE), - _str_to_bool(_get_attrib_mandatory(element, "local")), _get_text_mandatory(element), _get_attrib_mandatory_mapped(element, "idType", KEY_TYPES_INVERSE) ) @@ -568,8 +555,8 @@ def construct_aas_reference_expect_type(cls, element: etree.Element, type_: Type def construct_administrative_information(cls, element: etree.Element, object_class=model.AdministrativeInformation, **_kwargs: Any) -> model.AdministrativeInformation: return object_class( - _get_text_or_none(element.find(NS_AAS + "version")), - _get_text_or_none(element.find(NS_AAS + "revision")) + revision=_get_text_or_none(element.find(NS_AAS + "revision")), + version=_get_text_or_none(element.find(NS_AAS + "version")) ) @classmethod @@ -600,14 +587,21 @@ def construct_qualifier(cls, element: etree.Element, object_class=model.Qualifie return qualifier @classmethod - def construct_formula(cls, element: etree.Element, object_class=model.Formula, **_kwargs: Any) -> model.Formula: - formula = object_class() - depends_on_refs = element.find(NS_AAS + "dependsOnRefs") - if depends_on_refs is not None: - for ref in _failsafe_construct_multiple(depends_on_refs.findall(NS_AAS + "reference"), - cls.construct_reference, cls.failsafe): - formula.depends_on.add(ref) - return formula + def construct_extension(cls, element: etree.Element, object_class=model.Extension, **_kwargs: Any) \ + -> model.Extension: + extension = object_class( + _child_text_mandatory(element, NS_AAS + "name")) + value_type = _get_text_or_none(element.find(NS_AAS + "valueType")) + if value_type is not None: + extension.value_type = model.datatypes.XSD_TYPE_CLASSES[value_type] + value = _get_text_or_none(element.find(NS_AAS + "value")) + if value is not None: + extension.value = model.datatypes.from_xsd(value, extension.value_type) + refers_to = _failsafe_construct(element.find(NS_AAS + "RefersTo"), cls.construct_reference, cls.failsafe) + if refers_to is not None: + extension.refers_to = refers_to + cls._amend_abstract_attributes(extension, element) + return extension @classmethod def construct_identifier(cls, element: etree.Element, object_class=model.Identifier, **_kwargs: Any) \ @@ -624,29 +618,6 @@ def construct_security(cls, _element: etree.Element, object_class=model.Security """ return object_class() - @classmethod - def construct_view(cls, element: etree.Element, object_class=model.View, **_kwargs: Any) -> model.View: - view = object_class(_child_text_mandatory(element, NS_AAS + "idShort")) - contained_elements = element.find(NS_AAS + "containedElements") - if contained_elements is not None: - for ref in _failsafe_construct_multiple(contained_elements.findall(NS_AAS + "containedElementRef"), - cls._construct_referable_reference, cls.failsafe): - view.contained_element.add(ref) - cls._amend_abstract_attributes(view, element) - return view - - @classmethod - def construct_concept_dictionary(cls, element: etree.Element, object_class=model.ConceptDictionary, - **_kwargs: Any) -> model.ConceptDictionary: - concept_dictionary = object_class(_child_text_mandatory(element, NS_AAS + "idShort")) - concept_description = element.find(NS_AAS + "conceptDescriptionRefs") - if concept_description is not None: - for ref in _failsafe_construct_multiple(concept_description.findall(NS_AAS + "conceptDescriptionRef"), - cls._construct_concept_description_reference, cls.failsafe): - concept_dictionary.concept_description.add(ref) - cls._amend_abstract_attributes(concept_dictionary, element) - return concept_dictionary - @classmethod def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement: """ @@ -697,7 +668,6 @@ def construct_constraint(cls, element: etree.Element, **kwargs: Any) -> model.Co Overwrite construct_formula or construct_qualifier instead. """ constraints: Dict[str, Callable[..., model.Constraint]] = {NS_AAS + k: v for k, v in { - "formula": cls.construct_formula, "qualifier": cls.construct_qualifier }.items()} if element.tag not in constraints: @@ -728,8 +698,9 @@ def construct_annotated_relationship_element(cls, element: etree.Element, raise KeyError(f"{_element_pretty_identifier(data_element)} has no data element!") if len(data_element) > 1: logger.warning(f"{_element_pretty_identifier(data_element)} has more than one data element, " - "which is invalid! Deserializing all data elements anyways...") - for constructed in _failsafe_construct_multiple(data_element, cls.construct_data_element, cls.failsafe): + "using the first one...") + constructed = _failsafe_construct(data_element[0], cls.construct_data_element, cls.failsafe) + if constructed is not None: annotated_relationship_element.annotation.add(constructed) return annotated_relationship_element @@ -769,13 +740,16 @@ def construct_capability(cls, element: etree.Element, object_class=model.Capabil @classmethod def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_kwargs: Any) -> model.Entity: + global_asset_id = _failsafe_construct(element.find(NS_AAS + "globalAssetId"), + cls.construct_reference, cls.failsafe) + specific_asset_id = _failsafe_construct(element.find(NS_AAS + "specificAssetId"), + cls.construct_identifier_key_value_pair, cls.failsafe) entity = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), - _child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), - # pass the asset to the constructor, because self managed entities need asset references - asset=_failsafe_construct(element.find(NS_AAS + "assetRef"), cls._construct_asset_reference, cls.failsafe), - kind=_get_modeling_kind(element) - ) + id_short=_child_text_mandatory(element, NS_AAS + "idShort"), + entity_type=_child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), + global_asset_id=global_asset_id, + specific_asset_id=specific_asset_id) + if not cls.stripped: # TODO: remove wrapping submodelElement, in accordance to future schemas # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 @@ -785,9 +759,9 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") if len(submodel_element) > 1: logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " which is invalid! Deserializing all submodel elements anyways...") - for constructed in _failsafe_construct_multiple(submodel_element, cls.construct_submodel_element, - cls.failsafe): + " using the first one...") + constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) + if constructed is not None: entity.statement.add(constructed) cls._amend_abstract_attributes(entity, element) return entity @@ -892,14 +866,14 @@ def construct_relationship_element(cls, element: etree.Element, object_class=mod @classmethod def construct_submodel_element_collection(cls, element: etree.Element, - object_class_ordered=model.SubmodelElementCollectionOrdered, - object_class_unordered=model.SubmodelElementCollectionUnordered, **_kwargs: Any) -> model.SubmodelElementCollection: ordered = _str_to_bool(_child_text_mandatory(element, NS_AAS + "ordered")) - collection_type = object_class_ordered if ordered else object_class_unordered - collection = collection_type( + allow_duplicates = _str_to_bool(_child_text_mandatory(element, NS_AAS + "allowDuplicates")) + collection = model.SubmodelElementCollection.create( _child_text_mandatory(element, NS_AAS + "idShort"), - kind=_get_modeling_kind(element) + kind=_get_modeling_kind(element), + allow_duplicates=allow_duplicates, + ordered=ordered ) if not cls.stripped: value = _get_child_mandatory(element, NS_AAS + "value") @@ -910,9 +884,9 @@ def construct_submodel_element_collection(cls, element: etree.Element, raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") if len(submodel_element) > 1: logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " which is invalid! Deserializing all submodel elements anyways...") - for constructed in _failsafe_construct_multiple(submodel_element, cls.construct_submodel_element, - cls.failsafe): + " using the first one...") + constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) + if constructed is not None: collection.value.add(constructed) cls._amend_abstract_attributes(collection, element) return collection @@ -921,8 +895,9 @@ def construct_submodel_element_collection(cls, element: etree.Element, def construct_asset_administration_shell(cls, element: etree.Element, object_class=model.AssetAdministrationShell, **_kwargs: Any) -> model.AssetAdministrationShell: aas = object_class( - _child_construct_mandatory(element, NS_AAS + "assetRef", cls._construct_asset_reference), - _child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier) + identification=_child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier), + asset_information=_child_construct_mandatory(element, NS_AAS + "assetInformation", + cls.construct_asset_information) ) security = _failsafe_construct(element.find(NS_ABAC + "security"), cls.construct_security, cls.failsafe) if security is not None: @@ -933,15 +908,6 @@ def construct_asset_administration_shell(cls, element: etree.Element, object_cla for ref in _child_construct_multiple(submodels, NS_AAS + "submodelRef", cls._construct_submodel_reference, cls.failsafe): aas.submodel.add(ref) - views = element.find(NS_AAS + "views") - if views is not None: - for view in _child_construct_multiple(views, NS_AAS + "view", cls.construct_view, cls.failsafe): - aas.view.add(view) - concept_dictionaries = element.find(NS_AAS + "conceptDictionaries") - if concept_dictionaries is not None: - for cd in _child_construct_multiple(concept_dictionaries, NS_AAS + "conceptDictionary", - cls.construct_concept_dictionary, cls.failsafe): - aas.concept_dictionary.add(cd) derived_from = _failsafe_construct(element.find(NS_AAS + "derivedFrom"), cls._construct_asset_administration_shell_reference, cls.failsafe) if derived_from is not None: @@ -950,21 +916,37 @@ def construct_asset_administration_shell(cls, element: etree.Element, object_cla return aas @classmethod - def construct_asset(cls, element: etree.Element, object_class=model.Asset, **_kwargs: Any) -> model.Asset: - asset = object_class( - _child_text_mandatory_mapped(element, NS_AAS + "kind", ASSET_KIND_INVERSE), - _child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier) + def construct_identifier_key_value_pair(cls, element: etree.Element, object_class=model.IdentifierKeyValuePair, + **_kwargs: Any) -> model.IdentifierKeyValuePair: + return object_class( + external_subject_id=_child_construct_mandatory(element, NS_AAS + "externalSubjectId", + cls.construct_reference, namespace=NS_AAS), + key=_get_text_or_none(element.find(NS_AAS + "key")), + value=_get_text_or_none(element.find(NS_AAS + "value")) + ) + + @classmethod + def construct_asset_information(cls, element: etree.Element, object_class=model.AssetInformation, **_kwargs: Any) \ + -> model.AssetInformation: + asset_information = object_class( + _child_text_mandatory_mapped(element, NS_AAS + "assetKind", ASSET_KIND_INVERSE), ) - asset_identification_model = _failsafe_construct(element.find(NS_AAS + "assetIdentificationModelRef"), - cls._construct_submodel_reference, cls.failsafe) - if asset_identification_model is not None: - asset.asset_identification_model = asset_identification_model - bill_of_material = _failsafe_construct(element.find(NS_AAS + "billOfMaterialRef"), - cls._construct_submodel_reference, cls.failsafe) - if bill_of_material is not None: - asset.bill_of_material = bill_of_material - cls._amend_abstract_attributes(asset, element) - return asset + global_asset_id = _failsafe_construct(element.find(NS_AAS + "globalAssetId"), + cls.construct_reference, cls.failsafe) + if global_asset_id is not None: + asset_information.global_asset_id = global_asset_id + specific_assset_ids = element.find(NS_AAS + "specificAssetIds") + if specific_assset_ids is not None: + for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", + cls.construct_identifier_key_value_pair, cls.failsafe): + asset_information.specific_asset_id.add(id) + thumbnail = _failsafe_construct(element.find(NS_AAS + "defaultThumbNail"), + cls.construct_file, cls.failsafe) + if thumbnail is not None: + asset_information.default_thumbnail = thumbnail + + cls._amend_abstract_attributes(asset_information, element) + return asset_information @classmethod def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **_kwargs: Any) \ @@ -983,9 +965,9 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") if len(submodel_element) > 1: logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " which is invalid! Deserializing all submodel elements anyways...") - for constructed in _failsafe_construct_multiple(submodel_element, cls.construct_submodel_element, - cls.failsafe): + " using the first one...") + constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) + if constructed is not None: submodel.submodel_element.add(constructed) cls._amend_abstract_attributes(submodel, element) return submodel @@ -1185,17 +1167,15 @@ class XMLConstructables(enum.Enum): AAS_REFERENCE = enum.auto() ADMINISTRATIVE_INFORMATION = enum.auto() QUALIFIER = enum.auto() - FORMULA = enum.auto() IDENTIFIER = enum.auto() SECURITY = enum.auto() - VIEW = enum.auto() - CONCEPT_DICTIONARY = enum.auto() OPERATION_VARIABLE = enum.auto() ANNOTATED_RELATIONSHIP_ELEMENT = enum.auto() BASIC_EVENT = enum.auto() BLOB = enum.auto() CAPABILITY = enum.auto() ENTITY = enum.auto() + EXTENSION = enum.auto() FILE = enum.auto() MULTI_LANGUAGE_PROPERTY = enum.auto() OPERATION = enum.auto() @@ -1205,7 +1185,8 @@ class XMLConstructables(enum.Enum): RELATIONSHIP_ELEMENT = enum.auto() SUBMODEL_ELEMENT_COLLECTION = enum.auto() ASSET_ADMINISTRATION_SHELL = enum.auto() - ASSET = enum.auto() + ASSET_INFORMATION = enum.auto() + IDENTIFIER_KEY_VALUE_PAIR = enum.auto() SUBMODEL = enum.auto() VALUE_REFERENCE_PAIR = enum.auto() IEC61360_CONCEPT_DESCRIPTION = enum.auto() @@ -1248,16 +1229,10 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_administrative_information elif construct == XMLConstructables.QUALIFIER: constructor = decoder_.construct_qualifier - elif construct == XMLConstructables.FORMULA: - constructor = decoder_.construct_formula elif construct == XMLConstructables.IDENTIFIER: constructor = decoder_.construct_identifier elif construct == XMLConstructables.SECURITY: constructor = decoder_.construct_security - elif construct == XMLConstructables.VIEW: - constructor = decoder_.construct_view - elif construct == XMLConstructables.CONCEPT_DICTIONARY: - constructor = decoder_.construct_concept_dictionary elif construct == XMLConstructables.OPERATION_VARIABLE: constructor = decoder_.construct_operation_variable elif construct == XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT: @@ -1270,6 +1245,8 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_capability elif construct == XMLConstructables.ENTITY: constructor = decoder_.construct_entity + elif construct == XMLConstructables.EXTENSION: + constructor = decoder_.construct_extension elif construct == XMLConstructables.FILE: constructor = decoder_.construct_file elif construct == XMLConstructables.MULTI_LANGUAGE_PROPERTY: @@ -1288,8 +1265,10 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_submodel_element_collection elif construct == XMLConstructables.ASSET_ADMINISTRATION_SHELL: constructor = decoder_.construct_asset_administration_shell - elif construct == XMLConstructables.ASSET: - constructor = decoder_.construct_asset + elif construct == XMLConstructables.ASSET_INFORMATION: + constructor = decoder_.construct_asset_information + elif construct == XMLConstructables.IDENTIFIER_KEY_VALUE_PAIR: + constructor = decoder_.construct_identifier_key_value_pair elif construct == XMLConstructables.SUBMODEL: constructor = decoder_.construct_submodel elif construct == XMLConstructables.VALUE_REFERENCE_PAIR: @@ -1347,9 +1326,8 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif element_constructors: Dict[str, Callable[..., model.Identifiable]] = { "assetAdministrationShell": decoder_.construct_asset_administration_shell, - "asset": decoder_.construct_asset, - "submodel": decoder_.construct_submodel, - "conceptDescription": decoder_.construct_concept_description + "conceptDescription": decoder_.construct_concept_description, + "submodel": decoder_.construct_submodel } element_constructors = {NS_AAS + k: v for k, v in element_constructors.items()} diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index d734fb9..c971bce 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -1,9 +1,10 @@ # Copyright (c) 2020 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. +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 """ .. _adapter.xml.xml_serialization: @@ -31,17 +32,17 @@ # ############################################################## # Namespace definition -NS_AAS = "{http://www.admin-shell.io/aas/2/0}" -NS_ABAC = "{http://www.admin-shell.io/aas/abac/2/0}" -NS_AAS_COMMON = "{http://www.admin-shell.io/aas_common/2/0}" +NS_AAS = "{http://www.admin-shell.io/aas/3/0}" +NS_ABAC = "{http://www.admin-shell.io/aas/abac/3/0}" +NS_AAS_COMMON = "{http://www.admin-shell.io/aas_common/3/0}" NS_XSI = "{http://www.w3.org/2001/XMLSchema-instance}" NS_XS = "{http://www.w3.org/2001/XMLSchema}" -NS_IEC = "{http://www.admin-shell.io/IEC61360/2/0}" -NS_MAP = {"aas": "http://www.admin-shell.io/aas/2/0", - "abac": "http://www.admin-shell.io/aas/abac/2/0", - "aas_common": "http://www.admin-shell.io/aas_common/2/0", +NS_IEC = "{http://www.admin-shell.io/IEC61360/3/0}" +NS_MAP = {"aas": "http://www.admin-shell.io/aas/3/0", + "abac": "http://www.admin-shell.io/aas/abac/3/0", + "aas_common": "http://www.admin-shell.io/aas_common/3/0", "xsi": "http://www.w3.org/2001/XMLSchema-instance", - "IEC": "http://www.admin-shell.io/IEC61360/2/0", + "IEC": "http://www.admin-shell.io/IEC61360/3/0", "xs": "http://www.w3.org/2001/XMLSchema"} @@ -67,10 +68,10 @@ def _generate_element(name: str, def boolean_to_xml(obj: bool) -> str: """ - serialize a boolean to XML + Serialize a boolean to XML - :param obj: boolean - :return: string in the XML accepted form + :param obj: Boolean (`True`, `False`) + :return: String in the XML accepted form (`'true'`, `'false'`) """ if obj: return "true" @@ -90,23 +91,32 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: If the object obj is inheriting from any abstract AAS class, this function adds all the serialized information of those abstract classes to the generated element. - :param tag: tag of the element - :param obj: an object of the AAS - :return: parent element with the serialized information from the abstract classes + :param tag: Tag of the element + :param obj: An object of the AAS + :return: Parent element with the serialized information from the abstract classes """ elm = _generate_element(tag) + if isinstance(obj, model.HasExtension): + if obj.extension: + et_extension = _generate_element(NS_AAS + "extensions") + for extension in obj.extension: + if isinstance(extension, model.Extension): + et_extension.append(extension_to_xml(extension, tag=NS_AAS + "extension")) + elm.append(et_extension) if isinstance(obj, model.Referable): elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) + if obj.display_name: + elm.append(lang_string_set_to_xml(obj.display_name, tag=NS_AAS + "displayName")) if obj.category: elm.append(_generate_element(name=NS_AAS + "category", text=obj.category)) if obj.description: elm.append(lang_string_set_to_xml(obj.description, tag=NS_AAS + "description")) if isinstance(obj, model.Identifiable): + if obj.administration: + elm.append(administrative_information_to_xml(obj.administration)) elm.append(_generate_element(name=NS_AAS + "identification", text=obj.identification.id, attributes={"idType": _generic.IDENTIFIER_TYPES[obj.identification.id_type]})) - if obj.administration: - elm.append(administrative_information_to_xml(obj.administration)) if isinstance(obj, model.HasKind): if obj.kind is model.ModelingKind.TEMPLATE: elm.append(_generate_element(name=NS_AAS + "kind", text="Template")) @@ -118,13 +128,12 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: elm.append(reference_to_xml(obj.semantic_id, tag=NS_AAS+"semanticId")) if isinstance(obj, model.Qualifiable): if obj.qualifier: + et_qualifier = _generate_element(NS_AAS + "qualifiers") for qualifier in obj.qualifier: - et_qualifier = _generate_element(NS_AAS+"qualifier") + if isinstance(qualifier, model.Qualifier): et_qualifier.append(qualifier_to_xml(qualifier, tag=NS_AAS+"qualifier")) - if isinstance(qualifier, model.Formula): - et_qualifier.append(formula_to_xml(qualifier, tag=NS_AAS+"formula")) - elm.append(et_qualifier) + elm.append(et_qualifier) return elm @@ -152,11 +161,11 @@ def _value_to_xml(value: model.ValueDataType, def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: """ - serialization of objects of class LangStringSet to XML + Serialization of objects of class :class:`~aas.model.base.LangStringSet` to XML - :param obj: object of class LangStringSet - :param tag: tag name of the returned XML element (incl. namespace) - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.base.LangStringSet` + :param tag: Namespace+Tag name of the returned XML element. + :return: Serialized ElementTree object """ et_lss = _generate_element(name=tag) for language in obj: @@ -169,26 +178,26 @@ def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: def administrative_information_to_xml(obj: model.AdministrativeInformation, tag: str = NS_AAS+"administration") -> etree.Element: """ - serialization of objects of class AdministrativeInformation to XML + Serialization of objects of class :class:`~aas.model.base.AdministrativeInformation` to XML - :param obj: object of class AdministrativeInformation - :param tag: tag of the serialized element. default is "administration" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.base.AdministrativeInformation` + :param tag: Namespace+Tag of the serialized element. Default is "aas:administration" + :return: Serialized ElementTree object """ et_administration = _generate_element(tag) + if obj.revision: + et_administration.append(_generate_element(name=NS_AAS + "revision", text=obj.revision)) if obj.version: et_administration.append(_generate_element(name=NS_AAS + "version", text=obj.version)) - if obj.revision: - et_administration.append(_generate_element(name=NS_AAS + "revision", text=obj.revision)) return et_administration def data_element_to_xml(obj: model.DataElement) -> etree.Element: """ - serialization of objects of class DataElement to XML + Serialization of objects of class :class:`~aas.model.submodel.DataElement` to XML - :param obj: Object of class DataElement - :return: serialized ElementTree element + :param obj: Object of class :class:`~aas.model.submodel.DataElement` + :return: Serialized ElementTree element """ if isinstance(obj, model.MultiLanguageProperty): return multi_language_property_to_xml(obj) @@ -206,11 +215,11 @@ def data_element_to_xml(obj: model.DataElement) -> etree.Element: def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etree.Element: """ - serialization of objects of class Reference to XML + Serialization of objects of class :class:`~aas.model.base.Reference` to XML - :param obj: object of class Reference - :param tag: tag of the returned element - :return: serialized ElementTree + :param obj: Object of class :class:`~aas.model.base.Reference` + :param tag: Namespace+Tag of the returned element. Default is "aas:reference" + :return: Serialized ElementTree """ et_reference = _generate_element(tag) et_keys = _generate_element(name=NS_AAS + "keys") @@ -218,58 +227,61 @@ def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etr et_keys.append(_generate_element(name=NS_AAS + "key", text=aas_key.value, attributes={"idType": _generic.KEY_TYPES[aas_key.id_type], - "local": boolean_to_xml(aas_key.local), "type": _generic.KEY_ELEMENTS[aas_key.type]})) et_reference.append(et_keys) return et_reference -def formula_to_xml(obj: model.Formula, tag: str = NS_AAS+"formula") -> etree.Element: - """ - serialization of objects of class Formula to XML - - :param obj: object of class Formula - :param tag: tag of the ElementTree object, default is "formula" - :return: serialized ElementTree object - """ - et_formula = abstract_classes_to_xml(tag, obj) - if obj.depends_on: - et_depends_on = _generate_element(name=NS_AAS + "dependsOnRefs", text=None) - for aas_reference in obj.depends_on: - et_depends_on.append(reference_to_xml(aas_reference, NS_AAS+"reference")) - et_formula.append(et_depends_on) - return et_formula - - def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etree.Element: """ - serialization of objects of class Qualifier to XML + Serialization of objects of class :class:`~aas.model.base.Qualifier` to XML - :param obj: object of class Qualifier - :param tag: tag of the serialized ElementTree object, default is "qualifier" - :return: serialized ElementTreeObject + :param obj: Object of class :class:`~aas.model.base.Qualifier` + :param tag: Namespace+Tag of the serialized ElementTree object. Default is "aas:qualifier" + :return: Serialized ElementTreeObject """ et_qualifier = abstract_classes_to_xml(tag, obj) - et_qualifier.append(_generate_element(NS_AAS + "type", text=obj.type)) - et_qualifier.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) if obj.value_id: et_qualifier.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) if obj.value: et_qualifier.append(_value_to_xml(obj.value, obj.value_type)) + et_qualifier.append(_generate_element(NS_AAS + "type", text=obj.type)) + et_qualifier.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) return et_qualifier +def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etree.Element: + """ + Serialization of objects of class :class:`~aas.model.base.Extension` to XML + + :param obj: Object of class :class:`~aas.model.base.Extension` + :param tag: Namespace+Tag of the serialized ElementTree object. Default is "aas:extension" + :return: Serialized ElementTreeObject + """ + et_extension = abstract_classes_to_xml(tag, obj) + et_extension.append(_generate_element(NS_AAS + "name", text=obj.name)) + if obj.value_type: + et_extension.append(_generate_element(NS_AAS + "valueType", + text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) + if obj.value: + et_extension.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore # (value_type could be None) + if obj.refers_to: + et_extension.append(reference_to_xml(obj.refers_to, NS_AAS+"refersTo")) + + return et_extension + + def value_reference_pair_to_xml(obj: model.ValueReferencePair, tag: str = NS_AAS+"valueReferencePair") -> etree.Element: """ - serialization of objects of class ValueReferencePair to XML + Serialization of objects of class :class:`~aas.model.base.ValueReferencePair` to XML todo: couldn't find it in the official schema, so guessing how to implement serialization check namespace, tag and correct serialization - :param obj: object of class ValueReferencePair - :param tag: tag of the serialized element, default is "valueReferencePair" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.base.ValueReferencePair` + :param tag: Namespace+Tag of the serialized element. Default is "aas:valueReferencePair" + :return: Serialized ElementTree object """ et_vrp = _generate_element(tag) et_vrp.append(_value_to_xml(obj.value, obj.value_type)) @@ -280,13 +292,13 @@ def value_reference_pair_to_xml(obj: model.ValueReferencePair, def value_list_to_xml(obj: model.ValueList, tag: str = NS_AAS+"valueList") -> etree.Element: """ - serialization of objects of class ValueList to XML + Serialization of objects of class :class:`~aas.model.base.ValueList` to XML todo: couldn't find it in the official schema, so guessing how to implement serialization - :param obj: object of class ValueList - :param tag: tag of the serialized element, default is "valueList" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.base.ValueList` + :param tag: Namespace+Tag of the serialized element. Default is "aas:valueList" + :return: Serialized ElementTree object """ et_value_list = _generate_element(tag) for aas_reference_pair in obj: @@ -299,48 +311,54 @@ def value_list_to_xml(obj: model.ValueList, # ############################################################## -def view_to_xml(obj: model.View, tag: str = NS_AAS+"view") -> etree.Element: +def identifier_key_value_pair_to_xml(obj: model.IdentifierKeyValuePair, tag: str = NS_AAS+"identifierKeyValuePair") \ + -> etree.Element: """ - serialization of objects of class View to XML + Serialization of objects of class :class:`~aas.model.base.IdentifierKeyValuePair` to XML - :param obj: object of class View - :param tag: namespace+tag of the ElementTree object. default is "view" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.base.IdentifierKeyValuePair` + :param tag: Namespace+Tag of the ElementTree object. Default is "aas:identifierKeyValuePair" + :return: Serialized ElementTree object """ - et_view = abstract_classes_to_xml(tag, obj) - et_contained_elements = _generate_element(name=NS_AAS + "containedElements") - if obj.contained_element: - for contained_element in obj.contained_element: - et_contained_elements.append(reference_to_xml(contained_element, NS_AAS+"containedElementRef")) - et_view.append(et_contained_elements) - return et_view + et_asset_information = abstract_classes_to_xml(tag, obj) + et_asset_information.append(reference_to_xml(obj.external_subject_id, NS_AAS + "externalSubjectId")) + et_asset_information.append(_generate_element(name=NS_AAS + "key", text=obj.key)) + et_asset_information.append(_generate_element(name=NS_AAS + "value", text=obj.value)) + + return et_asset_information -def asset_to_xml(obj: model.Asset, tag: str = NS_AAS+"asset") -> etree.Element: +def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"assetInformation") -> etree.Element: """ - serialization of objects of class Asset to XML + Serialization of objects of class :class:`~aas.model.aas.AssetInformation` to XML - :param obj: object of class Asset - :param tag: namespace+tag of the ElementTree object. default is "asset" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.aas.AssetInformation` + :param tag: Namespace+Tag of the ElementTree object. Default is "aas:assetInformation" + :return: Serialized ElementTree object """ - et_asset = abstract_classes_to_xml(tag, obj) - if obj.asset_identification_model: - et_asset.append(reference_to_xml(obj.asset_identification_model, NS_AAS+"assetIdentificationModelRef")) - if obj.bill_of_material: - et_asset.append(reference_to_xml(obj.bill_of_material, NS_AAS+"billOfMaterialRef")) - et_asset.append(_generate_element(name=NS_AAS + "kind", text=_generic.ASSET_KIND[obj.kind])) - return et_asset + et_asset_information = abstract_classes_to_xml(tag, obj) + if obj.default_thumbnail: + et_asset_information.append(file_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbNail")) + if obj.global_asset_id: + et_asset_information.append(reference_to_xml(obj.global_asset_id, NS_AAS + "globalAssetId")) + et_asset_information.append(_generate_element(name=NS_AAS + "assetKind", text=_generic.ASSET_KIND[obj.asset_kind])) + et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") + if obj.specific_asset_id: + for specific_asset_id in obj.specific_asset_id: + et_specific_asset_id.append(identifier_key_value_pair_to_xml(specific_asset_id, NS_AAS+"specificAssetId")) + et_asset_information.append(et_specific_asset_id) + + return et_asset_information def concept_description_to_xml(obj: model.ConceptDescription, tag: str = NS_AAS+"conceptDescription") -> etree.Element: """ - serialization of objects of class ConceptDescription to XML + Serialization of objects of class :class:`~aas.model.concept.ConceptDescription` to XML - :param obj: object of class ConceptDescription - :param tag: tag of the ElementTree object. default is "conceptDescription" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.concept.ConceptDescription` + :param tag: Namespace+Tag of the ElementTree object. Default is "aas:conceptDescription" + :return: Serialized ElementTree object """ et_concept_description = abstract_classes_to_xml(tag, obj) if isinstance(obj, model.concept.IEC61360ConceptDescription): @@ -351,7 +369,6 @@ def concept_description_to_xml(obj: model.ConceptDescription, et_concept_description.append(et_embedded_data_specification) et_embedded_data_specification.append(reference_to_xml(model.Reference(tuple([model.Key( model.KeyElements.GLOBAL_REFERENCE, - False, "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0", model.KeyType.IRI )])), NS_AAS+"dataSpecification")) @@ -405,7 +422,6 @@ def _iec_reference_to_xml(ref: model.Reference, ref_tag: str = NS_AAS + "referen et_keys.append(_generate_element(name=NS_IEC + "key", text=aas_key.value, attributes={"idType": _generic.KEY_TYPES[aas_key.id_type], - "local": boolean_to_xml(aas_key.local), "type": _generic.KEY_ELEMENTS[aas_key.type]})) et_reference.append(et_keys) return et_reference @@ -468,55 +484,26 @@ def _iec_value_list_to_xml(vl: model.ValueList, return et_iec -def concept_dictionary_to_xml(obj: model.ConceptDictionary, - tag: str = NS_AAS+"conceptDictionary") -> etree.Element: - """ - serialization of objects of class ConceptDictionary to XML - - :param obj: object of class ConceptDictionary - :param tag: tag of the ElementTree object. default is "conceptDictionary" - :return: serialized ElementTree object - """ - et_concept_dictionary = abstract_classes_to_xml(tag, obj) - et_concept_descriptions_refs = _generate_element(NS_AAS + "conceptDescriptionRefs") - if obj.concept_description: - for reference in obj.concept_description: - et_concept_descriptions_refs.append(reference_to_xml(reference, NS_AAS+"conceptDescriptionRef")) - et_concept_dictionary.append(et_concept_descriptions_refs) - return et_concept_dictionary - - def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, tag: str = NS_AAS+"assetAdministrationShell") -> etree.Element: """ - serialization of objects of class AssetAdministrationShell to XML + Serialization of objects of class :class:`~aas.model.aas.AssetAdministrationShell` to XML - :param obj: object of class AssetAdministrationShell - :param tag: tag of the ElementTree object. default is "assetAdministrationShell" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.aas.AssetAdministrationShell` + :param tag: Namespace+Tag of the ElementTree object. Default is "aas:assetAdministrationShell" + :return: Serialized ElementTree object """ et_aas = abstract_classes_to_xml(tag, obj) + if obj.security: + et_aas.append(security_to_xml(obj.security, tag=NS_ABAC + "security")) if obj.derived_from: et_aas.append(reference_to_xml(obj.derived_from, tag=NS_AAS+"derivedFrom")) - et_aas.append(reference_to_xml(obj.asset, tag=NS_AAS+"assetRef")) if obj.submodel: et_submodels = _generate_element(NS_AAS + "submodelRefs") for reference in obj.submodel: et_submodels.append(reference_to_xml(reference, tag=NS_AAS+"submodelRef")) et_aas.append(et_submodels) - if obj.view: - et_views = _generate_element(NS_AAS + "views") - for view in obj.view: - et_views.append(view_to_xml(view, NS_AAS+"view")) - et_aas.append(et_views) - if obj.concept_dictionary: - et_concept_dictionaries = _generate_element(NS_AAS + "conceptDictionaries") - for concept_dictionary in obj.concept_dictionary: - et_concept_dictionaries.append(concept_dictionary_to_xml(concept_dictionary, - NS_AAS+"conceptDictionary")) - et_aas.append(et_concept_dictionaries) - if obj.security: - et_aas.append(security_to_xml(obj.security, tag=NS_ABAC+"security")) + et_aas.append(asset_information_to_xml(obj.asset_information, tag=NS_AAS + "assetInformation")) return et_aas @@ -528,13 +515,13 @@ def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, def security_to_xml(obj: model.Security, tag: str = NS_ABAC+"security") -> etree.Element: """ - serialization of objects of class Security to XML + Serialization of objects of class :class:`~aas.model.security.Security` to XML todo: This is not yet implemented - :param obj: object of class Security - :param tag: tag of the serialized element (optional). Default is "security" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.security.Security` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:security" + :return: Serialized ElementTree object """ return abstract_classes_to_xml(tag, obj) @@ -546,10 +533,10 @@ def security_to_xml(obj: model.Security, def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: """ - serialization of objects of class SubmodelElement to XML + Serialization of objects of class :class:`~aas.model.submodel.SubmodelElement` to XML - :param obj: object of class SubmodelElement - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.SubmodelElement` + :return: Serialized ElementTree object """ if isinstance(obj, model.DataElement): return data_element_to_xml(obj) @@ -572,11 +559,11 @@ def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: def submodel_to_xml(obj: model.Submodel, tag: str = NS_AAS+"submodel") -> etree.Element: """ - serialization of objects of class Submodel to XML + Serialization of objects of class :class:`~aas.model.submodel.Submodel` to XML - :param obj: object of class Submodel - :param tag: tag of the serialized element (optional). Default is "submodel" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.Submodel` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:submodel" + :return: Serialized ElementTree object """ et_submodel = abstract_classes_to_xml(tag, obj) et_submodel_elements = _generate_element(NS_AAS + "submodelElements") @@ -594,29 +581,29 @@ def submodel_to_xml(obj: model.Submodel, def property_to_xml(obj: model.Property, tag: str = NS_AAS+"property") -> etree.Element: """ - serialization of objects of class Property to XML + Serialization of objects of class :class:`~aas.model.submodel.Property` to XML - :param obj: object of class Property - :param tag: tag of the serialized element (optional), default is "property" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.Property` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:property" + :return: Serialized ElementTree object """ et_property = abstract_classes_to_xml(tag, obj) - et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) - if obj.value is not None: - et_property.append(_value_to_xml(obj.value, obj.value_type)) if obj.value_id: et_property.append(reference_to_xml(obj.value_id, NS_AAS + "valueId")) + if obj.value: + et_property.append(_value_to_xml(obj.value, obj.value_type)) + et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) return et_property def multi_language_property_to_xml(obj: model.MultiLanguageProperty, tag: str = NS_AAS+"multiLanguageProperty") -> etree.Element: """ - serialization of objects of class MultiLanguageProperty to XML + Serialization of objects of class :class:`~aas.model.submodel.MultiLanguageProperty` to XML - :param obj: object of class MultiLanguageProperty - :param tag: tag of the serialized element (optional), default is "multiLanguageProperty" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.MultiLanguageProperty` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:multiLanguageProperty" + :return: Serialized ElementTree object """ et_multi_language_property = abstract_classes_to_xml(tag, obj) if obj.value_id: @@ -629,30 +616,30 @@ def multi_language_property_to_xml(obj: model.MultiLanguageProperty, def range_to_xml(obj: model.Range, tag: str = NS_AAS+"range") -> etree.Element: """ - serialization of objects of class Range to XML + Serialization of objects of class :class:`~aas.model.submodel.Range` to XML - :param obj: object of class Range - :param tag: namespace+tag of the serialized element (optional), default is "range - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.Range` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:range" + :return: Serialized ElementTree object """ et_range = abstract_classes_to_xml(tag, obj) + if obj.max is not None: + et_range.append(_value_to_xml(obj.max, obj.value_type, tag=NS_AAS + "max")) + if obj.min is not None: + et_range.append(_value_to_xml(obj.min, obj.value_type, tag=NS_AAS + "min")) et_range.append(_generate_element(name=NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) - if obj.min is not None: - et_range.append(_value_to_xml(obj.min, obj.value_type, tag=NS_AAS+"min")) - if obj.max is not None: - et_range.append(_value_to_xml(obj.max, obj.value_type, tag=NS_AAS+"max")) return et_range def blob_to_xml(obj: model.Blob, tag: str = NS_AAS+"blob") -> etree.Element: """ - serialization of objects of class Blob to XML + Serialization of objects of class :class:`~aas.model.submodel.Blob` to XML - :param obj: object of class Blob - :param tag: tag of the serialized element, default is "blob" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.Blob` + :param tag: Namespace+Tag of the serialized element. Default is "blob" + :return: Serialized ElementTree object """ et_blob = abstract_classes_to_xml(tag, obj) et_value = etree.Element(NS_AAS + "value") @@ -666,27 +653,27 @@ def blob_to_xml(obj: model.Blob, def file_to_xml(obj: model.File, tag: str = NS_AAS+"file") -> etree.Element: """ - serialization of objects of class File to XML + Serialization of objects of class :class:`~aas.model.submodel.File` to XML - :param obj: object of class File - :param tag: tag of the serialized element, default is "file" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.File` + :param tag: Namespace+Tag of the serialized element. Default is "aas:file" + :return: Serialized ElementTree object """ et_file = abstract_classes_to_xml(tag, obj) - et_file.append(_generate_element(NS_AAS + "mimeType", text=obj.mime_type)) if obj.value: et_file.append(_generate_element(NS_AAS + "value", text=obj.value)) + et_file.append(_generate_element(NS_AAS + "mimeType", text=obj.mime_type)) return et_file def reference_element_to_xml(obj: model.ReferenceElement, tag: str = NS_AAS+"referenceElement") -> etree.Element: """ - serialization of objects of class ReferenceElement to XMl + Serialization of objects of class :class:`~aas.model.submodel.ReferenceElement` to XMl - :param obj: object of class ReferenceElement - :param tag: namespace+tag of the serialized element (optional), default is "referenceElement" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.ReferenceElement` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:referenceElement" + :return: Serialized ElementTree object """ et_reference_element = abstract_classes_to_xml(tag, obj) if obj.value: @@ -697,16 +684,19 @@ def reference_element_to_xml(obj: model.ReferenceElement, def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, tag: str = NS_AAS+"submodelElementCollection") -> etree.Element: """ - serialization of objects of class SubmodelElementCollection to XML + Serialization of objects of class :class:`~aas.model.submodel.SubmodelElementCollection` to XML Note that we do not have parameter "allowDuplicates" in out implementation - :param obj: object of class SubmodelElementCollection - :param tag: namespace+tag of the serialized element (optional), default is "submodelElementCollection" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.SubmodelElementCollection` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:submodelElementCollection" + :return: Serialized ElementTree object """ et_submodel_element_collection = abstract_classes_to_xml(tag, obj) # todo: remove wrapping submodelElement-tag, in accordance to future schema + et_submodel_element_collection.append(_generate_element(NS_AAS + "allowDuplicates", + text=boolean_to_xml(obj.allow_duplicates))) + et_submodel_element_collection.append(_generate_element(NS_AAS + "ordered", text=boolean_to_xml(obj.ordered))) et_value = _generate_element(NS_AAS + "value") if obj.value: for submodel_element in obj.value: @@ -714,19 +704,17 @@ def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, et_submodel_element.append(submodel_element_to_xml(submodel_element)) et_value.append(et_submodel_element) et_submodel_element_collection.append(et_value) - et_submodel_element_collection.append(_generate_element(NS_AAS + "ordered", text=boolean_to_xml(obj.ordered))) - et_submodel_element_collection.append(_generate_element(NS_AAS + "allowDuplicates", text="false")) return et_submodel_element_collection def relationship_element_to_xml(obj: model.RelationshipElement, tag: str = NS_AAS+"relationshipElement") -> etree.Element: """ - serialization of objects of class RelationshipElement to XML + Serialization of objects of class :class:`~aas.model.submodel.RelationshipElement` to XML - :param obj: object of class RelationshipElement - :param tag: tag of the serialized element (optional), default is "relationshipElement" - :return: serialized ELementTree object + :param obj: Object of class :class:`~aas.model.submodel.RelationshipElement` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:relationshipElement" + :return: Serialized ELementTree object """ et_relationship_element = abstract_classes_to_xml(tag, obj) et_relationship_element.append(reference_to_xml(obj.first, NS_AAS+"first")) @@ -737,11 +725,11 @@ def relationship_element_to_xml(obj: model.RelationshipElement, def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElement, tag: str = NS_AAS+"annotatedRelationshipElement") -> etree.Element: """ - serialization of objects of class AnnotatedRelationshipElement to XML + Serialization of objects of class :class:`~aas.model.submodel.AnnotatedRelationshipElement` to XML - :param obj: object of class AnnotatedRelationshipElement - :param tag: tag of the serialized element (optional), default is "annotatedRelationshipElement - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.AnnotatedRelationshipElement` + :param tag: Namespace+Tag of the serialized element (optional): Default is "aas:annotatedRelationshipElement" + :return: Serialized ElementTree object """ et_annotated_relationship_element = relationship_element_to_xml(obj, tag) et_annotations = _generate_element(name=NS_AAS+"annotations") @@ -757,11 +745,11 @@ def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElemen def operation_variable_to_xml(obj: model.OperationVariable, tag: str = NS_AAS+"operationVariable") -> etree.Element: """ - serialization of objects of class OperationVariable to XML + Serialization of objects of class :class:`~aas.model.submodel.OperationVariable` to XML - :param obj: object of class OperationVariable - :param tag: tag of the serialized element (optional), default is "operationVariable" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.OperationVariable` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operationVariable" + :return: Serialized ElementTree object """ et_operation_variable = _generate_element(tag) et_value = _generate_element(NS_AAS+"value") @@ -773,33 +761,33 @@ def operation_variable_to_xml(obj: model.OperationVariable, def operation_to_xml(obj: model.Operation, tag: str = NS_AAS+"operation") -> etree.Element: """ - serialization of objects of class Operation to XML + Serialization of objects of class :class:`~aas.model.submodel.Operation` to XML - :param obj: object of class Operation - :param tag: namespace+tag of the serialized element (optional), default is "operation" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.Operation` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operation" + :return: Serialized ElementTree object """ et_operation = abstract_classes_to_xml(tag, obj) + if obj.in_output_variable: + for in_out_ov in obj.in_output_variable: + et_operation.append(operation_variable_to_xml(in_out_ov, NS_AAS+"inoutputVariable")) if obj.input_variable: for input_ov in obj.input_variable: et_operation.append(operation_variable_to_xml(input_ov, NS_AAS+"inputVariable")) if obj.output_variable: for output_ov in obj.output_variable: et_operation.append(operation_variable_to_xml(output_ov, NS_AAS+"outputVariable")) - if obj.in_output_variable: - for in_out_ov in obj.in_output_variable: - et_operation.append(operation_variable_to_xml(in_out_ov, NS_AAS+"inoutputVariable")) return et_operation def capability_to_xml(obj: model.Capability, tag: str = NS_AAS+"capability") -> etree.Element: """ - serialization of objects of class Capability to XML + Serialization of objects of class :class:`~aas.model.submodel.Capability` to XML - :param obj: object of class Capability - :param tag: tag of the serialized element, default is "capability" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.Capability` + :param tag: Namespace+Tag of the serialized element, default is "aas:capability" + :return: Serialized ElementTree object """ return abstract_classes_to_xml(tag, obj) @@ -807,35 +795,37 @@ def capability_to_xml(obj: model.Capability, def entity_to_xml(obj: model.Entity, tag: str = NS_AAS+"entity") -> etree.Element: """ - serialization of objects of class Entity to XML + Serialization of objects of class :class:`~aas.model.submodel.Entity` to XML - :param obj: object of class Entity - :param tag: tag of the serialized element (optional), default is "entity" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.Entity` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:entity" + :return: Serialized ElementTree object """ # todo: remove wrapping submodelElement, in accordance to future schemas et_entity = abstract_classes_to_xml(tag, obj) + if obj.global_asset_id: + et_entity.append(reference_to_xml(obj.global_asset_id, NS_AAS + "globalAssetId")) + if obj.specific_asset_id: + et_entity.append(identifier_key_value_pair_to_xml(obj.specific_asset_id, NS_AAS+"specificAssetId")) + et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type])) et_statements = _generate_element(NS_AAS + "statements") for statement in obj.statement: # todo: remove the once the proposed changes get accepted - et_submodel_element = _generate_element(NS_AAS+"submodelElement") + et_submodel_element = _generate_element(NS_AAS + "submodelElement") et_submodel_element.append(submodel_element_to_xml(statement)) et_statements.append(et_submodel_element) et_entity.append(et_statements) - et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type])) - if obj.asset: - et_entity.append(reference_to_xml(obj.asset, NS_AAS+"assetRef")) return et_entity def basic_event_to_xml(obj: model.BasicEvent, tag: str = NS_AAS+"basicEvent") -> etree.Element: """ - serialization of objects of class BasicEvent to XML + Serialization of objects of class :class:`~aas.model.submodel.BasicEvent` to XML - :param obj: object of class BasicEvent - :param tag: tag of the serialized element (optional), default is "basicEvent" - :return: serialized ElementTree object + :param obj: Object of class :class:`~aas.model.submodel.BasicEvent` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas.basicEvent" + :return: Serialized ElementTree object """ et_basic_event = abstract_classes_to_xml(tag, obj) et_basic_event.append(reference_to_xml(obj.observed, NS_AAS+"observed")) @@ -860,14 +850,11 @@ def write_aas_xml_file(file: IO, :param kwargs: Additional keyword arguments to be passed to `tree.write()` """ # separate different kind of objects - assets = [] asset_administration_shells = [] submodels = [] concept_descriptions = [] for obj in data: - if isinstance(obj, model.Asset): - assets.append(obj) - elif isinstance(obj, model.AssetAdministrationShell): + if isinstance(obj, model.AssetAdministrationShell): asset_administration_shells.append(obj) elif isinstance(obj, model.Submodel): submodels.append(obj) @@ -879,18 +866,14 @@ def write_aas_xml_file(file: IO, et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") for aas_obj in asset_administration_shells: et_asset_administration_shells.append(asset_administration_shell_to_xml(aas_obj)) - et_assets = _generate_element(NS_AAS + "assets") - for ass_obj in assets: - et_assets.append(asset_to_xml(ass_obj)) - et_submodels = etree.Element(NS_AAS + "submodels") - for sub_obj in submodels: - et_submodels.append(submodel_to_xml(sub_obj)) et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") for con_obj in concept_descriptions: et_concept_descriptions.append(concept_description_to_xml(con_obj)) - root.insert(0, et_concept_descriptions) + et_submodels = etree.Element(NS_AAS + "submodels") + for sub_obj in submodels: + et_submodels.append(submodel_to_xml(sub_obj)) root.insert(0, et_submodels) - root.insert(0, et_assets) + root.insert(0, et_concept_descriptions) root.insert(0, et_asset_administration_shells) tree = etree.ElementTree(root) From 02d9c9f3dc5ddc26c53904350d3b7b75baf95126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 3 Apr 2022 02:35:54 +0200 Subject: [PATCH 038/474] remove AssetAdministrationShell/security --- basyx/aas/adapter/json/aasJSONSchema.json | 3 --- basyx/aas/adapter/json/json_deserialization.py | 2 -- basyx/aas/adapter/json/json_serialization.py | 2 -- basyx/aas/adapter/xml/AAS.xsd | 4 +--- basyx/aas/adapter/xml/xml_deserialization.py | 3 --- basyx/aas/adapter/xml/xml_serialization.py | 2 -- 6 files changed, 1 insertion(+), 15 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index de1e6dd..5d3ece7 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -191,9 +191,6 @@ "items": { "$ref": "#/definitions/Reference" } - }, - "security": { - "$ref": "#/definitions/Security" } }, "required": [ "assetInformation" ] diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 97dd02d..d5b4494 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -404,8 +404,6 @@ def _construct_asset_administration_shell( if not cls.stripped and 'submodels' in dct: for sm_data in _get_ts(dct, 'submodels', list): ret.submodel.add(cls._construct_aas_reference(sm_data, model.Submodel)) - if 'security' in dct: - ret.security = cls._construct_security(_get_ts(dct, 'security', dict)) if 'derivedFrom' in dct: ret.derived_from = cls._construct_aas_reference(_get_ts(dct, 'derivedFrom', dict), model.AssetAdministrationShell) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index cc3ccf3..bcc75bc 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -424,8 +424,6 @@ def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell data["assetInformation"] = obj.asset_information if not cls.stripped and obj.submodel: data["submodels"] = list(obj.submodel) - if obj.security: - data["security"] = obj.security return data # ################################################################# diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 39bf2b3..5e39f24 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -1,7 +1,6 @@ - + - @@ -48,7 +47,6 @@ - diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 8eae53d..f1cbfa8 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -899,9 +899,6 @@ def construct_asset_administration_shell(cls, element: etree.Element, object_cla asset_information=_child_construct_mandatory(element, NS_AAS + "assetInformation", cls.construct_asset_information) ) - security = _failsafe_construct(element.find(NS_ABAC + "security"), cls.construct_security, cls.failsafe) - if security is not None: - aas.security = security if not cls.stripped: submodels = element.find(NS_AAS + "submodelRefs") if submodels is not None: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index c971bce..5bd02d0 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -494,8 +494,6 @@ def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, :return: Serialized ElementTree object """ et_aas = abstract_classes_to_xml(tag, obj) - if obj.security: - et_aas.append(security_to_xml(obj.security, tag=NS_ABAC + "security")) if obj.derived_from: et_aas.append(reference_to_xml(obj.derived_from, tag=NS_AAS+"derivedFrom")) if obj.submodel: From 9be8c702c47bb62ca5f17f0cc43dac54d3073f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 5 Apr 2022 20:10:31 +0200 Subject: [PATCH 039/474] rename BasicEvent to BasicEventElement --- basyx/aas/adapter/_generic.py | 2 +- basyx/aas/adapter/json/aasJSONSchema.json | 10 +- .../aas/adapter/json/json_deserialization.py | 5 +- basyx/aas/adapter/json/json_serialization.py | 10 +- basyx/aas/adapter/xml/AAS.xsd | 6 +- basyx/aas/adapter/xml/IEC61360.xsd | 322 ++++++++---------- basyx/aas/adapter/xml/xml_deserialization.py | 18 +- basyx/aas/adapter/xml/xml_serialization.py | 19 +- 8 files changed, 186 insertions(+), 206 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 6ae76a6..e926ee0 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -26,7 +26,7 @@ model.KeyElements.CONCEPT_DESCRIPTION: 'ConceptDescription', model.KeyElements.SUBMODEL: 'Submodel', model.KeyElements.ANNOTATED_RELATIONSHIP_ELEMENT: 'AnnotatedRelationshipElement', - model.KeyElements.BASIC_EVENT: 'BasicEvent', + model.KeyElements.BASIC_EVENT_ELEMENT: 'BasicEventElement', model.KeyElements.BLOB: 'Blob', model.KeyElements.CAPABILITY: 'Capability', model.KeyElements.CONCEPT_DICTIONARY: 'ConceptDictionary', diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 5d3ece7..f8ebee1 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -271,7 +271,7 @@ "Submodel", "AccessPermissionRule", "AnnotatedRelationshipElement", - "BasicEvent", + "BasicEventElement", "Blob", "Capability", "DataElement", @@ -298,7 +298,7 @@ "Submodel", "AccessPermissionRule", "AnnotatedRelationshipElement", - "BasicEvent", + "BasicEventElement", "Blob", "Capability", "DataElement", @@ -660,7 +660,7 @@ { "$ref": "#/definitions/Capability" }, { "$ref": "#/definitions/Entity" }, { "$ref": "#/definitions/Event" }, - { "$ref": "#/definitions/BasicEvent" }, + { "$ref": "#/definitions/BasicEventElement" }, { "$ref": "#/definitions/MultiLanguageProperty" }, { "$ref": "#/definitions/Operation" }, { "$ref": "#/definitions/Property" }, @@ -695,7 +695,7 @@ { "$ref": "#/definitions/SubmodelElement" } ] }, - "BasicEvent": { + "BasicEventElement": { "allOf": [ { "$ref": "#/definitions/Event" }, { "properties": { @@ -892,7 +892,7 @@ { "$ref": "#/definitions/Capability" }, { "$ref": "#/definitions/Entity" }, { "$ref": "#/definitions/Event" }, - { "$ref": "#/definitions/BasicEvent" }, + { "$ref": "#/definitions/BasicEventElement" }, { "$ref": "#/definitions/MultiLanguageProperty" }, { "$ref": "#/definitions/Operation" }, { "$ref": "#/definitions/Property" }, diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index d5b4494..f669a15 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -175,7 +175,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object: 'Submodel': cls._construct_submodel, 'Capability': cls._construct_capability, 'Entity': cls._construct_entity, - 'BasicEvent': cls._construct_basic_event, + 'BasicEventElement': cls._construct_basic_event_element, 'Operation': cls._construct_operation, 'RelationshipElement': cls._construct_relationship_element, 'AnnotatedRelationshipElement': cls._construct_annotated_relationship_element, @@ -524,7 +524,8 @@ def _construct_capability(cls, dct: Dict[str, object], object_class=model.Capabi return ret @classmethod - def _construct_basic_event(cls, dct: Dict[str, object], object_class=model.BasicEvent) -> model.BasicEvent: + def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=model.BasicEventElement) \ + -> model.BasicEventElement: # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 ret = object_class(id_short=_get_ts(dct, "idShort", str), diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index bcc75bc..da65f98 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -87,8 +87,8 @@ def default(self, obj: object) -> object: return self._operation_variable_to_json(obj) if isinstance(obj, model.Capability): return self._capability_to_json(obj) - if isinstance(obj, model.BasicEvent): - return self._basic_event_to_json(obj) + if isinstance(obj, model.BasicEventElement): + return self._basic_event_element_to_json(obj) if isinstance(obj, model.Entity): return self._entity_to_json(obj) if isinstance(obj, model.ConceptDescription): @@ -663,11 +663,11 @@ def _event_to_json(cls, obj: model.Event) -> Dict[str, object]: # no attributes return {} @classmethod - def _basic_event_to_json(cls, obj: model.BasicEvent) -> Dict[str, object]: + def _basic_event_element_to_json(cls, obj: model.BasicEventElement) -> Dict[str, object]: """ - serialization of an object from class BasicEvent to json + serialization of an object from class BasicEventElement to json - :param obj: object of class BasicEvent + :param obj: object of class BasicEventElement :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 5e39f24..47832ee 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -70,7 +70,7 @@ - + @@ -260,7 +260,7 @@ - + @@ -417,7 +417,7 @@ - + diff --git a/basyx/aas/adapter/xml/IEC61360.xsd b/basyx/aas/adapter/xml/IEC61360.xsd index bdaee9d..1a53f10 100644 --- a/basyx/aas/adapter/xml/IEC61360.xsd +++ b/basyx/aas/adapter/xml/IEC61360.xsd @@ -1,171 +1,151 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index f1cbfa8..ee948d2 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -630,7 +630,7 @@ def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> mo # object_class_unordered submodel_elements: Dict[str, Callable[..., model.SubmodelElement]] = { "annotatedRelationshipElement": cls.construct_annotated_relationship_element, - "basicEvent": cls.construct_basic_event, + "basicEventElement": cls.construct_basic_event_element, "capability": cls.construct_capability, "entity": cls.construct_entity, "operation": cls.construct_operation, @@ -705,15 +705,15 @@ def construct_annotated_relationship_element(cls, element: etree.Element, return annotated_relationship_element @classmethod - def construct_basic_event(cls, element: etree.Element, object_class=model.BasicEvent, **_kwargs: Any) \ - -> model.BasicEvent: - basic_event = object_class( + def construct_basic_event_element(cls, element: etree.Element, object_class=model.BasicEventElement, + **_kwargs: Any) -> model.BasicEventElement: + basic_event_element = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), _child_construct_mandatory(element, NS_AAS + "observed", cls._construct_referable_reference), kind=_get_modeling_kind(element) ) - cls._amend_abstract_attributes(basic_event, element) - return basic_event + cls._amend_abstract_attributes(basic_event_element, element) + return basic_event_element @classmethod def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: @@ -1168,7 +1168,7 @@ class XMLConstructables(enum.Enum): SECURITY = enum.auto() OPERATION_VARIABLE = enum.auto() ANNOTATED_RELATIONSHIP_ELEMENT = enum.auto() - BASIC_EVENT = enum.auto() + BASIC_EVENT_ELEMENT = enum.auto() BLOB = enum.auto() CAPABILITY = enum.auto() ENTITY = enum.auto() @@ -1234,8 +1234,8 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_operation_variable elif construct == XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT: constructor = decoder_.construct_annotated_relationship_element - elif construct == XMLConstructables.BASIC_EVENT: - constructor = decoder_.construct_basic_event + elif construct == XMLConstructables.BASIC_EVENT_ELEMENT: + constructor = decoder_.construct_basic_event_element elif construct == XMLConstructables.BLOB: constructor = decoder_.construct_blob elif construct == XMLConstructables.CAPABILITY: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 5bd02d0..4a3aa55 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -538,8 +538,8 @@ def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: """ if isinstance(obj, model.DataElement): return data_element_to_xml(obj) - if isinstance(obj, model.BasicEvent): - return basic_event_to_xml(obj) + if isinstance(obj, model.BasicEventElement): + return basic_event_element_to_xml(obj) if isinstance(obj, model.Capability): return capability_to_xml(obj) if isinstance(obj, model.Entity): @@ -816,18 +816,17 @@ def entity_to_xml(obj: model.Entity, return et_entity -def basic_event_to_xml(obj: model.BasicEvent, - tag: str = NS_AAS+"basicEvent") -> etree.Element: +def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+"basicEventElement") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.BasicEvent` to XML + Serialization of objects of class :class:`~aas.model.submodel.BasicEventElement` to XML - :param obj: Object of class :class:`~aas.model.submodel.BasicEvent` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas.basicEvent" + :param obj: Object of class :class:`~aas.model.submodel.BasicEventElement` + :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:basicEventElement" :return: Serialized ElementTree object """ - et_basic_event = abstract_classes_to_xml(tag, obj) - et_basic_event.append(reference_to_xml(obj.observed, NS_AAS+"observed")) - return et_basic_event + et_basic_event_element = abstract_classes_to_xml(tag, obj) + et_basic_event_element.append(reference_to_xml(obj.observed, NS_AAS+"observed")) + return et_basic_event_element # ############################################################## From 8f2d47f93c9115bea6af00677bc0c191077870d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 6 Apr 2022 23:29:49 +0200 Subject: [PATCH 040/474] remove Constraint --- basyx/aas/adapter/json/aasJSONSchema.json | 33 +++---------------- .../aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/json/json_serialization.py | 18 +--------- basyx/aas/adapter/xml/AAS.xsd | 14 +++----- basyx/aas/adapter/xml/xml_deserialization.py | 20 ++--------- 5 files changed, 13 insertions(+), 74 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index f8ebee1..3e99d34 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -72,7 +72,7 @@ "qualifiers": { "type": "array", "items": { - "$ref": "#/definitions/Constraint" + "$ref": "#/definitions/Qualifier" } } } @@ -315,8 +315,6 @@ "SubmodelElementCollection", "GlobalReference", "FragmentReference", - "Constraint", - "Formula", "Qualifier" ] }, @@ -615,15 +613,6 @@ } ] }, - "Constraint": { - "type": "object", - "properties": { - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ "modelType" ] - }, "Operation": { "allOf": [ { "$ref": "#/definitions/SubmodelElement" }, @@ -951,29 +940,17 @@ }, "Qualifier": { "allOf": [ - { "$ref": "#/definitions/Constraint" }, { "$ref": "#/definitions/HasSemantics" }, { "$ref": "#/definitions/ValueObject" }, { "properties": { + "modelType": { + "$ref": "#/definitions/ModelType" + }, "type": { "type": "string" } }, - "required": [ "type" ] - } - ] - }, - "Formula": { - "allOf": [ - { "$ref": "#/definitions/Constraint" }, - { "properties": { - "dependsOn": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - } - } + "required": [ "type", "modelType" ] } ] }, diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index f669a15..6ec40b8 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -250,7 +250,7 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if isinstance(obj, model.Qualifiable) and not cls.stripped: if 'qualifiers' in dct: for constraint in _get_ts(dct, 'qualifiers', list): - if _expect_type(constraint, model.Constraint, str(obj), cls.failsafe): + if _expect_type(constraint, model.Qualifier, str(obj), cls.failsafe): obj.qualifier.add(constraint) if isinstance(obj, model.HasExtension) and not cls.stripped: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index da65f98..e149c7e 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -222,22 +222,6 @@ def _reference_to_json(cls, obj: model.Reference) -> Dict[str, object]: data['keys'] = list(obj.key) return data - @classmethod - def _constraint_to_json(cls, obj: model.Constraint) -> Dict[str, object]: # TODO check if correct for each class - """ - serialization of an object from class Constraint to json - - :param obj: object of class Constraint - :return: dict with the serialized attributes of this object - """ - CONSTRAINT_CLASSES = [model.Qualifier] - try: - const_type = next(iter(t for t in inspect.getmro(type(obj)) if t in CONSTRAINT_CLASSES)) - except StopIteration as e: - raise TypeError("Object of type {} is a Constraint but does not inherit from a known AAS Constraint type" - .format(obj.__class__.__name__)) from e - return {'modelType': {'name': const_type.__name__}} - @classmethod def _namespace_to_json(cls, obj): # not in specification yet """ @@ -258,7 +242,7 @@ def _qualifier_to_json(cls, obj: model.Qualifier) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data.update(cls._constraint_to_json(obj)) + data['modelType'] = {'name': model.Qualifier.__name__} if obj.value: data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None if obj.value_id: diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 47832ee..61e88c7 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -111,11 +111,10 @@ - - - + + - + @@ -194,11 +193,6 @@ - - - - - @@ -507,7 +501,7 @@ - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index ee948d2..14ea3f1 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -449,8 +449,8 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None if isinstance(obj, model.Qualifiable) and not cls.stripped: qualifiers_elem = element.find(NS_AAS + "qualifiers") if qualifiers_elem is not None and len(qualifiers_elem) > 0: - for constraint in _failsafe_construct_multiple(qualifiers_elem, cls.construct_constraint, cls.failsafe): - obj.qualifier.add(constraint) + for qualifier in _failsafe_construct_multiple(qualifiers_elem, cls.construct_qualifier, cls.failsafe): + obj.qualifier.add(qualifier) if isinstance(obj, model.HasExtension) and not cls.stripped: extension_elem = element.find(NS_AAS + "extension") if extension_elem is not None: @@ -661,19 +661,6 @@ def construct_data_element(cls, element: etree.Element, abstract_class_name: str raise KeyError(_element_pretty_identifier(element) + f" is not a valid {abstract_class_name}!") return data_elements[element.tag](element, **kwargs) - @classmethod - def construct_constraint(cls, element: etree.Element, **kwargs: Any) -> model.Constraint: - """ - This function does not support the object_class parameter. - Overwrite construct_formula or construct_qualifier instead. - """ - constraints: Dict[str, Callable[..., model.Constraint]] = {NS_AAS + k: v for k, v in { - "qualifier": cls.construct_qualifier - }.items()} - if element.tag not in constraints: - raise KeyError(_element_pretty_identifier(element) + " is not a valid Constraint!") - return constraints[element.tag](element, **kwargs) - @classmethod def construct_operation_variable(cls, element: etree.Element, object_class=model.OperationVariable, **_kwargs: Any) -> model.OperationVariable: @@ -1188,7 +1175,6 @@ class XMLConstructables(enum.Enum): VALUE_REFERENCE_PAIR = enum.auto() IEC61360_CONCEPT_DESCRIPTION = enum.auto() CONCEPT_DESCRIPTION = enum.auto() - CONSTRAINT = enum.auto() DATA_ELEMENT = enum.auto() SUBMODEL_ELEMENT = enum.auto() VALUE_LIST = enum.auto() @@ -1275,8 +1261,6 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool elif construct == XMLConstructables.CONCEPT_DESCRIPTION: constructor = decoder_.construct_concept_description # the following constructors decide which constructor to call based on the elements tag - elif construct == XMLConstructables.CONSTRAINT: - constructor = decoder_.construct_constraint elif construct == XMLConstructables.DATA_ELEMENT: constructor = decoder_.construct_data_element elif construct == XMLConstructables.SUBMODEL_ELEMENT: From 301f5933b412c3676c882491e1993ffb9efd34da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 1 Jun 2022 14:48:42 +0200 Subject: [PATCH 041/474] aasJSONschema: fix indentation of BasicEventElement --- basyx/aas/adapter/json/aasJSONSchema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 3e99d34..03afaf5 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -881,7 +881,7 @@ { "$ref": "#/definitions/Capability" }, { "$ref": "#/definitions/Entity" }, { "$ref": "#/definitions/Event" }, - { "$ref": "#/definitions/BasicEventElement" }, + { "$ref": "#/definitions/BasicEventElement" }, { "$ref": "#/definitions/MultiLanguageProperty" }, { "$ref": "#/definitions/Operation" }, { "$ref": "#/definitions/Property" }, From 049d94435685e9ee770a9a1f1cbba9b8b8fb2ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 15 Jun 2022 14:20:31 +0200 Subject: [PATCH 042/474] rename Event to EventElement --- basyx/aas/adapter/_generic.py | 2 +- basyx/aas/adapter/json/aasJSONSchema.json | 12 ++++++------ basyx/aas/adapter/json/json_serialization.py | 6 +++--- basyx/aas/adapter/xml/AAS.xsd | 6 +++--- basyx/aas/adapter/xml/IEC61360.xsd | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index e926ee0..65e818f 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -32,7 +32,7 @@ model.KeyElements.CONCEPT_DICTIONARY: 'ConceptDictionary', model.KeyElements.DATA_ELEMENT: 'DataElement', model.KeyElements.ENTITY: 'Entity', - model.KeyElements.EVENT: 'Event', + model.KeyElements.EVENT_ELEMENT: 'EventElement', model.KeyElements.FILE: 'File', model.KeyElements.MULTI_LANGUAGE_PROPERTY: 'MultiLanguageProperty', model.KeyElements.OPERATION: 'Operation', diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 03afaf5..93597ee 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -277,7 +277,7 @@ "DataElement", "File", "Entity", - "Event", + "EventElement", "MultiLanguageProperty", "Operation", "Property", @@ -304,7 +304,7 @@ "DataElement", "File", "Entity", - "Event", + "EventElement", "MultiLanguageProperty", "Operation", "Property", @@ -648,7 +648,7 @@ { "$ref": "#/definitions/File" }, { "$ref": "#/definitions/Capability" }, { "$ref": "#/definitions/Entity" }, - { "$ref": "#/definitions/Event" }, + { "$ref": "#/definitions/EventElement" }, { "$ref": "#/definitions/BasicEventElement" }, { "$ref": "#/definitions/MultiLanguageProperty" }, { "$ref": "#/definitions/Operation" }, @@ -679,14 +679,14 @@ } ] }, - "Event": { + "EventElement": { "allOf": [ { "$ref": "#/definitions/SubmodelElement" } ] }, "BasicEventElement": { "allOf": [ - { "$ref": "#/definitions/Event" }, + { "$ref": "#/definitions/EventElement" }, { "properties": { "observed": { "$ref": "#/definitions/Reference" @@ -880,7 +880,7 @@ { "$ref": "#/definitions/File" }, { "$ref": "#/definitions/Capability" }, { "$ref": "#/definitions/Entity" }, - { "$ref": "#/definitions/Event" }, + { "$ref": "#/definitions/EventElement" }, { "$ref": "#/definitions/BasicEventElement" }, { "$ref": "#/definitions/MultiLanguageProperty" }, { "$ref": "#/definitions/Operation" }, diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index e149c7e..122807f 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -637,11 +637,11 @@ def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]: return data @classmethod - def _event_to_json(cls, obj: model.Event) -> Dict[str, object]: # no attributes in specification yet + def _event_element_to_json(cls, obj: model.EventElement) -> Dict[str, object]: # no attributes in specification yet """ - serialization of an object from class Event to json + serialization of an object from class EventElement to json - :param obj: object of class Event + :param obj: object of class EventElement :return: dict with the serialized attributes of this object """ return {} diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 61e88c7..62416fb 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -72,7 +72,7 @@ - + @@ -164,7 +164,7 @@ - + @@ -261,7 +261,7 @@ - + diff --git a/basyx/aas/adapter/xml/IEC61360.xsd b/basyx/aas/adapter/xml/IEC61360.xsd index 1a53f10..b16fccb 100644 --- a/basyx/aas/adapter/xml/IEC61360.xsd +++ b/basyx/aas/adapter/xml/IEC61360.xsd @@ -62,7 +62,7 @@ - + From 957a9f400508f2bdcb5da2bcbcbf8219b85e86d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 22 Jun 2022 13:23:34 +0200 Subject: [PATCH 043/474] add Resource --- basyx/aas/adapter/json/aasJSONSchema.json | 11 +++++++++++ basyx/aas/adapter/json/json_deserialization.py | 8 ++++++++ basyx/aas/adapter/json/json_serialization.py | 16 ++++++++++++++++ basyx/aas/adapter/xml/AAS.xsd | 6 ++++++ basyx/aas/adapter/xml/xml_deserialization.py | 14 ++++++++++++++ basyx/aas/adapter/xml/xml_serialization.py | 16 ++++++++++++++++ 6 files changed, 71 insertions(+) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 93597ee..a8f59f7 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -842,6 +842,17 @@ } ] }, + "Resource": { + "properties": { + "path": { + "type": "string" + }, + "contentType": { + "type": "string" + } + }, + "required": [ "path" ] + }, "Blob": { "allOf": [ { "$ref": "#/definitions/SubmodelElement" }, diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 6ec40b8..697ad75 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -633,6 +633,14 @@ def _construct_file(cls, dct: Dict[str, object], object_class=model.File) -> mod ret.value = _get_ts(dct, 'value', str) return ret + @classmethod + def _construct_resource(cls, dct: Dict[str, object], object_class=model.Resource) -> model.Resource: + ret = object_class(path=_get_ts(dct, "path", str)) + cls._amend_abstract_attributes(ret, dct) + if 'contentType' in dct and dct['contentType'] is not None: + ret.content_type = _get_ts(dct, 'contentType', str) + return ret + @classmethod def _construct_multi_language_property( cls, dct: Dict[str, object], object_class=model.MultiLanguageProperty) -> model.MultiLanguageProperty: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 122807f..6525d7c 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -101,6 +101,8 @@ def default(self, obj: object) -> object: return self._multi_language_property_to_json(obj) if isinstance(obj, model.File): return self._file_to_json(obj) + if isinstance(obj, model.Resource): + return self._resource_to_json(obj) if isinstance(obj, model.Blob): return self._blob_to_json(obj) if isinstance(obj, model.ReferenceElement): @@ -522,6 +524,20 @@ def _file_to_json(cls, obj: model.File) -> Dict[str, object]: data.update({'value': obj.value, 'mimeType': obj.mime_type}) return data + @classmethod + def _resource_to_json(cls, obj: model.Resource) -> Dict[str, object]: + """ + serialization of an object from class Resource to json + + :param obj: object of class Resource + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['path'] = obj.path + if obj.content_type is not None: + data['contentType'] = obj.content_type + return data + @classmethod def _reference_element_to_json(cls, obj: model.ReferenceElement) -> Dict[str, object]: """ diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 62416fb..d6c0b22 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -193,6 +193,12 @@ + + + + + + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 14ea3f1..2cafd04 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -766,6 +766,17 @@ def construct_file(cls, element: etree.Element, object_class=model.File, **_kwar cls._amend_abstract_attributes(file, element) return file + @classmethod + def construct_resource(cls, element: etree.Element, object_class=model.Resource, **_kwargs: Any) -> model.Resource: + resource = object_class( + _child_text_mandatory(element, NS_AAS + "path") + ) + content_type = _get_text_or_none(element.find(NS_AAS + "contentType")) + if content_type is not None: + resource.content_type = content_type + cls._amend_abstract_attributes(resource, element) + return resource + @classmethod def construct_multi_language_property(cls, element: etree.Element, object_class=model.MultiLanguageProperty, **_kwargs: Any) -> model.MultiLanguageProperty: @@ -1161,6 +1172,7 @@ class XMLConstructables(enum.Enum): ENTITY = enum.auto() EXTENSION = enum.auto() FILE = enum.auto() + RESOURCE = enum.auto() MULTI_LANGUAGE_PROPERTY = enum.auto() OPERATION = enum.auto() PROPERTY = enum.auto() @@ -1232,6 +1244,8 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_extension elif construct == XMLConstructables.FILE: constructor = decoder_.construct_file + elif construct == XMLConstructables.RESOURCE: + constructor = decoder_.construct_resource elif construct == XMLConstructables.MULTI_LANGUAGE_PROPERTY: constructor = decoder_.construct_multi_language_property elif construct == XMLConstructables.OPERATION: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 4a3aa55..8e514c6 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -664,6 +664,22 @@ def file_to_xml(obj: model.File, return et_file +def resource_to_xml(obj: model.Resource, + tag: str = NS_AAS+"resource") -> etree.Element: + """ + Serialization of objects of class :class:`~aas.model.base.Resource` to XML + + :param obj: Object of class :class:`~aas.model.base.Resource` + :param tag: Namespace+Tag of the serialized element. Default is "aas:resource" + :return: Serialized ElementTree object + """ + et_resource = abstract_classes_to_xml(tag, obj) + et_resource.append(_generate_element(NS_AAS + "path", text=obj.path)) + if obj.content_type: + et_resource.append(_generate_element(NS_AAS + "contentType", text=obj.content_type)) + return et_resource + + def reference_element_to_xml(obj: model.ReferenceElement, tag: str = NS_AAS+"referenceElement") -> etree.Element: """ From aa626d2789df6c8a1eccaf736a7d8d1b70a7faf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 22 Jun 2022 13:34:10 +0200 Subject: [PATCH 044/474] change type of AssetInformation/defaultThumbnail from File to Resource --- basyx/aas/adapter/json/aasJSONSchema.json | 2 +- basyx/aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/xml/AAS.xsd | 2 +- basyx/aas/adapter/xml/xml_deserialization.py | 2 +- basyx/aas/adapter/xml/xml_serialization.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index a8f59f7..88010ea 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -560,7 +560,7 @@ } }, "thumbnail":{ - "$ref": "#/definitions/File" + "$ref": "#/definitions/Resource" } }, "required": [ "assetKind" ] diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 697ad75..79abfd3 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -390,7 +390,7 @@ def _construct_asset_information(cls, dct: Dict[str, object], object_class=model ret.specific_asset_id.add(cls._construct_identifier_key_value_pair(desc_data, model.IdentifierKeyValuePair)) if 'thumbnail' in dct: - ret.default_thumbnail = _get_ts(dct, 'thumbnail', model.File) + ret.default_thumbnail = cls._construct_resource(_get_ts(dct, 'thumbnail', dict)) return ret @classmethod diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index d6c0b22..ef6648e 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -59,7 +59,7 @@ - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 2cafd04..e3a5219 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -936,7 +936,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. cls.construct_identifier_key_value_pair, cls.failsafe): asset_information.specific_asset_id.add(id) thumbnail = _failsafe_construct(element.find(NS_AAS + "defaultThumbNail"), - cls.construct_file, cls.failsafe) + cls.construct_resource, cls.failsafe) if thumbnail is not None: asset_information.default_thumbnail = thumbnail diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 8e514c6..3c4c047 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -338,7 +338,7 @@ def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"ass """ et_asset_information = abstract_classes_to_xml(tag, obj) if obj.default_thumbnail: - et_asset_information.append(file_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbNail")) + et_asset_information.append(resource_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbNail")) if obj.global_asset_id: et_asset_information.append(reference_to_xml(obj.global_asset_id, NS_AAS + "globalAssetId")) et_asset_information.append(_generate_element(name=NS_AAS + "assetKind", text=_generic.ASSET_KIND[obj.asset_kind])) From e4631ecbbedb1f723485b0de1d88961faee68db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 22 Jun 2022 14:07:51 +0200 Subject: [PATCH 045/474] rename Blob/mimeType File/mimeType to /contentType --- basyx/aas/adapter/json/aasJSONSchema.json | 8 ++++---- basyx/aas/adapter/json/json_deserialization.py | 4 ++-- basyx/aas/adapter/json/json_serialization.py | 4 ++-- basyx/aas/adapter/xml/AAS.xsd | 4 ++-- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- basyx/aas/adapter/xml/xml_serialization.py | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 88010ea..9a8e468 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -834,11 +834,11 @@ "value": { "type": "string" }, - "mimeType": { + "contentType": { "type": "string" } }, - "required": [ "mimeType" ] + "required": [ "contentType" ] } ] }, @@ -860,11 +860,11 @@ "value": { "type": "string" }, - "mimeType": { + "contentType": { "type": "string" } }, - "required": [ "mimeType" ] + "required": [ "contentType" ] } ] }, diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 79abfd3..3035c55 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -615,7 +615,7 @@ def _construct_submodel_element_collection( @classmethod def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> model.Blob: ret = object_class(id_short=_get_ts(dct, "idShort", str), - mime_type=_get_ts(dct, "mimeType", str), + content_type=_get_ts(dct, "contentType", str), kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if 'value' in dct: @@ -626,7 +626,7 @@ def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> mod def _construct_file(cls, dct: Dict[str, object], object_class=model.File) -> model.File: ret = object_class(id_short=_get_ts(dct, "idShort", str), value=None, - mime_type=_get_ts(dct, "mimeType", str), + content_type=_get_ts(dct, "contentType", str), kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 6525d7c..72f5d16 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -507,7 +507,7 @@ def _blob_to_json(cls, obj: model.Blob) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data['mimeType'] = obj.mime_type + data['contentType'] = obj.content_type if obj.value is not None: data['value'] = base64.b64encode(obj.value).decode() return data @@ -521,7 +521,7 @@ def _file_to_json(cls, obj: model.File) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data.update({'value': obj.value, 'mimeType': obj.mime_type}) + data.update({'value': obj.value, 'contentType': obj.content_type}) return data @classmethod diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index ef6648e..4e7b731 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -84,7 +84,7 @@ - + @@ -188,7 +188,7 @@ - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index e3a5219..50a416e 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -706,7 +706,7 @@ def construct_basic_event_element(cls, element: etree.Element, object_class=mode def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: blob = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), - _child_text_mandatory(element, NS_AAS + "mimeType"), + _child_text_mandatory(element, NS_AAS + "contentType"), kind=_get_modeling_kind(element) ) value = _get_text_or_none(element.find(NS_AAS + "value")) @@ -757,7 +757,7 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ def construct_file(cls, element: etree.Element, object_class=model.File, **_kwargs: Any) -> model.File: file = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), - _child_text_mandatory(element, NS_AAS + "mimeType"), + _child_text_mandatory(element, NS_AAS + "contentType"), kind=_get_modeling_kind(element) ) value = _get_text_or_none(element.find(NS_AAS + "value")) diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 3c4c047..67ca161 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -644,7 +644,7 @@ def blob_to_xml(obj: model.Blob, if obj.value is not None: et_value.text = base64.b64encode(obj.value).decode() et_blob.append(et_value) - et_blob.append(_generate_element(NS_AAS + "mimeType", text=obj.mime_type)) + et_blob.append(_generate_element(NS_AAS + "contentType", text=obj.content_type)) return et_blob @@ -660,7 +660,7 @@ def file_to_xml(obj: model.File, et_file = abstract_classes_to_xml(tag, obj) if obj.value: et_file.append(_generate_element(NS_AAS + "value", text=obj.value)) - et_file.append(_generate_element(NS_AAS + "mimeType", text=obj.mime_type)) + et_file.append(_generate_element(NS_AAS + "contentType", text=obj.content_type)) return et_file From ba854a2d42236170a106e6f7b9fb19a98424b2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 25 Jun 2022 20:34:03 +0200 Subject: [PATCH 046/474] rename Identifiable/identification to Identifiable/id --- basyx/aas/adapter/aasx.py | 242 +++++++++++------- basyx/aas/adapter/json/aasJSONSchema.json | 4 +- .../aas/adapter/json/json_deserialization.py | 16 +- basyx/aas/adapter/json/json_serialization.py | 2 +- basyx/aas/adapter/xml/AAS.xsd | 2 +- basyx/aas/adapter/xml/xml_deserialization.py | 18 +- basyx/aas/adapter/xml/xml_serialization.py | 6 +- 7 files changed, 180 insertions(+), 110 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 5e47119..5225ae8 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -58,6 +58,7 @@ class AASXReader: with AASXReader("filename.aasx") as reader: meta_data = reader.get_core_properties() reader.read_into(objects, files) + """ def __init__(self, file: Union[os.PathLike, str, IO]): """ @@ -111,7 +112,7 @@ def get_thumbnail(self) -> Optional[bytes]: def read_into(self, object_store: model.AbstractObjectStore, file_store: "AbstractSupplementaryFileContainer", - override_existing: bool = False, **kwargs) -> Set[model.Identifier]: + override_existing: bool = False) -> Set[model.Identifier]: """ Read the contents of the AASX package and add them into a given :class:`ObjectStore ` @@ -147,14 +148,12 @@ def read_into(self, object_store: model.AbstractObjectStore, # Iterate AAS files for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ RELATIONSHIP_TYPE_AAS_SPEC]: - self._read_aas_part_into(aas_part, object_store, file_store, - read_identifiables, override_existing, **kwargs) + self._read_aas_part_into(aas_part, object_store, file_store, read_identifiables, override_existing) # Iterate split parts of AAS file for split_part in self.reader.get_related_parts_by_type(aas_part)[ RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: - self._read_aas_part_into(split_part, object_store, file_store, - read_identifiables, override_existing, **kwargs) + self._read_aas_part_into(split_part, object_store, file_store, read_identifiables, override_existing) return read_identifiables @@ -174,7 +173,7 @@ def _read_aas_part_into(self, part_name: str, object_store: model.AbstractObjectStore, file_store: "AbstractSupplementaryFileContainer", read_identifiables: Set[model.Identifier], - override_existing: bool, **kwargs) -> None: + override_existing: bool) -> None: """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. @@ -190,10 +189,10 @@ def _read_aas_part_into(self, part_name: str, :param override_existing: If True, existing objects in the object store are overridden with objects from the AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. """ - for obj in self._parse_aas_part(part_name, **kwargs): - if obj.identification in read_identifiables: + for obj in self._parse_aas_part(part_name): + if obj.id in read_identifiables: continue - if obj.identification in object_store: + if obj.id in object_store: if override_existing: logger.info("Overriding existing object in ObjectStore with {} ...".format(obj)) object_store.discard(obj) @@ -202,11 +201,11 @@ def _read_aas_part_into(self, part_name: str, "ObjectStore".format(obj)) continue object_store.add(obj) - read_identifiables.add(obj.identification) + read_identifiables.add(obj.id) if isinstance(obj, model.Submodel): self._collect_supplementary_files(part_name, obj, file_store) - def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore: + def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: """ Helper function to parse the AAS objects from a single JSON or XML part of the AASX package. @@ -220,12 +219,12 @@ def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore: if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_xml_file(p, **kwargs) + return read_aas_xml_file(p) elif content_type.split(";")[0] in ("text/json", "application/json") \ or content_type == "" and extension == "json": logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs) + return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig')) else: logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}" .format(part_name, content_type, extension)) @@ -306,7 +305,6 @@ def __init__(self, file: Union[os.PathLike, str, IO]): self._properties_part: Optional[str] = None # names and hashes of all supplementary file parts that have already been written self._supplementary_part_names: Dict[str, Optional[bytes]] = {} - self._aas_name_friendlyfier = NameFriendlyfier() # Open OPC package writer self.writer = pyecma376_2.ZipPackageWriter(file) @@ -317,83 +315,105 @@ def __init__(self, file: Union[os.PathLike, str, IO]): p.close() def write_aas(self, - aas_id: model.Identifier, + aas_ids: Union[model.Identifier, Iterable[model.Identifier]], object_store: model.AbstractObjectStore, file_store: "AbstractSupplementaryFileContainer", - write_json: bool = False, - submodel_split_parts: bool = True) -> None: + write_json: bool = False) -> None: """ - Convenience method to add an :class:`~aas.model.aas.AssetAdministrationShell` with all included and referenced + Convenience method to write one or more + :class:`AssetAdministrationShells ` with all included and referenced objects to the AASX package according to the part name conventions from DotAAS. - This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given - object_store. :class:`References <~aas.model.base.Reference>` to - the :class:`~aas.model.aas.Asset`, :class:`ConceptDescriptions ` and - :class:`Submodels ` are also resolved using the object_store. All of these objects - are written to aas-spec parts in the AASX package, following the conventions presented in "Details of the Asset - Administration Shell". For each Submodel, a aas-spec-split part is used. Supplementary files which are - referenced by a File object in any of the Submodels, are also added to the AASX package. - - Internally, this method uses :meth:`aas.adapter.aasx.AASXWriter.write_aas_objects` to write the individual AASX - parts for the AAS and each submodel. - - :param aas_id: :class:`~aas.model.base.Identifier` of the :class:`~aas.model.aas.AssetAdministrationShell` to - be added to the AASX file + This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve the + AASs from the given object_store. + :class:`References ` to :class:`Submodels ` and + :class:`ConceptDescriptions ` (via semanticId attributes) are also + resolved using the + `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` or `/aasx/data.json` in + the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". + Supplementary files which are referenced by a :class:`~aas.model.submodel.File` object in any of the + :class:`Submodels ` are also added to the AASX + package. + + This method uses `write_all_aas_objects()` to write the AASX part. + + .. attention:: + + This method **must only be used once** on a single AASX package. Otherwise, the `/aasx/data.json` + (or `...xml`) part would be written twice to the package, hiding the first part and possibly causing + problems when reading the package. + + To write multiple Asset Administration Shells to a single AASX package file, call this method once, passing + a list of AAS Identifiers to the `aas_ids` parameter. + + :param aas_ids: :class:`~aas.model.base.Identifier` or Iterable of + :class:`Identifiers ` of the AAS(s) to be written to the AASX file :param object_store: :class:`ObjectStore ` to retrieve the :class:`~aas.model.base.Identifiable` AAS objects (:class:`~aas.model.aas.AssetAdministrationShell`, - :class:`~aas.model.aas.Asset`, :class:`~aas.model.concept.ConceptDescription` and - :class:`~aas.model.submodel.Submodel`) from - :param file_store: :class:`SupplementaryFileContainer ` to - retrieve supplementary files from, which are referenced by :class:`~aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each submodel in the AASX package file - instead of XML parts. Defaults to `False`. - :param submodel_split_parts: If `True` (default), submodels are written to separate AASX parts instead of being - included in the AAS part with in the AASX package. - """ - aas_friendly_name = self._aas_name_friendlyfier.get_friendly_name(aas_id) - aas_part_name = "/aasx/{0}/{0}.aas.{1}".format(aas_friendly_name, "json" if write_json else "xml") - - aas = object_store.get_identifiable(aas_id) - if not isinstance(aas, model.AssetAdministrationShell): - raise ValueError(f"Identifier does not belong to an AssetAdminstrationShell object but to {aas!r}") - - objects_to_be_written: Set[model.Identifier] = {aas.identification} - - # Add the Asset object to the objects in the AAS part - objects_to_be_written.add(aas.asset.get_identifier()) - - # Add referenced ConceptDescriptions to the AAS part - for dictionary in aas.concept_dictionary: - for concept_description_ref in dictionary.concept_description: - objects_to_be_written.add(concept_description_ref.get_identifier()) - - # Write submodels: Either create a split part for each of them or otherwise add them to objects_to_be_written - aas_split_part_names: List[str] = [] - if submodel_split_parts: - # Create a AAS split part for each (available) submodel of the AAS - aas_friendlyfier = NameFriendlyfier() - for submodel_ref in aas.submodel: - submodel_identification = submodel_ref.get_identifier() - submodel_friendly_name = aas_friendlyfier.get_friendly_name(submodel_identification) - submodel_part_name = "/aasx/{0}/{1}/{1}.submodel.{2}".format(aas_friendly_name, submodel_friendly_name, - "json" if write_json else "xml") - self.write_aas_objects(submodel_part_name, [submodel_identification], object_store, file_store, - write_json, split_part=True) - aas_split_part_names.append(submodel_part_name) - else: + :class:`~aas.model.concept.ConceptDescription` and :class:`~aas.model.submodel.Submodel`) from + :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve + supplementary files from, which are referenced by :class:`~aas.model.submodel.File` objects + :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~aas.model.submodel.Submodel` + in the AASX package file instead of XML parts. Defaults to `False`. + :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable + :class:`Submodels ` and + :class:`ConceptDescriptions ` are skipped, logging a warning/info + message) + :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another + :class:`~aas.model.base.Identifiable` object) + """ + if isinstance(aas_ids, model.Identifier): + aas_ids = (aas_ids,) + + objects_to_be_written: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + for aas_id in aas_ids: + try: + aas = object_store.get_identifiable(aas_id) + # TODO add failsafe mode + except KeyError: + raise + if not isinstance(aas, model.AssetAdministrationShell): + raise TypeError(f"Identifier {aas_id} does not belong to an AssetAdminstrationShell object but to " + f"{aas!r}") + + # Add the AssetAdministrationShell object to the data part + objects_to_be_written.add(aas) + + # Add referenced Submodels to the data part for submodel_ref in aas.submodel: - objects_to_be_written.add(submodel_ref.get_identifier()) - - # Write AAS part - logger.debug("Writing AAS {} to part {} in AASX package ...".format(aas.identification, aas_part_name)) - self.write_aas_objects(aas_part_name, objects_to_be_written, object_store, file_store, write_json, - split_part=False, - additional_relationships=(pyecma376_2.OPCRelationship("r{}".format(i), - RELATIONSHIP_TYPE_AAS_SPEC_SPLIT, - submodel_part_name, - pyecma376_2.OPCTargetMode.INTERNAL) - for i, submodel_part_name in enumerate(aas_split_part_names))) + try: + submodel = submodel_ref.resolve(object_store) + except KeyError: + logger.warning("Could not find submodel %s. Skipping it.", str(submodel_ref)) + continue + objects_to_be_written.add(submodel) + + # Traverse object tree and check if semanticIds are referencing to existing ConceptDescriptions in the + # ObjectStore + concept_descriptions: List[model.ConceptDescription] = [] + for identifiable in objects_to_be_written: + for semantic_id in traversal.walk_semantic_ids_recursive(identifiable): + if not isinstance(semantic_id, model.AASReference) or semantic_id.type is not model.ConceptDescription: + logger.info("semanticId %s does not reference a ConceptDescription.", str(semantic_id)) + continue + try: + cd = semantic_id.resolve(object_store) + except KeyError: + logger.info("ConceptDescription for semantidId %s not found in object store.", str(semantic_id)) + continue + except model.UnexpectedTypeError as e: + logger.error("semantidId %s resolves to %s, which is not a ConceptDescription", + str(semantic_id), e.value) + continue + concept_descriptions.append(cd) + objects_to_be_written.update(concept_descriptions) + + # Write AAS data part + self.write_all_aas_objects("/aasx/data.{}".format("json" if write_json else "xml"), + objects_to_be_written, file_store, write_json) + # TODO remove `method` parameter in future version. + # Not actually required since you can always create a local dict def write_aas_objects(self, part_name: str, object_ids: Iterable[model.Identifier], @@ -403,8 +423,7 @@ def write_aas_objects(self, split_part: bool = False, additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: """ - Write a defined list of AAS objects to an XML or JSON part in the AASX package and append the referenced - supplementary files to the package. + A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given object_store. If the list @@ -412,7 +431,10 @@ def write_aas_objects(self, referenced by :class:`~aas.model.submodel.File` objects within those submodels, are also added to the AASX package. - You must make sure to call this method only once per unique `part_name` on a single package instance. + .. attention:: + + You must make sure to call this method or `write_all_aas_objects` only once per unique `part_name` on a + single package instance. :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. @@ -433,7 +455,6 @@ def write_aas_objects(self, logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) objects: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - supplementary_files: List[str] = [] # Retrieve objects and scan for referenced supplementary files for identifier in object_ids: @@ -443,6 +464,50 @@ def write_aas_objects(self, logger.error("Could not find object {} in ObjectStore".format(identifier)) continue objects.add(the_object) + + self.write_all_aas_objects(part_name, objects, file_store, write_json, split_part, additional_relationships) + + # TODO remove `split_part` parameter in future version. + # Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 + def write_all_aas_objects(self, + part_name: str, + objects: model.AbstractObjectStore[model.Identifiable], + file_store: "AbstractSupplementaryFileContainer", + write_json: bool = False, + split_part: bool = False, + additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: + """ + Write all AAS objects in a given :class:`ObjectStore ` to an XML or + JSON part in the AASX package and add the referenced supplementary files to the package. + + This method takes an :class:`ObjectStore ` and writes all contained + objects into an "aas_env" part in the AASX package. If + the ObjectStore includes :class:`~aas.model.submodel.Submodel` objects, supplementary files which are + referenced by :class:`~aas.model.submodel.File` objects + within those Submodels, are fetched from the `file_store` and added to the AASX package. + + .. attention:: + + You must make sure to call this method only once per unique `part_name` on a single package instance. + + :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 + part name and unique within the package. The extension of the part should match the data format (i.e. + '.json' if `write_json` else '.xml'). + :param objects: The objects to be written to the AASX package. Only these Identifiable objects (and included + Referable objects) are written to the package. + :param file_store: The SupplementaryFileContainer to retrieve supplementary files from (if there are any `File` + objects within the written objects. + :param write_json: If True, the part is written as a JSON file instead of an XML file. Defaults to False. + :param split_part: If True, no aas-spec relationship is added from the aasx-origin to this part. You must make + sure to reference it via a aas-spec-split relationship from another aas-spec part + :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object + part to be written, in addition to the aas-suppl relationships which are created automatically. + """ + logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) + supplementary_files: List[str] = [] + + # Retrieve objects and scan for referenced supplementary files + for the_object in objects: if isinstance(the_object, model.Submodel): for element in traversal.walk_submodel(the_object): if isinstance(element, model.File): @@ -458,6 +523,7 @@ def write_aas_objects(self, self._aas_part_names.append(part_name) # Write part + # TODO allow writing xml *and* JSON part with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p: if write_json: write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects) @@ -587,6 +653,8 @@ def _write_package_relationships(self): self.writer.write_relationships(package_relationships) +# TODO remove in future version. +# Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 class NameFriendlyfier: """ A simple helper class to create unique "AAS friendly names" according to DotAAS, section 7.6. @@ -602,6 +670,8 @@ def get_friendly_name(self, identifier: model.Identifier): """ Generate a friendly name from an AAS identifier. + TODO: This information is outdated. The whole class is no longer needed. + According to section 7.6 of "Details of the Asset Administration Shell", all non-alphanumerical characters are replaced with underscores. We also replace all non-ASCII characters to generate valid URIs as the result. If this replacement results in a collision with a previously generated friendly name of this NameFriendlifier, diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 9a8e468..6b6cfec 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -55,14 +55,14 @@ "allOf": [ { "$ref": "#/definitions/Referable" }, { "properties": { - "identification": { + "id": { "$ref": "#/definitions/Identifier" }, "administration": { "$ref": "#/definitions/AdministrativeInformation" } }, - "required": [ "identification" ] + "required": [ "id" ] } ] }, diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 3035c55..bdb0237 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -399,7 +399,7 @@ def _construct_asset_administration_shell( ret = object_class( asset_information=cls._construct_asset_information(_get_ts(dct, 'assetInformation', dict), model.AssetInformation), - identification=cls._construct_identifier(_get_ts(dct, 'identification', dict))) + id_=cls._construct_identifier(_get_ts(dct, 'id', dict))) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'submodels' in dct: for sm_data in _get_ts(dct, 'submodels', list): @@ -423,7 +423,7 @@ def _construct_concept_description(cls, dct: Dict[str, object], object_class=mod dct, _get_ts(dspec, 'dataSpecificationContent', dict)) # If this is not a special ConceptDescription, just construct one of the default object_class if ret is None: - ret = object_class(identification=cls._construct_identifier(_get_ts(dct, 'identification', dict))) + ret = object_class(id_=cls._construct_identifier(_get_ts(dct, 'id', dict))) cls._amend_abstract_attributes(ret, dct) if 'isCaseOf' in dct: for case_data in _get_ts(dct, "isCaseOf", list): @@ -434,7 +434,7 @@ def _construct_concept_description(cls, dct: Dict[str, object], object_class=mod def _construct_iec61360_concept_description(cls, dct: Dict[str, object], data_spec: Dict[str, object], object_class=model.concept.IEC61360ConceptDescription)\ -> model.concept.IEC61360ConceptDescription: - ret = object_class(identification=cls._construct_identifier(_get_ts(dct, 'identification', dict)), + ret = object_class(id_=cls._construct_identifier(_get_ts(dct, 'id', dict)), preferred_name=cls._construct_lang_string_set(_get_ts(data_spec, 'preferredName', list))) if 'dataType' in data_spec: ret.data_type = IEC61360_DATA_TYPES_INVERSE[_get_ts(data_spec, 'dataType', str)] @@ -508,7 +508,7 @@ def _construct_extension(cls, dct: Dict[str, object], object_class=model.Extensi @classmethod def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel) -> model.Submodel: - ret = object_class(identification=cls._construct_identifier(_get_ts(dct, 'identification', dict)), + ret = object_class(id_=cls._construct_identifier(_get_ts(dct, 'id', dict)), kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'submodelElements' in dct: @@ -787,16 +787,16 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r logger.warning("{} was in wrong list '{}'; nevertheless, we'll use it".format(item, name)) else: raise TypeError(error_message) - if item.identification in ret: + if item.id in ret: error_message = 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...") continue - existing_element = object_store.get(item.identification) + existing_element = object_store.get(item.id) if existing_element is not None: if not replace_existing: - error_message = f"object with identifier {item.identification} already exists " \ + error_message = f"object with identifier {item.id} already exists " \ f"in the object store: {existing_element}!" if not ignore_existing: raise KeyError(error_message + f" failed to insert {item}!") @@ -804,7 +804,7 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r continue object_store.discard(existing_element) object_store.add(item) - ret.add(item.identification) + ret.add(item.id) elif decoder_.failsafe: logger.error(error_message) else: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 72f5d16..514ee18 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -147,7 +147,7 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: .format(obj.__class__.__name__)) from e data['modelType'] = {'name': ref_type.__name__} if isinstance(obj, model.Identifiable): - data['identification'] = obj.identification + data['id'] = obj.id if obj.administration: data['administration'] = obj.administration if isinstance(obj, model.HasSemantics): diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 4e7b731..1a3bb8d 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -502,7 +502,7 @@ - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 50a416e..6cd9348 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -32,8 +32,8 @@ .. code-block:: - KeyError: aas:identification on line 252 has no attribute with name idType! - -> Failed to construct aas:identification on line 252 using construct_identifier! + KeyError: aas:id on line 252 has no attribute with name idType! + -> Failed to construct aas:id on line 252 using construct_identifier! -> Failed to construct aas:conceptDescription on line 247 using construct_concept_description! @@ -893,7 +893,7 @@ def construct_submodel_element_collection(cls, element: etree.Element, def construct_asset_administration_shell(cls, element: etree.Element, object_class=model.AssetAdministrationShell, **_kwargs: Any) -> model.AssetAdministrationShell: aas = object_class( - identification=_child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier), + id_=_child_construct_mandatory(element, NS_AAS + "id", cls.construct_identifier), asset_information=_child_construct_mandatory(element, NS_AAS + "assetInformation", cls.construct_asset_information) ) @@ -947,7 +947,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **_kwargs: Any) \ -> model.Submodel: submodel = object_class( - _child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier), + _child_construct_mandatory(element, NS_AAS + "id", cls.construct_identifier), kind=_get_modeling_kind(element) ) if not cls.stripped: @@ -1057,7 +1057,7 @@ def construct_iec61360_concept_description(cls, element: etree.Element, def construct_concept_description(cls, element: etree.Element, object_class=model.ConceptDescription, **_kwargs: Any) -> model.ConceptDescription: cd: Optional[model.ConceptDescription] = None - identifier = _child_construct_mandatory(element, NS_AAS + "identification", cls.construct_identifier) + identifier = _child_construct_mandatory(element, NS_AAS + "id", cls.construct_identifier) # Hack to detect IEC61360ConceptDescriptions, which are represented using dataSpecification according to DotAAS dspec_tag = NS_AAS + "embeddedDataSpecification" dspecs = element.findall(dspec_tag) @@ -1343,16 +1343,16 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif continue constructor = element_constructors[element_tag] for element in _child_construct_multiple(list_, element_tag, constructor, decoder_.failsafe): - if element.identification in ret: + if element.id in ret: error_message = f"{element} has a duplicate identifier already parsed in the document!" if not decoder_.failsafe: raise KeyError(error_message) logger.error(error_message + " skipping it...") continue - existing_element = object_store.get(element.identification) + existing_element = object_store.get(element.id) if existing_element is not None: if not replace_existing: - error_message = f"object with identifier {element.identification} already exists " \ + error_message = f"object with identifier {element.id} already exists " \ f"in the object store: {existing_element}!" if not ignore_existing: raise KeyError(error_message + f" failed to insert {element}!") @@ -1360,7 +1360,7 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif continue object_store.discard(existing_element) object_store.add(element) - ret.add(element.identification) + ret.add(element.id) return ret diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 67ca161..294cebb 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -114,9 +114,9 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: if isinstance(obj, model.Identifiable): if obj.administration: elm.append(administrative_information_to_xml(obj.administration)) - elm.append(_generate_element(name=NS_AAS + "identification", - text=obj.identification.id, - attributes={"idType": _generic.IDENTIFIER_TYPES[obj.identification.id_type]})) + elm.append(_generate_element(name=NS_AAS + "id", + text=obj.id.id, + attributes={"idType": _generic.IDENTIFIER_TYPES[obj.id.id_type]})) if isinstance(obj, model.HasKind): if obj.kind is model.ModelingKind.TEMPLATE: elm.append(_generate_element(name=NS_AAS + "kind", text="Template")) From c88c115ad0029009dc9241258c14ee99984fcb79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 18 Jul 2022 15:58:32 +0200 Subject: [PATCH 047/474] change type of Identifiable/id to str implement global and model references rename AASReference to ModelReference add Reference/referredSemanticId remove redundant definitions from XML schema (AAS and IEC61360) --- basyx/aas/adapter/_generic.py | 71 ++++------ basyx/aas/adapter/aasx.py | 13 +- basyx/aas/adapter/json/aasJSONSchema.json | 39 +++--- .../aas/adapter/json/json_deserialization.py | 73 +++++----- basyx/aas/adapter/json/json_serialization.py | 30 ++-- basyx/aas/adapter/xml/AAS.xsd | 88 +++--------- basyx/aas/adapter/xml/IEC61360.xsd | 116 +++------------- basyx/aas/adapter/xml/xml_deserialization.py | 130 ++++++++++-------- basyx/aas/adapter/xml/xml_serialization.py | 66 +++------ 9 files changed, 234 insertions(+), 392 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 65e818f..6b0391e 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -20,43 +20,33 @@ model.AssetKind.TYPE: 'Type', model.AssetKind.INSTANCE: 'Instance'} -KEY_ELEMENTS: Dict[model.KeyElements, str] = { - model.KeyElements.ASSET: 'Asset', - model.KeyElements.ASSET_ADMINISTRATION_SHELL: 'AssetAdministrationShell', - model.KeyElements.CONCEPT_DESCRIPTION: 'ConceptDescription', - model.KeyElements.SUBMODEL: 'Submodel', - model.KeyElements.ANNOTATED_RELATIONSHIP_ELEMENT: 'AnnotatedRelationshipElement', - model.KeyElements.BASIC_EVENT_ELEMENT: 'BasicEventElement', - model.KeyElements.BLOB: 'Blob', - model.KeyElements.CAPABILITY: 'Capability', - model.KeyElements.CONCEPT_DICTIONARY: 'ConceptDictionary', - model.KeyElements.DATA_ELEMENT: 'DataElement', - model.KeyElements.ENTITY: 'Entity', - model.KeyElements.EVENT_ELEMENT: 'EventElement', - model.KeyElements.FILE: 'File', - model.KeyElements.MULTI_LANGUAGE_PROPERTY: 'MultiLanguageProperty', - model.KeyElements.OPERATION: 'Operation', - model.KeyElements.PROPERTY: 'Property', - model.KeyElements.RANGE: 'Range', - model.KeyElements.REFERENCE_ELEMENT: 'ReferenceElement', - model.KeyElements.RELATIONSHIP_ELEMENT: 'RelationshipElement', - model.KeyElements.SUBMODEL_ELEMENT: 'SubmodelElement', - model.KeyElements.SUBMODEL_ELEMENT_COLLECTION: 'SubmodelElementCollection', - model.KeyElements.VIEW: 'View', - model.KeyElements.GLOBAL_REFERENCE: 'GlobalReference', - model.KeyElements.FRAGMENT_REFERENCE: 'FragmentReference'} +REFERENCE_TYPES: Dict[Type[model.Reference], str] = { + model.GlobalReference: 'GlobalReference', + model.ModelReference: 'ModelReference'} -KEY_TYPES: Dict[model.KeyType, str] = { - model.KeyType.CUSTOM: 'Custom', - model.KeyType.IRDI: 'IRDI', - model.KeyType.IRI: 'IRI', - model.KeyType.IDSHORT: 'IdShort', - model.KeyType.FRAGMENT_ID: 'FragmentId'} - -IDENTIFIER_TYPES: Dict[model.IdentifierType, str] = { - model.IdentifierType.CUSTOM: 'Custom', - model.IdentifierType.IRDI: 'IRDI', - model.IdentifierType.IRI: 'IRI'} +KEY_TYPES: Dict[model.KeyTypes, str] = { + model.KeyTypes.ASSET_ADMINISTRATION_SHELL: 'AssetAdministrationShell', + model.KeyTypes.CONCEPT_DESCRIPTION: 'ConceptDescription', + model.KeyTypes.SUBMODEL: 'Submodel', + model.KeyTypes.ANNOTATED_RELATIONSHIP_ELEMENT: 'AnnotatedRelationshipElement', + model.KeyTypes.BASIC_EVENT_ELEMENT: 'BasicEventElement', + model.KeyTypes.BLOB: 'Blob', + model.KeyTypes.CAPABILITY: 'Capability', + model.KeyTypes.CONCEPT_DICTIONARY: 'ConceptDictionary', + model.KeyTypes.DATA_ELEMENT: 'DataElement', + model.KeyTypes.ENTITY: 'Entity', + model.KeyTypes.EVENT_ELEMENT: 'EventElement', + model.KeyTypes.FILE: 'File', + model.KeyTypes.MULTI_LANGUAGE_PROPERTY: 'MultiLanguageProperty', + model.KeyTypes.OPERATION: 'Operation', + model.KeyTypes.PROPERTY: 'Property', + model.KeyTypes.RANGE: 'Range', + model.KeyTypes.REFERENCE_ELEMENT: 'ReferenceElement', + model.KeyTypes.RELATIONSHIP_ELEMENT: 'RelationshipElement', + model.KeyTypes.SUBMODEL_ELEMENT: 'SubmodelElement', + model.KeyTypes.SUBMODEL_ELEMENT_COLLECTION: 'SubmodelElementCollection', + model.KeyTypes.GLOBAL_REFERENCE: 'GlobalReference', + model.KeyTypes.FRAGMENT_REFERENCE: 'FragmentReference'} ENTITY_TYPES: Dict[model.EntityType, str] = { model.EntityType.CO_MANAGED_ENTITY: 'CoManagedEntity', @@ -86,13 +76,12 @@ MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} -KEY_ELEMENTS_INVERSE: Dict[str, model.KeyElements] = {v: k for k, v in KEY_ELEMENTS.items()} -KEY_TYPES_INVERSE: Dict[str, model.KeyType] = {v: k for k, v in KEY_TYPES.items()} -IDENTIFIER_TYPES_INVERSE: Dict[str, model.IdentifierType] = {v: k for k, v in IDENTIFIER_TYPES.items()} +REFERENCE_TYPES_INVERSE: Dict[str, Type[model.Reference]] = {v: k for k, v in REFERENCE_TYPES.items()} +KEY_TYPES_INVERSE: Dict[str, model.KeyTypes] = {v: k for k, v in KEY_TYPES.items()} ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()} IEC61360_DATA_TYPES_INVERSE: Dict[str, model.concept.IEC61360DataType] = {v: k for k, v in IEC61360_DATA_TYPES.items()} IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.concept.IEC61360LevelType] = \ {v: k for k, v in IEC61360_LEVEL_TYPES.items()} -KEY_ELEMENTS_CLASSES_INVERSE: Dict[model.KeyElements, Type[model.Referable]] = \ - {v: k for k, v in model.KEY_ELEMENTS_CLASSES.items()} +KEY_TYPES_CLASSES_INVERSE: Dict[model.KeyTypes, Type[model.Referable]] = \ + {v: k for k, v in model.KEY_TYPES_CLASSES.items()} diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 5225ae8..ada18d8 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -273,10 +273,10 @@ class AASXWriter: cp.created = datetime.datetime.now() with AASXWriter("filename.aasx") as writer: - writer.write_aas(Identifier("https://acplt.org/AssetAdministrationShell", IdentifierType.IRI), + writer.write_aas("https://acplt.org/AssetAdministrationShell", object_store, file_store) - writer.write_aas(Identifier("https://acplt.org/AssetAdministrationShell2", IdentifierType.IRI), + writer.write_aas("https://acplt.org/AssetAdministrationShell2", object_store, file_store) writer.write_core_properties(cp) @@ -393,7 +393,8 @@ def write_aas(self, concept_descriptions: List[model.ConceptDescription] = [] for identifiable in objects_to_be_written: for semantic_id in traversal.walk_semantic_ids_recursive(identifiable): - if not isinstance(semantic_id, model.AASReference) or semantic_id.type is not model.ConceptDescription: + if not isinstance(semantic_id, model.ModelReference) \ + or semantic_id.type is not model.ConceptDescription: logger.info("semanticId %s does not reference a ConceptDescription.", str(semantic_id)) continue try: @@ -682,15 +683,15 @@ def get_friendly_name(self, identifier: model.Identifier): .. code-block:: python friendlyfier = NameFriendlyfier() - friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS-a", model.IdentifierType.IRI)) + friendlyfier.get_friendly_name("http://example.com/AAS-a") > "http___example_com_AAS_a" - friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS+a", model.IdentifierType.IRI)) + friendlyfier.get_friendly_name("http://example.com/AAS+a") > "http___example_com_AAS_a_1" """ # friendlify name - raw_name = self.RE_NON_ALPHANUMERICAL.sub('_', identifier.id) + raw_name = self.RE_NON_ALPHANUMERICAL.sub('_', identifier) # Unify name (avoid collisions) amended_name = raw_name diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 6b6cfec..6781a7c 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -198,20 +198,7 @@ ] }, "Identifier": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "idType": { - "$ref": "#/definitions/KeyType" - } - }, - "required": [ "id", "idType" ] - }, - "KeyType": { - "type": "string", - "enum": ["Custom", "IRDI", "IRI", "IdShort", "FragmentId"] + "type": "string" }, "AdministrativeInformation": { "type": "object", @@ -239,31 +226,41 @@ "Reference": { "type": "object", "properties": { + "type": { + "$ref": "#/definitions/ReferenceTypes" + }, "keys": { "type": "array", "items": { "$ref": "#/definitions/Key" } + }, + "referredSemanticId": { + "$ref": "#/definitions/Reference" } }, - "required": [ "keys" ] + "required": [ "type", "keys" ] }, "Key": { "type": "object", "properties": { "type": { - "$ref": "#/definitions/KeyElements" - }, - "idType": { - "$ref": "#/definitions/KeyType" + "$ref": "#/definitions/KeyTypes" }, "value": { "type": "string" } }, - "required": [ "type", "idType", "value"] + "required": [ "type", "value"] + }, + "ReferenceTypes": { + "type": "string", + "enum": [ + "GlobalReference", + "ModelReference" + ] }, - "KeyElements": { + "KeyTypes": { "type": "string", "enum": [ "AssetAdministrationShell", diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index bdb0237..fe1d95c 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -37,9 +37,8 @@ from typing import Dict, Callable, TypeVar, Type, List, IO, Optional, Set from basyx.aas import model -from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ - IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ - KEY_ELEMENTS_CLASSES_INVERSE +from .._generic import MODELING_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 logger = logging.getLogger(__name__) @@ -278,8 +277,7 @@ def _get_kind(cls, dct: Dict[str, object]) -> model.ModelingKind: @classmethod def _construct_key(cls, dct: Dict[str, object], object_class=model.Key) -> model.Key: - return object_class(type_=KEY_ELEMENTS_INVERSE[_get_ts(dct, 'type', str)], - id_type=KEY_TYPES_INVERSE[_get_ts(dct, 'idType', str)], + return object_class(type_=KEY_TYPES_INVERSE[_get_ts(dct, 'type', str)], value=_get_ts(dct, 'value', str)) @classmethod @@ -290,23 +288,36 @@ def _construct_identifier_key_value_pair(cls, dct: Dict[str, object], object_cla external_subject_id=cls._construct_reference(_get_ts(dct, 'subjectId', dict))) @classmethod - def _construct_reference(cls, dct: Dict[str, object], object_class=model.Reference) -> model.Reference: + def _construct_reference(cls, dct: Dict[str, object]) -> model.Reference: + reference_type: Type[model.Reference] = REFERENCE_TYPES_INVERSE[_get_ts(dct, 'type', str)] + if reference_type is model.ModelReference: + return cls._construct_model_reference(dct, model.Referable) # type: ignore + elif reference_type is model.GlobalReference: + return cls._construct_global_reference(dct) + raise ValueError(f"Unsupported reference type {reference_type}!") + + @classmethod + def _construct_global_reference(cls, dct: Dict[str, object], object_class=model.GlobalReference)\ + -> model.GlobalReference: + reference_type: Type[model.Reference] = REFERENCE_TYPES_INVERSE[_get_ts(dct, 'type', str)] + if reference_type is not model.GlobalReference: + raise ValueError(f"Expected a reference of type {model.GlobalReference}, got {reference_type}!") keys = [cls._construct_key(key_data) for key_data in _get_ts(dct, "keys", list)] - return object_class(tuple(keys)) + return object_class(tuple(keys), cls._construct_reference(_get_ts(dct, 'referredSemanticId', dict)) + if 'referredSemanticId' in dct else None) @classmethod - def _construct_aas_reference(cls, dct: Dict[str, object], type_: Type[T], object_class=model.AASReference)\ - -> model.AASReference: + def _construct_model_reference(cls, dct: Dict[str, object], type_: Type[T], object_class=model.ModelReference)\ + -> model.ModelReference: + reference_type: Type[model.Reference] = REFERENCE_TYPES_INVERSE[_get_ts(dct, 'type', str)] + if reference_type is not model.ModelReference: + raise ValueError(f"Expected a reference of type {model.ModelReference}, got {reference_type}!") keys = [cls._construct_key(key_data) for key_data in _get_ts(dct, "keys", list)] - if keys and not issubclass(KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): + if keys and not issubclass(KEY_TYPES_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): logger.warning("type %s of last key of reference to %s does not match reference type %s", keys[-1].type.name, " / ".join(str(k) for k in keys), type_.__name__) - return object_class(tuple(keys), type_) - - @classmethod - def _construct_identifier(cls, dct: Dict[str, object], object_class=model.Identifier) -> model.Identifier: - return object_class(_get_ts(dct, 'id', str), - IDENTIFIER_TYPES_INVERSE[_get_ts(dct, 'idType', str)]) + return object_class(tuple(keys), type_, cls._construct_reference(_get_ts(dct, 'referredSemanticId', dict)) + if 'referredSemanticId' in dct else None) @classmethod def _construct_administrative_information( @@ -399,14 +410,14 @@ def _construct_asset_administration_shell( ret = object_class( asset_information=cls._construct_asset_information(_get_ts(dct, 'assetInformation', dict), model.AssetInformation), - id_=cls._construct_identifier(_get_ts(dct, 'id', dict))) + id_=_get_ts(dct, 'id', str)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'submodels' in dct: for sm_data in _get_ts(dct, 'submodels', list): - ret.submodel.add(cls._construct_aas_reference(sm_data, model.Submodel)) + ret.submodel.add(cls._construct_model_reference(sm_data, model.Submodel)) if 'derivedFrom' in dct: - ret.derived_from = cls._construct_aas_reference(_get_ts(dct, 'derivedFrom', dict), - model.AssetAdministrationShell) + ret.derived_from = cls._construct_model_reference(_get_ts(dct, 'derivedFrom', dict), + model.AssetAdministrationShell) return ret @classmethod @@ -423,7 +434,7 @@ def _construct_concept_description(cls, dct: Dict[str, object], object_class=mod dct, _get_ts(dspec, 'dataSpecificationContent', dict)) # If this is not a special ConceptDescription, just construct one of the default object_class if ret is None: - ret = object_class(id_=cls._construct_identifier(_get_ts(dct, 'id', dict))) + ret = object_class(id_=_get_ts(dct, 'id', str)) cls._amend_abstract_attributes(ret, dct) if 'isCaseOf' in dct: for case_data in _get_ts(dct, "isCaseOf", list): @@ -434,7 +445,7 @@ def _construct_concept_description(cls, dct: Dict[str, object], object_class=mod def _construct_iec61360_concept_description(cls, dct: Dict[str, object], data_spec: Dict[str, object], object_class=model.concept.IEC61360ConceptDescription)\ -> model.concept.IEC61360ConceptDescription: - ret = object_class(id_=cls._construct_identifier(_get_ts(dct, 'id', dict)), + ret = object_class(id_=_get_ts(dct, 'id', str), preferred_name=cls._construct_lang_string_set(_get_ts(data_spec, 'preferredName', list))) if 'dataType' in data_spec: ret.data_type = IEC61360_DATA_TYPES_INVERSE[_get_ts(data_spec, 'dataType', str)] @@ -508,7 +519,7 @@ def _construct_extension(cls, dct: Dict[str, object], object_class=model.Extensi @classmethod def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel) -> model.Submodel: - ret = object_class(id_=cls._construct_identifier(_get_ts(dct, 'id', dict)), + ret = object_class(id_=_get_ts(dct, 'id', str), kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'submodelElements' in dct: @@ -529,8 +540,8 @@ def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=mod # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 ret = object_class(id_short=_get_ts(dct, "idShort", str), - observed=cls._construct_aas_reference(_get_ts(dct, 'observed', dict), - model.Referable), # type: ignore + observed=cls._construct_model_reference(_get_ts(dct, 'observed', dict), + model.Referable), # type: ignore kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) return ret @@ -563,10 +574,10 @@ def _construct_relationship_element( # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 ret = object_class(id_short=_get_ts(dct, "idShort", str), - first=cls._construct_aas_reference(_get_ts(dct, 'first', dict), - model.Referable), # type: ignore - second=cls._construct_aas_reference(_get_ts(dct, 'second', dict), - model.Referable), # type: ignore + first=cls._construct_model_reference(_get_ts(dct, 'first', dict), + model.Referable), # type: ignore + second=cls._construct_model_reference(_get_ts(dct, 'second', dict), + model.Referable), # type: ignore kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) return ret @@ -579,8 +590,8 @@ def _construct_annotated_relationship_element( # see https://github.com/python/mypy/issues/5374 ret = object_class( id_short=_get_ts(dct, "idShort", str), - first=cls._construct_aas_reference(_get_ts(dct, 'first', dict), model.Referable), # type: ignore - second=cls._construct_aas_reference(_get_ts(dct, 'second', dict), model.Referable), # type: ignore + first=cls._construct_model_reference(_get_ts(dct, 'first', dict), model.Referable), # type: ignore + second=cls._construct_model_reference(_get_ts(dct, 'second', dict), model.Referable), # type: ignore kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'annotation' in dct: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 514ee18..7624430 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -65,8 +65,6 @@ def default(self, obj: object) -> object: """ if isinstance(obj, model.AssetAdministrationShell): return self._asset_administration_shell_to_json(obj) - if isinstance(obj, model.Identifier): - return self._identifier_to_json(obj) if isinstance(obj, model.AdministrativeInformation): return self._administrative_information_to_json(obj) if isinstance(obj, model.Reference): @@ -141,7 +139,7 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: if obj.description: data['description'] = cls._lang_string_set_to_json(obj.description) try: - ref_type = next(iter(t for t in inspect.getmro(type(obj)) if t in model.KEY_ELEMENTS_CLASSES)) + ref_type = next(iter(t for t in inspect.getmro(type(obj)) if t in model.KEY_TYPES_CLASSES)) except StopIteration as e: raise TypeError("Object of type {} is Referable but does not inherit from a known AAS type" .format(obj.__class__.__name__)) from e @@ -179,8 +177,7 @@ def _key_to_json(cls, obj: model.Key) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data.update({'type': _generic.KEY_ELEMENTS[obj.type], - 'idType': _generic.KEY_TYPES[obj.id_type], + data.update({'type': _generic.KEY_TYPES[obj.type], 'value': obj.value}) return data @@ -199,19 +196,6 @@ def _administrative_information_to_json(cls, obj: model.AdministrativeInformatio data['revision'] = obj.revision return data - @classmethod - def _identifier_to_json(cls, obj: model.Identifier) -> Dict[str, object]: - """ - serialization of an object from class Identifier to json - - :param obj: object of class Identifier - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['id'] = obj.id - data['idType'] = _generic.IDENTIFIER_TYPES[obj.id_type] - return data - @classmethod def _reference_to_json(cls, obj: model.Reference) -> Dict[str, object]: """ @@ -221,7 +205,10 @@ def _reference_to_json(cls, obj: model.Reference) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) + data['type'] = _generic.REFERENCE_TYPES[obj.__class__] data['keys'] = list(obj.key) + if obj.referred_semantic_id is not None: + data['referredSemanticId'] = cls._reference_to_json(obj.referred_semantic_id) return data @classmethod @@ -387,10 +374,9 @@ def _append_iec61360_concept_description_attrs(cls, obj: model.concept.IEC61360C if obj.level_types: data_spec['levelType'] = [_generic.IEC61360_LEVEL_TYPES[lt] for lt in obj.level_types] data['embeddedDataSpecifications'] = [ - {'dataSpecification': model.Reference(( - model.Key(model.KeyElements.GLOBAL_REFERENCE, - "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0", - model.KeyType.IRI),)), + {'dataSpecification': model.GlobalReference( + (model.Key(model.KeyTypes.GLOBAL_REFERENCE, + "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0", ),)), 'dataSpecificationContent': data_spec} ] diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 1a3bb8d..b3805c4 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -2,25 +2,7 @@ - - - - - - - - - - - - - - - - - - - + @@ -199,27 +181,6 @@ - - - - - - - - - - - - - - - - - - - - - @@ -228,14 +189,12 @@ - + - - - + - + @@ -243,17 +202,6 @@ - - - - - - - - - - - @@ -288,7 +236,7 @@ - + @@ -370,11 +318,26 @@ - + + + + + + + + + + + + + + + + @@ -502,7 +465,7 @@ - + @@ -529,13 +492,6 @@ - - - - - - - diff --git a/basyx/aas/adapter/xml/IEC61360.xsd b/basyx/aas/adapter/xml/IEC61360.xsd index b16fccb..daa1381 100644 --- a/basyx/aas/adapter/xml/IEC61360.xsd +++ b/basyx/aas/adapter/xml/IEC61360.xsd @@ -1,108 +1,24 @@ - - - - - - - - - - - - - - - - - - - + + - - - - - - + + + + + + - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -115,11 +31,11 @@ - + - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 6cd9348..e7109ec 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -51,9 +51,8 @@ from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar from .xml_serialization import NS_AAS, NS_ABAC, NS_IEC -from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE, \ - IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, \ - KEY_ELEMENTS_CLASSES_INVERSE +from .._generic import MODELING_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 logger = logging.getLogger(__name__) @@ -399,6 +398,19 @@ def _get_modeling_kind(element: etree.Element) -> model.ModelingKind: return modeling_kind if modeling_kind is not None else model.ModelingKind.INSTANCE +def _expect_reference_type(element: etree.Element, expected_type: Type[model.Reference]) -> None: + """ + Validates the type attribute of a Reference. + + :param element: The xml element. + :param expected_type: The expected type of the Reference. + :return: None + """ + actual_type = _get_attrib_mandatory_mapped(element, "type", REFERENCE_TYPES_INVERSE) + if actual_type is not expected_type: + raise ValueError(f"{_element_pretty_identifier(element)} is of type {actual_type}, expected {expected_type}!") + + class AASFromXmlDecoder: """ The default XML decoder class. @@ -483,73 +495,92 @@ def _construct_key_tuple(cls, element: etree.Element, namespace: str = NS_AAS, * return tuple(_child_construct_multiple(keys, namespace + "key", cls.construct_key, cls.failsafe)) @classmethod - def _construct_submodel_reference(cls, element: etree.Element, **kwargs: Any) -> model.AASReference[model.Submodel]: + def _construct_submodel_reference(cls, element: etree.Element, **kwargs: Any) \ + -> model.ModelReference[model.Submodel]: """ Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. """ - return cls.construct_aas_reference_expect_type(element, model.Submodel, **kwargs) + return cls.construct_model_reference_expect_type(element, model.Submodel, **kwargs) @classmethod def _construct_asset_administration_shell_reference(cls, element: etree.Element, **kwargs: Any) \ - -> model.AASReference[model.AssetAdministrationShell]: + -> model.ModelReference[model.AssetAdministrationShell]: """ Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. """ - return cls.construct_aas_reference_expect_type(element, model.AssetAdministrationShell, **kwargs) + return cls.construct_model_reference_expect_type(element, model.AssetAdministrationShell, **kwargs) @classmethod def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ - -> model.AASReference[model.Referable]: + -> model.ModelReference[model.Referable]: """ Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. """ # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - return cls.construct_aas_reference_expect_type(element, model.Referable, **kwargs) # type: ignore + return cls.construct_model_reference_expect_type(element, model.Referable, **kwargs) # type: ignore @classmethod def construct_key(cls, element: etree.Element, object_class=model.Key, **_kwargs: Any) \ -> model.Key: return object_class( - _get_attrib_mandatory_mapped(element, "type", KEY_ELEMENTS_INVERSE), - _get_text_mandatory(element), - _get_attrib_mandatory_mapped(element, "idType", KEY_TYPES_INVERSE) + _get_attrib_mandatory_mapped(element, "type", KEY_TYPES_INVERSE), + _get_text_mandatory(element) ) @classmethod - def construct_reference(cls, element: etree.Element, namespace: str = NS_AAS, object_class=model.Reference, - **_kwargs: Any) -> model.Reference: - return object_class(cls._construct_key_tuple(element, namespace=namespace)) + def construct_reference(cls, element: etree.Element, namespace: str = NS_AAS, **kwargs: Any) -> model.Reference: + reference_type: Type[model.Reference] = _get_attrib_mandatory_mapped(element, "type", REFERENCE_TYPES_INVERSE) + references: Dict[Type[model.Reference], Callable[..., model.Reference]] = { + model.GlobalReference: cls.construct_global_reference, + model.ModelReference: cls.construct_model_reference + } + if reference_type not in references: + raise KeyError(_element_pretty_identifier(element) + f" is of unsupported Reference type {reference_type}!") + return references[reference_type](element, namespace=namespace, **kwargs) @classmethod - def construct_aas_reference(cls, element: etree.Element, object_class=model.AASReference, **_kwargs: Any) \ - -> model.AASReference: + def construct_global_reference(cls, element: etree.Element, namespace: str = NS_AAS, + object_class=model.GlobalReference, **_kwargs: Any) \ + -> model.GlobalReference: + _expect_reference_type(element, model.GlobalReference) + return object_class(cls._construct_key_tuple(element, namespace=namespace), + _failsafe_construct(element.find(NS_AAS + "referredSemanticId"), cls.construct_reference, + cls.failsafe, namespace=namespace)) + + @classmethod + def construct_model_reference(cls, element: etree.Element, object_class=model.ModelReference, **_kwargs: Any) \ + -> model.ModelReference: """ - This constructor for AASReference determines the type of the AASReference by its keys. If no keys are present, - it will default to the type Referable. This behaviour is wanted in read_aas_xml_element(). + This constructor for ModelReference determines the type of the ModelReference by its keys. If no keys are + present, it will default to the type Referable. This behaviour is wanted in read_aas_xml_element(). """ + _expect_reference_type(element, model.ModelReference) keys = cls._construct_key_tuple(element) # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 type_: Type[model.Referable] = model.Referable # type: ignore if len(keys) > 0: - type_ = KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, model.Referable) # type: ignore - return object_class(keys, type_) + type_ = KEY_TYPES_CLASSES_INVERSE.get(keys[-1].type, model.Referable) # type: ignore + return object_class(keys, type_, _failsafe_construct(element.find(NS_AAS + "referredSemanticId"), + cls.construct_reference, cls.failsafe)) @classmethod - def construct_aas_reference_expect_type(cls, element: etree.Element, type_: Type[model.base._RT], - object_class=model.AASReference, **_kwargs: Any) \ - -> model.AASReference[model.base._RT]: + def construct_model_reference_expect_type(cls, element: etree.Element, type_: Type[model.base._RT], + object_class=model.ModelReference, **_kwargs: Any) \ + -> model.ModelReference[model.base._RT]: """ - This constructor for AASReference allows passing an expected type, which is checked against the type of the last - key of the reference. This constructor function is used by other constructor functions, since all expect a + This constructor for ModelReference allows passing an expected type, which is checked against the type of the + last key of the reference. This constructor function is used by other constructor functions, since all expect a specific target type. """ + _expect_reference_type(element, model.ModelReference) keys = cls._construct_key_tuple(element) - if keys and not issubclass(KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): + if keys and not issubclass(KEY_TYPES_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): logger.warning("type %s of last key of reference to %s does not match reference type %s", keys[-1].type.name, " / ".join(str(k) for k in keys), type_.__name__) - return object_class(keys, type_) + return object_class(keys, type_, _failsafe_construct(element.find(NS_AAS + "referredSemanticId"), + cls.construct_reference, cls.failsafe)) @classmethod def construct_administrative_information(cls, element: etree.Element, object_class=model.AdministrativeInformation, @@ -603,14 +634,6 @@ def construct_extension(cls, element: etree.Element, object_class=model.Extensio cls._amend_abstract_attributes(extension, element) return extension - @classmethod - def construct_identifier(cls, element: etree.Element, object_class=model.Identifier, **_kwargs: Any) \ - -> model.Identifier: - return object_class( - _get_text_mandatory(element), - _get_attrib_mandatory_mapped(element, "idType", IDENTIFIER_TYPES_INVERSE) - ) - @classmethod def construct_security(cls, _element: etree.Element, object_class=model.Security, **_kwargs: Any) -> model.Security: """ @@ -893,7 +916,7 @@ def construct_submodel_element_collection(cls, element: etree.Element, def construct_asset_administration_shell(cls, element: etree.Element, object_class=model.AssetAdministrationShell, **_kwargs: Any) -> model.AssetAdministrationShell: aas = object_class( - id_=_child_construct_mandatory(element, NS_AAS + "id", cls.construct_identifier), + id_=_child_text_mandatory(element, NS_AAS + "id"), asset_information=_child_construct_mandatory(element, NS_AAS + "assetInformation", cls.construct_asset_information) ) @@ -915,7 +938,7 @@ def construct_identifier_key_value_pair(cls, element: etree.Element, object_clas **_kwargs: Any) -> model.IdentifierKeyValuePair: return object_class( external_subject_id=_child_construct_mandatory(element, NS_AAS + "externalSubjectId", - cls.construct_reference, namespace=NS_AAS), + cls.construct_reference), key=_get_text_or_none(element.find(NS_AAS + "key")), value=_get_text_or_none(element.find(NS_AAS + "value")) ) @@ -947,7 +970,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **_kwargs: Any) \ -> model.Submodel: submodel = object_class( - _child_construct_mandatory(element, NS_AAS + "id", cls.construct_identifier), + _child_text_mandatory(element, NS_AAS + "id"), kind=_get_modeling_kind(element) ) if not cls.stripped: @@ -976,7 +999,7 @@ def construct_value_reference_pair(cls, element: etree.Element, value_format: Op return object_class( value_format, model.datatypes.from_xsd(_child_text_mandatory(element, NS_IEC + "value"), value_format), - _child_construct_mandatory(element, NS_IEC + "valueId", cls.construct_reference, namespace=NS_IEC) + _child_construct_mandatory(element, NS_IEC + "valueId", cls.construct_reference) ) @classmethod @@ -999,25 +1022,23 @@ def construct_iec61360_concept_description(cls, element: etree.Element, raise ValueError("No identifier given!") cd = object_class( identifier, - _child_construct_mandatory(element, NS_IEC + "preferredName", cls.construct_lang_string_set, - namespace=NS_IEC) + _child_construct_mandatory(element, NS_IEC + "preferredName", cls.construct_lang_string_set) ) data_type = _get_text_mapped_or_none(element.find(NS_IEC + "dataType"), IEC61360_DATA_TYPES_INVERSE) if data_type is not None: cd.data_type = data_type definition = _failsafe_construct(element.find(NS_IEC + "definition"), cls.construct_lang_string_set, - cls.failsafe, namespace=NS_IEC) + cls.failsafe) if definition is not None: cd.definition = definition short_name = _failsafe_construct(element.find(NS_IEC + "shortName"), cls.construct_lang_string_set, - cls.failsafe, namespace=NS_IEC) + cls.failsafe) if short_name is not None: cd.short_name = short_name unit = _get_text_or_none(element.find(NS_IEC + "unit")) if unit is not None: cd.unit = unit - unit_id = _failsafe_construct(element.find(NS_IEC + "unitId"), cls.construct_reference, cls.failsafe, - namespace=NS_IEC) + unit_id = _failsafe_construct(element.find(NS_IEC + "unitId"), cls.construct_reference, cls.failsafe) if unit_id is not None: cd.unit_id = unit_id source_of_definition = _get_text_or_none(element.find(NS_IEC + "sourceOfDefinition")) @@ -1037,8 +1058,7 @@ def construct_iec61360_concept_description(cls, element: etree.Element, value = _get_text_or_none(element.find(NS_IEC + "value")) if value is not None and value_format is not None: cd.value = model.datatypes.from_xsd(value, value_format) - value_id = _failsafe_construct(element.find(NS_IEC + "valueId"), cls.construct_reference, cls.failsafe, - namespace=NS_IEC) + value_id = _failsafe_construct(element.find(NS_IEC + "valueId"), cls.construct_reference, cls.failsafe) if value_id is not None: cd.value_id = value_id for level_type_element in element.findall(NS_IEC + "levelType"): @@ -1057,7 +1077,7 @@ def construct_iec61360_concept_description(cls, element: etree.Element, def construct_concept_description(cls, element: etree.Element, object_class=model.ConceptDescription, **_kwargs: Any) -> model.ConceptDescription: cd: Optional[model.ConceptDescription] = None - identifier = _child_construct_mandatory(element, NS_AAS + "id", cls.construct_identifier) + identifier = _child_text_mandatory(element, NS_AAS + "id") # Hack to detect IEC61360ConceptDescriptions, which are represented using dataSpecification according to DotAAS dspec_tag = NS_AAS + "embeddedDataSpecification" dspecs = element.findall(dspec_tag) @@ -1159,10 +1179,10 @@ class XMLConstructables(enum.Enum): """ KEY = enum.auto() REFERENCE = enum.auto() - AAS_REFERENCE = enum.auto() + MODEL_REFERENCE = enum.auto() + GLOBAL_REFERENCE = enum.auto() ADMINISTRATIVE_INFORMATION = enum.auto() QUALIFIER = enum.auto() - IDENTIFIER = enum.auto() SECURITY = enum.auto() OPERATION_VARIABLE = enum.auto() ANNOTATED_RELATIONSHIP_ELEMENT = enum.auto() @@ -1218,14 +1238,14 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_key elif construct == XMLConstructables.REFERENCE: constructor = decoder_.construct_reference - elif construct == XMLConstructables.AAS_REFERENCE: - constructor = decoder_.construct_aas_reference + elif construct == XMLConstructables.MODEL_REFERENCE: + constructor = decoder_.construct_model_reference + elif construct == XMLConstructables.GLOBAL_REFERENCE: + constructor = decoder_.construct_global_reference elif construct == XMLConstructables.ADMINISTRATIVE_INFORMATION: constructor = decoder_.construct_administrative_information elif construct == XMLConstructables.QUALIFIER: constructor = decoder_.construct_qualifier - elif construct == XMLConstructables.IDENTIFIER: - constructor = decoder_.construct_identifier elif construct == XMLConstructables.SECURITY: constructor = decoder_.construct_security elif construct == XMLConstructables.OPERATION_VARIABLE: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 294cebb..a86e802 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -114,9 +114,7 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: if isinstance(obj, model.Identifiable): if obj.administration: elm.append(administrative_information_to_xml(obj.administration)) - elm.append(_generate_element(name=NS_AAS + "id", - text=obj.id.id, - attributes={"idType": _generic.IDENTIFIER_TYPES[obj.id.id_type]})) + elm.append(_generate_element(name=NS_AAS + "id", text=obj.id)) if isinstance(obj, model.HasKind): if obj.kind is model.ModelingKind.TEMPLATE: elm.append(_generate_element(name=NS_AAS + "kind", text="Template")) @@ -221,14 +219,15 @@ def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etr :param tag: Namespace+Tag of the returned element. Default is "aas:reference" :return: Serialized ElementTree """ - et_reference = _generate_element(tag) + et_reference = _generate_element(tag, attributes={"type": _generic.REFERENCE_TYPES[obj.__class__]}) et_keys = _generate_element(name=NS_AAS + "keys") for aas_key in obj.key: et_keys.append(_generate_element(name=NS_AAS + "key", text=aas_key.value, - attributes={"idType": _generic.KEY_TYPES[aas_key.id_type], - "type": _generic.KEY_ELEMENTS[aas_key.type]})) + attributes={"type": _generic.KEY_TYPES[aas_key.type]})) et_reference.append(et_keys) + if obj.referred_semantic_id is not None: + et_reference.append(reference_to_xml(obj.referred_semantic_id, NS_AAS + "referredSemanticId")) return et_reference @@ -367,11 +366,11 @@ def concept_description_to_xml(obj: model.ConceptDescription, et_data_spec_content.append(_iec61360_concept_description_to_xml(obj)) et_embedded_data_specification.append(et_data_spec_content) et_concept_description.append(et_embedded_data_specification) - et_embedded_data_specification.append(reference_to_xml(model.Reference(tuple([model.Key( - model.KeyElements.GLOBAL_REFERENCE, - "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0", - model.KeyType.IRI - )])), NS_AAS+"dataSpecification")) + et_embedded_data_specification.append(reference_to_xml(model.GlobalReference( + (model.Key( + model.KeyTypes.GLOBAL_REFERENCE, + "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0" + ),)), NS_AAS+"dataSpecification")) if obj.is_case_of: for reference in obj.is_case_of: et_concept_description.append(reference_to_xml(reference, NS_AAS+"isCaseOf")) @@ -393,39 +392,6 @@ def _iec61360_concept_description_to_xml(obj: model.concept.IEC61360ConceptDescr :return: serialized ElementTree object """ - def _iec_lang_string_set_to_xml(lss: model.LangStringSet, lss_tag: str) -> etree.Element: - """ - serialization of objects of class LangStringSet to XML - - :param lss: object of class LangStringSet - :param lss_tag: lss_tag name of the returned XML element (incl. namespace) - :return: serialized ElementTree object - """ - et_lss = _generate_element(name=lss_tag) - for language in lss: - et_lss.append(_generate_element(name=NS_IEC + "langString", - text=lss[language], - attributes={"lang": language})) - return et_lss - - def _iec_reference_to_xml(ref: model.Reference, ref_tag: str = NS_AAS + "reference") -> etree.Element: - """ - serialization of objects of class Reference to XML - - :param ref: object of class Reference - :param ref_tag: ref_tag of the returned element - :return: serialized ElementTree - """ - et_reference = _generate_element(ref_tag) - et_keys = _generate_element(name=NS_IEC + "keys") - for aas_key in ref.key: - et_keys.append(_generate_element(name=NS_IEC + "key", - text=aas_key.value, - attributes={"idType": _generic.KEY_TYPES[aas_key.id_type], - "type": _generic.KEY_ELEMENTS[aas_key.type]})) - et_reference.append(et_keys) - return et_reference - def _iec_value_reference_pair_to_xml(vrp: model.ValueReferencePair, vrp_tag: str = NS_IEC + "valueReferencePair") -> etree.Element: """ @@ -436,7 +402,7 @@ def _iec_value_reference_pair_to_xml(vrp: model.ValueReferencePair, :return: serialized ElementTree object """ et_vrp = _generate_element(vrp_tag) - et_vrp.append(_iec_reference_to_xml(vrp.value_id, NS_IEC + "valueId")) + et_vrp.append(reference_to_xml(vrp.value_id, NS_IEC + "valueId")) et_vrp.append(_value_to_xml(vrp.value, vrp.value_type, tag=NS_IEC+"value")) return et_vrp @@ -455,13 +421,13 @@ def _iec_value_list_to_xml(vl: model.ValueList, return et_value_list et_iec = _generate_element(tag) - et_iec.append(_iec_lang_string_set_to_xml(obj.preferred_name, NS_IEC + "preferredName")) + et_iec.append(lang_string_set_to_xml(obj.preferred_name, NS_IEC + "preferredName")) if obj.short_name: - et_iec.append(_iec_lang_string_set_to_xml(obj.short_name, NS_IEC + "shortName")) + et_iec.append(lang_string_set_to_xml(obj.short_name, NS_IEC + "shortName")) if obj.unit: et_iec.append(_generate_element(NS_IEC+"unit", text=obj.unit)) if obj.unit_id: - et_iec.append(_iec_reference_to_xml(obj.unit_id, NS_IEC+"unitId")) + et_iec.append(reference_to_xml(obj.unit_id, NS_IEC+"unitId")) if obj.source_of_definition: et_iec.append(_generate_element(NS_IEC+"sourceOfDefinition", text=obj.source_of_definition)) if obj.symbol: @@ -469,7 +435,7 @@ def _iec_value_list_to_xml(vl: model.ValueList, if obj.data_type: et_iec.append(_generate_element(NS_IEC+"dataType", text=_generic.IEC61360_DATA_TYPES[obj.data_type])) if obj.definition: - et_iec.append(_iec_lang_string_set_to_xml(obj.definition, NS_IEC + "definition")) + et_iec.append(lang_string_set_to_xml(obj.definition, NS_IEC + "definition")) if obj.value_format: et_iec.append(_generate_element(NS_IEC+"valueFormat", text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) if obj.value_list: @@ -477,7 +443,7 @@ def _iec_value_list_to_xml(vl: model.ValueList, if obj.value: et_iec.append(_generate_element(NS_IEC+"value", text=model.datatypes.xsd_repr(obj.value))) if obj.value_id: - et_iec.append(_iec_reference_to_xml(obj.value_id, NS_IEC+"valueId")) + et_iec.append(reference_to_xml(obj.value_id, NS_IEC+"valueId")) if obj.level_types: for level_type in obj.level_types: et_iec.append(_generate_element(NS_IEC+"levelType", text=_generic.IEC61360_LEVEL_TYPES[level_type])) From 00015511f1b94ad3fa4e692f15356fef40b9f504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Jul 2022 19:16:23 +0200 Subject: [PATCH 048/474] rename IdentifierKeyValuePair to SpecificAssetId rename SpecificAssetId/key to SpecificAssetId/name change type of SpecificAssetId/externalSubjectId from Reference to GlobalReference --- basyx/aas/adapter/json/aasJSONSchema.json | 10 ++++----- .../aas/adapter/json/json_deserialization.py | 19 +++++++++------- basyx/aas/adapter/json/json_serialization.py | 12 +++++----- basyx/aas/adapter/xml/AAS.xsd | 8 +++---- basyx/aas/adapter/xml/xml_deserialization.py | 22 ++++++++++--------- basyx/aas/adapter/xml/xml_serialization.py | 12 +++++----- 6 files changed, 44 insertions(+), 39 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 6781a7c..d622aa4 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -553,7 +553,7 @@ "externalAssetIds":{ "type": "array", "items": { - "$ref": "#/definitions/IdentifierKeyValuePair" + "$ref": "#/definitions/SpecificAssetId" } }, "thumbnail":{ @@ -564,10 +564,10 @@ } ] }, - "IdentifierKeyValuePair":{ + "SpecificAssetId":{ "allOf": [{ "$ref": "#/definitions/HasSemantics"}, { "properties": { - "key": { + "name": { "dataType":"string" }, "value": { @@ -578,7 +578,7 @@ "$ref": "#/definitions/Reference" } }, - "required": [ "key","value","subjectId" ] + "required": [ "name","value","subjectId" ] } ] }, @@ -714,7 +714,7 @@ "$ref": "#/definitions/Reference" }, "specificAssetIds":{ - "$ref": "#/definitions/IdentifierKeyValuePair" + "$ref": "#/definitions/SpecificAssetId" } }, "required": [ "entityType" ] diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index fe1d95c..61183c1 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -167,7 +167,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object: AAS_CLASS_PARSERS: Dict[str, Callable[[Dict[str, object]], object]] = { 'AssetAdministrationShell': cls._construct_asset_administration_shell, 'AssetInformation': cls._construct_asset_information, - 'IdentifierKeyValuePair': cls._construct_identifier_key_value_pair, + 'SpecificAssetId': cls._construct_specific_asset_id, 'ConceptDescription': cls._construct_concept_description, 'Qualifier': cls._construct_qualifier, 'Extension': cls._construct_extension, @@ -281,11 +281,14 @@ def _construct_key(cls, dct: Dict[str, object], object_class=model.Key) -> model value=_get_ts(dct, 'value', str)) @classmethod - def _construct_identifier_key_value_pair(cls, dct: Dict[str, object], object_class=model.IdentifierKeyValuePair) \ - -> model.IdentifierKeyValuePair: - return object_class(key=_get_ts(dct, 'key', str), + def _construct_specific_asset_id(cls, dct: Dict[str, object], object_class=model.SpecificAssetId) \ + -> model.SpecificAssetId: + # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable + return object_class(name=_get_ts(dct, 'name', str), value=_get_ts(dct, 'value', str), - external_subject_id=cls._construct_reference(_get_ts(dct, 'subjectId', dict))) + external_subject_id=cls._construct_global_reference(_get_ts(dct, 'subjectId', dict)), + semantic_id=cls._construct_reference(_get_ts(dct, 'semanticId', dict)) + if 'semanticId' in dct else None) @classmethod def _construct_reference(cls, dct: Dict[str, object]) -> model.Reference: @@ -398,8 +401,8 @@ def _construct_asset_information(cls, dct: Dict[str, object], object_class=model ret.global_asset_id = cls._construct_reference(_get_ts(dct, 'globalAssetId', dict)) if 'externalAssetIds' in dct: for desc_data in _get_ts(dct, "externalAssetIds", list): - ret.specific_asset_id.add(cls._construct_identifier_key_value_pair(desc_data, - model.IdentifierKeyValuePair)) + ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, + model.SpecificAssetId)) if 'thumbnail' in dct: ret.default_thumbnail = cls._construct_resource(_get_ts(dct, 'thumbnail', dict)) return ret @@ -481,7 +484,7 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> global_asset_id = cls._construct_reference(_get_ts(dct, 'globalAssetId', dict)) specific_asset_id = None if 'externalAssetId' in dct: - specific_asset_id = cls._construct_identifier_key_value_pair(_get_ts(dct, 'externalAssetId', dict)) + specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'externalAssetId', dict)) ret = object_class(id_short=_get_ts(dct, "idShort", str), entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 7624430..6f9a924 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -75,8 +75,8 @@ def default(self, obj: object) -> object: return self._value_reference_pair_to_json(obj) if isinstance(obj, model.AssetInformation): return self._asset_information_to_json(obj) - if isinstance(obj, model.IdentifierKeyValuePair): - return self._identifier_key_value_pair_to_json(obj) + if isinstance(obj, model.SpecificAssetId): + return self._specific_asset_id_to_json(obj) if isinstance(obj, model.Submodel): return self._submodel_to_json(obj) if isinstance(obj, model.Operation): @@ -287,15 +287,15 @@ def _value_list_to_json(cls, obj: model.ValueList) -> Dict[str, object]: # ############################################################ @classmethod - def _identifier_key_value_pair_to_json(cls, obj: model.IdentifierKeyValuePair) -> Dict[str, object]: + def _specific_asset_id_to_json(cls, obj: model.SpecificAssetId) -> Dict[str, object]: """ - serialization of an object from class IdentifierKeyValuePair to json + serialization of an object from class SpecificAssetId to json - :param obj: object of class IdentifierKeyValuePair + :param obj: object of class SpecificAssetId :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data['key'] = obj.key + data['name'] = obj.name data['value'] = obj.value data['subjectId'] = obj.external_subject_id return data diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index b3805c4..2f548bd 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -49,7 +49,7 @@ - + @@ -132,7 +132,7 @@ - + @@ -181,11 +181,11 @@ - + - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index e7109ec..5bb567d 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -753,7 +753,7 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ global_asset_id = _failsafe_construct(element.find(NS_AAS + "globalAssetId"), cls.construct_reference, cls.failsafe) specific_asset_id = _failsafe_construct(element.find(NS_AAS + "specificAssetId"), - cls.construct_identifier_key_value_pair, cls.failsafe) + cls.construct_specific_asset_id, cls.failsafe) entity = object_class( id_short=_child_text_mandatory(element, NS_AAS + "idShort"), entity_type=_child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), @@ -934,13 +934,15 @@ def construct_asset_administration_shell(cls, element: etree.Element, object_cla return aas @classmethod - def construct_identifier_key_value_pair(cls, element: etree.Element, object_class=model.IdentifierKeyValuePair, - **_kwargs: Any) -> model.IdentifierKeyValuePair: + def construct_specific_asset_id(cls, element: etree.Element, object_class=model.SpecificAssetId, + **_kwargs: Any) -> model.SpecificAssetId: + # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable return object_class( external_subject_id=_child_construct_mandatory(element, NS_AAS + "externalSubjectId", - cls.construct_reference), - key=_get_text_or_none(element.find(NS_AAS + "key")), - value=_get_text_or_none(element.find(NS_AAS + "value")) + cls.construct_global_reference), + name=_get_text_or_none(element.find(NS_AAS + "name")), + value=_get_text_or_none(element.find(NS_AAS + "value")), + semantic_id=_failsafe_construct(element.find(NS_AAS + "semanticId"), cls.construct_reference, cls.failsafe) ) @classmethod @@ -956,7 +958,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. specific_assset_ids = element.find(NS_AAS + "specificAssetIds") if specific_assset_ids is not None: for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", - cls.construct_identifier_key_value_pair, cls.failsafe): + cls.construct_specific_asset_id, cls.failsafe): asset_information.specific_asset_id.add(id) thumbnail = _failsafe_construct(element.find(NS_AAS + "defaultThumbNail"), cls.construct_resource, cls.failsafe) @@ -1202,7 +1204,7 @@ class XMLConstructables(enum.Enum): SUBMODEL_ELEMENT_COLLECTION = enum.auto() ASSET_ADMINISTRATION_SHELL = enum.auto() ASSET_INFORMATION = enum.auto() - IDENTIFIER_KEY_VALUE_PAIR = enum.auto() + SPECIFIC_ASSET_ID = enum.auto() SUBMODEL = enum.auto() VALUE_REFERENCE_PAIR = enum.auto() IEC61360_CONCEPT_DESCRIPTION = enum.auto() @@ -1284,8 +1286,8 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_asset_administration_shell elif construct == XMLConstructables.ASSET_INFORMATION: constructor = decoder_.construct_asset_information - elif construct == XMLConstructables.IDENTIFIER_KEY_VALUE_PAIR: - constructor = decoder_.construct_identifier_key_value_pair + elif construct == XMLConstructables.SPECIFIC_ASSET_ID: + constructor = decoder_.construct_specific_asset_id elif construct == XMLConstructables.SUBMODEL: constructor = decoder_.construct_submodel elif construct == XMLConstructables.VALUE_REFERENCE_PAIR: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index a86e802..1ecb1f5 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -310,18 +310,18 @@ def value_list_to_xml(obj: model.ValueList, # ############################################################## -def identifier_key_value_pair_to_xml(obj: model.IdentifierKeyValuePair, tag: str = NS_AAS+"identifierKeyValuePair") \ +def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "specifidAssetId") \ -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.IdentifierKeyValuePair` to XML + Serialization of objects of class :class:`~aas.model.base.SpecificAssetId` to XML - :param obj: Object of class :class:`~aas.model.base.IdentifierKeyValuePair` + :param obj: Object of class :class:`~aas.model.base.SpecificAssetId` :param tag: Namespace+Tag of the ElementTree object. Default is "aas:identifierKeyValuePair" :return: Serialized ElementTree object """ et_asset_information = abstract_classes_to_xml(tag, obj) et_asset_information.append(reference_to_xml(obj.external_subject_id, NS_AAS + "externalSubjectId")) - et_asset_information.append(_generate_element(name=NS_AAS + "key", text=obj.key)) + et_asset_information.append(_generate_element(name=NS_AAS + "name", text=obj.name)) et_asset_information.append(_generate_element(name=NS_AAS + "value", text=obj.value)) return et_asset_information @@ -344,7 +344,7 @@ def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"ass et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") if obj.specific_asset_id: for specific_asset_id in obj.specific_asset_id: - et_specific_asset_id.append(identifier_key_value_pair_to_xml(specific_asset_id, NS_AAS+"specificAssetId")) + et_specific_asset_id.append(specific_asset_id_to_xml(specific_asset_id, NS_AAS + "specificAssetId")) et_asset_information.append(et_specific_asset_id) return et_asset_information @@ -786,7 +786,7 @@ def entity_to_xml(obj: model.Entity, if obj.global_asset_id: et_entity.append(reference_to_xml(obj.global_asset_id, NS_AAS + "globalAssetId")) if obj.specific_asset_id: - et_entity.append(identifier_key_value_pair_to_xml(obj.specific_asset_id, NS_AAS+"specificAssetId")) + et_entity.append(specific_asset_id_to_xml(obj.specific_asset_id, NS_AAS + "specificAssetId")) et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type])) et_statements = _generate_element(NS_AAS + "statements") for statement in obj.statement: From 6387ca981e8147b56562dba40bef1fbaff8b06ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 21 Jul 2022 00:10:56 +0200 Subject: [PATCH 049/474] change type of Extension/refersTo from Reference to ModelReference also make it an Iterable --- basyx/aas/adapter/json/aasJSONSchema.json | 7 +++++-- basyx/aas/adapter/json/json_deserialization.py | 3 ++- basyx/aas/adapter/json/json_serialization.py | 2 +- basyx/aas/adapter/xml/AAS.xsd | 2 +- basyx/aas/adapter/xml/xml_deserialization.py | 5 ++--- basyx/aas/adapter/xml/xml_serialization.py | 4 ++-- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index d622aa4..3eeafce 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -167,9 +167,12 @@ "value":{ "type": "string" }, - "refersTo":{ + "refersTo": { + "type": "array", + "items": { "$ref": "#/definitions/Reference" - } + } + } }, "required": [ "name" ] } diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 61183c1..bf9b998 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -517,7 +517,8 @@ def _construct_extension(cls, dct: Dict[str, object], object_class=model.Extensi if 'value' in dct: ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) if 'refersTo' in dct: - ret.refers_to = cls._construct_reference(_get_ts(dct, 'refersTo', dict)) + ret.refers_to = [cls._construct_model_reference(refers_to, model.Referable) # type: ignore + for refers_to in _get_ts(dct, 'refersTo', list)] return ret @classmethod diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 6f9a924..8a5f436 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -252,7 +252,7 @@ def _extension_to_json(cls, obj: model.Extension) -> Dict[str, object]: if obj.value: data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None if obj.refers_to: - data['refersTo'] = obj.refers_to + data['refersTo'] = list(obj.refers_to) if obj.value_type: data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] data['name'] = obj.name diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 2f548bd..93f3eae 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -157,7 +157,7 @@ - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 5bb567d..16cece8 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -628,9 +628,8 @@ def construct_extension(cls, element: etree.Element, object_class=model.Extensio value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: extension.value = model.datatypes.from_xsd(value, extension.value_type) - refers_to = _failsafe_construct(element.find(NS_AAS + "RefersTo"), cls.construct_reference, cls.failsafe) - if refers_to is not None: - extension.refers_to = refers_to + extension.refers_to = _failsafe_construct_multiple(element.findall(NS_AAS + "refersTo"), + cls._construct_referable_reference, cls.failsafe) cls._amend_abstract_attributes(extension, element) return extension diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 1ecb1f5..5e69891 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -264,8 +264,8 @@ def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etr text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) if obj.value: et_extension.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore # (value_type could be None) - if obj.refers_to: - et_extension.append(reference_to_xml(obj.refers_to, NS_AAS+"refersTo")) + for refers_to in obj.refers_to: + et_extension.append(reference_to_xml(refers_to, NS_AAS+"refersTo")) return et_extension From 94615606224121aae3a9501d3863816ed952d08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 21 Jul 2022 00:17:33 +0200 Subject: [PATCH 050/474] restructure KeyTypes make CONCEPT_DICTIONARY a private member since it has been removed from the spec --- basyx/aas/adapter/_generic.py | 1 - basyx/aas/adapter/xml/AAS.xsd | 1 - 2 files changed, 2 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 6b0391e..7aa5f93 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -32,7 +32,6 @@ model.KeyTypes.BASIC_EVENT_ELEMENT: 'BasicEventElement', model.KeyTypes.BLOB: 'Blob', model.KeyTypes.CAPABILITY: 'Capability', - model.KeyTypes.CONCEPT_DICTIONARY: 'ConceptDictionary', model.KeyTypes.DATA_ELEMENT: 'DataElement', model.KeyTypes.ENTITY: 'Entity', model.KeyTypes.EVENT_ELEMENT: 'EventElement', diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 93f3eae..b95d23d 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -212,7 +212,6 @@ - From cb90dfa7a8638b8635a6610c4e36b9ee2852ee1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 21 Jul 2022 00:48:06 +0200 Subject: [PATCH 051/474] change type of RelationshipElement/{first,second} from ModelReference to Reference --- basyx/aas/adapter/json/json_deserialization.py | 10 ++++------ basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index bf9b998..08cebc0 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -578,10 +578,8 @@ def _construct_relationship_element( # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 ret = object_class(id_short=_get_ts(dct, "idShort", str), - first=cls._construct_model_reference(_get_ts(dct, 'first', dict), - model.Referable), # type: ignore - second=cls._construct_model_reference(_get_ts(dct, 'second', dict), - model.Referable), # type: ignore + first=cls._construct_reference(_get_ts(dct, 'first', dict)), + second=cls._construct_reference(_get_ts(dct, 'second', dict)), kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) return ret @@ -594,8 +592,8 @@ def _construct_annotated_relationship_element( # see https://github.com/python/mypy/issues/5374 ret = object_class( id_short=_get_ts(dct, "idShort", str), - first=cls._construct_model_reference(_get_ts(dct, 'first', dict), model.Referable), # type: ignore - second=cls._construct_model_reference(_get_ts(dct, 'second', dict), model.Referable), # type: ignore + first=cls._construct_reference(_get_ts(dct, 'first', dict)), + second=cls._construct_reference(_get_ts(dct, 'second', dict)), kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'annotation' in dct: diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 16cece8..a1b8133 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -478,8 +478,8 @@ def _construct_relationship_element_internal(cls, element: etree.Element, object """ relationship_element = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), - _child_construct_mandatory(element, NS_AAS + "first", cls._construct_referable_reference), - _child_construct_mandatory(element, NS_AAS + "second", cls._construct_referable_reference), + _child_construct_mandatory(element, NS_AAS + "first", cls.construct_reference), + _child_construct_mandatory(element, NS_AAS + "second", cls.construct_reference), kind=_get_modeling_kind(element) ) cls._amend_abstract_attributes(relationship_element, element) From 8878730a38ae401d780ca98daf877bed4a1850ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 13 Aug 2022 17:10:02 +0200 Subject: [PATCH 052/474] remove ACCESS_PERMISSION_RULE from KeyTypes reflect this change in the json/xml schemata add missing comments to protected KeyTypes enum members --- basyx/aas/adapter/json/aasJSONSchema.json | 1 - basyx/aas/adapter/xml/AAS.xsd | 1 - 2 files changed, 2 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 3eeafce..68c9c5a 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -269,7 +269,6 @@ "AssetAdministrationShell", "ConceptDescription", "Submodel", - "AccessPermissionRule", "AnnotatedRelationshipElement", "BasicEventElement", "Blob", diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index b95d23d..b4f295d 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -205,7 +205,6 @@ - From a6e8cfaf192b65f28c00dbe73a61c02d26691379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 14 Aug 2022 20:06:25 +0200 Subject: [PATCH 053/474] model: rename DataTypeDef to DataTypeDefXsd In V3.0RC02 DataTypeDef has been split into DataTypeDefRdf and DataTypeDefXsd. Since DataTypeDefRdf only consists of rdf::langString and currently isn't used anywhere in the DotAAS specification, just rename DataTypeDef to DataTypeDefXsd. --- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- basyx/aas/adapter/xml/xml_serialization.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index a1b8133..b2bec7b 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -992,7 +992,7 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, return submodel @classmethod - def construct_value_reference_pair(cls, element: etree.Element, value_format: Optional[model.DataTypeDef] = None, + def construct_value_reference_pair(cls, element: etree.Element, value_format: Optional[model.DataTypeDefXsd] = None, object_class=model.ValueReferencePair, **_kwargs: Any) \ -> model.ValueReferencePair: if value_format is None: @@ -1004,7 +1004,7 @@ def construct_value_reference_pair(cls, element: etree.Element, value_format: Op ) @classmethod - def construct_value_list(cls, element: etree.Element, value_format: Optional[model.DataTypeDef] = None, + def construct_value_list(cls, element: etree.Element, value_format: Optional[model.DataTypeDefXsd] = None, **_kwargs: Any) -> model.ValueList: """ This function doesn't support the object_class parameter, because ValueList is just a generic type alias. diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 5e69891..09666e7 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -141,13 +141,13 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: def _value_to_xml(value: model.ValueDataType, - value_type: model.DataTypeDef, + value_type: model.DataTypeDefXsd, tag: str = NS_AAS+"value") -> etree.Element: """ Serialization of objects of class ValueDataType to XML :param value: model.ValueDataType object - :param value_type: Corresponding model.DataTypeDef + :param value_type: Corresponding model.DataTypeDefXsd :param tag: tag of the serialized ValueDataType object :return: Serialized ElementTree.Element object """ From 59901ca800f4aeccb0fd887a8ee110f224abab69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 17 Oct 2022 22:55:10 +0200 Subject: [PATCH 054/474] model: update SubmodelElementCollection In V3.0RC02 the attributes `allowDuplicates` and `ordered` were removed from `SubmodelElementCollection`. Additionally the `semanticId` of contained elements is no longer unique. Because of that, the extra classes `SubmodelElementCollectionOrdered`, `SubmodelElementCollectionUnordered`, `SubmodelElementCollectionOrderedUniqueSemanticId` and `SubmodelElementCollectionUnorderedUniqueSemanticId` aren't needed anymore. Also ignore two tests which will be modified and re-enabled later. --- basyx/aas/adapter/json/aasJSONSchema.json | 6 ------ .../aas/adapter/json/json_deserialization.py | 17 +++-------------- basyx/aas/adapter/json/json_serialization.py | 9 +++------ basyx/aas/adapter/xml/AAS.xsd | 2 -- basyx/aas/adapter/xml/xml_deserialization.py | 19 +++++-------------- basyx/aas/adapter/xml/xml_serialization.py | 3 --- 6 files changed, 11 insertions(+), 45 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 68c9c5a..6e3288a 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -901,12 +901,6 @@ { "$ref": "#/definitions/SubmodelElementCollection" } ] } - }, - "allowDuplicates": { - "type": "boolean" - }, - "ordered": { - "type": "boolean" } } } diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 08cebc0..c195194 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -603,21 +603,10 @@ def _construct_annotated_relationship_element( return ret @classmethod - def _construct_submodel_element_collection( - cls, - dct: Dict[str, object])\ + def _construct_submodel_element_collection(cls, dct: Dict[str, object], + object_class=model.SubmodelElementCollection)\ -> model.SubmodelElementCollection: - ret: model.SubmodelElementCollection - ordered = False - allow_duplicates = False - if 'ordered' in dct: - ordered = _get_ts(dct, "ordered", bool) - if 'allowDuplicates' in dct: - allow_duplicates = _get_ts(dct, "allowDuplicates", bool) - ret = model.SubmodelElementCollection.create(id_short=_get_ts(dct, "idShort", str), - kind=cls._get_kind(dct), - ordered=ordered, - allow_duplicates=allow_duplicates) + ret = object_class(id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'value' in dct: for element in _get_ts(dct, "value", list): diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 8a5f436..331f995 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -540,17 +540,14 @@ def _reference_element_to_json(cls, obj: model.ReferenceElement) -> Dict[str, ob @classmethod def _submodel_element_collection_to_json(cls, obj: model.SubmodelElementCollection) -> Dict[str, object]: """ - serialization of an object from class SubmodelElementCollectionOrdered and SubmodelElementCollectionUnordered to - json + serialization of an object from class SubmodelElementCollection to json - :param obj: object of class SubmodelElementCollectionOrdered and SubmodelElementCollectionUnordered + :param obj: object of class SubmodelElementCollection :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - if not cls.stripped and obj.value: + if not cls.stripped and len(obj.value) > 0: data['value'] = list(obj.value) - data['ordered'] = obj.ordered - data['allowDuplicates'] = obj.allow_duplicates return data @classmethod diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index b4f295d..9050ba3 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -400,8 +400,6 @@ - - diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index b2bec7b..5ebba10 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -646,11 +646,7 @@ def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> mo This function doesn't support the object_class parameter. Overwrite each individual SubmodelElement/DataElement constructor function instead. """ - # unlike in construct_data_elements, we have to declare a submodel_elements dict without namespace here first - # because mypy doesn't automatically infer Callable[..., model.SubmodelElement] for the functions, because - # construct_submodel_element_collection doesn't have the object_class parameter, but object_class_ordered and - # object_class_unordered - submodel_elements: Dict[str, Callable[..., model.SubmodelElement]] = { + submodel_elements: Dict[str, Callable[..., model.SubmodelElement]] = {NS_AAS + k: v for k, v in { "annotatedRelationshipElement": cls.construct_annotated_relationship_element, "basicEventElement": cls.construct_basic_event_element, "capability": cls.construct_capability, @@ -658,8 +654,7 @@ def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> mo "operation": cls.construct_operation, "relationshipElement": cls.construct_relationship_element, "submodelElementCollection": cls.construct_submodel_element_collection - } - submodel_elements = {NS_AAS + k: v for k, v in submodel_elements.items()} + }.items()} if element.tag not in submodel_elements: return cls.construct_data_element(element, abstract_class_name="SubmodelElement", **kwargs) return submodel_elements[element.tag](element, **kwargs) @@ -885,15 +880,11 @@ def construct_relationship_element(cls, element: etree.Element, object_class=mod return cls._construct_relationship_element_internal(element, object_class=object_class, **_kwargs) @classmethod - def construct_submodel_element_collection(cls, element: etree.Element, + def construct_submodel_element_collection(cls, element: etree.Element, object_class=model.SubmodelElementCollection, **_kwargs: Any) -> model.SubmodelElementCollection: - ordered = _str_to_bool(_child_text_mandatory(element, NS_AAS + "ordered")) - allow_duplicates = _str_to_bool(_child_text_mandatory(element, NS_AAS + "allowDuplicates")) - collection = model.SubmodelElementCollection.create( + collection = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), - kind=_get_modeling_kind(element), - allow_duplicates=allow_duplicates, - ordered=ordered + kind=_get_modeling_kind(element) ) if not cls.stripped: value = _get_child_mandatory(element, NS_AAS + "value") diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 09666e7..2344147 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -674,9 +674,6 @@ def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, """ et_submodel_element_collection = abstract_classes_to_xml(tag, obj) # todo: remove wrapping submodelElement-tag, in accordance to future schema - et_submodel_element_collection.append(_generate_element(NS_AAS + "allowDuplicates", - text=boolean_to_xml(obj.allow_duplicates))) - et_submodel_element_collection.append(_generate_element(NS_AAS + "ordered", text=boolean_to_xml(obj.ordered))) et_value = _generate_element(NS_AAS + "value") if obj.value: for submodel_element in obj.value: From 1588cffdb63b8599738c0c332f4d6836c9c7f4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 21 Oct 2022 01:11:04 +0200 Subject: [PATCH 055/474] add SubmodelElementList In turn re-enable the unittests that were disabled in 1d6941f05ada1bacfe588b8c3a4c79a98e8cb91f. --- basyx/aas/adapter/json/aasJSONSchema.json | 199 +++++++++--------- .../aas/adapter/json/json_deserialization.py | 27 +++ basyx/aas/adapter/json/json_serialization.py | 21 ++ basyx/aas/adapter/xml/AAS.xsd | 110 +++++++++- basyx/aas/adapter/xml/xml_deserialization.py | 33 ++- basyx/aas/adapter/xml/xml_serialization.py | 22 ++ 6 files changed, 306 insertions(+), 106 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 6e3288a..c8bf46b 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -117,53 +117,8 @@ "type": "string" }, "valueType":{ - "type": "string", - "enum": [ - "anyUri", - "base64Binary", - "boolean", - "date", - "dateTime", - "dateTimeStamp", - "decimal", - "integer", - "long", - "int", - "short", - "byte", - "nonNegativeInteger", - "positiveInteger", - "unsignedLong", - "unsignedInt", - "unsignedShort", - "unsignedByte", - "nonPositiveInteger", - "negativeInteger", - "double", - "duration", - "dayTimeDuration", - "yearMonthDuration", - "float", - "gDay", - "gMonth", - "gMonthDay", - "gYear", - "gYearMonth", - "hexBinary", - "NOTATION", - "QName", - "string", - "normalizedString", - "token", - "language", - "Name", - "NCName", - "ENTITY", - "ID", - "IDREF", - "NMTOKEN", - "time" - ]}, + "$ref": "#/definitions/DataTypeDefXsd" + }, "value":{ "type": "string" }, @@ -285,6 +240,7 @@ "RelationshipElement", "SubmodelElement", "SubmodelElementCollection", + "SubmodelElementList", "GlobalReference", "FragmentReference" ] @@ -312,6 +268,7 @@ "RelationshipElement", "SubmodelElement", "SubmodelElementCollection", + "SubmodelElementList", "GlobalReference", "FragmentReference", "Qualifier" @@ -494,53 +451,8 @@ "$ref": "#/definitions/Reference" }, "valueType": { - "type": "string", - "enum": [ - "anyUri", - "base64Binary", - "boolean", - "date", - "dateTime", - "dateTimeStamp", - "decimal", - "integer", - "long", - "int", - "short", - "byte", - "nonNegativeInteger", - "positiveInteger", - "unsignedLong", - "unsignedInt", - "unsignedShort", - "unsignedByte", - "nonPositiveInteger", - "negativeInteger", - "double", - "duration", - "dayTimeDuration", - "yearMonthDuration", - "float", - "gDay", - "gMonth", - "gMonthDay", - "gYear", - "gYearMonth", - "hexBinary", - "NOTATION", - "QName", - "string", - "normalizedString", - "token", - "language", - "Name", - "NCName", - "ENTITY", - "ID", - "IDREF", - "NMTOKEN", - "time" - ]} + "$ref": "#/definitions/DataTypeDefXsd" + } } }, "AssetInformation": { @@ -655,7 +567,8 @@ { "$ref": "#/definitions/Range" }, { "$ref": "#/definitions/ReferenceElement" }, { "$ref": "#/definitions/RelationshipElement" }, - { "$ref": "#/definitions/SubmodelElementCollection" } + { "$ref": "#/definitions/SubmodelElementCollection" }, + { "$ref": "#/definitions/SubmodelElementList" } ] } }, @@ -898,7 +811,8 @@ { "$ref": "#/definitions/Range" }, { "$ref": "#/definitions/ReferenceElement" }, { "$ref": "#/definitions/RelationshipElement" }, - { "$ref": "#/definitions/SubmodelElementCollection" } + { "$ref": "#/definitions/SubmodelElementCollection" }, + { "$ref": "#/definitions/SubmodelElementList" } ] } } @@ -906,6 +820,99 @@ } ] }, + "SubmodelElementList": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "orderRelevant": { + "type": "boolean" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement" + }, + "minItems": 1 + }, + "semanticIdListElement": { + "$ref": "#/definitions/Reference" + }, + "typeValueListElement": { + "$ref": "#/definitions/AasSubmodelElements" + }, + "valueTypeListElement": { + "$ref": "#/definitions/DataTypeDefXsd" + } + }, + "required": [ + "typeValueListElement" + ] + } + ] + }, + "DataTypeDefXsd": { + "type": "string", + "enum": [ + "xs:anyURI", + "xs:base64Binary", + "xs:boolean", + "xs:byte", + "xs:date", + "xs:dateTime", + "xs:dateTimeStamp", + "xs:dayTimeDuration", + "xs:decimal", + "xs:double", + "xs:duration", + "xs:float", + "xs:gDay", + "xs:gMonth", + "xs:gMonthDay", + "xs:gYear", + "xs:gYearMonth", + "xs:hexBinary", + "xs:int", + "xs:integer", + "xs:long", + "xs:negativeInteger", + "xs:nonNegativeInteger", + "xs:nonPositiveInteger", + "xs:positiveInteger", + "xs:short", + "xs:string", + "xs:time", + "xs:unsignedByte", + "xs:unsignedInt", + "xs:unsignedLong", + "xs:unsignedShort", + "xs:yearMonthDuration" + ] + }, + "AasSubmodelElements": { + "type": "string", + "enum": [ + "AnnotatedRelationshipElement", + "BasicEventElement", + "Blob", + "Capability", + "DataElement", + "Entity", + "EventElement", + "File", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "SubmodelElement", + "SubmodelElementCollection", + "SubmodelElementList" + ] + }, "RelationshipElement": { "allOf": [ { "$ref": "#/definitions/SubmodelElement" }, diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index c195194..96aa225 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -179,6 +179,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object: 'RelationshipElement': cls._construct_relationship_element, 'AnnotatedRelationshipElement': cls._construct_annotated_relationship_element, 'SubmodelElementCollection': cls._construct_submodel_element_collection, + 'SubmodelElementList': cls._construct_submodel_element_list, 'Blob': cls._construct_blob, 'File': cls._construct_file, 'MultiLanguageProperty': cls._construct_multi_language_property, @@ -614,6 +615,32 @@ def _construct_submodel_element_collection(cls, dct: Dict[str, object], ret.value.add(element) return ret + @classmethod + def _construct_submodel_element_list(cls, dct: Dict[str, object], object_class=model.SubmodelElementList)\ + -> model.SubmodelElementList: + type_value_list_element = KEY_TYPES_CLASSES_INVERSE[ + KEY_TYPES_INVERSE[_get_ts(dct, 'typeValueListElement', str)]] + if not issubclass(type_value_list_element, model.SubmodelElement): + raise ValueError("Expected a SubmodelElementList with a typeValueListElement that is a subclass of" + f"{model.SubmodelElement}, got {type_value_list_element}!") + order_relevant = _get_ts(dct, 'orderRelevant', bool) if 'orderRelevant' in dct else True + semantic_id_list_element = cls._construct_reference(_get_ts(dct, 'semanticIdListElement', dict))\ + if 'semanticIdListElement' in dct else None + value_type_list_element = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueTypeListElement', str)]\ + if 'valueTypeListElement' in dct else None + ret = object_class(id_short=_get_ts(dct, 'idShort', str), + type_value_list_element=type_value_list_element, + order_relevant=order_relevant, + semantic_id_list_element=semantic_id_list_element, + value_type_list_element=value_type_list_element, + kind=cls._get_kind(dct)) + cls._amend_abstract_attributes(ret, dct) + if not cls.stripped and 'value' in dct: + for element in _get_ts(dct, 'value', list): + if _expect_type(element, type_value_list_element, str(ret), cls.failsafe): + ret.value.add(element) + return ret + @classmethod def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> model.Blob: ret = object_class(id_short=_get_ts(dct, "idShort", str), diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 331f995..22466f9 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -107,6 +107,8 @@ def default(self, obj: object) -> object: return self._reference_element_to_json(obj) if isinstance(obj, model.SubmodelElementCollection): return self._submodel_element_collection_to_json(obj) + if isinstance(obj, model.SubmodelElementList): + return self._submodel_element_list_to_json(obj) if isinstance(obj, model.AnnotatedRelationshipElement): return self._annotated_relationship_element_to_json(obj) if isinstance(obj, model.RelationshipElement): @@ -550,6 +552,25 @@ def _submodel_element_collection_to_json(cls, obj: model.SubmodelElementCollecti data['value'] = list(obj.value) return data + @classmethod + def _submodel_element_list_to_json(cls, obj: model.SubmodelElementList) -> Dict[str, object]: + """ + serialization of an object from class SubmodelElementList to json + + :param obj: object of class SubmodelElementList + :return: dict with the serialized attributes of this object + """ + data = cls._abstract_classes_to_json(obj) + data['orderRelevant'] = obj.order_relevant + data['typeValueListElement'] = _generic.KEY_TYPES[model.KEY_TYPES_CLASSES[obj.type_value_list_element]] + if obj.semantic_id_list_element is not None: + data['semanticIdListElement'] = obj.semantic_id_list_element + if obj.value_type_list_element is not None: + data['valueTypeListElement'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type_list_element] + if not cls.stripped and len(obj.value) > 0: + data['value'] = list(obj.value) + return data + @classmethod def _relationship_element_to_json(cls, obj: model.RelationshipElement) -> Dict[str, object]: """ diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 9050ba3..73f0681 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -116,11 +116,6 @@ - - - - - @@ -155,7 +150,7 @@ - + @@ -226,6 +221,7 @@ + @@ -286,7 +282,7 @@ - + @@ -297,7 +293,7 @@ - + @@ -311,7 +307,7 @@ - + @@ -384,6 +380,7 @@ + @@ -405,6 +402,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 5ebba10..06e2dcd 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -653,7 +653,8 @@ def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> mo "entity": cls.construct_entity, "operation": cls.construct_operation, "relationshipElement": cls.construct_relationship_element, - "submodelElementCollection": cls.construct_submodel_element_collection + "submodelElementCollection": cls.construct_submodel_element_collection, + "submodelElementList": cls.construct_submodel_element_list }.items()} if element.tag not in submodel_elements: return cls.construct_data_element(element, abstract_class_name="SubmodelElement", **kwargs) @@ -902,6 +903,33 @@ def construct_submodel_element_collection(cls, element: etree.Element, object_cl cls._amend_abstract_attributes(collection, element) return collection + @classmethod + def construct_submodel_element_list(cls, element: etree.Element, object_class=model.SubmodelElementList, + **_kwargs: Any) -> model.SubmodelElementList: + type_value_list_element = KEY_TYPES_CLASSES_INVERSE[ + _child_text_mandatory_mapped(element, NS_AAS + "typeValueListElement", KEY_TYPES_INVERSE)] + if not issubclass(type_value_list_element, model.SubmodelElement): + raise ValueError("Expected a SubmodelElementList with a typeValueListElement that is a subclass of" + f"{model.SubmodelElement}, got {type_value_list_element}!") + order_relevant = element.find(NS_AAS + "orderRelevant") + list_ = object_class( + _child_text_mandatory(element, NS_AAS + "idShort"), + type_value_list_element, + semantic_id_list_element=_failsafe_construct(element.find(NS_AAS + "semanticIdListElement"), + cls.construct_reference, cls.failsafe), + value_type_list_element=_get_text_mapped_or_none(element.find(NS_AAS + "valueTypeListElement"), + model.datatypes.XSD_TYPE_CLASSES), + order_relevant=_str_to_bool(_get_text_mandatory(order_relevant)) + if order_relevant is not None else True, + kind=_get_modeling_kind(element) + ) + if not cls.stripped: + value = element.find(NS_AAS + "value") + if value is not None: + list_.value.extend(_failsafe_construct_multiple(value, cls.construct_submodel_element, cls.failsafe)) + cls._amend_abstract_attributes(list_, element) + return list_ + @classmethod def construct_asset_administration_shell(cls, element: etree.Element, object_class=model.AssetAdministrationShell, **_kwargs: Any) -> model.AssetAdministrationShell: @@ -1192,6 +1220,7 @@ class XMLConstructables(enum.Enum): REFERENCE_ELEMENT = enum.auto() RELATIONSHIP_ELEMENT = enum.auto() SUBMODEL_ELEMENT_COLLECTION = enum.auto() + SUBMODEL_ELEMENT_LIST = enum.auto() ASSET_ADMINISTRATION_SHELL = enum.auto() ASSET_INFORMATION = enum.auto() SPECIFIC_ASSET_ID = enum.auto() @@ -1272,6 +1301,8 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_relationship_element elif construct == XMLConstructables.SUBMODEL_ELEMENT_COLLECTION: constructor = decoder_.construct_submodel_element_collection + elif construct == XMLConstructables.SUBMODEL_ELEMENT_LIST: + constructor = decoder_.construct_submodel_element_list elif construct == XMLConstructables.ASSET_ADMINISTRATION_SHELL: constructor = decoder_.construct_asset_administration_shell elif construct == XMLConstructables.ASSET_INFORMATION: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 2344147..9592582 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -518,6 +518,8 @@ def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: return relationship_element_to_xml(obj) if isinstance(obj, model.SubmodelElementCollection): return submodel_element_collection_to_xml(obj) + if isinstance(obj, model.SubmodelElementList): + return submodel_element_list_to_xml(obj) def submodel_to_xml(obj: model.Submodel, @@ -684,6 +686,26 @@ def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, return et_submodel_element_collection +def submodel_element_list_to_xml(obj: model.SubmodelElementList, + tag: str = NS_AAS+"submodelElementList") -> etree.Element: + et_submodel_element_list = abstract_classes_to_xml(tag, obj) + et_submodel_element_list.append(_generate_element(NS_AAS + "orderRelevant", boolean_to_xml(obj.order_relevant))) + if len(obj.value) > 0: + et_value = _generate_element(NS_AAS + "value") + for se in obj.value: + et_value.append(submodel_element_to_xml(se)) + et_submodel_element_list.append(et_value) + if obj.semantic_id_list_element is not None: + et_submodel_element_list.append(reference_to_xml(obj.semantic_id_list_element, + NS_AAS + "semanticIdListElement")) + et_submodel_element_list.append(_generate_element(NS_AAS + "typeValueListElement", _generic.KEY_TYPES[ + model.KEY_TYPES_CLASSES[obj.type_value_list_element]])) + if obj.value_type_list_element is not None: + et_submodel_element_list.append(_generate_element(NS_AAS + "valueTypeListElement", + model.datatypes.XSD_TYPE_NAMES[obj.value_type_list_element])) + return et_submodel_element_list + + def relationship_element_to_xml(obj: model.RelationshipElement, tag: str = NS_AAS+"relationshipElement") -> etree.Element: """ From 69d0f9d5b751180e9744910c0c234bcc18c620ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 6 Jan 2023 03:23:43 +0100 Subject: [PATCH 056/474] add missing attributes to BasicEventElement --- basyx/aas/adapter/_generic.py | 10 +++ basyx/aas/adapter/json/aasJSONSchema.json | 62 ++++++++++++++++--- .../aas/adapter/json/json_deserialization.py | 15 ++++- basyx/aas/adapter/json/json_serialization.py | 12 ++++ basyx/aas/adapter/xml/AAS.xsd | 45 +++++++++++++- basyx/aas/adapter/xml/xml_deserialization.py | 21 ++++++- basyx/aas/adapter/xml/xml_serialization.py | 15 +++++ 7 files changed, 169 insertions(+), 11 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 7aa5f93..b8da1df 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -20,6 +20,14 @@ model.AssetKind.TYPE: 'Type', model.AssetKind.INSTANCE: 'Instance'} +DIRECTION: Dict[model.Direction, str] = { + model.Direction.INPUT: 'input', + model.Direction.OUTPUT: 'output'} + +STATE_OF_EVENT: Dict[model.StateOfEvent, str] = { + model.StateOfEvent.ON: 'on', + model.StateOfEvent.OFF: 'off'} + REFERENCE_TYPES: Dict[Type[model.Reference], str] = { model.GlobalReference: 'GlobalReference', model.ModelReference: 'ModelReference'} @@ -75,6 +83,8 @@ MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} +DIRECTION_INVERSE: Dict[str, model.Direction] = {v: k for k, v in DIRECTION.items()} +STATE_OF_EVENT_INVERSE: Dict[str, model.StateOfEvent] = {v: k for k, v in STATE_OF_EVENT.items()} REFERENCE_TYPES_INVERSE: Dict[str, Type[model.Reference]] = {v: k for k, v in REFERENCE_TYPES.items()} KEY_TYPES_INVERSE: Dict[str, model.KeyTypes] = {v: k for k, v in KEY_TYPES.items()} ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()} diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index c8bf46b..14adc7d 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -596,18 +596,64 @@ { "$ref": "#/definitions/SubmodelElement" } ] }, - "BasicEventElement": { - "allOf": [ - { "$ref": "#/definitions/EventElement" }, - { "properties": { + "Direction": { + "type": "string", + "enum": [ + "input", + "output" + ] + }, + "StateOfEvent": { + "type": "string", + "enum": [ + "off", + "on" + ] + }, + "BasicEventElement": { + "allOf": [ + { + "$ref": "#/definitions/EventElement" + }, + { + "properties": { "observed": { "$ref": "#/definitions/Reference" + }, + "direction": { + "$ref": "#/definitions/Direction" + }, + "state": { + "$ref": "#/definitions/StateOfEvent" + }, + "messageTopic": { + "type": "string", + "minLength": 1 + }, + "messageBroker": { + "$ref": "#/definitions/Reference" + }, + "lastUpdate": { + "type": "string", + "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|[+-]00:00)$" + }, + "minInterval": { + "type": "string", + "pattern": "^P(([0-9]+Y|[0-9]+Y[0-9]+M|[0-9]+Y[0-9]+M[0-9]+D|[0-9]+Y[0-9]+D|[0-9]+M|[0-9]+M[0-9]+D|[0-9]+D)(T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))?|T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))$" + }, + "maxInterval": { + "type": "string", + "pattern": "^P(([0-9]+Y|[0-9]+Y[0-9]+M|[0-9]+Y[0-9]+M[0-9]+D|[0-9]+Y[0-9]+D|[0-9]+M|[0-9]+M[0-9]+D|[0-9]+D)(T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))?|T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))$" } }, - "required": [ "observed" ] - } - ] - }, + "required": [ + "observed", + "direction", + "state" + ] + } + ] + }, "EntityType": { "type": "string", "enum": ["CoManagedEntity", "SelfManagedEntity"] diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 96aa225..d64dc80 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -38,7 +38,8 @@ from basyx.aas import model from .._generic import MODELING_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 + IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, KEY_TYPES_CLASSES_INVERSE, REFERENCE_TYPES_INVERSE,\ + DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE logger = logging.getLogger(__name__) @@ -547,8 +548,20 @@ def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=mod ret = object_class(id_short=_get_ts(dct, "idShort", str), observed=cls._construct_model_reference(_get_ts(dct, 'observed', dict), model.Referable), # type: ignore + direction=DIRECTION_INVERSE[_get_ts(dct, "direction", str)], + state=STATE_OF_EVENT_INVERSE[_get_ts(dct, "state", str)], kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) + if 'messageTopic' in dct: + ret.message_topic = _get_ts(dct, 'messageTopic', str) + if 'messageBroker' in dct: + ret.message_broker = cls._construct_reference(_get_ts(dct, 'messageBroker', dict)) + if 'lastUpdate' in dct: + ret.last_update = model.datatypes.from_xsd(_get_ts(dct, 'lastUpdate', str), model.datatypes.DateTime) + if 'minInterval' in dct: + ret.min_interval = model.datatypes.from_xsd(_get_ts(dct, 'minInterval', str), model.datatypes.Duration) + if 'maxInterval' in dct: + ret.max_interval = model.datatypes.from_xsd(_get_ts(dct, 'maxInterval', str), model.datatypes.Duration) return ret @classmethod diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 22466f9..d87c2c9 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -676,6 +676,18 @@ def _basic_event_element_to_json(cls, obj: model.BasicEventElement) -> Dict[str, """ data = cls._abstract_classes_to_json(obj) data['observed'] = obj.observed + data['direction'] = _generic.DIRECTION[obj.direction] + data['state'] = _generic.STATE_OF_EVENT[obj.state] + if obj.message_topic is not None: + data['messageTopic'] = obj.message_topic + if obj.message_broker is not None: + data['messageBroker'] = cls._reference_to_json(obj.message_broker) + if obj.last_update is not None: + data['lastUpdate'] = model.datatypes.xsd_repr(obj.last_update) + if obj.min_interval is not None: + data['minInterval'] = model.datatypes.xsd_repr(obj.min_interval) + if obj.max_interval is not None: + data['maxInterval'] = model.datatypes.xsd_repr(obj.max_interval) return data diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 73f0681..44eadb4 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -52,11 +52,54 @@ + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 06e2dcd..0511b0c 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -52,7 +52,8 @@ from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar from .xml_serialization import NS_AAS, NS_ABAC, NS_IEC from .._generic import MODELING_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 + IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, KEY_TYPES_CLASSES_INVERSE, REFERENCE_TYPES_INVERSE,\ + DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE logger = logging.getLogger(__name__) @@ -715,8 +716,26 @@ def construct_basic_event_element(cls, element: etree.Element, object_class=mode basic_event_element = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), _child_construct_mandatory(element, NS_AAS + "observed", cls._construct_referable_reference), + _child_text_mandatory_mapped(element, NS_AAS + "direction", DIRECTION_INVERSE), + _child_text_mandatory_mapped(element, NS_AAS + "state", STATE_OF_EVENT_INVERSE), kind=_get_modeling_kind(element) ) + message_topic = _get_text_or_none(element.find(NS_AAS + "messageTopic")) + if message_topic is not None: + basic_event_element.message_topic = message_topic + message_broker = element.find(NS_AAS + "messageBroker") + if message_broker is not None: + basic_event_element.message_broker = _failsafe_construct(message_broker, cls.construct_reference, + cls.failsafe) + last_update = _get_text_or_none(element.find(NS_AAS + "lastUpdate")) + if last_update is not None: + basic_event_element.last_update = model.datatypes.from_xsd(last_update, model.datatypes.DateTime) + min_interval = _get_text_or_none(element.find(NS_AAS + "minInterval")) + if min_interval is not None: + basic_event_element.min_interval = model.datatypes.from_xsd(min_interval, model.datatypes.Duration) + max_interval = _get_text_or_none(element.find(NS_AAS + "maxInterval")) + if max_interval is not None: + basic_event_element.max_interval = model.datatypes.from_xsd(max_interval, model.datatypes.Duration) cls._amend_abstract_attributes(basic_event_element, element) return basic_event_element diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 9592582..76999a3 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -827,6 +827,21 @@ def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+" """ et_basic_event_element = abstract_classes_to_xml(tag, obj) et_basic_event_element.append(reference_to_xml(obj.observed, NS_AAS+"observed")) + et_basic_event_element.append(_generate_element(NS_AAS+"direction", text=_generic.DIRECTION[obj.direction])) + et_basic_event_element.append(_generate_element(NS_AAS+"state", text=_generic.STATE_OF_EVENT[obj.state])) + if obj.message_topic is not None: + et_basic_event_element.append(_generate_element(NS_AAS+"messageTopic", text=obj.message_topic)) + if obj.message_broker is not None: + et_basic_event_element.append(reference_to_xml(obj.message_broker, NS_AAS+"messageBroker")) + if obj.last_update is not None: + et_basic_event_element.append(_generate_element(NS_AAS+"lastUpdate", + text=model.datatypes.xsd_repr(obj.last_update))) + if obj.min_interval is not None: + et_basic_event_element.append(_generate_element(NS_AAS+"minInterval", + text=model.datatypes.xsd_repr(obj.min_interval))) + if obj.max_interval is not None: + et_basic_event_element.append(_generate_element(NS_AAS+"maxInterval", + text=model.datatypes.xsd_repr(obj.max_interval))) return et_basic_event_element From 0c37ad31b431ad1c3a1a094f32b762edee35a953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 20 Mar 2023 15:51:30 +0100 Subject: [PATCH 057/474] begin integration of new XML and JSON schemata --- basyx/aas/adapter/json/aasJSONSchema.json | 2037 +++++++++-------- .../aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/xml/AAS.xsd | 1963 +++++++++++----- basyx/aas/adapter/xml/xml_deserialization.py | 56 +- basyx/aas/adapter/xml/xml_serialization.py | 100 +- 5 files changed, 2432 insertions(+), 1726 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 14adc7d..36a7303 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -1,870 +1,1176 @@ { "$schema": "https://json-schema.org/draft/2019-09/schema", "title": "AssetAdministrationShellEnvironment", - "$id": "http://www.admin-shell.io/schema/json/V3.0RC01", "type": "object", - "required": ["assetAdministrationShells", "submodels", "conceptDescriptions"], - "properties": { - "assetAdministrationShells": { - "type": "array", - "items": { - "$ref": "#/definitions/AssetAdministrationShell" - } + "allOf": [ + { + "$ref": "#/definitions/Environment" + } + ], + "$id": "https://admin-shell.io/aas/3/0/RC02", + "definitions": { + "AasSubmodelElements": { + "type": "string", + "enum": [ + "AnnotatedRelationshipElement", + "BasicEventElement", + "Blob", + "Capability", + "DataElement", + "Entity", + "EventElement", + "File", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "SubmodelElement", + "SubmodelElementCollection", + "SubmodelElementList" + ] + }, + "AdministrativeInformation": { + "allOf": [ + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "version": { + "type": "string", + "minLength": 1 + }, + "revision": { + "type": "string", + "minLength": 1 + } + } + } + ] + }, + "AnnotatedRelationshipElement": { + "allOf": [ + { + "$ref": "#/definitions/RelationshipElement" + }, + { + "properties": { + "annotations": { + "type": "array", + "items": { + "$ref": "#/definitions/DataElement" + }, + "minItems": 1 + } + } + } + ] + }, + "AssetAdministrationShell": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "derivedFrom": { + "$ref": "#/definitions/Reference" + }, + "assetInformation": { + "$ref": "#/definitions/AssetInformation" + }, + "submodels": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + } + }, + "required": [ + "assetInformation" + ] + } + ] + }, + "AssetInformation": { + "type": "object", + "properties": { + "assetKind": { + "$ref": "#/definitions/AssetKind" + }, + "globalAssetId": { + "$ref": "#/definitions/Reference" + }, + "specificAssetIds": { + "type": "array", + "items": { + "$ref": "#/definitions/SpecificAssetId" + }, + "minItems": 1 + }, + "defaultThumbnail": { + "$ref": "#/definitions/Resource" + } + }, + "required": [ + "assetKind" + ] + }, + "AssetKind": { + "type": "string", + "enum": [ + "Instance", + "Type" + ] + }, + "BasicEventElement": { + "allOf": [ + { + "$ref": "#/definitions/EventElement" + }, + { + "properties": { + "observed": { + "$ref": "#/definitions/Reference" + }, + "direction": { + "$ref": "#/definitions/Direction" + }, + "state": { + "$ref": "#/definitions/StateOfEvent" + }, + "messageTopic": { + "type": "string", + "minLength": 1 + }, + "messageBroker": { + "$ref": "#/definitions/Reference" + }, + "lastUpdate": { + "type": "string", + "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|[+-]00:00)$" + }, + "minInterval": { + "type": "string", + "pattern": "^P(([0-9]+Y|[0-9]+Y[0-9]+M|[0-9]+Y[0-9]+M[0-9]+D|[0-9]+Y[0-9]+D|[0-9]+M|[0-9]+M[0-9]+D|[0-9]+D)(T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))?|T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))$" + }, + "maxInterval": { + "type": "string", + "pattern": "^P(([0-9]+Y|[0-9]+Y[0-9]+M|[0-9]+Y[0-9]+M[0-9]+D|[0-9]+Y[0-9]+D|[0-9]+M|[0-9]+M[0-9]+D|[0-9]+D)(T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))?|T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))$" + } + }, + "required": [ + "observed", + "direction", + "state" + ] + } + ] + }, + "Blob": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "value": { + "type": "string", + "contentEncoding": "base64" + }, + "contentType": { + "type": "string", + "minLength": 1, + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \t]*;[ \t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\t !#-\\[\\]-~]|[\\x80-\\xff])|\\\\([\t !-~]|[\\x80-\\xff]))*\"))*$" + } + }, + "required": [ + "contentType" + ] + } + ] + }, + "Capability": { + "$ref": "#/definitions/SubmodelElement" + }, + "ConceptDescription": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "isCaseOf": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + } + } + } + ] + }, + "DataElement": { + "$ref": "#/definitions/SubmodelElement" + }, + "DataSpecificationContent": { + "type": "object", + "properties": { + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": [ + "modelType" + ] + }, + "DataSpecificationIEC61360": { + "allOf": [ + { + "$ref": "#/definitions/DataSpecificationContent" + }, + { + "properties": { + "preferredName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + }, + "minItems": 1 + }, + "shortName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + }, + "minItems": 1 + }, + "unit": { + "type": "string", + "minLength": 1 + }, + "unitId": { + "$ref": "#/definitions/Reference" + }, + "sourceOfDefinition": { + "type": "string", + "minLength": 1 + }, + "symbol": { + "type": "string", + "minLength": 1 + }, + "dataType": { + "$ref": "#/definitions/DataTypeIEC61360" + }, + "definition": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + }, + "minItems": 1 + }, + "valueFormat": { + "type": "string", + "minLength": 1 + }, + "valueList": { + "$ref": "#/definitions/ValueList" + }, + "value": { + "type": "string" + }, + "levelType": { + "$ref": "#/definitions/LevelType" + } + }, + "required": [ + "preferredName" + ] + } + ] + }, + "DataSpecificationPhysicalUnit": { + "allOf": [ + { + "$ref": "#/definitions/DataSpecificationContent" + }, + { + "properties": { + "unitName": { + "type": "string", + "minLength": 1 + }, + "unitSymbol": { + "type": "string", + "minLength": 1 + }, + "definition": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + }, + "minItems": 1 + }, + "siNotation": { + "type": "string", + "minLength": 1 + }, + "siName": { + "type": "string", + "minLength": 1 + }, + "dinNotation": { + "type": "string", + "minLength": 1 + }, + "eceName": { + "type": "string", + "minLength": 1 + }, + "eceCode": { + "type": "string", + "minLength": 1 + }, + "nistName": { + "type": "string", + "minLength": 1 + }, + "sourceOfDefinition": { + "type": "string", + "minLength": 1 + }, + "conversionFactor": { + "type": "string", + "minLength": 1 + }, + "registrationAuthorityId": { + "type": "string", + "minLength": 1 + }, + "supplier": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "unitName", + "unitSymbol", + "definition" + ] + } + ] + }, + "DataTypeDefXsd": { + "type": "string", + "enum": [ + "xs:anyURI", + "xs:base64Binary", + "xs:boolean", + "xs:byte", + "xs:date", + "xs:dateTime", + "xs:dateTimeStamp", + "xs:dayTimeDuration", + "xs:decimal", + "xs:double", + "xs:duration", + "xs:float", + "xs:gDay", + "xs:gMonth", + "xs:gMonthDay", + "xs:gYear", + "xs:gYearMonth", + "xs:hexBinary", + "xs:int", + "xs:integer", + "xs:long", + "xs:negativeInteger", + "xs:nonNegativeInteger", + "xs:nonPositiveInteger", + "xs:positiveInteger", + "xs:short", + "xs:string", + "xs:time", + "xs:unsignedByte", + "xs:unsignedInt", + "xs:unsignedLong", + "xs:unsignedShort", + "xs:yearMonthDuration" + ] + }, + "DataTypeIEC61360": { + "type": "string", + "enum": [ + "BLOB", + "BOOLEAN", + "DATE", + "FILE", + "HTML", + "INTEGER_COUNT", + "INTEGER_CURRENCY", + "INTEGER_MEASURE", + "IRDI", + "IRI", + "RATIONAL", + "RATIONAL_MEASURE", + "REAL_COUNT", + "REAL_CURRENCY", + "REAL_MEASURE", + "STRING", + "STRING_TRANSLATABLE", + "TIME", + "TIMESTAMP" + ] }, - "submodels": { - "type": "array", - "items": { - "$ref": "#/definitions/Submodel" - } + "Direction": { + "type": "string", + "enum": [ + "input", + "output" + ] }, - "conceptDescriptions": { - "type": "array", - "items": { - "$ref": "#/definitions/ConceptDescription" - } - } - }, - "definitions": { - "Referable": { - "allOf":[ - {"$ref": "#/definitions/HasExtensions"}, - {"properties": { - "idShort": { - "type": "string" - }, - "category": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "description": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } + "EmbeddedDataSpecification": { + "type": "object", + "properties": { + "dataSpecification": { + "$ref": "#/definitions/Reference" }, - "modelType": { - "$ref": "#/definitions/ModelType" + "dataSpecificationContent": { + "$ref": "#/definitions/DataSpecificationContent" } }, - "required": ["modelType" ] - }] -}, - "Identifiable": { - "allOf": [ - { "$ref": "#/definitions/Referable" }, - { "properties": { - "id": { - "$ref": "#/definitions/Identifier" + "required": [ + "dataSpecification", + "dataSpecificationContent" + ] + }, + "Entity": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "statements": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement" + }, + "minItems": 1 }, - "administration": { - "$ref": "#/definitions/AdministrativeInformation" + "entityType": { + "$ref": "#/definitions/EntityType" + }, + "globalAssetId": { + "$ref": "#/definitions/Reference" + }, + "specificAssetId": { + "$ref": "#/definitions/SpecificAssetId" } }, - "required": [ "id" ] - } - ] - }, - "Qualifiable": { - "type": "object", - "properties": { - "qualifiers": { + "required": [ + "entityType" + ] + } + ] + }, + "EntityType": { + "type": "string", + "enum": [ + "CoManagedEntity", + "SelfManagedEntity" + ] + }, + "Environment": { + "type": "object", + "properties": { + "assetAdministrationShells": { "type": "array", "items": { - "$ref": "#/definitions/Qualifier" - } - } - } - }, - "HasSemantics": { - "type": "object", - "properties": { - "semanticId": { - "$ref": "#/definitions/Reference" - } - } - }, - "HasDataSpecification": { - "type": "object", - "properties": { - "embeddedDataSpecifications": { + "$ref": "#/definitions/AssetAdministrationShell" + }, + "minItems": 1 + }, + "submodels": { "type": "array", "items": { - "$ref": "#/definitions/EmbeddedDataSpecification" - } - } - } - }, - "HasExtensions": { - "type": "object", - "properties": { - "extensions": { + "$ref": "#/definitions/Submodel" + }, + "minItems": 1 + }, + "conceptDescriptions": { "type": "array", "items": { - "$ref": "#/definitions/Extension" - } + "$ref": "#/definitions/ConceptDescription" + }, + "minItems": 1 } - } - }, - "Extension": { - "allOf": [ - { - "$ref": "#/definitions/HasSemantics" + } }, - { "properties": { - "name": { - "type": "string" - }, - "valueType":{ - "$ref": "#/definitions/DataTypeDefXsd" + "EventElement": { + "$ref": "#/definitions/SubmodelElement" }, - "value":{ - "type": "string" - }, - "refersTo": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" + "EventPayload": { + "type": "object", + "properties": { + "source": { + "$ref": "#/definitions/Reference" + }, + "sourceSemanticId": { + "$ref": "#/definitions/Reference" + }, + "observableReference": { + "$ref": "#/definitions/Reference" + }, + "observableSemanticId": { + "$ref": "#/definitions/Reference" + }, + "topic": { + "type": "string", + "minLength": 1 + }, + "subjectId": { + "$ref": "#/definitions/Reference" + }, + "timeStamp": { + "type": "string", + "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)Z$" + }, + "payload": { + "type": "string", + "minLength": 1 } - } + }, + "required": [ + "source", + "observableReference", + "timeStamp" + ] + }, + "Extension": { + "allOf": [ + { + "$ref": "#/definitions/HasSemantics" }, - "required": [ "name" ] - } - ] -}, - "AssetAdministrationShell": { - "allOf": [ - { "$ref": "#/definitions/Identifiable" }, - { "$ref": "#/definitions/HasDataSpecification" }, - { "properties": { - "derivedFrom": { - "$ref": "#/definitions/Reference" + { + "properties": { + "name": { + "type": "string", + "minLength": 1 }, - "assetInformation": { - "$ref": "#/definitions/AssetInformation" + "valueType": { + "$ref": "#/definitions/DataTypeDefXsd" }, - "submodels": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } + "value": { + "type": "string" + }, + "refersTo": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "name" + ] + } + ] + }, + "File": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "value": { + "type": "string", + "minLength": 1, + "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$" + }, + "contentType": { + "type": "string", + "minLength": 1, + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \t]*;[ \t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\t !#-\\[\\]-~]|[\\x80-\\xff])|\\\\([\t !-~]|[\\x80-\\xff]))*\"))*$" } }, - "required": [ "assetInformation" ] - } - ] + "required": [ + "contentType" + ] + } + ] }, - "Identifier": { - "type": "string" + "HasDataSpecification": { + "type": "object", + "properties": { + "embeddedDataSpecifications": { + "type": "array", + "items": { + "$ref": "#/definitions/EmbeddedDataSpecification" + }, + "minItems": 1 + } + } }, - "AdministrativeInformation": { + "HasExtensions": { "type": "object", "properties": { - "version": { - "type": "string" - }, - "revision": { - "type": "string" + "extensions": { + "type": "array", + "items": { + "$ref": "#/definitions/Extension" + }, + "minItems": 1 } } }, - "LangString": { + "HasKind": { "type": "object", "properties": { - "language": { - "type": "string" - }, - "text": { - "type": "string" + "kind": { + "$ref": "#/definitions/ModelingKind" } - }, - "required": [ "language", "text" ] + } }, - "Reference": { + "HasSemantics": { "type": "object", "properties": { - "type": { - "$ref": "#/definitions/ReferenceTypes" + "semanticId": { + "$ref": "#/definitions/Reference" }, - "keys": { + "supplementalSemanticIds": { "type": "array", "items": { - "$ref": "#/definitions/Key" - } + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + } + } + }, + "Identifiable": { + "allOf": [ + { + "$ref": "#/definitions/Referable" }, - "referredSemanticId": { - "$ref": "#/definitions/Reference" + { + "properties": { + "administration": { + "$ref": "#/definitions/AdministrativeInformation" + }, + "id": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "id" + ] } - }, - "required": [ "type", "keys" ] + ] }, "Key": { "type": "object", "properties": { "type": { - "$ref": "#/definitions/KeyTypes" + "$ref": "#/definitions/KeyTypes" }, "value": { - "type": "string" + "type": "string", + "minLength": 1 } }, - "required": [ "type", "value"] - }, - "ReferenceTypes": { - "type": "string", - "enum": [ - "GlobalReference", - "ModelReference" + "required": [ + "type", + "value" ] }, "KeyTypes": { "type": "string", "enum": [ - "AssetAdministrationShell", - "ConceptDescription", - "Submodel", - "AnnotatedRelationshipElement", - "BasicEventElement", - "Blob", - "Capability", - "DataElement", - "File", - "Entity", - "EventElement", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "SubmodelElement", - "SubmodelElementCollection", - "SubmodelElementList", - "GlobalReference", - "FragmentReference" - ] - }, - "ModelTypes": { - "type": "string", - "enum": [ - "AssetAdministrationShell", - "ConceptDescription", - "Submodel", - "AccessPermissionRule", - "AnnotatedRelationshipElement", - "BasicEventElement", - "Blob", - "Capability", - "DataElement", - "File", - "Entity", - "EventElement", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "SubmodelElement", - "SubmodelElementCollection", - "SubmodelElementList", + "AnnotatedRelationshipElement", + "AssetAdministrationShell", + "BasicEventElement", + "Blob", + "Capability", + "ConceptDescription", + "DataElement", + "Entity", + "EventElement", + "File", + "FragmentReference", "GlobalReference", - "FragmentReference", - "Qualifier" - ] - }, - "ModelType": { - "type": "object", - "properties": { - "name": { - "$ref": "#/definitions/ModelTypes" - } - }, - "required": [ "name" ] - }, - "EmbeddedDataSpecification": { - "type": "object", - "properties": { - "dataSpecification": { - "$ref": "#/definitions/Reference" - }, - "dataSpecificationContent": { - "$ref": "#/definitions/DataSpecificationContent" - } - }, - "required": [ "dataSpecification", "dataSpecificationContent" ] - }, - "DataSpecificationContent": { - "oneOf": [ - { "$ref": "#/definitions/DataSpecificationIEC61360Content" }, - { "$ref": "#/definitions/DataSpecificationPhysicalUnitContent" } - ] - }, - "DataSpecificationPhysicalUnitContent": { - "type": "object", - "properties": { - "unitName": { - "type": "string" - }, - "unitSymbol": { - "type": "string" - }, - "definition": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "siNotation": { - "type": "string" - }, - "siName": { - "type": "string" - }, - "dinNotation": { - "type": "string" - }, - "eceName": { - "type": "string" - }, - "eceCode": { - "type": "string" - }, - "nistName": { - "type": "string" - }, - "sourceOfDefinition": { - "type": "string" - }, - "conversionFactor": { - "type": "string" - }, - "registrationAuthorityId": { - "type": "string" - }, - "supplier": { - "type": "string" - } - }, - "required": [ "unitName", "unitSymbol", "definition" ] - }, - "DataSpecificationIEC61360Content": { - "allOf": [ - { "$ref": "#/definitions/ValueObject" }, - { - "type": "object", - "properties": { - "dataType": { - "enum": [ - "DATE", - "STRING", - "STRING_TRANSLATABLE", - "REAL_MEASURE", - "REAL_COUNT", - "REAL_CURRENCY", - "BOOLEAN", - "URL", - "RATIONAL", - "RATIONAL_MEASURE", - "TIME", - "TIMESTAMP", - "INTEGER_COUNT", - "INTEGER_MEASURE", - "INTEGER_CURRENCY" - ] - }, - "definition": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "preferredName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "shortName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "sourceOfDefinition": { - "type": "string" - }, - "symbol": { - "type": "string" - }, - "unit": { - "type": "string" - }, - "unitId": { - "$ref": "#/definitions/Reference" - }, - "valueFormat": { - "type": "string" - }, - "valueList": { - "$ref": "#/definitions/ValueList" - }, - "levelType": { - "type": "array", - "items": { - "$ref": "#/definitions/LevelType" - } - } - }, - "required": [ "preferredName" ] - } + "Identifiable", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "Referable", + "ReferenceElement", + "RelationshipElement", + "Submodel", + "SubmodelElement", + "SubmodelElementCollection", + "SubmodelElementList" ] - }, - "LevelType": { - "type": "string", - "enum": [ "Min", "Max", "Nom", "Typ" ] }, - "ValueList": { + "LangString": { "type": "object", "properties": { - "valueReferencePairTypes": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/ValueReferencePairType" - } + "language": { + "type": "string", + "pattern": "^(([a-zA-Z]{2,3}(-[a-zA-Z]{3}(-[a-zA-Z]{3}){2})?|[a-zA-Z]{4}|[a-zA-Z]{5,8})(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-(([a-zA-Z0-9]){5,8}|[0-9]([a-zA-Z0-9]){3}))*(-[0-9A-WY-Za-wy-z](-([a-zA-Z0-9]){2,8})+)*(-[xX](-([a-zA-Z0-9]){1,8})+)?|[xX](-([a-zA-Z0-9]){1,8})+|((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)))$" + }, + "text": { + "type": "string" } }, - "required": [ "valueReferencePairTypes" ] + "required": [ + "language", + "text" + ] }, - "ValueReferencePairType": { - "allOf": [ - { "$ref": "#/definitions/ValueObject" } - ] + "LevelType": { + "type": "string", + "enum": [ + "Max", + "Min", + "Nom", + "Typ" + ] }, - "ValueObject": { - "type": "object", - "properties": { - "value": { "type": "string" }, - "valueId": { - "$ref": "#/definitions/Reference" - }, - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - } - } - }, - "AssetInformation": { - "allOf": [ - { "properties": { - "assetKind": { - "$ref": "#/definitions/AssetKind" - }, - "globalAssetId":{ - "$ref": "#/definitions/Reference" - }, - "externalAssetIds":{ - "type": "array", - "items": { - "$ref": "#/definitions/SpecificAssetId" - } - }, - "thumbnail":{ - "$ref": "#/definitions/Resource" - } - }, - "required": [ "assetKind" ] - } - ] - }, - "SpecificAssetId":{ - "allOf": [{ "$ref": "#/definitions/HasSemantics"}, - { "properties": { - "name": { - "dataType":"string" - }, - "value": { - "dataType":"string" - }, - - "subjectId":{ - "$ref": "#/definitions/Reference" - } - }, - "required": [ "name","value","subjectId" ] - } - ] -}, - "AssetKind": { + "ModelType": { "type": "string", - "enum": ["Type", "Instance"] + "enum": [ + "AnnotatedRelationshipElement", + "AssetAdministrationShell", + "BasicEventElement", + "Blob", + "Capability", + "ConceptDescription", + "DataSpecificationIEC61360", + "DataSpecificationPhysicalUnit", + "Entity", + "File", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "Submodel", + "SubmodelElementCollection", + "SubmodelElementList" + ] }, "ModelingKind": { "type": "string", - "enum": ["Template", "Instance"] + "enum": [ + "Instance", + "Template" + ] }, - "Submodel": { - "allOf": [ - { "$ref": "#/definitions/Identifiable" }, - { "$ref": "#/definitions/HasDataSpecification" }, - { "$ref": "#/definitions/Qualifiable" }, - { "$ref": "#/definitions/HasSemantics" }, - { "properties": { - "kind": { - "$ref": "#/definitions/ModelingKind" - }, - "submodelElements": { + "MultiLanguageProperty": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "value": { "type": "array", "items": { - "$ref": "#/definitions/SubmodelElement" - } + "$ref": "#/definitions/LangString" + }, + "minItems": 1 + }, + "valueId": { + "$ref": "#/definitions/Reference" } } - } - ] + } + ] }, "Operation": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "inputVariable": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "inputVariables": { "type": "array", "items": { "$ref": "#/definitions/OperationVariable" - } + }, + "minItems": 1 }, - "outputVariable": { + "outputVariables": { "type": "array", "items": { "$ref": "#/definitions/OperationVariable" - } + }, + "minItems": 1 }, - "inoutputVariable": { + "inoutputVariables": { "type": "array", "items": { "$ref": "#/definitions/OperationVariable" - } + }, + "minItems": 1 } } - } - ] + } + ] }, "OperationVariable": { "type": "object", "properties": { "value": { - "oneOf": [ - { "$ref": "#/definitions/Blob" }, - { "$ref": "#/definitions/File" }, - { "$ref": "#/definitions/Capability" }, - { "$ref": "#/definitions/Entity" }, - { "$ref": "#/definitions/EventElement" }, - { "$ref": "#/definitions/BasicEventElement" }, - { "$ref": "#/definitions/MultiLanguageProperty" }, - { "$ref": "#/definitions/Operation" }, - { "$ref": "#/definitions/Property" }, - { "$ref": "#/definitions/Range" }, - { "$ref": "#/definitions/ReferenceElement" }, - { "$ref": "#/definitions/RelationshipElement" }, - { "$ref": "#/definitions/SubmodelElementCollection" }, - { "$ref": "#/definitions/SubmodelElementList" } - ] + "$ref": "#/definitions/SubmodelElement" } }, - "required": [ "value" ] + "required": [ + "value" + ] }, - "SubmodelElement": { - "allOf": [ - { "$ref": "#/definitions/Referable" }, - { "$ref": "#/definitions/HasDataSpecification" }, - { "$ref": "#/definitions/HasSemantics" }, - { "$ref": "#/definitions/Qualifiable" }, - { "properties": { - "kind": { - "$ref": "#/definitions/ModelingKind" + "Property": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "valueType": { + "$ref": "#/definitions/DataTypeDefXsd" + }, + "value": { + "type": "string" }, - "idShort":{ - "dataType": "string" + "valueId": { + "$ref": "#/definitions/Reference" } - },"required":["idShort"] - } - ] - }, - "EventElement": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" } - ] - }, - "Direction": { - "type": "string", - "enum": [ - "input", - "output" + }, + "required": [ + "valueType" + ] + } ] }, - "StateOfEvent": { - "type": "string", - "enum": [ - "off", - "on" + "Qualifiable": { + "type": "object", + "properties": { + "qualifiers": { + "type": "array", + "items": { + "$ref": "#/definitions/Qualifier" + }, + "minItems": 1 + }, + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": [ + "modelType" ] }, - "BasicEventElement": { + "Qualifier": { "allOf": [ { - "$ref": "#/definitions/EventElement" + "$ref": "#/definitions/HasSemantics" }, { "properties": { - "observed": { - "$ref": "#/definitions/Reference" - }, - "direction": { - "$ref": "#/definitions/Direction" - }, - "state": { - "$ref": "#/definitions/StateOfEvent" + "kind": { + "$ref": "#/definitions/QualifierKind" }, - "messageTopic": { + "type": { "type": "string", "minLength": 1 }, - "messageBroker": { - "$ref": "#/definitions/Reference" - }, - "lastUpdate": { - "type": "string", - "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|[+-]00:00)$" + "valueType": { + "$ref": "#/definitions/DataTypeDefXsd" }, - "minInterval": { - "type": "string", - "pattern": "^P(([0-9]+Y|[0-9]+Y[0-9]+M|[0-9]+Y[0-9]+M[0-9]+D|[0-9]+Y[0-9]+D|[0-9]+M|[0-9]+M[0-9]+D|[0-9]+D)(T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))?|T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))$" + "value": { + "type": "string" }, - "maxInterval": { - "type": "string", - "pattern": "^P(([0-9]+Y|[0-9]+Y[0-9]+M|[0-9]+Y[0-9]+M[0-9]+D|[0-9]+Y[0-9]+D|[0-9]+M|[0-9]+M[0-9]+D|[0-9]+D)(T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))?|T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))$" + "valueId": { + "$ref": "#/definitions/Reference" } }, "required": [ - "observed", - "direction", - "state" + "type", + "valueType" ] } ] }, - "EntityType": { + "QualifierKind": { "type": "string", - "enum": ["CoManagedEntity", "SelfManagedEntity"] + "enum": [ + "ConceptQualifier", + "TemplateQualifier", + "ValueQualifier" + ] }, - "Entity": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "statements": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement" - } + "Range": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "valueType": { + "$ref": "#/definitions/DataTypeDefXsd" }, - "entityType": { - "$ref": "#/definitions/EntityType" + "min": { + "type": "string" }, - "globalAssetId":{ - "$ref": "#/definitions/Reference" - }, - "specificAssetIds":{ - "$ref": "#/definitions/SpecificAssetId" + "max": { + "type": "string" } }, - "required": [ "entityType" ] - } - ] + "required": [ + "valueType" + ] + } + ] }, - "ConceptDescription": { - "allOf": [ - { "$ref": "#/definitions/Identifiable" }, - { "$ref": "#/definitions/HasDataSpecification" }, - { "properties": { - "isCaseOf": { + "Referable": { + "allOf": [ + { + "$ref": "#/definitions/HasExtensions" + }, + { + "properties": { + "category": { + "type": "string", + "minLength": 1 + }, + "idShort": { + "type": "string", + "maxLength": 128, + "pattern": "^[a-zA-Z][a-zA-Z0-9_]+$" + }, + "displayName": { "type": "array", "items": { - "$ref": "#/definitions/Reference" - } + "$ref": "#/definitions/LangString" + }, + "minItems": 1 + }, + "description": { + "type": "array", + "items": { + "$ref": "#/definitions/LangString" + }, + "minItems": 1 + }, + "checksum": { + "type": "string", + "minLength": 1 + }, + "modelType": { + "$ref": "#/definitions/ModelType" } - } - } - ] - }, - "Capability": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" } - ] - }, - "Property": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "$ref": "#/definitions/ValueObject" } - ] + }, + "required": [ + "modelType" + ] + } + ] }, - "Range": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "valueType": { - "type": "string", - "enum": [ - "anyUri", - "base64Binary", - "boolean", - "date", - "dateTime", - "dateTimeStamp", - "decimal", - "integer", - "long", - "int", - "short", - "byte", - "nonNegativeInteger", - "positiveInteger", - "unsignedLong", - "unsignedInt", - "unsignedShort", - "unsignedByte", - "nonPositiveInteger", - "negativeInteger", - "double", - "duration", - "dayTimeDuration", - "yearMonthDuration", - "float", - "gDay", - "gMonth", - "gMonthDay", - "gYear", - "gYearMonth", - "hexBinary", - "NOTATION", - "QName", - "string", - "normalizedString", - "token", - "language", - "Name", - "NCName", - "ENTITY", - "ID", - "IDREF", - "NMTOKEN", - "time" - ] - }, - "min": { "type": "string" }, - "max": { "type": "string" } + "Reference": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/ReferenceTypes" + }, + "referredSemanticId": { + "$ref": "#/definitions/Reference" + }, + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/Key" }, - "required": [ "valueType"] + "minItems": 1 } - ] + }, + "required": [ + "type", + "keys" + ] }, - "MultiLanguageProperty": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/LangString" - } - }, - "valueId": { - "$ref": "#/definitions/Reference" + "ReferenceElement": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "value": { + "$ref": "#/definitions/Reference" } } } - ] + ] }, - "File": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "value": { - "type": "string" + "ReferenceTypes": { + "type": "string", + "enum": [ + "GlobalReference", + "ModelReference" + ] + }, + "RelationshipElement": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "first": { + "$ref": "#/definitions/Reference" }, - "contentType": { - "type": "string" + "second": { + "$ref": "#/definitions/Reference" } }, - "required": [ "contentType" ] + "required": [ + "first", + "second" + ] } - ] + ] }, "Resource": { - "properties": { - "path": { - "type": "string" - }, - "contentType": { - "type": "string" - } + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$" }, - "required": [ "path" ] + "contentType": { + "type": "string", + "minLength": 1, + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \t]*;[ \t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\t !#-\\[\\]-~]|[\\x80-\\xff])|\\\\([\t !-~]|[\\x80-\\xff]))*\"))*$" + } + }, + "required": [ + "path" + ] }, - "Blob": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "value": { - "type": "string" + "SpecificAssetId": { + "allOf": [ + { + "$ref": "#/definitions/HasSemantics" + }, + { + "properties": { + "name": { + "type": "string", + "minLength": 1 }, - "contentType": { - "type": "string" + "value": { + "type": "string", + "minLength": 1 + }, + "externalSubjectId": { + "$ref": "#/definitions/Reference" } }, - "required": [ "contentType" ] + "required": [ + "name", + "value", + "externalSubjectId" + ] } - ] + ] }, - "ReferenceElement": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "value": { - "$ref": "#/definitions/Reference" + "StateOfEvent": { + "type": "string", + "enum": [ + "off", + "on" + ] + }, + "Submodel": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasKind" + }, + { + "$ref": "#/definitions/HasSemantics" + }, + { + "$ref": "#/definitions/Qualifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "submodelElements": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement" + }, + "minItems": 1 } } } - ] + ] + }, + "SubmodelElement": { + "allOf": [ + { + "$ref": "#/definitions/Referable" + }, + { + "$ref": "#/definitions/HasKind" + }, + { + "$ref": "#/definitions/HasSemantics" + }, + { + "$ref": "#/definitions/Qualifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + } + ] }, "SubmodelElementCollection": { "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "value": { + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "value": { "type": "array", "items": { - "oneOf": [ - { "$ref": "#/definitions/Blob" }, - { "$ref": "#/definitions/File" }, - { "$ref": "#/definitions/Capability" }, - { "$ref": "#/definitions/Entity" }, - { "$ref": "#/definitions/EventElement" }, - { "$ref": "#/definitions/BasicEventElement" }, - { "$ref": "#/definitions/MultiLanguageProperty" }, - { "$ref": "#/definitions/Operation" }, - { "$ref": "#/definitions/Property" }, - { "$ref": "#/definitions/Range" }, - { "$ref": "#/definitions/ReferenceElement" }, - { "$ref": "#/definitions/RelationshipElement" }, - { "$ref": "#/definitions/SubmodelElementCollection" }, - { "$ref": "#/definitions/SubmodelElementList" } - ] - } + "$ref": "#/definitions/SubmodelElement" + }, + "minItems": 1 } } } - ] + ] }, "SubmodelElementList": { "allOf": [ @@ -899,332 +1205,35 @@ } ] }, - "DataTypeDefXsd": { - "type": "string", - "enum": [ - "xs:anyURI", - "xs:base64Binary", - "xs:boolean", - "xs:byte", - "xs:date", - "xs:dateTime", - "xs:dateTimeStamp", - "xs:dayTimeDuration", - "xs:decimal", - "xs:double", - "xs:duration", - "xs:float", - "xs:gDay", - "xs:gMonth", - "xs:gMonthDay", - "xs:gYear", - "xs:gYearMonth", - "xs:hexBinary", - "xs:int", - "xs:integer", - "xs:long", - "xs:negativeInteger", - "xs:nonNegativeInteger", - "xs:nonPositiveInteger", - "xs:positiveInteger", - "xs:short", - "xs:string", - "xs:time", - "xs:unsignedByte", - "xs:unsignedInt", - "xs:unsignedLong", - "xs:unsignedShort", - "xs:yearMonthDuration" - ] - }, - "AasSubmodelElements": { - "type": "string", - "enum": [ - "AnnotatedRelationshipElement", - "BasicEventElement", - "Blob", - "Capability", - "DataElement", - "Entity", - "EventElement", - "File", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "SubmodelElement", - "SubmodelElementCollection", - "SubmodelElementList" - ] - }, - "RelationshipElement": { - "allOf": [ - { "$ref": "#/definitions/SubmodelElement" }, - { "properties": { - "first": { - "$ref": "#/definitions/Reference" - }, - "second": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ "first", "second" ] - } - ] - }, - "AnnotatedRelationshipElement": { - "allOf": [ - { "$ref": "#/definitions/RelationshipElement" }, - { "properties": { - "annotation": { - "type": "array", - "items": { - "oneOf": [ - { "$ref": "#/definitions/Blob" }, - { "$ref": "#/definitions/File" }, - { "$ref": "#/definitions/MultiLanguageProperty" }, - { "$ref": "#/definitions/Property" }, - { "$ref": "#/definitions/Range" }, - { "$ref": "#/definitions/ReferenceElement" } - ] - } - } - } - } - ] - }, - "Qualifier": { - "allOf": [ - { "$ref": "#/definitions/HasSemantics" }, - { "$ref": "#/definitions/ValueObject" }, - { "properties": { - "modelType": { - "$ref": "#/definitions/ModelType" - }, - "type": { - "type": "string" - } - }, - "required": [ "type", "modelType" ] - } - ] - }, - "Security": { - "type": "object", - "properties": { - "accessControlPolicyPoints": { - "$ref": "#/definitions/AccessControlPolicyPoints" - }, - "certificate": { - "type": "array", - "items": { - "oneOf": [ - { "$ref": "#/definitions/BlobCertificate" } - ] - } - }, - "requiredCertificateExtension": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - } - }, - "required": [ "accessControlPolicyPoints" ] - }, - "Certificate": { - "type": "object" - }, - "BlobCertificate": { - "allOf": [ - { "$ref": "#/definitions/Certificate" }, - { "properties": { - "blobCertificate": { - "$ref": "#/definitions/Blob" - }, - "containedExtension": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - }, - "lastCertificate": { - "type": "boolean" - } - } - } - ] - }, - "AccessControlPolicyPoints": { - "type": "object", - "properties": { - "policyAdministrationPoint": { - "$ref": "#/definitions/PolicyAdministrationPoint" - }, - "policyDecisionPoint": { - "$ref": "#/definitions/PolicyDecisionPoint" - }, - "policyEnforcementPoint": { - "$ref": "#/definitions/PolicyEnforcementPoint" - }, - "policyInformationPoints": { - "$ref": "#/definitions/PolicyInformationPoints" - } - }, - "required": [ "policyAdministrationPoint", "policyDecisionPoint", "policyEnforcementPoint" ] - }, - "PolicyAdministrationPoint": { - "type": "object", - "properties": { - "localAccessControl": { - "$ref": "#/definitions/AccessControl" - }, - "externalAccessControl": { - "type": "boolean" - } - }, - "required": [ "externalAccessControl" ] - }, - "PolicyInformationPoints": { - "type": "object", - "properties": { - "internalInformationPoint": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - } - }, - "externalInformationPoint": { - "type": "boolean" - } - }, - "required": [ "externalInformationPoint" ] - }, - "PolicyEnforcementPoint": { - "type": "object", - "properties": { - "externalPolicyEnforcementPoint": { - "type": "boolean" - } - }, - "required": [ "externalPolicyEnforcementPoint" ] - }, - "PolicyDecisionPoint": { - "type": "object", - "properties": { - "externalPolicyDecisionPoints": { - "type": "boolean" - } - }, - "required": [ "externalPolicyDecisionPoints" ] - }, - "AccessControl": { - "type": "object", - "properties": { - "selectableSubjectAttributes": { - "$ref": "#/definitions/Reference" - }, - "defaultSubjectAttributes": { - "$ref": "#/definitions/Reference" - }, - "selectablePermissions": { - "$ref": "#/definitions/Reference" - }, - "defaultPermissions": { - "$ref": "#/definitions/Reference" - }, - "selectableEnvironmentAttributes": { - "$ref": "#/definitions/Reference" - }, - "defaultEnvironmentAttributes": { - "$ref": "#/definitions/Reference" - }, - "accessPermissionRule": { - "type": "array", - "items": { - "$ref": "#/definitions/AccessPermissionRule" - } - } - } - }, - "AccessPermissionRule": { - "allOf": [ - { "$ref": "#/definitions/Referable" }, - { "$ref": "#/definitions/Qualifiable" }, - { "properties": { - "targetSubjectAttributes": { - "type": "array", - "items": { - "$ref": "#/definitions/SubjectAttributes" - }, - "minItems": 1 - }, - "permissionsPerObject": { - "type": "array", - "items": { - "$ref": "#/definitions/PermissionsPerObject" - } - } - }, - "required": [ "targetSubjectAttributes" ] - } - ] - }, - "SubjectAttributes": { + "ValueList": { "type": "object", "properties": { - "subjectAttributes": { + "valueReferencePairs": { "type": "array", "items": { - "$ref": "#/definitions/Reference" + "$ref": "#/definitions/ValueReferencePair" }, "minItems": 1 } - } + }, + "required": [ + "valueReferencePairs" + ] }, - "PermissionsPerObject": { + "ValueReferencePair": { "type": "object", "properties": { - "object": { - "$ref": "#/definitions/Reference" - }, - "targetObjectAttributes": { - "$ref": "#/definitions/ObjectAttributes" + "value": { + "type": "string" }, - "permission": { - "type": "array", - "items": { - "$ref": "#/definitions/Permission" - } - } - } - }, - "ObjectAttributes": { - "type": "object", - "properties": { - "objectAttribute": { - "type": "array", - "items": { - "$ref": "#/definitions/Property" - }, - "minItems": 1 - } - } - }, - "Permission": { - "type": "object", - "properties": { - "permission": { + "valueId": { "$ref": "#/definitions/Reference" - }, - "kindOfPermission": { - "type": "string", - "enum": ["Allow", "Deny", "NotApplicable", "Undefined"] } }, - "required": [ "permission", "kindOfPermission" ] + "required": [ + "value", + "valueId" + ] } } -} +} \ No newline at end of file diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index d64dc80..4f6be4e 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -362,7 +362,7 @@ def _construct_lang_string_set(cls, lst: List[Dict[str, object]]) -> Optional[mo logger.error(error_message, exc_info=e) else: raise type(e)(error_message) from e - return ret + return model.LangStringSet(ret) @classmethod def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList: diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 44eadb4..2858e33 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -1,632 +1,1331 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 0511b0c..694d373 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -50,7 +50,7 @@ import enum from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar -from .xml_serialization import NS_AAS, NS_ABAC, NS_IEC +from .xml_serialization import NS_AAS from .._generic import MODELING_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 @@ -525,13 +525,14 @@ def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ def construct_key(cls, element: etree.Element, object_class=model.Key, **_kwargs: Any) \ -> model.Key: return object_class( - _get_attrib_mandatory_mapped(element, "type", KEY_TYPES_INVERSE), - _get_text_mandatory(element) + _child_text_mandatory_mapped(element, NS_AAS + "type", KEY_TYPES_INVERSE), + _child_text_mandatory(element, NS_AAS + "value") ) @classmethod def construct_reference(cls, element: etree.Element, namespace: str = NS_AAS, **kwargs: Any) -> model.Reference: - reference_type: Type[model.Reference] = _get_attrib_mandatory_mapped(element, "type", REFERENCE_TYPES_INVERSE) + reference_type: Type[model.Reference] = _child_text_mandatory_mapped(element, NS_AAS + "type", + REFERENCE_TYPES_INVERSE) references: Dict[Type[model.Reference], Callable[..., model.Reference]] = { model.GlobalReference: cls.construct_global_reference, model.ModelReference: cls.construct_model_reference @@ -597,10 +598,11 @@ def construct_lang_string_set(cls, element: etree.Element, namespace: str = NS_A """ This function doesn't support the object_class parameter, because LangStringSet is just a generic type alias. """ - lss: model.LangStringSet = {} + lss: Dict[str, str] = {} for lang_string in _get_all_children_expect_tag(element, namespace + "langString", cls.failsafe): - lss[_get_attrib_mandatory(lang_string, "lang")] = _get_text_mandatory(lang_string) - return lss + lss[_child_text_mandatory(lang_string, namespace + "language")] = _child_text_mandatory(lang_string, + namespace + "text") + return model.LangStringSet(lss) @classmethod def construct_qualifier(cls, element: etree.Element, object_class=model.Qualifier, **_kwargs: Any) \ @@ -958,9 +960,9 @@ def construct_asset_administration_shell(cls, element: etree.Element, object_cla cls.construct_asset_information) ) if not cls.stripped: - submodels = element.find(NS_AAS + "submodelRefs") + submodels = element.find(NS_AAS + "submodels") if submodels is not None: - for ref in _child_construct_multiple(submodels, NS_AAS + "submodelRef", + for ref in _child_construct_multiple(submodels, NS_AAS + "reference", cls._construct_submodel_reference, cls.failsafe): aas.submodel.add(ref) derived_from = _failsafe_construct(element.find(NS_AAS + "derivedFrom"), @@ -1037,8 +1039,8 @@ def construct_value_reference_pair(cls, element: etree.Element, value_format: Op raise ValueError("No value format given!") return object_class( value_format, - model.datatypes.from_xsd(_child_text_mandatory(element, NS_IEC + "value"), value_format), - _child_construct_mandatory(element, NS_IEC + "valueId", cls.construct_reference) + model.datatypes.from_xsd(_child_text_mandatory(element, NS_AAS + "value"), value_format), + _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference) ) @classmethod @@ -1048,7 +1050,7 @@ def construct_value_list(cls, element: etree.Element, value_format: Optional[mod This function doesn't support the object_class parameter, because ValueList is just a generic type alias. """ return set( - _child_construct_multiple(element, NS_IEC + "valueReferencePair", cls.construct_value_reference_pair, + _child_construct_multiple(element, NS_AAS + "valueReferencePair", cls.construct_value_reference_pair, cls.failsafe, value_format=value_format) ) @@ -1061,46 +1063,46 @@ def construct_iec61360_concept_description(cls, element: etree.Element, raise ValueError("No identifier given!") cd = object_class( identifier, - _child_construct_mandatory(element, NS_IEC + "preferredName", cls.construct_lang_string_set) + _child_construct_mandatory(element, NS_AAS + "preferredName", cls.construct_lang_string_set) ) - data_type = _get_text_mapped_or_none(element.find(NS_IEC + "dataType"), IEC61360_DATA_TYPES_INVERSE) + data_type = _get_text_mapped_or_none(element.find(NS_AAS + "dataType"), IEC61360_DATA_TYPES_INVERSE) if data_type is not None: cd.data_type = data_type - definition = _failsafe_construct(element.find(NS_IEC + "definition"), cls.construct_lang_string_set, + definition = _failsafe_construct(element.find(NS_AAS + "definition"), cls.construct_lang_string_set, cls.failsafe) if definition is not None: cd.definition = definition - short_name = _failsafe_construct(element.find(NS_IEC + "shortName"), cls.construct_lang_string_set, + short_name = _failsafe_construct(element.find(NS_AAS + "shortName"), cls.construct_lang_string_set, cls.failsafe) if short_name is not None: cd.short_name = short_name - unit = _get_text_or_none(element.find(NS_IEC + "unit")) + unit = _get_text_or_none(element.find(NS_AAS + "unit")) if unit is not None: cd.unit = unit - unit_id = _failsafe_construct(element.find(NS_IEC + "unitId"), cls.construct_reference, cls.failsafe) + unit_id = _failsafe_construct(element.find(NS_AAS + "unitId"), cls.construct_reference, cls.failsafe) if unit_id is not None: cd.unit_id = unit_id - source_of_definition = _get_text_or_none(element.find(NS_IEC + "sourceOfDefinition")) + source_of_definition = _get_text_or_none(element.find(NS_AAS + "sourceOfDefinition")) if source_of_definition is not None: cd.source_of_definition = source_of_definition - symbol = _get_text_or_none(element.find(NS_IEC + "symbol")) + symbol = _get_text_or_none(element.find(NS_AAS + "symbol")) if symbol is not None: cd.symbol = symbol - value_format = _get_text_mapped_or_none(element.find(NS_IEC + "valueFormat"), + value_format = _get_text_mapped_or_none(element.find(NS_AAS + "valueFormat"), model.datatypes.XSD_TYPE_CLASSES) if value_format is not None: cd.value_format = value_format - value_list = _failsafe_construct(element.find(NS_IEC + "valueList"), cls.construct_value_list, cls.failsafe, + value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe, value_format=value_format) if value_list is not None: cd.value_list = value_list - value = _get_text_or_none(element.find(NS_IEC + "value")) + value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None and value_format is not None: cd.value = model.datatypes.from_xsd(value, value_format) - value_id = _failsafe_construct(element.find(NS_IEC + "valueId"), cls.construct_reference, cls.failsafe) + value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) if value_id is not None: cd.value_id = value_id - for level_type_element in element.findall(NS_IEC + "levelType"): + for level_type_element in element.findall(NS_AAS + "levelType"): level_type = _get_text_mapped_or_none(level_type_element, IEC61360_LEVEL_TYPES_INVERSE) if level_type is None: error_message = f"{_element_pretty_identifier(level_type_element)} has invalid value: " \ @@ -1336,6 +1338,8 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_iec61360_concept_description elif construct == XMLConstructables.CONCEPT_DESCRIPTION: constructor = decoder_.construct_concept_description + elif construct == XMLConstructables.LANG_STRING_SET: + constructor = decoder_.construct_lang_string_set # the following constructors decide which constructor to call based on the elements tag elif construct == XMLConstructables.DATA_ELEMENT: constructor = decoder_.construct_data_element @@ -1344,8 +1348,6 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool # type aliases elif construct == XMLConstructables.VALUE_LIST: constructor = decoder_.construct_value_list - elif construct == XMLConstructables.LANG_STRING_SET: - constructor = decoder_.construct_lang_string_set else: raise ValueError(f"{construct.name} cannot be constructed!") diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 76999a3..b7e8e0f 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -32,18 +32,10 @@ # ############################################################## # Namespace definition -NS_AAS = "{http://www.admin-shell.io/aas/3/0}" -NS_ABAC = "{http://www.admin-shell.io/aas/abac/3/0}" -NS_AAS_COMMON = "{http://www.admin-shell.io/aas_common/3/0}" -NS_XSI = "{http://www.w3.org/2001/XMLSchema-instance}" -NS_XS = "{http://www.w3.org/2001/XMLSchema}" -NS_IEC = "{http://www.admin-shell.io/IEC61360/3/0}" -NS_MAP = {"aas": "http://www.admin-shell.io/aas/3/0", - "abac": "http://www.admin-shell.io/aas/abac/3/0", - "aas_common": "http://www.admin-shell.io/aas_common/3/0", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", - "IEC": "http://www.admin-shell.io/IEC61360/3/0", - "xs": "http://www.w3.org/2001/XMLSchema"} +NS_AAS = "{https://admin-shell.io/aas/3/0/RC02}" +NS_ABAC = "{http://admin-shell.io/aas/abac/3/0/RC02}" +NS_MAP = {"aas": "https://admin-shell.io/aas/3/0/RC02", + "abac": "https://admin-shell.io/aas/abac/3/0/RC02"} def _generate_element(name: str, @@ -166,10 +158,11 @@ def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: :return: Serialized ElementTree object """ et_lss = _generate_element(name=tag) - for language in obj: - et_lss.append(_generate_element(name=NS_AAS + "langString", - text=obj[language], - attributes={"lang": language})) + for language, text in obj.items(): + et_ls = _generate_element(name=NS_AAS + "langString") + et_ls.append(_generate_element(name=NS_AAS + "language", text=language)) + et_ls.append(_generate_element(name=NS_AAS + "text", text=text)) + et_lss.append(et_ls) return et_lss @@ -183,10 +176,10 @@ def administrative_information_to_xml(obj: model.AdministrativeInformation, :return: Serialized ElementTree object """ et_administration = _generate_element(tag) - if obj.revision: - et_administration.append(_generate_element(name=NS_AAS + "revision", text=obj.revision)) if obj.version: et_administration.append(_generate_element(name=NS_AAS + "version", text=obj.version)) + if obj.revision: + et_administration.append(_generate_element(name=NS_AAS + "revision", text=obj.revision)) return et_administration @@ -219,15 +212,18 @@ def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etr :param tag: Namespace+Tag of the returned element. Default is "aas:reference" :return: Serialized ElementTree """ - et_reference = _generate_element(tag, attributes={"type": _generic.REFERENCE_TYPES[obj.__class__]}) + et_reference = _generate_element(tag) + et_reference.append(_generate_element(NS_AAS + "type", text=_generic.REFERENCE_TYPES[obj.__class__])) + if obj.referred_semantic_id is not None: + et_reference.append(reference_to_xml(obj.referred_semantic_id, NS_AAS + "referredSemanticId")) et_keys = _generate_element(name=NS_AAS + "keys") for aas_key in obj.key: - et_keys.append(_generate_element(name=NS_AAS + "key", - text=aas_key.value, - attributes={"type": _generic.KEY_TYPES[aas_key.type]})) + et_key = _generate_element(name=NS_AAS + "key") + et_key.append(_generate_element(name=NS_AAS + "type", text=_generic.KEY_TYPES[aas_key.type])) + et_key.append(_generate_element(name=NS_AAS + "value", text=aas_key.value)) + et_keys.append(et_key) et_reference.append(et_keys) - if obj.referred_semantic_id is not None: - et_reference.append(reference_to_xml(obj.referred_semantic_id, NS_AAS + "referredSemanticId")) + return et_reference @@ -320,9 +316,9 @@ def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "sp :return: Serialized ElementTree object """ et_asset_information = abstract_classes_to_xml(tag, obj) - et_asset_information.append(reference_to_xml(obj.external_subject_id, NS_AAS + "externalSubjectId")) et_asset_information.append(_generate_element(name=NS_AAS + "name", text=obj.name)) et_asset_information.append(_generate_element(name=NS_AAS + "value", text=obj.value)) + et_asset_information.append(reference_to_xml(obj.external_subject_id, NS_AAS + "externalSubjectId")) return et_asset_information @@ -336,16 +332,16 @@ def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"ass :return: Serialized ElementTree object """ et_asset_information = abstract_classes_to_xml(tag, obj) - if obj.default_thumbnail: - et_asset_information.append(resource_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbNail")) + et_asset_information.append(_generate_element(name=NS_AAS + "assetKind", text=_generic.ASSET_KIND[obj.asset_kind])) if obj.global_asset_id: et_asset_information.append(reference_to_xml(obj.global_asset_id, NS_AAS + "globalAssetId")) - et_asset_information.append(_generate_element(name=NS_AAS + "assetKind", text=_generic.ASSET_KIND[obj.asset_kind])) - et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") if obj.specific_asset_id: + et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") for specific_asset_id in obj.specific_asset_id: et_specific_asset_id.append(specific_asset_id_to_xml(specific_asset_id, NS_AAS + "specificAssetId")) - et_asset_information.append(et_specific_asset_id) + et_asset_information.append(et_specific_asset_id) + if obj.default_thumbnail: + et_asset_information.append(resource_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbNail")) return et_asset_information @@ -393,7 +389,7 @@ def _iec61360_concept_description_to_xml(obj: model.concept.IEC61360ConceptDescr """ def _iec_value_reference_pair_to_xml(vrp: model.ValueReferencePair, - vrp_tag: str = NS_IEC + "valueReferencePair") -> etree.Element: + vrp_tag: str = NS_AAS + "valueReferencePair") -> etree.Element: """ serialization of objects of class ValueReferencePair to XML @@ -402,12 +398,12 @@ def _iec_value_reference_pair_to_xml(vrp: model.ValueReferencePair, :return: serialized ElementTree object """ et_vrp = _generate_element(vrp_tag) - et_vrp.append(reference_to_xml(vrp.value_id, NS_IEC + "valueId")) - et_vrp.append(_value_to_xml(vrp.value, vrp.value_type, tag=NS_IEC+"value")) + et_vrp.append(reference_to_xml(vrp.value_id, NS_AAS + "valueId")) + et_vrp.append(_value_to_xml(vrp.value, vrp.value_type, tag=NS_AAS + "value")) return et_vrp def _iec_value_list_to_xml(vl: model.ValueList, - vl_tag: str = NS_IEC + "valueList") -> etree.Element: + vl_tag: str = NS_AAS + "valueList") -> etree.Element: """ serialization of objects of class ValueList to XML @@ -417,36 +413,36 @@ def _iec_value_list_to_xml(vl: model.ValueList, """ et_value_list = _generate_element(vl_tag) for aas_reference_pair in vl: - et_value_list.append(_iec_value_reference_pair_to_xml(aas_reference_pair, NS_IEC+"valueReferencePair")) + et_value_list.append(_iec_value_reference_pair_to_xml(aas_reference_pair, NS_AAS + "valueReferencePair")) return et_value_list et_iec = _generate_element(tag) - et_iec.append(lang_string_set_to_xml(obj.preferred_name, NS_IEC + "preferredName")) + et_iec.append(lang_string_set_to_xml(obj.preferred_name, NS_AAS + "preferredName")) if obj.short_name: - et_iec.append(lang_string_set_to_xml(obj.short_name, NS_IEC + "shortName")) + et_iec.append(lang_string_set_to_xml(obj.short_name, NS_AAS + "shortName")) if obj.unit: - et_iec.append(_generate_element(NS_IEC+"unit", text=obj.unit)) + et_iec.append(_generate_element(NS_AAS + "unit", text=obj.unit)) if obj.unit_id: - et_iec.append(reference_to_xml(obj.unit_id, NS_IEC+"unitId")) + et_iec.append(reference_to_xml(obj.unit_id, NS_AAS + "unitId")) if obj.source_of_definition: - et_iec.append(_generate_element(NS_IEC+"sourceOfDefinition", text=obj.source_of_definition)) + et_iec.append(_generate_element(NS_AAS + "sourceOfDefinition", text=obj.source_of_definition)) if obj.symbol: - et_iec.append(_generate_element(NS_IEC+"symbol", text=obj.symbol)) + et_iec.append(_generate_element(NS_AAS + "symbol", text=obj.symbol)) if obj.data_type: - et_iec.append(_generate_element(NS_IEC+"dataType", text=_generic.IEC61360_DATA_TYPES[obj.data_type])) + et_iec.append(_generate_element(NS_AAS + "dataType", text=_generic.IEC61360_DATA_TYPES[obj.data_type])) if obj.definition: - et_iec.append(lang_string_set_to_xml(obj.definition, NS_IEC + "definition")) + et_iec.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition")) if obj.value_format: - et_iec.append(_generate_element(NS_IEC+"valueFormat", text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) + et_iec.append(_generate_element(NS_AAS + "valueFormat", text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) if obj.value_list: - et_iec.append(_iec_value_list_to_xml(obj.value_list, NS_IEC+"valueList")) + et_iec.append(_iec_value_list_to_xml(obj.value_list, NS_AAS + "valueList")) if obj.value: - et_iec.append(_generate_element(NS_IEC+"value", text=model.datatypes.xsd_repr(obj.value))) + et_iec.append(_generate_element(NS_AAS + "value", text=model.datatypes.xsd_repr(obj.value))) if obj.value_id: - et_iec.append(reference_to_xml(obj.value_id, NS_IEC+"valueId")) + et_iec.append(reference_to_xml(obj.value_id, NS_AAS + "valueId")) if obj.level_types: for level_type in obj.level_types: - et_iec.append(_generate_element(NS_IEC+"levelType", text=_generic.IEC61360_LEVEL_TYPES[level_type])) + et_iec.append(_generate_element(NS_AAS + "levelType", text=_generic.IEC61360_LEVEL_TYPES[level_type])) return et_iec @@ -462,12 +458,12 @@ def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, et_aas = abstract_classes_to_xml(tag, obj) if obj.derived_from: et_aas.append(reference_to_xml(obj.derived_from, tag=NS_AAS+"derivedFrom")) + et_aas.append(asset_information_to_xml(obj.asset_information, tag=NS_AAS + "assetInformation")) if obj.submodel: - et_submodels = _generate_element(NS_AAS + "submodelRefs") + et_submodels = _generate_element(NS_AAS + "submodels") for reference in obj.submodel: - et_submodels.append(reference_to_xml(reference, tag=NS_AAS+"submodelRef")) + et_submodels.append(reference_to_xml(reference, tag=NS_AAS+"reference")) et_aas.append(et_submodels) - et_aas.append(asset_information_to_xml(obj.asset_information, tag=NS_AAS + "assetInformation")) return et_aas @@ -875,7 +871,7 @@ def write_aas_xml_file(file: IO, concept_descriptions.append(obj) # serialize objects to XML - root = etree.Element(NS_AAS + "aasenv", nsmap=NS_MAP) + root = etree.Element(NS_AAS + "environment", nsmap=NS_MAP) et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") for aas_obj in asset_administration_shells: et_asset_administration_shells.append(asset_administration_shell_to_xml(aas_obj)) From f23a3d51af743556da60bba06aff9a6b26b37ab8 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 20 Mar 2023 16:37:49 +0100 Subject: [PATCH 058/474] Refactor AASToJsonEncoder.default() - Use a dictionary called mapping that maps each object type to its corresponding serialization method. --- basyx/aas/adapter/json/json_serialization.py | 87 ++++++++------------ 1 file changed, 33 insertions(+), 54 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index d87c2c9..ab1691c 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -63,60 +63,39 @@ def default(self, obj: object) -> object: :param obj: The object to serialize to json :return: The serialized object """ - if isinstance(obj, model.AssetAdministrationShell): - return self._asset_administration_shell_to_json(obj) - if isinstance(obj, model.AdministrativeInformation): - return self._administrative_information_to_json(obj) - if isinstance(obj, model.Reference): - return self._reference_to_json(obj) - if isinstance(obj, model.Key): - return self._key_to_json(obj) - if isinstance(obj, model.ValueReferencePair): - return self._value_reference_pair_to_json(obj) - if isinstance(obj, model.AssetInformation): - return self._asset_information_to_json(obj) - if isinstance(obj, model.SpecificAssetId): - return self._specific_asset_id_to_json(obj) - if isinstance(obj, model.Submodel): - return self._submodel_to_json(obj) - if isinstance(obj, model.Operation): - return self._operation_to_json(obj) - if isinstance(obj, model.OperationVariable): - return self._operation_variable_to_json(obj) - if isinstance(obj, model.Capability): - return self._capability_to_json(obj) - if isinstance(obj, model.BasicEventElement): - return self._basic_event_element_to_json(obj) - if isinstance(obj, model.Entity): - return self._entity_to_json(obj) - if isinstance(obj, model.ConceptDescription): - return self._concept_description_to_json(obj) - if isinstance(obj, model.Property): - return self._property_to_json(obj) - if isinstance(obj, model.Range): - return self._range_to_json(obj) - if isinstance(obj, model.MultiLanguageProperty): - return self._multi_language_property_to_json(obj) - if isinstance(obj, model.File): - return self._file_to_json(obj) - if isinstance(obj, model.Resource): - return self._resource_to_json(obj) - if isinstance(obj, model.Blob): - return self._blob_to_json(obj) - if isinstance(obj, model.ReferenceElement): - return self._reference_element_to_json(obj) - if isinstance(obj, model.SubmodelElementCollection): - return self._submodel_element_collection_to_json(obj) - if isinstance(obj, model.SubmodelElementList): - return self._submodel_element_list_to_json(obj) - if isinstance(obj, model.AnnotatedRelationshipElement): - return self._annotated_relationship_element_to_json(obj) - if isinstance(obj, model.RelationshipElement): - return self._relationship_element_to_json(obj) - if isinstance(obj, model.Qualifier): - return self._qualifier_to_json(obj) - if isinstance(obj, model.Extension): - return self._extension_to_json(obj) + mapping = { + model.AdministrativeInformation: self._administrative_information_to_json, + model.AnnotatedRelationshipElement: self._annotated_relationship_element_to_json, + model.AssetAdministrationShell: self._asset_administration_shell_to_json, + model.AssetInformation: self._asset_information_to_json, + model.BasicEventElement: self._basic_event_element_to_json, + model.Blob: self._blob_to_json, + model.Capability: self._capability_to_json, + model.ConceptDescription: self._concept_description_to_json, + model.Entity: self._entity_to_json, + model.Extension: self._extension_to_json, + model.File: self._file_to_json, + model.Key: self._key_to_json, + model.MultiLanguageProperty: self._multi_language_property_to_json, + model.Operation: self._operation_to_json, + model.OperationVariable: self._operation_variable_to_json, + model.Property: self._property_to_json, + model.Qualifier: self._qualifier_to_json, + model.Range: self._range_to_json, + model.Reference: self._reference_to_json, + model.ReferenceElement: self._reference_element_to_json, + model.RelationshipElement: self._relationship_element_to_json, + model.Resource: self._resource_to_json, + model.SpecificAssetId: self._specific_asset_id_to_json, + model.Submodel: self._submodel_to_json, + model.SubmodelElementCollection: self._submodel_element_collection_to_json, + model.SubmodelElementList: self._submodel_element_list_to_json, + model.ValueReferencePair: self._value_reference_pair_to_json, + } + for typ in mapping: + if isinstance(obj, typ): + mapping_method = mapping[typ] + return mapping_method(obj) return super().default(obj) @classmethod From b8dfff683e6dd10e300b0a8ffd2ff419541cb224 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 20 Mar 2023 17:45:45 +0100 Subject: [PATCH 059/474] Refactor AnnotatedRelationshipElement for JSON annotation->in JSON: annotations --- basyx/aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/json/json_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 4f6be4e..b0b9136 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -610,7 +610,7 @@ def _construct_annotated_relationship_element( second=cls._construct_reference(_get_ts(dct, 'second', dict)), kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) - if not cls.stripped and 'annotation' in dct: + if not cls.stripped and 'annotations' in dct: for element in _get_ts(dct, "annotation", list): if _expect_type(element, model.DataElement, str(ret), cls.failsafe): ret.annotation.add(element) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index ab1691c..e9cd83c 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -573,7 +573,7 @@ def _annotated_relationship_element_to_json(cls, obj: model.AnnotatedRelationshi data = cls._abstract_classes_to_json(obj) data.update({'first': obj.first, 'second': obj.second}) if not cls.stripped and obj.annotation: - data['annotation'] = list(obj.annotation) + data['annotations'] = list(obj.annotation) return data @classmethod From 5307609222862c089992492619b0699f02292aad Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 20 Mar 2023 17:47:06 +0100 Subject: [PATCH 060/474] Refactor Operation for JSON inputVariable->in JSON: inputVariables outputVariable->in JSON: outputVariables inoutputVariable->in JSON: inoutputVariables --- basyx/aas/adapter/json/json_deserialization.py | 6 +++--- basyx/aas/adapter/json/json_serialization.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index b0b9136..7426a30 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -570,9 +570,9 @@ def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operati cls._amend_abstract_attributes(ret, dct) # Deserialize variables (they are not Referable, thus we don't - for json_name, target in (('inputVariable', ret.input_variable), - ('outputVariable', ret.output_variable), - ('inoutputVariable', ret.in_output_variable)): + for json_name, target in (('inputVariables', ret.input_variable), + ('outputVariables', ret.output_variable), + ('inoutputVariables', ret.in_output_variable)): if json_name in dct: for variable_data in _get_ts(dct, json_name, list): try: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index e9cd83c..0efff53 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -598,11 +598,11 @@ def _operation_to_json(cls, obj: model.Operation) -> Dict[str, object]: """ data = cls._abstract_classes_to_json(obj) if obj.input_variable: - data['inputVariable'] = list(obj.input_variable) + data['inputVariables'] = list(obj.input_variable) if obj.output_variable: - data['outputVariable'] = list(obj.output_variable) + data['outputVariables'] = list(obj.output_variable) if obj.in_output_variable: - data['inoutputVariable'] = list(obj.in_output_variable) + data['inoutputVariables'] = list(obj.in_output_variable) return data @classmethod From 30f0870de0bbe3dcdbaf634642059745f2ca5ef8 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 20 Mar 2023 18:04:50 +0100 Subject: [PATCH 061/474] Refactor AssetInformation for JSON externalAssetIds->specificAssetIds thumbnail->defaultThumbnail --- basyx/aas/adapter/json/json_deserialization.py | 8 ++++---- basyx/aas/adapter/json/json_serialization.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 7426a30..9515084 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -401,12 +401,12 @@ def _construct_asset_information(cls, dct: Dict[str, object], object_class=model cls._amend_abstract_attributes(ret, dct) if 'globalAssetId' in dct: ret.global_asset_id = cls._construct_reference(_get_ts(dct, 'globalAssetId', dict)) - if 'externalAssetIds' in dct: - for desc_data in _get_ts(dct, "externalAssetIds", list): + if 'specificAssetIds' in dct: + for desc_data in _get_ts(dct, "specificAssetIds", list): ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) - if 'thumbnail' in dct: - ret.default_thumbnail = cls._construct_resource(_get_ts(dct, 'thumbnail', dict)) + if 'defaultThumbnail' in dct: + ret.default_thumbnail = cls._construct_resource(_get_ts(dct, 'defaultThumbnail', dict)) return ret @classmethod diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 0efff53..0058efd 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -294,9 +294,9 @@ def _asset_information_to_json(cls, obj: model.AssetInformation) -> Dict[str, ob if obj.global_asset_id: data['globalAssetId'] = obj.global_asset_id if obj.specific_asset_id: - data['externalAssetIds'] = list(obj.specific_asset_id) + data['specificAssetIds'] = list(obj.specific_asset_id) if obj.default_thumbnail: - data['thumbnail'] = obj.default_thumbnail + data['defaultThumbnail'] = obj.default_thumbnail return data @classmethod From 90249cb5a6045b6c2bf85e33c1821e690f333138 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 20 Mar 2023 18:13:31 +0100 Subject: [PATCH 062/474] Refactor Entity for JSON externalAssetIds->specificAssetIds --- basyx/aas/adapter/json/json_deserialization.py | 4 ++-- basyx/aas/adapter/json/json_serialization.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 9515084..bbc4788 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -485,8 +485,8 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> if 'globalAssetId' in dct: global_asset_id = cls._construct_reference(_get_ts(dct, 'globalAssetId', dict)) specific_asset_id = None - if 'externalAssetId' in dct: - specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'externalAssetId', dict)) + if 'specificAssetIds' in dct: + specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'specificAssetIds', dict)) ret = object_class(id_short=_get_ts(dct, "idShort", str), entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 0058efd..3629f7f 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -632,7 +632,7 @@ def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]: if obj.global_asset_id: data['globalAssetId'] = obj.global_asset_id if obj.specific_asset_id: - data['externalAssetId'] = obj.specific_asset_id + data['specificAssetIds'] = obj.specific_asset_id return data @classmethod From 074b8ed8a21c804e1f553d44091afd4df6d633ab Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 20 Mar 2023 18:33:13 +0100 Subject: [PATCH 063/474] Refactor SpecificAssetId for JSON subjectId->externalSubjectId --- basyx/aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/json/json_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index bbc4788..6bdb21a 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -288,7 +288,7 @@ def _construct_specific_asset_id(cls, dct: Dict[str, object], object_class=model # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable return object_class(name=_get_ts(dct, 'name', str), value=_get_ts(dct, 'value', str), - external_subject_id=cls._construct_global_reference(_get_ts(dct, 'subjectId', dict)), + external_subject_id=cls._construct_global_reference(_get_ts(dct, 'externalSubjectId', dict)), semantic_id=cls._construct_reference(_get_ts(dct, 'semanticId', dict)) if 'semanticId' in dct else None) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 3629f7f..dd35faf 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -278,7 +278,7 @@ def _specific_asset_id_to_json(cls, obj: model.SpecificAssetId) -> Dict[str, obj data = cls._abstract_classes_to_json(obj) data['name'] = obj.name data['value'] = obj.value - data['subjectId'] = obj.external_subject_id + data['externalSubjectId'] = obj.external_subject_id return data @classmethod From a30351d9f206ff7a3c60b788a80a97ba58cc8be1 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 13:50:39 +0100 Subject: [PATCH 064/474] Save names in "modelType", not in dict --- basyx/aas/adapter/json/json_deserialization.py | 4 ++-- basyx/aas/adapter/json/json_serialization.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 6bdb21a..6bb8f13 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -190,13 +190,13 @@ def object_hook(cls, dct: Dict[str, object]) -> object: } # Get modelType and constructor function - if not isinstance(dct['modelType'], dict) or 'name' not in dct['modelType']: + if not isinstance(dct['modelType'], str) or dct['modelType'] not in dct['modelType']: logger.warning("JSON object has unexpected format of modelType: %s", dct['modelType']) # Even in strict mode, we consider 'modelType' attributes of wrong type as non-AAS objects instead of # raising an exception. However, the object's type will probably checked later by read_json_aas_file() or # _expect_type() return dct - model_type = dct['modelType']['name'] + model_type = dct['modelType'] if model_type not in AAS_CLASS_PARSERS: if not cls.failsafe: raise TypeError("Found JSON object with modelType=\"%s\", which is not a known AAS class" % model_type) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index dd35faf..165aaa5 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -124,7 +124,7 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: except StopIteration as e: raise TypeError("Object of type {} is Referable but does not inherit from a known AAS type" .format(obj.__class__.__name__)) from e - data['modelType'] = {'name': ref_type.__name__} + data['modelType'] = ref_type.__name__ if isinstance(obj, model.Identifiable): data['id'] = obj.id if obj.administration: @@ -212,7 +212,7 @@ def _qualifier_to_json(cls, obj: model.Qualifier) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data['modelType'] = {'name': model.Qualifier.__name__} + data['modelType'] = model.Qualifier.__name__ if obj.value: data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None if obj.value_id: From f1d0f3b97043a5a615e272ad23aaf6d5519622db Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 13:52:26 +0100 Subject: [PATCH 065/474] Remove AAS, Submodels and CS lists, as not required in Schema --- basyx/aas/adapter/json/json_serialization.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 165aaa5..48bd206 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -704,11 +704,13 @@ def _create_dict(data: model.AbstractObjectStore) -> dict: submodels.append(obj) elif isinstance(obj, model.ConceptDescription): concept_descriptions.append(obj) - dict_ = { - 'assetAdministrationShells': asset_administration_shells, - 'submodels': submodels, - 'conceptDescriptions': concept_descriptions, - } + dict_ = {} + if asset_administration_shells: + dict_['assetAdministrationShells'] = asset_administration_shells + if submodels: + dict_['submodels'] = submodels + if concept_descriptions: + dict_['conceptDescriptions'] = concept_descriptions return dict_ From aa2cb7c2acc5bffff145c9001a2f0553b2d4c3cb Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 14:20:54 +0100 Subject: [PATCH 066/474] Remove Req, that AAS, Submodels and CS lists must be in JSON --- basyx/aas/adapter/json/json_deserialization.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 6bb8f13..8d920c4 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -813,12 +813,9 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r try: lst = _get_ts(data, name, list) except (KeyError, TypeError) as e: - error_message = "Could not find list '{}' in AAS JSON file".format(name) - if decoder_.failsafe: - logger.warning(error_message) - continue - else: - raise type(e)(error_message) from e + info_message = "Could not find list '{}' in AAS JSON file".format(name) + logger.info(info_message) + continue for item in lst: error_message = "Expected a {} in list '{}', but found {}".format( From a927548ff07f46d1571a89e876f9531d2cb4372d Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 15:37:02 +0100 Subject: [PATCH 067/474] Fix json serialisation for File 'value' is not required in Schema --- basyx/aas/adapter/json/json_serialization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 48bd206..cbcb1b0 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -488,7 +488,9 @@ def _file_to_json(cls, obj: model.File) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data.update({'value': obj.value, 'contentType': obj.content_type}) + data['contentType'] = obj.content_type + if obj.value is not None: + data['value'] = obj.value return data @classmethod From 800eeb81c2020c0803b149b60ca8089d4b666acf Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 16:41:34 +0100 Subject: [PATCH 068/474] Fix json serialisation for Property 'value' is optional in Schema --- basyx/aas/adapter/json/json_serialization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index cbcb1b0..770273e 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -430,7 +430,8 @@ def _property_to_json(cls, obj: model.Property) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None + if obj.value is not None: + data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value_id: data['valueId'] = obj.value_id data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] From 6f016008c8d9063861916b5474ca5080d72f915a Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 18:04:03 +0100 Subject: [PATCH 069/474] Add Qualifier.kind and remove "modelType" for Qualifier - Add `kind` attribute to Qualifier class - Init QualifierKind enum class - remove "modelType" in json de-/serialisation for Qualifier --- basyx/aas/adapter/_generic.py | 6 ++++++ basyx/aas/adapter/json/json_deserialization.py | 11 ++++++----- basyx/aas/adapter/json/json_serialization.py | 3 ++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index b8da1df..61efb8e 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -20,6 +20,11 @@ model.AssetKind.TYPE: 'Type', model.AssetKind.INSTANCE: 'Instance'} +QUALIFIER_KIND: Dict[model.QualifierKind, str] = { + model.QualifierKind.CONCEPT_QUALIFIER: 'ConceptQualifier', + model.QualifierKind.TEMPLATE_QUALIFIER: 'TemplateQualifier', + model.QualifierKind.VALUE_QUALIFIER: 'ValueQualifier'} + DIRECTION: Dict[model.Direction, str] = { model.Direction.INPUT: 'input', model.Direction.OUTPUT: 'output'} @@ -83,6 +88,7 @@ MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} +QUALIFIER_KIND_INVERSE: Dict[str, model.QualifierKind] = {v: k for k, v in QUALIFIER_KIND.items()} DIRECTION_INVERSE: Dict[str, model.Direction] = {v: k for k, v in DIRECTION.items()} STATE_OF_EVENT_INVERSE: Dict[str, model.StateOfEvent] = {v: k for k, v in STATE_OF_EVENT.items()} REFERENCE_TYPES_INVERSE: Dict[str, Type[model.Reference]] = {v: k for k, v in REFERENCE_TYPES.items()} diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 8d920c4..f70c308 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -39,7 +39,7 @@ from basyx.aas import model from .._generic import MODELING_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 + DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE logger = logging.getLogger(__name__) @@ -170,7 +170,6 @@ def object_hook(cls, dct: Dict[str, object]) -> object: 'AssetInformation': cls._construct_asset_information, 'SpecificAssetId': cls._construct_specific_asset_id, 'ConceptDescription': cls._construct_concept_description, - 'Qualifier': cls._construct_qualifier, 'Extension': cls._construct_extension, 'Submodel': cls._construct_submodel, 'Capability': cls._construct_capability, @@ -250,9 +249,9 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None # However, the `cls._get_kind()` function may assist by retrieving them from the JSON object if isinstance(obj, model.Qualifiable) and not cls.stripped: if 'qualifiers' in dct: - for constraint in _get_ts(dct, 'qualifiers', list): - if _expect_type(constraint, model.Qualifier, str(obj), cls.failsafe): - obj.qualifier.add(constraint) + for constraint_dct in _get_ts(dct, 'qualifiers', list): + constraint = cls._construct_qualifier(constraint_dct) + obj.qualifier.add(constraint) if isinstance(obj, model.HasExtension) and not cls.stripped: if 'extensions' in dct: @@ -508,6 +507,8 @@ def _construct_qualifier(cls, dct: Dict[str, object], object_class=model.Qualifi ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) if 'valueId' in dct: ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) + if 'kind' in dct: + ret.kind = QUALIFIER_KIND_INVERSE[_get_ts(dct, 'kind', str)] return ret @classmethod diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 770273e..3c91fb0 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -212,11 +212,12 @@ def _qualifier_to_json(cls, obj: model.Qualifier) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data['modelType'] = model.Qualifier.__name__ if obj.value: data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None if obj.value_id: data['valueId'] = obj.value_id + if obj.kind is not model.QualifierKind.CONCEPT_QUALIFIER: + data['kind'] = _generic.QUALIFIER_KIND[obj.kind] data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] data['type'] = obj.type return data From 8ffeaf9dbb92ca97ee1b407f37aec67bacf30b81 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 18:09:55 +0100 Subject: [PATCH 070/474] Set 'min' and 'max' as optional in json serialisation of `Range` --- basyx/aas/adapter/json/json_serialization.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 3c91fb0..dbfb9a4 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -462,9 +462,11 @@ def _range_to_json(cls, obj: model.Range) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data.update({'valueType': model.datatypes.XSD_TYPE_NAMES[obj.value_type], - 'min': model.datatypes.xsd_repr(obj.min) if obj.min is not None else None, - 'max': model.datatypes.xsd_repr(obj.max) if obj.max is not None else None}) + data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] + if obj.min is not None: + data['min'] = model.datatypes.xsd_repr(obj.min) + if obj.max is not None: + data['max'] = model.datatypes.xsd_repr(obj.max) return data @classmethod From 622c161274a7fed643826dfb84202f345d0b995a Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 21 Mar 2023 18:12:56 +0100 Subject: [PATCH 071/474] Set 'orderRelevant' as optional in json serialisation of `SubmodelElementList` --- basyx/aas/adapter/json/json_serialization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index dbfb9a4..8e9d48f 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -546,8 +546,9 @@ def _submodel_element_list_to_json(cls, obj: model.SubmodelElementList) -> Dict[ :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - data['orderRelevant'] = obj.order_relevant data['typeValueListElement'] = _generic.KEY_TYPES[model.KEY_TYPES_CLASSES[obj.type_value_list_element]] + if obj.order_relevant is not True: + data['orderRelevant'] = obj.order_relevant if obj.semantic_id_list_element is not None: data['semanticIdListElement'] = obj.semantic_id_list_element if obj.value_type_list_element is not None: From 1cf587bd8f71e4730771293e69e2c8c3bf2fc805 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 27 Mar 2023 11:05:44 +0200 Subject: [PATCH 072/474] Add json de-/serial. of 'supplementalSemanticIds' --- basyx/aas/adapter/json/json_deserialization.py | 3 +++ basyx/aas/adapter/json/json_serialization.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index f70c308..ca212b1 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -243,6 +243,9 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if 'administration' in dct: obj.administration = cls._construct_administrative_information(_get_ts(dct, 'administration', dict)) if isinstance(obj, model.HasSemantics): + if 'supplementalSemanticIds' in dct: + obj.supplemental_semantic_id = [cls._construct_reference(ref) # type: ignore + for ref in _get_ts(dct, 'supplementalSemanticIds', list)] if 'semanticId' in dct: obj.semantic_id = cls._construct_reference(_get_ts(dct, 'semanticId', dict)) # `HasKind` provides only mandatory, immutable attributes; so we cannot do anything here, after object creation. diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 8e9d48f..2dab906 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -132,6 +132,8 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: if isinstance(obj, model.HasSemantics): if obj.semantic_id: data['semanticId'] = obj.semantic_id + if obj.supplemental_semantic_id: + data['supplementalSemanticIds'] = list(obj.supplemental_semantic_id) if isinstance(obj, model.HasKind): if obj.kind is model.ModelingKind.TEMPLATE: data['kind'] = _generic.MODELING_KIND[obj.kind] From b9dfe1b611bace4239348db71dae446e9847075c Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 27 Mar 2023 12:00:54 +0200 Subject: [PATCH 073/474] Fix json deserial. of AnnotatedRelationshipElement --- basyx/aas/adapter/json/json_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index ca212b1..95185e7 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -615,7 +615,7 @@ def _construct_annotated_relationship_element( kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'annotations' in dct: - for element in _get_ts(dct, "annotation", list): + for element in _get_ts(dct, 'annotations', list): if _expect_type(element, model.DataElement, str(ret), cls.failsafe): ret.annotation.add(element) return ret From 388b39f2cfcc0fc819ba85a72b13e9591a08cf0e Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 27 Mar 2023 13:37:43 +0200 Subject: [PATCH 074/474] Fix json deserialization of Entity param kind was missing --- basyx/aas/adapter/json/json_deserialization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 95185e7..35d9d5a 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -491,6 +491,7 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'specificAssetIds', dict)) ret = object_class(id_short=_get_ts(dct, "idShort", str), + kind=cls._get_kind(dct), entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], global_asset_id=global_asset_id, specific_asset_id=specific_asset_id) From a6bc26afd20e427f948f398820dc529f86b33570 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 27 Mar 2023 13:54:29 +0200 Subject: [PATCH 075/474] Fix deserialization of SpecificAssetId Add missing attr "supplemental_semantic_id" --- basyx/aas/adapter/json/json_deserialization.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 35d9d5a..bbbe951 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -292,7 +292,12 @@ def _construct_specific_asset_id(cls, dct: Dict[str, object], object_class=model value=_get_ts(dct, 'value', str), external_subject_id=cls._construct_global_reference(_get_ts(dct, 'externalSubjectId', dict)), semantic_id=cls._construct_reference(_get_ts(dct, 'semanticId', dict)) - if 'semanticId' in dct else None) + if 'semanticId' in dct else None, + supplemental_semantic_id=[ + cls._construct_reference(ref) for ref in + _get_ts(dct, 'supplementalSemanticIds',list)] + if 'supplementalSemanticIds' in dct else () + ) @classmethod def _construct_reference(cls, dct: Dict[str, object]) -> model.Reference: From 2faecd404281b7f67d0b4f5761299ce27f3d386b Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 27 Mar 2023 15:59:35 +0200 Subject: [PATCH 076/474] Add `HasDataSpecification` + IEC61360 Classes - Implement `HasDataSpecification` and make it as ancestor of `AssetAdministrationShell`, `Submodel`, `SubmodelElement`, `ConceptDescription`, `AdministrativeInformation`, (conform to V30RC02) - Move `AdministrativeInformation` down, as we want it to be defined after its parent `HasDataSpecification` - Remove `IEC61360ConceptDescription` and move `IEC61360DataType`, `IEC61360LevelType` to base.py - Implement `DataSpecificationIEC61360`, `DataSpecificationPhysicalUnit` and its de-/serialization --- basyx/aas/adapter/_generic.py | 40 +++--- .../aas/adapter/json/json_deserialization.py | 122 ++++++++++++------ basyx/aas/adapter/json/json_serialization.py | 92 ++++++++++--- 3 files changed, 172 insertions(+), 82 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 61efb8e..0f52f1a 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -64,26 +64,26 @@ model.EntityType.CO_MANAGED_ENTITY: 'CoManagedEntity', model.EntityType.SELF_MANAGED_ENTITY: 'SelfManagedEntity'} -IEC61360_DATA_TYPES: Dict[model.concept.IEC61360DataType, str] = { - model.concept.IEC61360DataType.DATE: 'DATE', - model.concept.IEC61360DataType.STRING: 'STRING', - model.concept.IEC61360DataType.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE', - model.concept.IEC61360DataType.REAL_MEASURE: 'REAL_MEASURE', - model.concept.IEC61360DataType.REAL_COUNT: 'REAL_COUNT', - model.concept.IEC61360DataType.REAL_CURRENCY: 'REAL_CURRENCY', - model.concept.IEC61360DataType.BOOLEAN: 'BOOLEAN', - model.concept.IEC61360DataType.URL: 'URL', - model.concept.IEC61360DataType.RATIONAL: 'RATIONAL', - model.concept.IEC61360DataType.RATIONAL_MEASURE: 'RATIONAL_MEASURE', - model.concept.IEC61360DataType.TIME: 'TIME', - model.concept.IEC61360DataType.TIMESTAMP: 'TIMESTAMP', +IEC61360_DATA_TYPES: Dict[model.base.IEC61360DataType, str] = { + model.base.IEC61360DataType.DATE: 'DATE', + model.base.IEC61360DataType.STRING: 'STRING', + model.base.IEC61360DataType.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE', + model.base.IEC61360DataType.REAL_MEASURE: 'REAL_MEASURE', + model.base.IEC61360DataType.REAL_COUNT: 'REAL_COUNT', + model.base.IEC61360DataType.REAL_CURRENCY: 'REAL_CURRENCY', + model.base.IEC61360DataType.BOOLEAN: 'BOOLEAN', + model.base.IEC61360DataType.URL: 'URL', + model.base.IEC61360DataType.RATIONAL: 'RATIONAL', + model.base.IEC61360DataType.RATIONAL_MEASURE: 'RATIONAL_MEASURE', + model.base.IEC61360DataType.TIME: 'TIME', + model.base.IEC61360DataType.TIMESTAMP: 'TIMESTAMP', } -IEC61360_LEVEL_TYPES: Dict[model.concept.IEC61360LevelType, str] = { - model.concept.IEC61360LevelType.MIN: 'Min', - model.concept.IEC61360LevelType.MAX: 'Max', - model.concept.IEC61360LevelType.NOM: 'Nom', - model.concept.IEC61360LevelType.TYP: 'Typ', +IEC61360_LEVEL_TYPES: Dict[model.base.IEC61360LevelType, str] = { + model.base.IEC61360LevelType.MIN: 'Min', + model.base.IEC61360LevelType.MAX: 'Max', + model.base.IEC61360LevelType.NOM: 'Nom', + model.base.IEC61360LevelType.TYP: 'Typ', } MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} @@ -94,8 +94,8 @@ REFERENCE_TYPES_INVERSE: Dict[str, Type[model.Reference]] = {v: k for k, v in REFERENCE_TYPES.items()} KEY_TYPES_INVERSE: Dict[str, model.KeyTypes] = {v: k for k, v in KEY_TYPES.items()} ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()} -IEC61360_DATA_TYPES_INVERSE: Dict[str, model.concept.IEC61360DataType] = {v: k for k, v in IEC61360_DATA_TYPES.items()} -IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.concept.IEC61360LevelType] = \ +IEC61360_DATA_TYPES_INVERSE: Dict[str, model.base.IEC61360DataType] = {v: k for k, v in IEC61360_DATA_TYPES.items()} +IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.base.IEC61360LevelType] = \ {v: k for k, v in IEC61360_LEVEL_TYPES.items()} KEY_TYPES_CLASSES_INVERSE: Dict[model.KeyTypes, Type[model.Referable]] = \ diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index bbbe951..0c8f3b7 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -186,6 +186,8 @@ def object_hook(cls, dct: Dict[str, object]) -> object: 'Property': cls._construct_property, 'Range': cls._construct_range, 'ReferenceElement': cls._construct_reference_element, + 'DataSpecificationIEC61360': cls._construct_iec61360_data_specification_content, + 'DataSpecificationPhysicalUnit': cls._construct_iec61360_physical_unit_data_specification_content, } # Get modelType and constructor function @@ -255,7 +257,19 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None for constraint_dct in _get_ts(dct, 'qualifiers', list): constraint = cls._construct_qualifier(constraint_dct) obj.qualifier.add(constraint) - + if isinstance(obj, model.HasDataSpecification) and not cls.stripped: + if 'embeddedDataSpecifications' in dct: + for dspec in _get_ts(dct, 'embeddedDataSpecifications', list): + dspec_ref = cls._construct_reference( + _get_ts(dspec, 'dataSpecification', dict)) + if "dataSpecificationContent" in dspec: + content = _get_ts(dspec, 'dataSpecificationContent', model.DataSpecificationContent) + obj.embedded_data_specifications.append( + model.Embedded_data_specification( + data_specification=dspec_ref, + data_specification_content=content + ) + ) if isinstance(obj, model.HasExtension) and not cls.stripped: if 'extensions' in dct: for extension in _get_ts(dct, 'extensions', list): @@ -336,6 +350,7 @@ def _construct_administrative_information( cls, dct: Dict[str, object], object_class=model.AdministrativeInformation)\ -> model.AdministrativeInformation: ret = object_class() + cls._amend_abstract_attributes(ret, dct) if 'version' in dct: ret.version = _get_ts(dct, 'version', str) if 'revision' in dct: @@ -435,18 +450,7 @@ def _construct_asset_administration_shell( @classmethod def _construct_concept_description(cls, dct: Dict[str, object], object_class=model.ConceptDescription)\ -> model.ConceptDescription: - # Hack to detect IEC61360ConceptDescriptions, which are represented using dataSpecification according to DotAAS - ret = None - if 'embeddedDataSpecifications' in dct: - for dspec in _get_ts(dct, 'embeddedDataSpecifications', list): - dspec_ref = cls._construct_reference(_get_ts(dspec, 'dataSpecification', dict)) - if dspec_ref.key and (dspec_ref.key[0].value == - "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0"): - ret = cls._construct_iec61360_concept_description( - dct, _get_ts(dspec, 'dataSpecificationContent', dict)) - # If this is not a special ConceptDescription, just construct one of the default object_class - if ret is None: - ret = object_class(id_=_get_ts(dct, 'id', str)) + ret = object_class(id_=_get_ts(dct, 'id', str)) cls._amend_abstract_attributes(ret, dct) if 'isCaseOf' in dct: for case_data in _get_ts(dct, "isCaseOf", list): @@ -454,36 +458,68 @@ def _construct_concept_description(cls, dct: Dict[str, object], object_class=mod return ret @classmethod - def _construct_iec61360_concept_description(cls, dct: Dict[str, object], data_spec: Dict[str, object], - object_class=model.concept.IEC61360ConceptDescription)\ - -> model.concept.IEC61360ConceptDescription: - ret = object_class(id_=_get_ts(dct, 'id', str), - preferred_name=cls._construct_lang_string_set(_get_ts(data_spec, 'preferredName', list))) - if 'dataType' in data_spec: - ret.data_type = IEC61360_DATA_TYPES_INVERSE[_get_ts(data_spec, 'dataType', str)] - if 'definition' in data_spec: - ret.definition = cls._construct_lang_string_set(_get_ts(data_spec, 'definition', list)) - if 'shortName' in data_spec: - ret.short_name = cls._construct_lang_string_set(_get_ts(data_spec, 'shortName', list)) - if 'unit' in data_spec: - ret.unit = _get_ts(data_spec, 'unit', str) - if 'unitId' in data_spec: - ret.unit_id = cls._construct_reference(_get_ts(data_spec, 'unitId', dict)) - if 'sourceOfDefinition' in data_spec: - ret.source_of_definition = _get_ts(data_spec, 'sourceOfDefinition', str) - if 'symbol' in data_spec: - ret.symbol = _get_ts(data_spec, 'symbol', str) - if 'valueFormat' in data_spec: - ret.value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(data_spec, 'valueFormat', str)] - if 'valueList' in data_spec: - ret.value_list = cls._construct_value_list(_get_ts(data_spec, 'valueList', dict)) - if 'value' in data_spec: - ret.value = model.datatypes.from_xsd(_get_ts(data_spec, 'value', str), ret.value_format) - if 'valueId' in data_spec: - ret.value_id = cls._construct_reference(_get_ts(data_spec, 'valueId', dict)) - if 'levelType' in data_spec: - ret.level_types = set(IEC61360_LEVEL_TYPES_INVERSE[level_type] - for level_type in _get_ts(data_spec, 'levelType', list)) + def _construct_iec61360_physical_unit_data_specification_content(cls, dct: Dict[str, object], + object_class=model.base.DataSpecificationPhysicalUnit)\ + -> model.base.DataSpecificationPhysicalUnit: + ret = object_class( + unit_name=_get_ts(dct, 'unitName', str), + unit_symbol=_get_ts(dct, 'unitSymbol', str), + definition=cls._construct_lang_string_set(_get_ts(dct, 'definition', list)) + ) + if 'siNotation' in dct: + ret.SI_notation = _get_ts(dct, 'siNotation', str) + if 'siName' in dct: + ret.SI_name = _get_ts(dct, 'siName', str) + if 'dinNotation' in dct: + ret.DIN_notation = _get_ts(dct, 'dinNotation', str) + if 'eceName' in dct: + ret.ECE_name = _get_ts(dct, 'eceName', str) + if 'eceCode' in dct: + ret.ECE_code = _get_ts(dct, 'eceCode', str) + if 'nistName' in dct: + ret.NIST_name = _get_ts(dct, 'nistName', str) + if 'sourceOfDefinition' in dct: + ret.source_of_definition = _get_ts(dct, 'sourceOfDefinition', str) + if 'conversionFactor' in dct: + ret.conversion_factor = _get_ts(dct, 'conversionFactor', str) + if 'registrationAuthorityId' in dct: + ret.registration_authority_id = _get_ts(dct, 'registrationAuthorityId', str) + if 'supplier' in dct: + ret.supplier = _get_ts(dct, 'supplier', str) + return ret + + @classmethod + def _construct_iec61360_data_specification_content(cls, dct: Dict[str, object], + object_class=model.base.DataSpecificationIEC61360)\ + -> model.base.DataSpecificationIEC61360: + ret = object_class(preferred_name=cls._construct_lang_string_set(_get_ts(dct, 'preferredName', list))) + if 'dataType' in dct: + ret.data_type = IEC61360_DATA_TYPES_INVERSE[_get_ts(dct, 'dataType', str)] + if 'definition' in dct: + ret.definition = cls._construct_lang_string_set(_get_ts(dct, 'definition', list)) + if 'shortName' in dct: + ret.short_name = cls._construct_lang_string_set(_get_ts(dct, 'shortName', list)) + if 'unit' in dct: + ret.unit = _get_ts(dct, 'unit', str) + if 'unitId' in dct: + ret.unit_id = cls._construct_reference(_get_ts(dct, 'unitId', dict)) + if 'sourceOfDefinition' in dct: + ret.source_of_definition = _get_ts(dct, 'sourceOfDefinition', str) + if 'symbol' in dct: + ret.symbol = _get_ts(dct, 'symbol', str) + if 'valueFormat' in dct: + ret.value_format = _get_ts(dct, 'valueFormat', str) + # ret.value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueFormat', str)] + if 'valueList' in dct: + ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict)) + if 'value' in dct: + ret.value = _get_ts(dct, 'value', str) + # ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_format) + if 'valueId' in dct: + ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) + if 'levelType' in dct: + # TODO fix in V3.0 + ret.level_types = set([IEC61360_LEVEL_TYPES_INVERSE[_get_ts(dct, 'levelType', str)]]) return ret @classmethod diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 2dab906..13cfc74 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -110,6 +110,14 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: if isinstance(obj, model.HasExtension) and not cls.stripped: if obj.extension: data['extensions'] = list(obj.extension) + if isinstance(obj, model.HasDataSpecification) and not cls.stripped: + if obj.embedded_data_specifications: + data['embeddedDataSpecifications'] = [ + {'dataSpecification': spec.data_specification, + 'dataSpecificationContent': cls._data_specification_content_to_json(spec.data_specification_content)} + for spec in obj.embedded_data_specifications + ] + if isinstance(obj, model.Referable): if obj.id_short: data['idShort'] = obj.id_short @@ -313,24 +321,36 @@ def _concept_description_to_json(cls, obj: model.ConceptDescription) -> Dict[str data = cls._abstract_classes_to_json(obj) if obj.is_case_of: data['isCaseOf'] = list(obj.is_case_of) + return data - if isinstance(obj, model.concept.IEC61360ConceptDescription): - cls._append_iec61360_concept_description_attrs(obj, data) + @classmethod + def _data_specification_content_to_json( + cls, obj: model.base.DataSpecificationContent) -> None: + """ + serialization of an object from class DataSpecificationContent to json - return data + :param obj: object of class DataSpecificationContent + :return: dict with the serialized attributes of this object + """ + if isinstance(obj, model.base.DataSpecificationIEC61360): + return cls._iec61360_specification_content_to_json(obj) + elif isinstance(obj, model.base.DataSpecificationPhysicalUnit): + return cls._iec61360_physical_unit_specification_content_to_json(obj) + else: + raise TypeError(f"For the given type there is no implemented serialization " + f"yet: {type(obj)}") @classmethod - def _append_iec61360_concept_description_attrs(cls, obj: model.concept.IEC61360ConceptDescription, - data: Dict[str, object]) -> None: + def _iec61360_specification_content_to_json( + cls, obj: model.base.DataSpecificationIEC61360) -> None: """ - Add the 'embeddedDataSpecifications' attribute to IEC61360ConceptDescription's JSON representation. + serialization of an object from class DataSpecificationIEC61360 to json - `IEC61360ConceptDescription` is not a distinct class according DotAAS, but instead is built by referencing - "DataSpecificationIEC61360" as dataSpecification. However, we implemented it as an explicit class, inheriting - from ConceptDescription, but we want to generate compliant JSON documents. So, we fake the JSON structure of an - object with dataSpecifications. + :param obj: object of class DataSpecificationIEC61360 + :return: dict with the serialized attributes of this object """ data_spec: Dict[str, object] = { + 'modelType': 'DataSpecificationIEC61360', 'preferredName': cls._lang_string_set_to_json(obj.preferred_name) } if obj.data_type is not None: @@ -348,21 +368,55 @@ def _append_iec61360_concept_description_attrs(cls, obj: model.concept.IEC61360C if obj.symbol is not None: data_spec['symbol'] = obj.symbol if obj.value_format is not None: - data_spec['valueFormat'] = model.datatypes.XSD_TYPE_NAMES[obj.value_format] + data_spec['valueFormat'] = obj.value_format if obj.value_list is not None: data_spec['valueList'] = cls._value_list_to_json(obj.value_list) if obj.value is not None: - data_spec['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None + data_spec['value'] = obj.value + # data_spec['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None if obj.value_id is not None: data_spec['valueId'] = obj.value_id if obj.level_types: - data_spec['levelType'] = [_generic.IEC61360_LEVEL_TYPES[lt] for lt in obj.level_types] - data['embeddedDataSpecifications'] = [ - {'dataSpecification': model.GlobalReference( - (model.Key(model.KeyTypes.GLOBAL_REFERENCE, - "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0", ),)), - 'dataSpecificationContent': data_spec} - ] + # TODO fix in V3.0 + data_spec['levelType'] = [_generic.IEC61360_LEVEL_TYPES[lt] for lt in obj.level_types][0] + return data_spec + + @classmethod + def _iec61360_physical_unit_specification_content_to_json( + cls, obj: model.base.DataSpecificationPhysicalUnit) -> None: + """ + serialization of an object from class DataSpecificationPhysicalUnit to json + + :param obj: object of class DataSpecificationPhysicalUnit + :return: dict with the serialized attributes of this object + """ + data_spec: Dict[str, object] = { + 'modelType': 'DataSpecificationPhysicalUnit', + 'unitName': obj.unit_name, + 'unitSymbol': obj.unit_symbol, + 'definition': cls._lang_string_set_to_json(obj.definition) + } + if obj.SI_notation is not None: + data_spec['siNotation'] = obj.SI_notation + if obj.SI_name is not None: + data_spec['siName'] = obj.SI_name + if obj.DIN_notation is not None: + data_spec['dinNotation'] = obj.DIN_notation + if obj.ECE_name is not None: + data_spec['eceName'] = obj.ECE_name + if obj.ECE_code is not None: + data_spec['eceCode'] = obj.ECE_code + if obj.NIST_name is not None: + data_spec['nistName'] = obj.NIST_name + if obj.source_of_definition is not None: + data_spec['sourceOfDefinition'] = obj.source_of_definition + if obj.conversion_factor is not None: + data_spec['conversionFactor'] = obj.conversion_factor + if obj.registration_authority_id is not None: + data_spec['registrationAuthorityId'] = obj.registration_authority_id + if obj.supplier is not None: + data_spec['supplier'] = obj.supplier + return data_spec @classmethod def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell) -> Dict[str, object]: From 8396032ea7bf4dde32471e74042b9ce13f8aa4d9 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 27 Mar 2023 17:40:43 +0200 Subject: [PATCH 077/474] Refactor "valueReferencePairTypes" to json schema --- basyx/aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/json/json_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 0c8f3b7..e49d658 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -389,7 +389,7 @@ def _construct_lang_string_set(cls, lst: List[Dict[str, object]]) -> Optional[mo @classmethod def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList: ret: model.ValueList = set() - for element in _get_ts(dct, 'valueReferencePairTypes', list): + for element in _get_ts(dct, 'valueReferencePairs', list): try: ret.add(cls._construct_value_reference_pair(element)) except (KeyError, TypeError) as e: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 13cfc74..2c6f497 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -272,7 +272,7 @@ def _value_list_to_json(cls, obj: model.ValueList) -> Dict[str, object]: :param obj: object of class ValueList :return: dict with the serialized attributes of this object """ - return {'valueReferencePairTypes': list(obj)} + return {'valueReferencePairs': list(obj)} # ############################################################ # transformation functions to serialize classes from model.aas From 0813d566f4b0802232aa5b059e487f4afbe6bced Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 27 Mar 2023 17:42:58 +0200 Subject: [PATCH 078/474] Make `ValueReferencePair.value_type` optional As the DotAAS meta model does not require value_type, make it optional --- basyx/aas/adapter/json/json_deserialization.py | 9 ++++++--- basyx/aas/adapter/json/json_serialization.py | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index e49d658..31052b6 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -404,9 +404,12 @@ def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList: @classmethod def _construct_value_reference_pair(cls, dct: Dict[str, object], object_class=model.ValueReferencePair) -> \ model.ValueReferencePair: - value_type = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)] - return object_class(value_type=value_type, - value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_type), + if 'valueType' in dct: + value_type = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)] + return object_class(value_type=value_type, + value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_type), + value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict))) + return object_class(value=_get_ts(dct, 'value', str), value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict))) # ############################################################################# diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 2c6f497..617719c 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -259,9 +259,10 @@ def _value_reference_pair_to_json(cls, obj: model.ValueReferencePair) -> Dict[st :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) + if obj.value_type: + data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] data.update({'value': model.datatypes.xsd_repr(obj.value), - 'valueId': obj.value_id, - 'valueType': model.datatypes.XSD_TYPE_NAMES[obj.value_type]}) + 'valueId': obj.value_id}) return data @classmethod From 8a0fb6350f08ebc79fd64e51df5ee8b6dd4c9195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 27 Mar 2023 12:14:41 +0200 Subject: [PATCH 079/474] adapter.json: remove meaningless modelType check --- basyx/aas/adapter/json/json_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 31052b6..87c8ecd 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -191,7 +191,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object: } # Get modelType and constructor function - if not isinstance(dct['modelType'], str) or dct['modelType'] not in dct['modelType']: + if not isinstance(dct['modelType'], str): logger.warning("JSON object has unexpected format of modelType: %s", dct['modelType']) # Even in strict mode, we consider 'modelType' attributes of wrong type as non-AAS objects instead of # raising an exception. However, the object's type will probably checked later by read_json_aas_file() or From a4f98a6ca4570dc6c0a91b0cdcc2bab52ea7917c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 27 Mar 2023 12:23:38 +0200 Subject: [PATCH 080/474] Revert "Set 'orderRelevant' as optional in json serialisation of `SubmodelElementList`" This reverts commit b84b66b80821d9ee3902a90a2a65263e6f6f849d. It's better to always serialize the value instead of having multiple places where the default value is specified. --- basyx/aas/adapter/json/json_serialization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 617719c..6aa648d 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -603,9 +603,8 @@ def _submodel_element_list_to_json(cls, obj: model.SubmodelElementList) -> Dict[ :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) + data['orderRelevant'] = obj.order_relevant data['typeValueListElement'] = _generic.KEY_TYPES[model.KEY_TYPES_CLASSES[obj.type_value_list_element]] - if obj.order_relevant is not True: - data['orderRelevant'] = obj.order_relevant if obj.semantic_id_list_element is not None: data['semanticIdListElement'] = obj.semantic_id_list_element if obj.value_type_list_element is not None: From 1cfb678072013e580ecabe418618f3ade0b34552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 28 Mar 2023 16:56:38 +0200 Subject: [PATCH 081/474] adapter.json: always serialize values with non-None default values It's better to always serialize optional values instead of specifying their default value in multiple locations. See also: 21f423eb4a112e86f8a44e5060e168c3fe7ea3e7 --- basyx/aas/adapter/json/json_serialization.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 6aa648d..7c1cc9c 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -226,8 +226,9 @@ def _qualifier_to_json(cls, obj: model.Qualifier) -> Dict[str, object]: data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None if obj.value_id: data['valueId'] = obj.value_id - if obj.kind is not model.QualifierKind.CONCEPT_QUALIFIER: - data['kind'] = _generic.QUALIFIER_KIND[obj.kind] + # Even though kind is optional in the schema, it's better to always serialize it instead of specifying + # the default value in multiple locations. + data['kind'] = _generic.QUALIFIER_KIND[obj.kind] data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] data['type'] = obj.type return data @@ -603,6 +604,8 @@ def _submodel_element_list_to_json(cls, obj: model.SubmodelElementList) -> Dict[ :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) + # Even though orderRelevant is optional in the schema, it's better to always serialize it instead of specifying + # the default value in multiple locations. data['orderRelevant'] = obj.order_relevant data['typeValueListElement'] = _generic.KEY_TYPES[model.KEY_TYPES_CLASSES[obj.type_value_list_element]] if obj.semantic_id_list_element is not None: From 97744f09fc9c03c87ceb3b8e9c3709923cbe3703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 28 Mar 2023 17:10:14 +0200 Subject: [PATCH 082/474] adapter.json: add objects with a for loop This saves us a 'type: ignore' comment. --- basyx/aas/adapter/json/json_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 87c8ecd..64e8a64 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -246,8 +246,8 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None obj.administration = cls._construct_administrative_information(_get_ts(dct, 'administration', dict)) if isinstance(obj, model.HasSemantics): if 'supplementalSemanticIds' in dct: - obj.supplemental_semantic_id = [cls._construct_reference(ref) # type: ignore - for ref in _get_ts(dct, 'supplementalSemanticIds', list)] + for ref in _get_ts(dct, 'supplementalSemanticIds', list): + obj.supplemental_semantic_id.append(cls._construct_reference(ref)) if 'semanticId' in dct: obj.semantic_id = cls._construct_reference(_get_ts(dct, 'semanticId', dict)) # `HasKind` provides only mandatory, immutable attributes; so we cannot do anything here, after object creation. From 00f16c48127d01f5e72b8df4daa331da4face3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 28 Mar 2023 18:06:25 +0200 Subject: [PATCH 083/474] model: refactor class and attribute names Class names: PascalCase Attribute names: snake_case --- basyx/aas/adapter/json/json_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 64e8a64..3ce0e43 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -265,7 +265,7 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if "dataSpecificationContent" in dspec: content = _get_ts(dspec, 'dataSpecificationContent', model.DataSpecificationContent) obj.embedded_data_specifications.append( - model.Embedded_data_specification( + model.EmbeddedDataSpecification( data_specification=dspec_ref, data_specification_content=content ) From 9730f50a53b12739f4ba462b0dc0e8d12fc9a969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 1 Apr 2023 13:59:42 +0200 Subject: [PATCH 084/474] fix some mypy errors --- basyx/aas/adapter/json/json_deserialization.py | 7 +++++-- basyx/aas/adapter/json/json_serialization.py | 18 +++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 3ce0e43..08f7937 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -260,10 +260,13 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if isinstance(obj, model.HasDataSpecification) and not cls.stripped: if 'embeddedDataSpecifications' in dct: for dspec in _get_ts(dct, 'embeddedDataSpecifications', list): - dspec_ref = cls._construct_reference( + dspec_ref = cls._construct_global_reference( _get_ts(dspec, 'dataSpecification', dict)) if "dataSpecificationContent" in dspec: - content = _get_ts(dspec, 'dataSpecificationContent', model.DataSpecificationContent) + # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + content = _get_ts(dspec, 'dataSpecificationContent', + model.DataSpecificationContent) # type: ignore obj.embedded_data_specifications.append( model.EmbeddedDataSpecification( data_specification=dspec_ref, diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 7c1cc9c..4138732 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -29,7 +29,7 @@ """ import base64 import inspect -from typing import List, Dict, IO, Optional, Type +from typing import List, Dict, IO, Optional, Type, Callable import json from basyx.aas import model @@ -63,7 +63,7 @@ def default(self, obj: object) -> object: :param obj: The object to serialize to json :return: The serialized object """ - mapping = { + mapping: Dict[Type, Callable] = { model.AdministrativeInformation: self._administrative_information_to_json, model.AnnotatedRelationshipElement: self._annotated_relationship_element_to_json, model.AssetAdministrationShell: self._asset_administration_shell_to_json, @@ -327,7 +327,7 @@ def _concept_description_to_json(cls, obj: model.ConceptDescription) -> Dict[str @classmethod def _data_specification_content_to_json( - cls, obj: model.base.DataSpecificationContent) -> None: + cls, obj: model.base.DataSpecificationContent) -> Dict[str, object]: """ serialization of an object from class DataSpecificationContent to json @@ -344,7 +344,7 @@ def _data_specification_content_to_json( @classmethod def _iec61360_specification_content_to_json( - cls, obj: model.base.DataSpecificationIEC61360) -> None: + cls, obj: model.base.DataSpecificationIEC61360) -> Dict[str, object]: """ serialization of an object from class DataSpecificationIEC61360 to json @@ -385,7 +385,7 @@ def _iec61360_specification_content_to_json( @classmethod def _iec61360_physical_unit_specification_content_to_json( - cls, obj: model.base.DataSpecificationPhysicalUnit) -> None: + cls, obj: model.base.DataSpecificationPhysicalUnit) -> Dict[str, object]: """ serialization of an object from class DataSpecificationPhysicalUnit to json @@ -760,9 +760,9 @@ def _select_encoder(stripped: bool, encoder: Optional[Type[AASToJsonEncoder]] = def _create_dict(data: model.AbstractObjectStore) -> dict: # separate different kind of objects - asset_administration_shells = [] - submodels = [] - concept_descriptions = [] + asset_administration_shells: List[model.AssetAdministrationShell] = [] + submodels: List[model.Submodel] = [] + concept_descriptions: List[model.ConceptDescription] = [] for obj in data: if isinstance(obj, model.AssetAdministrationShell): asset_administration_shells.append(obj) @@ -770,7 +770,7 @@ def _create_dict(data: model.AbstractObjectStore) -> dict: submodels.append(obj) elif isinstance(obj, model.ConceptDescription): concept_descriptions.append(obj) - dict_ = {} + dict_: Dict[str, List] = {} if asset_administration_shells: dict_['assetAdministrationShells'] = asset_administration_shells if submodels: From 25da0b22529138c65becedb166fe0c8efdd0fe27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 1 Apr 2023 14:10:55 +0200 Subject: [PATCH 085/474] fix codestyle --- .../aas/adapter/json/json_deserialization.py | 20 +++++++++---------- basyx/aas/adapter/json/json_serialization.py | 11 +++++----- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 08f7937..7838136 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -186,8 +186,8 @@ def object_hook(cls, dct: Dict[str, object]) -> object: 'Property': cls._construct_property, 'Range': cls._construct_range, 'ReferenceElement': cls._construct_reference_element, - 'DataSpecificationIEC61360': cls._construct_iec61360_data_specification_content, - 'DataSpecificationPhysicalUnit': cls._construct_iec61360_physical_unit_data_specification_content, + 'DataSpecificationIEC61360': cls._construct_data_specification_iec61360, + 'DataSpecificationPhysicalUnit': cls._construct_data_specification_physical_unit, } # Get modelType and constructor function @@ -307,14 +307,14 @@ def _construct_specific_asset_id(cls, dct: Dict[str, object], object_class=model # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable return object_class(name=_get_ts(dct, 'name', str), value=_get_ts(dct, 'value', str), - external_subject_id=cls._construct_global_reference(_get_ts(dct, 'externalSubjectId', dict)), + external_subject_id=cls._construct_global_reference( + _get_ts(dct, 'externalSubjectId', dict)), semantic_id=cls._construct_reference(_get_ts(dct, 'semanticId', dict)) if 'semanticId' in dct else None, supplemental_semantic_id=[ cls._construct_reference(ref) for ref in - _get_ts(dct, 'supplementalSemanticIds',list)] - if 'supplementalSemanticIds' in dct else () - ) + _get_ts(dct, 'supplementalSemanticIds', list)] + if 'supplementalSemanticIds' in dct else ()) @classmethod def _construct_reference(cls, dct: Dict[str, object]) -> model.Reference: @@ -464,8 +464,8 @@ def _construct_concept_description(cls, dct: Dict[str, object], object_class=mod return ret @classmethod - def _construct_iec61360_physical_unit_data_specification_content(cls, dct: Dict[str, object], - object_class=model.base.DataSpecificationPhysicalUnit)\ + def _construct_data_specification_physical_unit(cls, dct: Dict[str, object], + object_class=model.base.DataSpecificationPhysicalUnit)\ -> model.base.DataSpecificationPhysicalUnit: ret = object_class( unit_name=_get_ts(dct, 'unitName', str), @@ -495,8 +495,8 @@ def _construct_iec61360_physical_unit_data_specification_content(cls, dct: Dict[ return ret @classmethod - def _construct_iec61360_data_specification_content(cls, dct: Dict[str, object], - object_class=model.base.DataSpecificationIEC61360)\ + def _construct_data_specification_iec61360(cls, dct: Dict[str, object], + object_class=model.base.DataSpecificationIEC61360)\ -> model.base.DataSpecificationIEC61360: ret = object_class(preferred_name=cls._construct_lang_string_set(_get_ts(dct, 'preferredName', list))) if 'dataType' in dct: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 4138732..1a4e477 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -114,7 +114,8 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: if obj.embedded_data_specifications: data['embeddedDataSpecifications'] = [ {'dataSpecification': spec.data_specification, - 'dataSpecificationContent': cls._data_specification_content_to_json(spec.data_specification_content)} + 'dataSpecificationContent': cls._data_specification_content_to_json( + spec.data_specification_content)} for spec in obj.embedded_data_specifications ] @@ -335,15 +336,15 @@ def _data_specification_content_to_json( :return: dict with the serialized attributes of this object """ if isinstance(obj, model.base.DataSpecificationIEC61360): - return cls._iec61360_specification_content_to_json(obj) + return cls._data_specification_iec61360_to_json(obj) elif isinstance(obj, model.base.DataSpecificationPhysicalUnit): - return cls._iec61360_physical_unit_specification_content_to_json(obj) + return cls._data_specification_physical_unit_to_json(obj) else: raise TypeError(f"For the given type there is no implemented serialization " f"yet: {type(obj)}") @classmethod - def _iec61360_specification_content_to_json( + def _data_specification_iec61360_to_json( cls, obj: model.base.DataSpecificationIEC61360) -> Dict[str, object]: """ serialization of an object from class DataSpecificationIEC61360 to json @@ -384,7 +385,7 @@ def _iec61360_specification_content_to_json( return data_spec @classmethod - def _iec61360_physical_unit_specification_content_to_json( + def _data_specification_physical_unit_to_json( cls, obj: model.base.DataSpecificationPhysicalUnit) -> Dict[str, object]: """ serialization of an object from class DataSpecificationPhysicalUnit to json From 343d1766892eab50abbe8861db6e2c4e95a43a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 7 Apr 2023 16:01:14 +0200 Subject: [PATCH 086/474] update implementation and adapters for new DataSpecification classes and schema Changes: - fix bugs in XML/JSON schema by copying some sections from the V3.0 schema - remove `ABAC.xsd` and `IEC61360.xsd` (not needed anymore, now unified in a single schema) - adapter.json: fix `HasDataSpecification` (de-)serialization - adapter.xml: add `HasDataSpecification` (de-)serialization - move XML namespace definitions to `adapter._generic` - remove `ConceptDescriptionIEC61360` - remove `example_concept_description.py` (as it only contained `ConceptDescriptionIEC61360`) - fix some minor issues in `AASDataChecker` (type hints, error messages, etc.) - add `HasDataSpecification` support to `AASDataChecker` - add `EmbeddedDataSpecifications` to example data - add `__repr__()` to `EmbeddedDataSpecification`, `DataSpecificationIEC61360` and `DataSpecificationPhysicalUnit` - remove `value_id` attribute from `DataSpecificationIEC61360` - rename attribute accesses of `DataSpecificationPhysicalUnit` that were missed in 8f374098adf85f5bd100e86c7880dfd617c50ec8 - update tests and compliance tool example files in accordance to the changes --- basyx/aas/adapter/_generic.py | 14 +- basyx/aas/adapter/json/aasJSONSchema.json | 32 +- .../aas/adapter/json/json_deserialization.py | 72 ++-- basyx/aas/adapter/json/json_serialization.py | 56 +-- basyx/aas/adapter/xml/AAS.xsd | 28 +- basyx/aas/adapter/xml/AAS_ABAC.xsd | 185 --------- basyx/aas/adapter/xml/IEC61360.xsd | 67 ---- basyx/aas/adapter/xml/xml_deserialization.py | 342 ++++++++++------- basyx/aas/adapter/xml/xml_serialization.py | 363 ++++++++++-------- 9 files changed, 507 insertions(+), 652 deletions(-) delete mode 100644 basyx/aas/adapter/xml/AAS_ABAC.xsd delete mode 100644 basyx/aas/adapter/xml/IEC61360.xsd diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 0f52f1a..baebd17 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -12,6 +12,12 @@ from basyx.aas import model +# XML Namespace definition +XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0", + "abac": "https://admin-shell.io/aas/abac/3/0"} +XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" +XML_NS_ABAC = "{" + XML_NS_MAP["abac"] + "}" + MODELING_KIND: Dict[model.ModelingKind, str] = { model.ModelingKind.TEMPLATE: 'Template', model.ModelingKind.INSTANCE: 'Instance'} @@ -80,10 +86,10 @@ } IEC61360_LEVEL_TYPES: Dict[model.base.IEC61360LevelType, str] = { - model.base.IEC61360LevelType.MIN: 'Min', - model.base.IEC61360LevelType.MAX: 'Max', - model.base.IEC61360LevelType.NOM: 'Nom', - model.base.IEC61360LevelType.TYP: 'Typ', + model.base.IEC61360LevelType.MIN: 'min', + model.base.IEC61360LevelType.NOM: 'nom', + model.base.IEC61360LevelType.TYP: 'typ', + model.base.IEC61360LevelType.MAX: 'max', } MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 36a7303..63b23c9 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -576,7 +576,11 @@ "type": "string" }, "refersTo": { - "$ref": "#/definitions/Reference" + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 } }, "required": [ @@ -739,12 +743,26 @@ ] }, "LevelType": { - "type": "string", - "enum": [ - "Max", - "Min", - "Nom", - "Typ" + "type": "object", + "properties": { + "min": { + "type": "boolean" + }, + "nom": { + "type": "boolean" + }, + "typ": { + "type": "boolean" + }, + "max": { + "type": "boolean" + } + }, + "required": [ + "min", + "nom", + "typ", + "max" ] }, "ModelType": { diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 7838136..3d3bd5f 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -245,11 +245,11 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if 'administration' in dct: obj.administration = cls._construct_administrative_information(_get_ts(dct, 'administration', dict)) if isinstance(obj, model.HasSemantics): + if 'semanticId' in dct: + obj.semantic_id = cls._construct_reference(_get_ts(dct, 'semanticId', dict)) if 'supplementalSemanticIds' in dct: for ref in _get_ts(dct, 'supplementalSemanticIds', list): obj.supplemental_semantic_id.append(cls._construct_reference(ref)) - if 'semanticId' in dct: - obj.semantic_id = cls._construct_reference(_get_ts(dct, 'semanticId', dict)) # `HasKind` provides only mandatory, immutable attributes; so we cannot do anything here, after object creation. # However, the `cls._get_kind()` function may assist by retrieving them from the JSON object if isinstance(obj, model.Qualifiable) and not cls.stripped: @@ -260,19 +260,16 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if isinstance(obj, model.HasDataSpecification) and not cls.stripped: if 'embeddedDataSpecifications' in dct: for dspec in _get_ts(dct, 'embeddedDataSpecifications', list): - dspec_ref = cls._construct_global_reference( - _get_ts(dspec, 'dataSpecification', dict)) - if "dataSpecificationContent" in dspec: + obj.embedded_data_specifications.append( # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - content = _get_ts(dspec, 'dataSpecificationContent', - model.DataSpecificationContent) # type: ignore - obj.embedded_data_specifications.append( - model.EmbeddedDataSpecification( - data_specification=dspec_ref, - data_specification_content=content - ) + model.EmbeddedDataSpecification( + data_specification=cls._construct_global_reference(_get_ts(dspec, 'dataSpecification', + dict)), + data_specification_content=_get_ts(dspec, 'dataSpecificationContent', + model.DataSpecificationContent) # type: ignore ) + ) if isinstance(obj, model.HasExtension) and not cls.stripped: if 'extensions' in dct: for extension in _get_ts(dct, 'extensions', list): @@ -390,11 +387,12 @@ def _construct_lang_string_set(cls, lst: List[Dict[str, object]]) -> Optional[mo return model.LangStringSet(ret) @classmethod - def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList: + def _construct_value_list(cls, dct: Dict[str, object], value_format: Optional[model.DataTypeDefXsd] = None) \ + -> model.ValueList: ret: model.ValueList = set() for element in _get_ts(dct, 'valueReferencePairs', list): try: - ret.add(cls._construct_value_reference_pair(element)) + ret.add(cls._construct_value_reference_pair(element, value_format=value_format)) except (KeyError, TypeError) as e: error_message = "Error while trying to convert JSON object into ValueReferencePair: {} >>> {}".format( e, pprint.pformat(element, depth=2, width=2 ** 14, compact=True)) @@ -405,15 +403,12 @@ def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList: return ret @classmethod - def _construct_value_reference_pair(cls, dct: Dict[str, object], object_class=model.ValueReferencePair) -> \ - model.ValueReferencePair: - if 'valueType' in dct: - value_type = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)] - return object_class(value_type=value_type, - value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_type), - value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict))) - return object_class(value=_get_ts(dct, 'value', str), - value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict))) + def _construct_value_reference_pair(cls, dct: Dict[str, object], + value_format: Optional[model.DataTypeDefXsd] = None, + object_class=model.ValueReferencePair) -> model.ValueReferencePair: + return object_class(value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_format), # type: ignore + value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict)), + value_type=value_format) # ############################################################################# # Direct Constructor Methods (for classes with `modelType`) starting from here @@ -473,17 +468,17 @@ def _construct_data_specification_physical_unit(cls, dct: Dict[str, object], definition=cls._construct_lang_string_set(_get_ts(dct, 'definition', list)) ) if 'siNotation' in dct: - ret.SI_notation = _get_ts(dct, 'siNotation', str) + ret.si_notation = _get_ts(dct, 'siNotation', str) if 'siName' in dct: - ret.SI_name = _get_ts(dct, 'siName', str) + ret.si_name = _get_ts(dct, 'siName', str) if 'dinNotation' in dct: - ret.DIN_notation = _get_ts(dct, 'dinNotation', str) + ret.din_notation = _get_ts(dct, 'dinNotation', str) if 'eceName' in dct: - ret.ECE_name = _get_ts(dct, 'eceName', str) + ret.ece_name = _get_ts(dct, 'eceName', str) if 'eceCode' in dct: - ret.ECE_code = _get_ts(dct, 'eceCode', str) + ret.ece_code = _get_ts(dct, 'eceCode', str) if 'nistName' in dct: - ret.NIST_name = _get_ts(dct, 'nistName', str) + ret.nist_name = _get_ts(dct, 'nistName', str) if 'sourceOfDefinition' in dct: ret.source_of_definition = _get_ts(dct, 'sourceOfDefinition', str) if 'conversionFactor' in dct: @@ -513,19 +508,20 @@ def _construct_data_specification_iec61360(cls, dct: Dict[str, object], ret.source_of_definition = _get_ts(dct, 'sourceOfDefinition', str) if 'symbol' in dct: ret.symbol = _get_ts(dct, 'symbol', str) + value_format: Optional[model.DataTypeDefXsd] = None if 'valueFormat' in dct: - ret.value_format = _get_ts(dct, 'valueFormat', str) - # ret.value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueFormat', str)] + value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueFormat', str)] + ret.value_format = value_format if 'valueList' in dct: - ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict)) + ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict), value_format=value_format) if 'value' in dct: - ret.value = _get_ts(dct, 'value', str) - # ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_format) + ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_format) if 'valueId' in dct: ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) if 'levelType' in dct: - # TODO fix in V3.0 - ret.level_types = set([IEC61360_LEVEL_TYPES_INVERSE[_get_ts(dct, 'levelType', str)]]) + for k, v in _get_ts(dct, 'levelType', dict).items(): + if v: + ret.level_types.add(IEC61360_LEVEL_TYPES_INVERSE[k]) return ret @classmethod @@ -864,9 +860,7 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r ('conceptDescriptions', model.ConceptDescription)): try: lst = _get_ts(data, name, list) - except (KeyError, TypeError) as e: - info_message = "Could not find list '{}' in AAS JSON file".format(name) - logger.info(info_message) + except (KeyError, TypeError): continue for item in lst: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 1a4e477..01eae0d 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -72,6 +72,8 @@ def default(self, obj: object) -> object: model.Blob: self._blob_to_json, model.Capability: self._capability_to_json, model.ConceptDescription: self._concept_description_to_json, + model.DataSpecificationIEC61360: self._data_specification_iec61360_to_json, + model.DataSpecificationPhysicalUnit: self._data_specification_physical_unit_to_json, model.Entity: self._entity_to_json, model.Extension: self._extension_to_json, model.File: self._file_to_json, @@ -114,8 +116,7 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: if obj.embedded_data_specifications: data['embeddedDataSpecifications'] = [ {'dataSpecification': spec.data_specification, - 'dataSpecificationContent': cls._data_specification_content_to_json( - spec.data_specification_content)} + 'dataSpecificationContent': spec.data_specification_content} for spec in obj.embedded_data_specifications ] @@ -326,23 +327,6 @@ def _concept_description_to_json(cls, obj: model.ConceptDescription) -> Dict[str data['isCaseOf'] = list(obj.is_case_of) return data - @classmethod - def _data_specification_content_to_json( - cls, obj: model.base.DataSpecificationContent) -> Dict[str, object]: - """ - serialization of an object from class DataSpecificationContent to json - - :param obj: object of class DataSpecificationContent - :return: dict with the serialized attributes of this object - """ - if isinstance(obj, model.base.DataSpecificationIEC61360): - return cls._data_specification_iec61360_to_json(obj) - elif isinstance(obj, model.base.DataSpecificationPhysicalUnit): - return cls._data_specification_physical_unit_to_json(obj) - else: - raise TypeError(f"For the given type there is no implemented serialization " - f"yet: {type(obj)}") - @classmethod def _data_specification_iec61360_to_json( cls, obj: model.base.DataSpecificationIEC61360) -> Dict[str, object]: @@ -371,17 +355,13 @@ def _data_specification_iec61360_to_json( if obj.symbol is not None: data_spec['symbol'] = obj.symbol if obj.value_format is not None: - data_spec['valueFormat'] = obj.value_format + data_spec['valueFormat'] = model.datatypes.XSD_TYPE_NAMES[obj.value_format] if obj.value_list is not None: data_spec['valueList'] = cls._value_list_to_json(obj.value_list) if obj.value is not None: - data_spec['value'] = obj.value - # data_spec['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None - if obj.value_id is not None: - data_spec['valueId'] = obj.value_id + data_spec['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None if obj.level_types: - # TODO fix in V3.0 - data_spec['levelType'] = [_generic.IEC61360_LEVEL_TYPES[lt] for lt in obj.level_types][0] + data_spec['levelType'] = {v: k in obj.level_types for k, v in _generic.IEC61360_LEVEL_TYPES.items()} return data_spec @classmethod @@ -399,18 +379,18 @@ def _data_specification_physical_unit_to_json( 'unitSymbol': obj.unit_symbol, 'definition': cls._lang_string_set_to_json(obj.definition) } - if obj.SI_notation is not None: - data_spec['siNotation'] = obj.SI_notation - if obj.SI_name is not None: - data_spec['siName'] = obj.SI_name - if obj.DIN_notation is not None: - data_spec['dinNotation'] = obj.DIN_notation - if obj.ECE_name is not None: - data_spec['eceName'] = obj.ECE_name - if obj.ECE_code is not None: - data_spec['eceCode'] = obj.ECE_code - if obj.NIST_name is not None: - data_spec['nistName'] = obj.NIST_name + if obj.si_notation is not None: + data_spec['siNotation'] = obj.si_notation + if obj.si_name is not None: + data_spec['siName'] = obj.si_name + if obj.din_notation is not None: + data_spec['dinNotation'] = obj.din_notation + if obj.ece_name is not None: + data_spec['eceName'] = obj.ece_name + if obj.ece_code is not None: + data_spec['eceCode'] = obj.ece_code + if obj.nist_name is not None: + data_spec['nistName'] = obj.nist_name if obj.source_of_definition is not None: data_spec['sourceOfDefinition'] = obj.source_of_definition if obj.conversion_factor is not None: diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 2858e33..002126a 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -1,5 +1,5 @@ - + @@ -77,7 +77,7 @@ - + @@ -421,7 +421,8 @@ - + + @@ -1064,14 +1065,19 @@ - - - - - - - - + + + + + + + + + + + + + diff --git a/basyx/aas/adapter/xml/AAS_ABAC.xsd b/basyx/aas/adapter/xml/AAS_ABAC.xsd deleted file mode 100644 index a735cce..0000000 --- a/basyx/aas/adapter/xml/AAS_ABAC.xsd +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/basyx/aas/adapter/xml/IEC61360.xsd b/basyx/aas/adapter/xml/IEC61360.xsd deleted file mode 100644 index daa1381..0000000 --- a/basyx/aas/adapter/xml/IEC61360.xsd +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 694d373..3525331 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -50,11 +50,12 @@ import enum from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar -from .xml_serialization import NS_AAS -from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_TYPES_INVERSE, ENTITY_TYPES_INVERSE,\ +from .._generic import XML_NS_AAS, MODELING_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 +NS_AAS = XML_NS_AAS + logger = logging.getLogger(__name__) T = TypeVar("T") @@ -407,7 +408,7 @@ def _expect_reference_type(element: etree.Element, expected_type: Type[model.Ref :param expected_type: The expected type of the Reference. :return: None """ - actual_type = _get_attrib_mandatory_mapped(element, "type", REFERENCE_TYPES_INVERSE) + actual_type = _child_text_mandatory_mapped(element, NS_AAS + "type", REFERENCE_TYPES_INVERSE) if actual_type is not expected_type: raise ValueError(f"{_element_pretty_identifier(element)} is of type {actual_type}, expected {expected_type}!") @@ -459,11 +460,23 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None cls.failsafe) if semantic_id is not None: obj.semantic_id = semantic_id + supplemental_semantic_ids = element.find(NS_AAS + "supplementalSemanticIds") + if supplemental_semantic_ids is not None: + for supplemental_semantic_id in _child_construct_multiple(supplemental_semantic_ids, + NS_AAS + "reference", cls.construct_reference, + cls.failsafe): + obj.supplemental_semantic_id.append(supplemental_semantic_id) if isinstance(obj, model.Qualifiable) and not cls.stripped: qualifiers_elem = element.find(NS_AAS + "qualifiers") if qualifiers_elem is not None and len(qualifiers_elem) > 0: for qualifier in _failsafe_construct_multiple(qualifiers_elem, cls.construct_qualifier, cls.failsafe): obj.qualifier.add(qualifier) + if isinstance(obj, model.HasDataSpecification) and not cls.stripped: + embedded_data_specifications_elem = element.find(NS_AAS + "embeddedDataSpecifications") + if embedded_data_specifications_elem is not None: + for eds in _failsafe_construct_multiple(embedded_data_specifications_elem, + cls.construct_embedded_data_specification, cls.failsafe): + obj.embedded_data_specifications.append(eds) if isinstance(obj, model.HasExtension) and not cls.stripped: extension_elem = element.find(NS_AAS + "extension") if extension_elem is not None: @@ -587,10 +600,12 @@ def construct_model_reference_expect_type(cls, element: etree.Element, type_: Ty @classmethod def construct_administrative_information(cls, element: etree.Element, object_class=model.AdministrativeInformation, **_kwargs: Any) -> model.AdministrativeInformation: - return object_class( + administrative_information = object_class( revision=_get_text_or_none(element.find(NS_AAS + "revision")), version=_get_text_or_none(element.find(NS_AAS + "version")) ) + cls._amend_abstract_attributes(administrative_information, element) + return administrative_information @classmethod def construct_lang_string_set(cls, element: etree.Element, namespace: str = NS_AAS, **_kwargs: Any) \ @@ -701,15 +716,11 @@ def construct_annotated_relationship_element(cls, element: etree.Element, -> model.AnnotatedRelationshipElement: annotated_relationship_element = cls._construct_relationship_element_internal(element, object_class) if not cls.stripped: - for data_element in _get_child_mandatory(element, NS_AAS + "annotations"): - if len(data_element) == 0: - raise KeyError(f"{_element_pretty_identifier(data_element)} has no data element!") - if len(data_element) > 1: - logger.warning(f"{_element_pretty_identifier(data_element)} has more than one data element, " - "using the first one...") - constructed = _failsafe_construct(data_element[0], cls.construct_data_element, cls.failsafe) - if constructed is not None: - annotated_relationship_element.annotation.add(constructed) + annotations = element.find(NS_AAS + "annotations") + if annotations is not None: + for data_element in _failsafe_construct_multiple(annotations, cls.construct_data_element, + cls.failsafe): + annotated_relationship_element.annotation.add(data_element) return annotated_relationship_element @classmethod @@ -777,18 +788,11 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ specific_asset_id=specific_asset_id) if not cls.stripped: - # TODO: remove wrapping submodelElement, in accordance to future schemas - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 - statements = _get_child_mandatory(element, NS_AAS + "statements") - for submodel_element in _get_all_children_expect_tag(statements, NS_AAS + "submodelElement", cls.failsafe): - if len(submodel_element) == 0: - raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") - if len(submodel_element) > 1: - logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " using the first one...") - constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) - if constructed is not None: - entity.statement.add(constructed) + statements = element.find(NS_AAS + "statements") + if statements is not None: + for submodel_element in _failsafe_construct_multiple(statements, cls.construct_submodel_element, + cls.failsafe): + entity.statement.add(submodel_element) cls._amend_abstract_attributes(entity, element) return entity @@ -839,15 +843,21 @@ def construct_operation(cls, element: etree.Element, object_class=model.Operatio _child_text_mandatory(element, NS_AAS + "idShort"), kind=_get_modeling_kind(element) ) - for input_variable in _failsafe_construct_multiple(element.findall(NS_AAS + "inputVariable"), - cls.construct_operation_variable, cls.failsafe): - operation.input_variable.append(input_variable) - for output_variable in _failsafe_construct_multiple(element.findall(NS_AAS + "outputVariable"), + input_variables = element.find(NS_AAS + "inputVariables") + if input_variables is not None: + for input_variable in _child_construct_multiple(input_variables, NS_AAS + "operationVariable", cls.construct_operation_variable, cls.failsafe): - operation.output_variable.append(output_variable) - for in_output_variable in _failsafe_construct_multiple(element.findall(NS_AAS + "inoutputVariable"), - cls.construct_operation_variable, cls.failsafe): - operation.in_output_variable.append(in_output_variable) + operation.input_variable.append(input_variable) + output_variables = element.find(NS_AAS + "outputVariables") + if output_variables is not None: + for output_variable in _child_construct_multiple(output_variables, NS_AAS + "operationVariable", + cls.construct_operation_variable, cls.failsafe): + operation.output_variable.append(output_variable) + in_output_variables = element.find(NS_AAS + "inoutputVariables") + if in_output_variables is not None: + for in_output_variable in _child_construct_multiple(in_output_variables, NS_AAS + "operationVariable", + cls.construct_operation_variable, cls.failsafe): + operation.in_output_variable.append(in_output_variable) cls._amend_abstract_attributes(operation, element) return operation @@ -909,18 +919,11 @@ def construct_submodel_element_collection(cls, element: etree.Element, object_cl kind=_get_modeling_kind(element) ) if not cls.stripped: - value = _get_child_mandatory(element, NS_AAS + "value") - # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 - for submodel_element in _get_all_children_expect_tag(value, NS_AAS + "submodelElement", cls.failsafe): - if len(submodel_element) == 0: - raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") - if len(submodel_element) > 1: - logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " using the first one...") - constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) - if constructed is not None: - collection.value.add(constructed) + value = element.find(NS_AAS + "value") + if value is not None: + for submodel_element in _failsafe_construct_multiple(value, cls.construct_submodel_element, + cls.failsafe): + collection.value.add(submodel_element) cls._amend_abstract_attributes(collection, element) return collection @@ -999,7 +1002,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", cls.construct_specific_asset_id, cls.failsafe): asset_information.specific_asset_id.add(id) - thumbnail = _failsafe_construct(element.find(NS_AAS + "defaultThumbNail"), + thumbnail = _failsafe_construct(element.find(NS_AAS + "defaultThumbnail"), cls.construct_resource, cls.failsafe) if thumbnail is not None: asset_information.default_thumbnail = thumbnail @@ -1015,19 +1018,11 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, kind=_get_modeling_kind(element) ) if not cls.stripped: - # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 - for submodel_element in _get_all_children_expect_tag( - _get_child_mandatory(element, NS_AAS + "submodelElements"), NS_AAS + "submodelElement", - cls.failsafe): - if len(submodel_element) == 0: - raise KeyError(f"{_element_pretty_identifier(submodel_element)} has no submodel element!") - if len(submodel_element) > 1: - logger.warning(f"{_element_pretty_identifier(submodel_element)} has more than one submodel element," - " using the first one...") - constructed = _failsafe_construct(submodel_element[0], cls.construct_submodel_element, cls.failsafe) - if constructed is not None: - submodel.submodel_element.add(constructed) + submodel_elements = element.find(NS_AAS + "submodelElements") + if submodel_elements is not None: + for submodel_element in _failsafe_construct_multiple(submodel_elements, cls.construct_submodel_element, + cls.failsafe): + submodel.submodel_element.add(submodel_element) cls._amend_abstract_attributes(submodel, element) return submodel @@ -1035,12 +1030,10 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, def construct_value_reference_pair(cls, element: etree.Element, value_format: Optional[model.DataTypeDefXsd] = None, object_class=model.ValueReferencePair, **_kwargs: Any) \ -> model.ValueReferencePair: - if value_format is None: - raise ValueError("No value format given!") return object_class( - value_format, - model.datatypes.from_xsd(_child_text_mandatory(element, NS_AAS + "value"), value_format), - _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference) + model.datatypes.from_xsd(_child_text_mandatory(element, NS_AAS + "value"), value_format), # type: ignore + _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference), + value_format ) @classmethod @@ -1049,101 +1042,160 @@ def construct_value_list(cls, element: etree.Element, value_format: Optional[mod """ This function doesn't support the object_class parameter, because ValueList is just a generic type alias. """ + return set( - _child_construct_multiple(element, NS_AAS + "valueReferencePair", cls.construct_value_reference_pair, + _child_construct_multiple(_get_child_mandatory(element, NS_AAS + "valueReferencePairs"), + NS_AAS + "valueReferencePair", cls.construct_value_reference_pair, cls.failsafe, value_format=value_format) ) @classmethod - def construct_iec61360_concept_description(cls, element: etree.Element, - identifier: Optional[model.Identifier] = None, - object_class=model.IEC61360ConceptDescription, **_kwargs: Any) \ - -> model.IEC61360ConceptDescription: - if identifier is None: - raise ValueError("No identifier given!") - cd = object_class( - identifier, - _child_construct_mandatory(element, NS_AAS + "preferredName", cls.construct_lang_string_set) + def construct_concept_description(cls, element: etree.Element, object_class=model.ConceptDescription, + **_kwargs: Any) -> model.ConceptDescription: + cd = object_class(_child_text_mandatory(element, NS_AAS + "id")) + is_case_of = element.find(NS_AAS + "isCaseOf") + if is_case_of is not None: + for ref in _child_construct_multiple(is_case_of, NS_AAS + "reference", cls.construct_reference, + cls.failsafe): + cd.is_case_of.add(ref) + cls._amend_abstract_attributes(cd, element) + return cd + + @classmethod + def construct_embedded_data_specification(cls, element: etree.Element, object_class=model.EmbeddedDataSpecification, + **_kwargs: Any) -> model.EmbeddedDataSpecification: + data_specification_content = _get_child_mandatory(element, NS_AAS + "dataSpecificationContent") + if len(data_specification_content) == 0: + raise KeyError(f"{_element_pretty_identifier(data_specification_content)} has no data specification!") + if len(data_specification_content) > 1: + logger.warning(f"{_element_pretty_identifier(data_specification_content)} has more than one " + "data specification, using the first one...") + embedded_data_specification = object_class( + _child_construct_mandatory(element, NS_AAS + "dataSpecification", cls.construct_global_reference), + _failsafe_construct_mandatory(data_specification_content[0], cls.construct_data_specification_content) ) - data_type = _get_text_mapped_or_none(element.find(NS_AAS + "dataType"), IEC61360_DATA_TYPES_INVERSE) - if data_type is not None: - cd.data_type = data_type - definition = _failsafe_construct(element.find(NS_AAS + "definition"), cls.construct_lang_string_set, - cls.failsafe) - if definition is not None: - cd.definition = definition + cls._amend_abstract_attributes(embedded_data_specification, element) + return embedded_data_specification + + @classmethod + def construct_data_specification_content(cls, element: etree.Element, **kwargs: Any) \ + -> model.DataSpecificationContent: + """ + This function doesn't support the object_class parameter. + Overwrite each individual DataSpecificationContent constructor function instead. + """ + data_specification_contents: Dict[str, Callable[..., model.DataSpecificationContent]] = \ + {NS_AAS + k: v for k, v in { + "dataSpecificationIec61360": cls.construct_data_specification_iec61360, + "dataSpecificationPhysicalUnit": cls.construct_data_specification_physical_unit, + }.items()} + if element.tag not in data_specification_contents: + raise KeyError(f"{_element_pretty_identifier(element)} is not a valid DataSpecificationContent!") + return data_specification_contents[element.tag](element, **kwargs) + + @classmethod + def construct_data_specification_physical_unit(cls, element: etree.Element, + object_class=model.DataSpecificationPhysicalUnit, **_kwargs: Any) \ + -> model.DataSpecificationPhysicalUnit: + dspu = object_class(_child_text_mandatory(element, NS_AAS + "unitName"), + _child_text_mandatory(element, NS_AAS + "unitSymbol"), + _child_construct_mandatory(element, NS_AAS + "definition", cls.construct_lang_string_set)) + si_notation = _get_text_or_none(element.find(NS_AAS + "siNotation")) + if si_notation is not None: + dspu.si_notation = si_notation + si_name = _get_text_or_none(element.find(NS_AAS + "siName")) + if si_name is not None: + dspu.si_name = si_name + din_notation = _get_text_or_none(element.find(NS_AAS + "dinNotation")) + if din_notation is not None: + dspu.din_notation = din_notation + ece_name = _get_text_or_none(element.find(NS_AAS + "eceName")) + if ece_name is not None: + dspu.ece_name = ece_name + ece_code = _get_text_or_none(element.find(NS_AAS + "eceCode")) + if ece_code is not None: + dspu.ece_code = ece_code + nist_name = _get_text_or_none(element.find(NS_AAS + "nistName")) + if nist_name is not None: + dspu.nist_name = nist_name + source_of_definition = _get_text_or_none(element.find(NS_AAS + "sourceOfDefinition")) + if source_of_definition is not None: + dspu.source_of_definition = source_of_definition + conversion_factor = _get_text_or_none(element.find(NS_AAS + "conversionFactor")) + if conversion_factor is not None: + dspu.conversion_factor = conversion_factor + registration_authority_id = _get_text_or_none(element.find(NS_AAS + "registrationAuthorityId")) + if registration_authority_id is not None: + dspu.registration_authority_id = registration_authority_id + supplier = _get_text_or_none(element.find(NS_AAS + "supplier")) + if supplier is not None: + dspu.supplier = supplier + cls._amend_abstract_attributes(dspu, element) + return dspu + + @classmethod + def construct_data_specification_iec61360(cls, element: etree.Element, object_class=model.DataSpecificationIEC61360, + **_kwargs: Any) -> model.DataSpecificationIEC61360: + ds_iec = object_class(_child_construct_mandatory(element, NS_AAS + "preferredName", + cls.construct_lang_string_set)) short_name = _failsafe_construct(element.find(NS_AAS + "shortName"), cls.construct_lang_string_set, cls.failsafe) if short_name is not None: - cd.short_name = short_name + ds_iec.short_name = short_name unit = _get_text_or_none(element.find(NS_AAS + "unit")) if unit is not None: - cd.unit = unit + ds_iec.unit = unit unit_id = _failsafe_construct(element.find(NS_AAS + "unitId"), cls.construct_reference, cls.failsafe) if unit_id is not None: - cd.unit_id = unit_id - source_of_definition = _get_text_or_none(element.find(NS_AAS + "sourceOfDefinition")) - if source_of_definition is not None: - cd.source_of_definition = source_of_definition + ds_iec.unit_id = unit_id + source_of_definiion = _get_text_or_none(element.find(NS_AAS + "sourceOfDefinition")) + if source_of_definiion is not None: + ds_iec.source_of_definition = source_of_definiion symbol = _get_text_or_none(element.find(NS_AAS + "symbol")) if symbol is not None: - cd.symbol = symbol - value_format = _get_text_mapped_or_none(element.find(NS_AAS + "valueFormat"), - model.datatypes.XSD_TYPE_CLASSES) + ds_iec.symbol = symbol + data_type = _get_text_mapped_or_none(element.find(NS_AAS + "dataType"), IEC61360_DATA_TYPES_INVERSE) + if data_type is not None: + ds_iec.data_type = data_type + definition = _failsafe_construct(element.find(NS_AAS + "definition"), cls.construct_lang_string_set, + cls.failsafe) + if definition is not None: + ds_iec.definition = definition + value_format = _get_text_mapped_or_none(element.find(NS_AAS + "valueFormat"), model.datatypes.XSD_TYPE_CLASSES) if value_format is not None: - cd.value_format = value_format + ds_iec.value_format = value_format value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe, value_format=value_format) if value_list is not None: - cd.value_list = value_list + ds_iec.value_list = value_list value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None and value_format is not None: - cd.value = model.datatypes.from_xsd(value, value_format) - value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) - if value_id is not None: - cd.value_id = value_id - for level_type_element in element.findall(NS_AAS + "levelType"): - level_type = _get_text_mapped_or_none(level_type_element, IEC61360_LEVEL_TYPES_INVERSE) - if level_type is None: - error_message = f"{_element_pretty_identifier(level_type_element)} has invalid value: " \ - + str(level_type_element.text) - if not cls.failsafe: - raise ValueError(error_message) - logger.warning(error_message) - continue - cd.level_types.add(level_type) - return cd - - @classmethod - def construct_concept_description(cls, element: etree.Element, object_class=model.ConceptDescription, - **_kwargs: Any) -> model.ConceptDescription: - cd: Optional[model.ConceptDescription] = None - identifier = _child_text_mandatory(element, NS_AAS + "id") - # Hack to detect IEC61360ConceptDescriptions, which are represented using dataSpecification according to DotAAS - dspec_tag = NS_AAS + "embeddedDataSpecification" - dspecs = element.findall(dspec_tag) - if len(dspecs) > 1: - logger.warning(f"{_element_pretty_identifier(element)} has more than one " - f"{_tag_replace_namespace(dspec_tag, element.nsmap)}. This model currently supports only one" - f" per {_tag_replace_namespace(element.tag, element.nsmap)}!") - if len(dspecs) > 0: - dspec = dspecs[0] - dspec_content = dspec.find(NS_AAS + "dataSpecificationContent") - if dspec_content is not None: - dspec_ref = _failsafe_construct(dspec.find(NS_AAS + "dataSpecification"), cls.construct_reference, - cls.failsafe) - if dspec_ref is not None and len(dspec_ref.key) > 0 and dspec_ref.key[0].value == \ - "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0": - cd = _failsafe_construct(dspec_content.find(NS_AAS + "dataSpecificationIEC61360"), - cls.construct_iec61360_concept_description, cls.failsafe, - identifier=identifier) - if cd is None: - cd = object_class(identifier) - for ref in _failsafe_construct_multiple(element.findall(NS_AAS + "isCaseOf"), cls.construct_reference, - cls.failsafe): - cd.is_case_of.add(ref) - cls._amend_abstract_attributes(cd, element) - return cd + ds_iec.value = model.datatypes.from_xsd(value, value_format) + level_type = element.find(NS_AAS + "levelType") + if level_type is not None: + for child in level_type: + tag = child.tag.split(NS_AAS, 1)[-1] + if tag not in IEC61360_LEVEL_TYPES_INVERSE: + error_message = f"{_element_pretty_identifier(element)} has invalid levelType: {tag}" + if not cls.failsafe: + raise ValueError(error_message) + logger.warning(error_message) + continue + try: + if child.text is None: + raise ValueError + level_type_value = _str_to_bool(child.text) + except ValueError: + error_message = f"levelType {tag} of {_element_pretty_identifier(element)} has invalid boolean: " \ + + str(child.text) + if not cls.failsafe: + raise ValueError(error_message) + logger.warning(error_message) + continue + if level_type_value: + ds_iec.level_types.add(IEC61360_LEVEL_TYPES_INVERSE[tag]) + cls._amend_abstract_attributes(ds_iec, element) + return ds_iec class StrictAASFromXmlDecoder(AASFromXmlDecoder): @@ -1253,6 +1305,10 @@ class XMLConstructables(enum.Enum): SUBMODEL_ELEMENT = enum.auto() VALUE_LIST = enum.auto() LANG_STRING_SET = enum.auto() + EMBEDDED_DATA_SPECIFICATION = enum.auto() + DATA_SPECIFICATION_CONTENT = enum.auto() + DATA_SPECIFICATION_IEC61360 = enum.auto() + DATA_SPECIFICATION_PHYSICAL_UNIT = enum.auto() def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool = True, stripped: bool = False, @@ -1334,17 +1390,23 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_submodel elif construct == XMLConstructables.VALUE_REFERENCE_PAIR: constructor = decoder_.construct_value_reference_pair - elif construct == XMLConstructables.IEC61360_CONCEPT_DESCRIPTION: - constructor = decoder_.construct_iec61360_concept_description elif construct == XMLConstructables.CONCEPT_DESCRIPTION: constructor = decoder_.construct_concept_description elif construct == XMLConstructables.LANG_STRING_SET: constructor = decoder_.construct_lang_string_set + elif construct == XMLConstructables.EMBEDDED_DATA_SPECIFICATION: + constructor = decoder_.construct_embedded_data_specification + elif construct == XMLConstructables.DATA_SPECIFICATION_IEC61360: + constructor = decoder_.construct_data_specification_iec61360 + elif construct == XMLConstructables.DATA_SPECIFICATION_PHYSICAL_UNIT: + constructor = decoder_.construct_data_specification_physical_unit # the following constructors decide which constructor to call based on the elements tag elif construct == XMLConstructables.DATA_ELEMENT: constructor = decoder_.construct_data_element elif construct == XMLConstructables.SUBMODEL_ELEMENT: constructor = decoder_.construct_submodel_element + elif construct == XMLConstructables.DATA_SPECIFICATION_CONTENT: + constructor = decoder_.construct_data_specification_content # type aliases elif construct == XMLConstructables.VALUE_LIST: constructor = decoder_.construct_value_list diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index b7e8e0f..21f5ff3 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.xml.xml_serialization: @@ -26,18 +25,13 @@ from basyx.aas import model from .. import _generic +NS_AAS = _generic.XML_NS_AAS + # ############################################################## # functions to manipulate etree.Elements more effectively # ############################################################## -# Namespace definition -NS_AAS = "{https://admin-shell.io/aas/3/0/RC02}" -NS_ABAC = "{http://admin-shell.io/aas/abac/3/0/RC02}" -NS_MAP = {"aas": "https://admin-shell.io/aas/3/0/RC02", - "abac": "https://admin-shell.io/aas/abac/3/0/RC02"} - - def _generate_element(name: str, text: Optional[str] = None, attributes: Optional[Dict] = None) -> etree.Element: @@ -96,11 +90,11 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: et_extension.append(extension_to_xml(extension, tag=NS_AAS + "extension")) elm.append(et_extension) if isinstance(obj, model.Referable): + if obj.category: + elm.append(_generate_element(name=NS_AAS + "category", text=obj.category)) elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) if obj.display_name: elm.append(lang_string_set_to_xml(obj.display_name, tag=NS_AAS + "displayName")) - if obj.category: - elm.append(_generate_element(name=NS_AAS + "category", text=obj.category)) if obj.description: elm.append(lang_string_set_to_xml(obj.description, tag=NS_AAS + "description")) if isinstance(obj, model.Identifiable): @@ -116,14 +110,23 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: if isinstance(obj, model.HasSemantics): if obj.semantic_id: elm.append(reference_to_xml(obj.semantic_id, tag=NS_AAS+"semanticId")) + if obj.supplemental_semantic_id: + et_supplemental_semantic_ids = _generate_element(NS_AAS + "supplementalSemanticIds") + for supplemental_semantic_id in obj.supplemental_semantic_id: + et_supplemental_semantic_ids.append(reference_to_xml(supplemental_semantic_id, NS_AAS+"reference")) + elm.append(et_supplemental_semantic_ids) if isinstance(obj, model.Qualifiable): if obj.qualifier: et_qualifier = _generate_element(NS_AAS + "qualifiers") for qualifier in obj.qualifier: - - if isinstance(qualifier, model.Qualifier): - et_qualifier.append(qualifier_to_xml(qualifier, tag=NS_AAS+"qualifier")) + et_qualifier.append(qualifier_to_xml(qualifier, tag=NS_AAS+"qualifier")) elm.append(et_qualifier) + if isinstance(obj, model.HasDataSpecification): + if obj.embedded_data_specifications: + et_embedded_data_specifications = _generate_element(NS_AAS + "embeddedDataSpecifications") + for eds in obj.embedded_data_specifications: + et_embedded_data_specifications.append(embedded_data_specification_to_xml(eds)) + elm.append(et_embedded_data_specifications) return elm @@ -145,6 +148,7 @@ def _value_to_xml(value: model.ValueDataType, """ # todo: add "{NS_XSI+"type": "xs:"+model.datatypes.XSD_TYPE_NAMES[value_type]}" as attribute, if the schema allows # it + # TODO: if this is ever changed, check value_reference_pair_to_xml() return _generate_element(tag, text=model.datatypes.xsd_repr(value)) @@ -175,7 +179,7 @@ def administrative_information_to_xml(obj: model.AdministrativeInformation, :param tag: Namespace+Tag of the serialized element. Default is "aas:administration" :return: Serialized ElementTree object """ - et_administration = _generate_element(tag) + et_administration = abstract_classes_to_xml(tag, obj) if obj.version: et_administration.append(_generate_element(name=NS_AAS + "version", text=obj.version)) if obj.revision: @@ -236,12 +240,13 @@ def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etr :return: Serialized ElementTreeObject """ et_qualifier = abstract_classes_to_xml(tag, obj) - if obj.value_id: - et_qualifier.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) - if obj.value: - et_qualifier.append(_value_to_xml(obj.value, obj.value_type)) + et_qualifier.append(_generate_element(NS_AAS + "kind", text=_generic.QUALIFIER_KIND[obj.kind])) et_qualifier.append(_generate_element(NS_AAS + "type", text=obj.type)) et_qualifier.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) + if obj.value: + et_qualifier.append(_value_to_xml(obj.value, obj.value_type)) + if obj.value_id: + et_qualifier.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) return et_qualifier @@ -279,8 +284,9 @@ def value_reference_pair_to_xml(obj: model.ValueReferencePair, :return: Serialized ElementTree object """ et_vrp = _generate_element(tag) - et_vrp.append(_value_to_xml(obj.value, obj.value_type)) - et_vrp.append(reference_to_xml(obj.value_id, "valueId")) + # TODO: value_type isn't used at all by _value_to_xml(), thus we can ignore the type here for now + et_vrp.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore + et_vrp.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) return et_vrp @@ -296,8 +302,10 @@ def value_list_to_xml(obj: model.ValueList, :return: Serialized ElementTree object """ et_value_list = _generate_element(tag) + et_value_reference_pairs = _generate_element(NS_AAS+"valueReferencePairs") for aas_reference_pair in obj: - et_value_list.append(value_reference_pair_to_xml(aas_reference_pair, "valueReferencePair")) + et_value_reference_pairs.append(value_reference_pair_to_xml(aas_reference_pair, NS_AAS+"valueReferencePair")) + et_value_list.append(et_value_reference_pairs) return et_value_list @@ -341,7 +349,7 @@ def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"ass et_specific_asset_id.append(specific_asset_id_to_xml(specific_asset_id, NS_AAS + "specificAssetId")) et_asset_information.append(et_specific_asset_id) if obj.default_thumbnail: - et_asset_information.append(resource_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbNail")) + et_asset_information.append(resource_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbnail")) return et_asset_information @@ -356,94 +364,130 @@ def concept_description_to_xml(obj: model.ConceptDescription, :return: Serialized ElementTree object """ et_concept_description = abstract_classes_to_xml(tag, obj) - if isinstance(obj, model.concept.IEC61360ConceptDescription): - et_embedded_data_specification = _generate_element(NS_AAS+"embeddedDataSpecification") - et_data_spec_content = _generate_element(NS_AAS+"dataSpecificationContent") - et_data_spec_content.append(_iec61360_concept_description_to_xml(obj)) - et_embedded_data_specification.append(et_data_spec_content) - et_concept_description.append(et_embedded_data_specification) - et_embedded_data_specification.append(reference_to_xml(model.GlobalReference( - (model.Key( - model.KeyTypes.GLOBAL_REFERENCE, - "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0" - ),)), NS_AAS+"dataSpecification")) if obj.is_case_of: + et_is_case_of = _generate_element(NS_AAS+"isCaseOf") for reference in obj.is_case_of: - et_concept_description.append(reference_to_xml(reference, NS_AAS+"isCaseOf")) + et_is_case_of.append(reference_to_xml(reference, NS_AAS+"reference")) + et_concept_description.append(et_is_case_of) return et_concept_description -def _iec61360_concept_description_to_xml(obj: model.concept.IEC61360ConceptDescription, - tag: str = NS_AAS+"dataSpecificationIEC61360") -> etree.Element: - """ - Add the 'embeddedDataSpecifications' attribute to IEC61360ConceptDescription's JSON representation. - - `IEC61360ConceptDescription` is not a distinct class according DotAAS, but instead is built by referencing - "DataSpecificationIEC61360" as dataSpecification. However, we implemented it as an explicit class, inheriting from - ConceptDescription, but we want to generate compliant XML documents. So, we fake the XML structure of an object - with dataSpecifications. - - :param obj: model.concept.IEC61360ConceptDescription object - :param tag: name of the serialized lss_tag - :return: serialized ElementTree object - """ - - def _iec_value_reference_pair_to_xml(vrp: model.ValueReferencePair, - vrp_tag: str = NS_AAS + "valueReferencePair") -> etree.Element: - """ - serialization of objects of class ValueReferencePair to XML - - :param vrp: object of class ValueReferencePair - :param vrp_tag: vl_tag of the serialized element, default is "valueReferencePair" - :return: serialized ElementTree object - """ - et_vrp = _generate_element(vrp_tag) - et_vrp.append(reference_to_xml(vrp.value_id, NS_AAS + "valueId")) - et_vrp.append(_value_to_xml(vrp.value, vrp.value_type, tag=NS_AAS + "value")) - return et_vrp - - def _iec_value_list_to_xml(vl: model.ValueList, - vl_tag: str = NS_AAS + "valueList") -> etree.Element: - """ - serialization of objects of class ValueList to XML - - :param vl: object of class ValueList - :param vl_tag: vl_tag of the serialized element, default is "valueList" - :return: serialized ElementTree object - """ - et_value_list = _generate_element(vl_tag) - for aas_reference_pair in vl: - et_value_list.append(_iec_value_reference_pair_to_xml(aas_reference_pair, NS_AAS + "valueReferencePair")) - return et_value_list - - et_iec = _generate_element(tag) - et_iec.append(lang_string_set_to_xml(obj.preferred_name, NS_AAS + "preferredName")) - if obj.short_name: - et_iec.append(lang_string_set_to_xml(obj.short_name, NS_AAS + "shortName")) - if obj.unit: - et_iec.append(_generate_element(NS_AAS + "unit", text=obj.unit)) - if obj.unit_id: - et_iec.append(reference_to_xml(obj.unit_id, NS_AAS + "unitId")) - if obj.source_of_definition: - et_iec.append(_generate_element(NS_AAS + "sourceOfDefinition", text=obj.source_of_definition)) - if obj.symbol: - et_iec.append(_generate_element(NS_AAS + "symbol", text=obj.symbol)) - if obj.data_type: - et_iec.append(_generate_element(NS_AAS + "dataType", text=_generic.IEC61360_DATA_TYPES[obj.data_type])) - if obj.definition: - et_iec.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition")) - if obj.value_format: - et_iec.append(_generate_element(NS_AAS + "valueFormat", text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) +def embedded_data_specification_to_xml(obj: model.EmbeddedDataSpecification, + tag: str = NS_AAS+"embeddedDataSpecification") -> etree.Element: + """ + Serialization of objects of class :class:`~aas.model.base.EmbeddedDataSpecification` to XML + + :param obj: Object of class :class:`~aas.model.base.EmbeddedDataSpecification` + :param tag: Namespace+Tag of the ElementTree object. Default is "aas:embeddedDataSpecification" + :return: Serialized ElementTree object + """ + et_embedded_data_specification = abstract_classes_to_xml(tag, obj) + et_embedded_data_specification.append(reference_to_xml(obj.data_specification, tag=NS_AAS + "dataSpecification")) + et_embedded_data_specification.append(data_specification_content_to_xml(obj.data_specification_content)) + return et_embedded_data_specification + + +def data_specification_content_to_xml(obj: model.DataSpecificationContent, + tag: str = NS_AAS+"dataSpecificationContent") -> etree.Element: + """ + Serialization of objects of class :class:`~aas.model.base.DataSpecificationContent` to XML + + :param obj: Object of class :class:`~aas.model.base.DataSpecificationContent` + :param tag: Namespace+Tag of the ElementTree object. Default is "aas:dataSpecificationContent" + :return: Serialized ElementTree object + """ + et_data_specification_content = abstract_classes_to_xml(tag, obj) + if isinstance(obj, model.DataSpecificationIEC61360): + et_data_specification_content.append(data_specification_iec61360_to_xml(obj)) + elif isinstance(obj, model.DataSpecificationPhysicalUnit): + et_data_specification_content.append(data_specification_physical_unit_to_xml(obj)) + else: + raise TypeError(f"Serialization of {obj.__class__} to XML is not supported!") + return et_data_specification_content + + +def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, + tag: str = NS_AAS+"dataSpecificationIec61360") -> etree.Element: + """ + Serialization of objects of class :class:`~aas.model.base.DataSpecificationIEC61360` to XML + + :param obj: Object of class :class:`~aas.model.base.DataSpecificationIEC61360` + :param tag: Namespace+Tag of the ElementTree object. Default is "aas:dataSpecificationIec61360" + :return: Serialized ElementTree object + """ + et_data_specification_iec61360 = abstract_classes_to_xml(tag, obj) + et_data_specification_iec61360.append(lang_string_set_to_xml(obj.preferred_name, NS_AAS + "preferredName")) + if obj.short_name is not None: + et_data_specification_iec61360.append(lang_string_set_to_xml(obj.short_name, NS_AAS + "shortName")) + if obj.unit is not None: + et_data_specification_iec61360.append(_generate_element(NS_AAS + "unit", text=obj.unit)) + if obj.unit_id is not None: + et_data_specification_iec61360.append(reference_to_xml(obj.unit_id, NS_AAS + "unitId")) + if obj.source_of_definition is not None: + et_data_specification_iec61360.append(_generate_element(NS_AAS + "sourceOfDefinition", + text=obj.source_of_definition)) + if obj.symbol is not None: + et_data_specification_iec61360.append(_generate_element(NS_AAS + "symbol", text=obj.symbol)) + if obj.data_type is not None: + et_data_specification_iec61360.append(_generate_element(NS_AAS + "dataType", + text=_generic.IEC61360_DATA_TYPES[obj.data_type])) + if obj.definition is not None: + et_data_specification_iec61360.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition")) + if obj.value_format is not None: + et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat", + text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) + # this can be either None or an empty set, both of which are equivalent to the bool false + # thus we don't check 'is not None' for this property if obj.value_list: - et_iec.append(_iec_value_list_to_xml(obj.value_list, NS_AAS + "valueList")) - if obj.value: - et_iec.append(_generate_element(NS_AAS + "value", text=model.datatypes.xsd_repr(obj.value))) - if obj.value_id: - et_iec.append(reference_to_xml(obj.value_id, NS_AAS + "valueId")) + et_data_specification_iec61360.append(value_list_to_xml(obj.value_list)) + if obj.value is not None: + et_data_specification_iec61360.append(_generate_element(NS_AAS + "value", + text=model.datatypes.xsd_repr(obj.value))) if obj.level_types: - for level_type in obj.level_types: - et_iec.append(_generate_element(NS_AAS + "levelType", text=_generic.IEC61360_LEVEL_TYPES[level_type])) - return et_iec + et_level_types = _generate_element(NS_AAS + "levelType") + for k, v in _generic.IEC61360_LEVEL_TYPES.items(): + et_level_types.append(_generate_element(NS_AAS + v, text=boolean_to_xml(k in obj.level_types))) + et_data_specification_iec61360.append(et_level_types) + return et_data_specification_iec61360 + + +def data_specification_physical_unit_to_xml(obj: model.DataSpecificationPhysicalUnit, + tag: str = NS_AAS+"dataSpecificationPhysicalUnit") -> etree.Element: + """ + Serialization of objects of class :class:`~aas.model.base.DataSpecificationPhysicalUnit` to XML + + :param obj: Object of class :class:`~aas.model.base.DataSpecificationPhysicalUnit` + :param tag: Namespace+Tag of the ElementTree object. Default is "aas:dataSpecificationPhysicalUnit" + :return: Serialized ElementTree object + """ + et_data_specification_physical_unit = abstract_classes_to_xml(tag, obj) + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "unitName", text=obj.unit_name)) + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "unitSymbol", text=obj.unit_symbol)) + et_data_specification_physical_unit.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition")) + if obj.si_notation is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "siNotation", text=obj.si_notation)) + if obj.si_name is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "siName", text=obj.si_name)) + if obj.din_notation is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "dinNotation", text=obj.din_notation)) + if obj.ece_name is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "eceName", text=obj.ece_name)) + if obj.ece_code is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "eceCode", text=obj.ece_code)) + if obj.nist_name is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "nistName", text=obj.nist_name)) + if obj.source_of_definition is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "sourceOfDefinition", + text=obj.source_of_definition)) + if obj.conversion_factor is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "conversionFactor", + text=obj.conversion_factor)) + if obj.registration_authority_id is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "registrationAuthorityId", + text=obj.registration_authority_id)) + if obj.supplier is not None: + et_data_specification_physical_unit.append(_generate_element(NS_AAS + "supplier", text=obj.supplier)) + return et_data_specification_physical_unit def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, @@ -473,7 +517,7 @@ def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, def security_to_xml(obj: model.Security, - tag: str = NS_ABAC+"security") -> etree.Element: + tag: str = _generic.XML_NS_ABAC+"security") -> etree.Element: """ Serialization of objects of class :class:`~aas.model.security.Security` to XML @@ -528,15 +572,11 @@ def submodel_to_xml(obj: model.Submodel, :return: Serialized ElementTree object """ et_submodel = abstract_classes_to_xml(tag, obj) - et_submodel_elements = _generate_element(NS_AAS + "submodelElements") if obj.submodel_element: + et_submodel_elements = _generate_element(NS_AAS + "submodelElements") for submodel_element in obj.submodel_element: - # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 - et_submodel_element = _generate_element(NS_AAS+"submodelElement") - et_submodel_element.append(submodel_element_to_xml(submodel_element)) - et_submodel_elements.append(et_submodel_element) - et_submodel.append(et_submodel_elements) + et_submodel_elements.append(submodel_element_to_xml(submodel_element)) + et_submodel.append(et_submodel_elements) return et_submodel @@ -550,11 +590,11 @@ def property_to_xml(obj: model.Property, :return: Serialized ElementTree object """ et_property = abstract_classes_to_xml(tag, obj) + et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) + if obj.value is not None: + et_property.append(_value_to_xml(obj.value, obj.value_type)) if obj.value_id: et_property.append(reference_to_xml(obj.value_id, NS_AAS + "valueId")) - if obj.value: - et_property.append(_value_to_xml(obj.value, obj.value_type)) - et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) return et_property @@ -568,10 +608,10 @@ def multi_language_property_to_xml(obj: model.MultiLanguageProperty, :return: Serialized ElementTree object """ et_multi_language_property = abstract_classes_to_xml(tag, obj) - if obj.value_id: - et_multi_language_property.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) if obj.value: et_multi_language_property.append(lang_string_set_to_xml(obj.value, tag=NS_AAS + "value")) + if obj.value_id: + et_multi_language_property.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) return et_multi_language_property @@ -585,12 +625,12 @@ def range_to_xml(obj: model.Range, :return: Serialized ElementTree object """ et_range = abstract_classes_to_xml(tag, obj) - if obj.max is not None: - et_range.append(_value_to_xml(obj.max, obj.value_type, tag=NS_AAS + "max")) - if obj.min is not None: - et_range.append(_value_to_xml(obj.min, obj.value_type, tag=NS_AAS + "min")) et_range.append(_generate_element(name=NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) + if obj.min is not None: + et_range.append(_value_to_xml(obj.min, obj.value_type, tag=NS_AAS + "min")) + if obj.max is not None: + et_range.append(_value_to_xml(obj.max, obj.value_type, tag=NS_AAS + "max")) return et_range @@ -671,14 +711,11 @@ def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, :return: Serialized ElementTree object """ et_submodel_element_collection = abstract_classes_to_xml(tag, obj) - # todo: remove wrapping submodelElement-tag, in accordance to future schema - et_value = _generate_element(NS_AAS + "value") if obj.value: + et_value = _generate_element(NS_AAS + "value") for submodel_element in obj.value: - et_submodel_element = _generate_element(NS_AAS+"submodelElement") - et_submodel_element.append(submodel_element_to_xml(submodel_element)) - et_value.append(et_submodel_element) - et_submodel_element_collection.append(et_value) + et_value.append(submodel_element_to_xml(submodel_element)) + et_submodel_element_collection.append(et_value) return et_submodel_element_collection @@ -727,13 +764,11 @@ def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElemen :return: Serialized ElementTree object """ et_annotated_relationship_element = relationship_element_to_xml(obj, tag) - et_annotations = _generate_element(name=NS_AAS+"annotations") if obj.annotation: + et_annotations = _generate_element(name=NS_AAS + "annotations") for data_element in obj.annotation: - et_data_element = _generate_element(name=NS_AAS+"dataElement") - et_data_element.append(data_element_to_xml(data_element)) - et_annotations.append(et_data_element) - et_annotated_relationship_element.append(et_annotations) + et_annotations.append(data_element_to_xml(data_element)) + et_annotated_relationship_element.append(et_annotations) return et_annotated_relationship_element @@ -763,15 +798,21 @@ def operation_to_xml(obj: model.Operation, :return: Serialized ElementTree object """ et_operation = abstract_classes_to_xml(tag, obj) - if obj.in_output_variable: - for in_out_ov in obj.in_output_variable: - et_operation.append(operation_variable_to_xml(in_out_ov, NS_AAS+"inoutputVariable")) if obj.input_variable: + et_input_variables = _generate_element(NS_AAS+"inputVariables") for input_ov in obj.input_variable: - et_operation.append(operation_variable_to_xml(input_ov, NS_AAS+"inputVariable")) + et_input_variables.append(operation_variable_to_xml(input_ov, NS_AAS+"operationVariable")) + et_operation.append(et_input_variables) if obj.output_variable: + et_output_variables = _generate_element(NS_AAS+"outputVariables") for output_ov in obj.output_variable: - et_operation.append(operation_variable_to_xml(output_ov, NS_AAS+"outputVariable")) + et_output_variables.append(operation_variable_to_xml(output_ov, NS_AAS+"operationVariable")) + et_operation.append(et_output_variables) + if obj.in_output_variable: + et_inoutput_variables = _generate_element(NS_AAS+"inoutputVariables") + for in_out_ov in obj.in_output_variable: + et_inoutput_variables.append(operation_variable_to_xml(in_out_ov, NS_AAS+"operationVariable")) + et_operation.append(et_inoutput_variables) return et_operation @@ -796,20 +837,17 @@ def entity_to_xml(obj: model.Entity, :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:entity" :return: Serialized ElementTree object """ - # todo: remove wrapping submodelElement, in accordance to future schemas et_entity = abstract_classes_to_xml(tag, obj) + if obj.statement: + et_statements = _generate_element(NS_AAS + "statements") + for statement in obj.statement: + et_statements.append(submodel_element_to_xml(statement)) + et_entity.append(et_statements) + et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type])) if obj.global_asset_id: et_entity.append(reference_to_xml(obj.global_asset_id, NS_AAS + "globalAssetId")) if obj.specific_asset_id: et_entity.append(specific_asset_id_to_xml(obj.specific_asset_id, NS_AAS + "specificAssetId")) - et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type])) - et_statements = _generate_element(NS_AAS + "statements") - for statement in obj.statement: - # todo: remove the once the proposed changes get accepted - et_submodel_element = _generate_element(NS_AAS + "submodelElement") - et_submodel_element.append(submodel_element_to_xml(statement)) - et_statements.append(et_submodel_element) - et_entity.append(et_statements) return et_entity @@ -871,19 +909,22 @@ def write_aas_xml_file(file: IO, concept_descriptions.append(obj) # serialize objects to XML - root = etree.Element(NS_AAS + "environment", nsmap=NS_MAP) - et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") - for aas_obj in asset_administration_shells: - et_asset_administration_shells.append(asset_administration_shell_to_xml(aas_obj)) - et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") - for con_obj in concept_descriptions: - et_concept_descriptions.append(concept_description_to_xml(con_obj)) - et_submodels = etree.Element(NS_AAS + "submodels") - for sub_obj in submodels: - et_submodels.append(submodel_to_xml(sub_obj)) - root.insert(0, et_submodels) - root.insert(0, et_concept_descriptions) - root.insert(0, et_asset_administration_shells) + root = etree.Element(NS_AAS + "environment", nsmap=_generic.XML_NS_MAP) + if asset_administration_shells: + et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") + for aas_obj in asset_administration_shells: + et_asset_administration_shells.append(asset_administration_shell_to_xml(aas_obj)) + root.append(et_asset_administration_shells) + if submodels: + et_submodels = etree.Element(NS_AAS + "submodels") + for sub_obj in submodels: + et_submodels.append(submodel_to_xml(sub_obj)) + root.append(et_submodels) + if concept_descriptions: + et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") + for con_obj in concept_descriptions: + et_concept_descriptions.append(concept_description_to_xml(con_obj)) + root.append(et_concept_descriptions) tree = etree.ElementTree(root) tree.write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) From 41c033cdf7cc992021186245be34ed04a4d8b481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 7 Apr 2023 17:40:25 +0200 Subject: [PATCH 087/474] adapter.xml: support deserialization of `Qualifier.kind` Furthermore, check `Qualifier.kind` with `AASDataChecker` and add examplary values to example files. --- basyx/aas/adapter/xml/xml_deserialization.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 3525331..1c6954c 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -52,7 +52,7 @@ from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar from .._generic import XML_NS_AAS, MODELING_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 + DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE NS_AAS = XML_NS_AAS @@ -626,6 +626,9 @@ def construct_qualifier(cls, element: etree.Element, object_class=model.Qualifie _child_text_mandatory(element, NS_AAS + "type"), _child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) + kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), QUALIFIER_KIND_INVERSE) + if kind is not None: + qualifier.kind = kind value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: qualifier.value = model.datatypes.from_xsd(value, qualifier.value_type) From 186ef28bdedd94179087704ad7ed948e694f1a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 7 Apr 2023 19:09:24 +0200 Subject: [PATCH 088/474] adapter: adjust for `DataSpecificationIEC61360.value_format` defaulting to `String` 2373167850169dc1debc9d10179765c3d26a9c0f and 9da564c4b8906270b3bb21df892c0f566f098246 changed the default `value_format` and `value_type` of `DataSpecificationIEC61360` and `ValueReferencePair` to `String`. This commit adjusts the JSON/XML adapters accordingly. --- basyx/aas/adapter/json/json_deserialization.py | 14 +++++--------- basyx/aas/adapter/json/json_serialization.py | 5 +---- basyx/aas/adapter/xml/xml_deserialization.py | 10 +++++----- basyx/aas/adapter/xml/xml_serialization.py | 5 ++--- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 3d3bd5f..506338a 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -387,8 +387,7 @@ def _construct_lang_string_set(cls, lst: List[Dict[str, object]]) -> Optional[mo return model.LangStringSet(ret) @classmethod - def _construct_value_list(cls, dct: Dict[str, object], value_format: Optional[model.DataTypeDefXsd] = None) \ - -> model.ValueList: + def _construct_value_list(cls, dct: Dict[str, object], value_format: model.DataTypeDefXsd) -> model.ValueList: ret: model.ValueList = set() for element in _get_ts(dct, 'valueReferencePairs', list): try: @@ -403,10 +402,9 @@ def _construct_value_list(cls, dct: Dict[str, object], value_format: Optional[mo return ret @classmethod - def _construct_value_reference_pair(cls, dct: Dict[str, object], - value_format: Optional[model.DataTypeDefXsd] = None, + def _construct_value_reference_pair(cls, dct: Dict[str, object], value_format: model.DataTypeDefXsd, object_class=model.ValueReferencePair) -> model.ValueReferencePair: - return object_class(value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_format), # type: ignore + return object_class(value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_format), value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict)), value_type=value_format) @@ -508,12 +506,10 @@ def _construct_data_specification_iec61360(cls, dct: Dict[str, object], ret.source_of_definition = _get_ts(dct, 'sourceOfDefinition', str) if 'symbol' in dct: ret.symbol = _get_ts(dct, 'symbol', str) - value_format: Optional[model.DataTypeDefXsd] = None if 'valueFormat' in dct: - value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueFormat', str)] - ret.value_format = value_format + ret.value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueFormat', str)] if 'valueList' in dct: - ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict), value_format=value_format) + ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict), value_format=ret.value_format) if 'value' in dct: ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_format) if 'valueId' in dct: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 01eae0d..9ab4334 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -262,8 +262,6 @@ def _value_reference_pair_to_json(cls, obj: model.ValueReferencePair) -> Dict[st :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - if obj.value_type: - data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] data.update({'value': model.datatypes.xsd_repr(obj.value), 'valueId': obj.value_id}) return data @@ -354,8 +352,7 @@ def _data_specification_iec61360_to_json( data_spec['sourceOfDefinition'] = obj.source_of_definition if obj.symbol is not None: data_spec['symbol'] = obj.symbol - if obj.value_format is not None: - data_spec['valueFormat'] = model.datatypes.XSD_TYPE_NAMES[obj.value_format] + data_spec['valueFormat'] = model.datatypes.XSD_TYPE_NAMES[obj.value_format] if obj.value_list is not None: data_spec['valueList'] = cls._value_list_to_json(obj.value_list) if obj.value is not None: diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 1c6954c..3ab6ede 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1030,18 +1030,18 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, return submodel @classmethod - def construct_value_reference_pair(cls, element: etree.Element, value_format: Optional[model.DataTypeDefXsd] = None, + def construct_value_reference_pair(cls, element: etree.Element, value_format: model.DataTypeDefXsd, object_class=model.ValueReferencePair, **_kwargs: Any) \ -> model.ValueReferencePair: return object_class( - model.datatypes.from_xsd(_child_text_mandatory(element, NS_AAS + "value"), value_format), # type: ignore + model.datatypes.from_xsd(_child_text_mandatory(element, NS_AAS + "value"), value_format), _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference), value_format ) @classmethod - def construct_value_list(cls, element: etree.Element, value_format: Optional[model.DataTypeDefXsd] = None, - **_kwargs: Any) -> model.ValueList: + def construct_value_list(cls, element: etree.Element, value_format: model.DataTypeDefXsd, **_kwargs: Any) \ + -> model.ValueList: """ This function doesn't support the object_class parameter, because ValueList is just a generic type alias. """ @@ -1168,7 +1168,7 @@ def construct_data_specification_iec61360(cls, element: etree.Element, object_cl if value_format is not None: ds_iec.value_format = value_format value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe, - value_format=value_format) + value_format=ds_iec.value_format) if value_list is not None: ds_iec.value_list = value_list value = _get_text_or_none(element.find(NS_AAS + "value")) diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 21f5ff3..6b0a0c6 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -433,9 +433,8 @@ def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, text=_generic.IEC61360_DATA_TYPES[obj.data_type])) if obj.definition is not None: et_data_specification_iec61360.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition")) - if obj.value_format is not None: - et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat", - text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) + et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat", + text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) # this can be either None or an empty set, both of which are equivalent to the bool false # thus we don't check 'is not None' for this property if obj.value_list: From db60c3129ab5ac6980d18a5709111af3cde34a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 7 Apr 2023 23:05:46 +0200 Subject: [PATCH 089/474] model: remove security.py The security model wasn't implemented and isn't part of the new V3.0 schemata. The (de-)serialization functions weren't used since the removal of the security attribute from `AssetAdministrationShell` in 3376535eb87a6ee0b7da68f40e9132b83a088c01. In turn, also remove security (de-)serialization functions from JSON/XML adapters. Remove ABAC XML namespace definition as it isn't used. --- basyx/aas/adapter/_generic.py | 4 +--- .../aas/adapter/json/json_deserialization.py | 4 ---- basyx/aas/adapter/json/json_serialization.py | 15 --------------- basyx/aas/adapter/xml/xml_deserialization.py | 9 --------- basyx/aas/adapter/xml/xml_serialization.py | 19 ------------------- 5 files changed, 1 insertion(+), 50 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index baebd17..982328e 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -13,10 +13,8 @@ from basyx.aas import model # XML Namespace definition -XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0", - "abac": "https://admin-shell.io/aas/abac/3/0"} +XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" -XML_NS_ABAC = "{" + XML_NS_MAP["abac"] + "}" MODELING_KIND: Dict[model.ModelingKind, str] = { model.ModelingKind.TEMPLATE: 'Template', diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 506338a..778bdc2 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -359,10 +359,6 @@ def _construct_administrative_information( logger.warning("Ignoring 'revision' attribute of AdministrativeInformation object due to missing 'version'") return ret - @classmethod - def _construct_security(cls, _dct: Dict[str, object], object_class=model.Security) -> model.Security: - return object_class() - @classmethod def _construct_operation_variable( cls, dct: Dict[str, object], object_class=model.OperationVariable) -> model.OperationVariable: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 9ab4334..6f216bb 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -416,21 +416,6 @@ def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell data["submodels"] = list(obj.submodel) return data - # ################################################################# - # transformation functions to serialize classes from model.security - # ################################################################# - - @classmethod - def _security_to_json(cls, obj: model.Security) -> Dict[str, object]: # has no attributes in our implementation - """ - serialization of an object from class Security to json - - :param obj: object of class Security - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - return data - # ################################################################# # transformation functions to serialize classes from model.submodel # ################################################################# diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 3ab6ede..10c6228 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -654,13 +654,6 @@ def construct_extension(cls, element: etree.Element, object_class=model.Extensio cls._amend_abstract_attributes(extension, element) return extension - @classmethod - def construct_security(cls, _element: etree.Element, object_class=model.Security, **_kwargs: Any) -> model.Security: - """ - TODO: this is just a stub implementation - """ - return object_class() - @classmethod def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement: """ @@ -1347,8 +1340,6 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_administrative_information elif construct == XMLConstructables.QUALIFIER: constructor = decoder_.construct_qualifier - elif construct == XMLConstructables.SECURITY: - constructor = decoder_.construct_security elif construct == XMLConstructables.OPERATION_VARIABLE: constructor = decoder_.construct_operation_variable elif construct == XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 6b0a0c6..76951bb 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -510,25 +510,6 @@ def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, return et_aas -# ############################################################## -# transformation functions to serialize classes from model.security -# ############################################################## - - -def security_to_xml(obj: model.Security, - tag: str = _generic.XML_NS_ABAC+"security") -> etree.Element: - """ - Serialization of objects of class :class:`~aas.model.security.Security` to XML - - todo: This is not yet implemented - - :param obj: Object of class :class:`~aas.model.security.Security` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:security" - :return: Serialized ElementTree object - """ - return abstract_classes_to_xml(tag, obj) - - # ############################################################## # transformation functions to serialize classes from model.submodel # ############################################################## From 9541a97194ba3f53e3246347ff5beeda74d0f6ca Mon Sep 17 00:00:00 2001 From: David Niebert Date: Sat, 29 Apr 2023 10:23:51 +0200 Subject: [PATCH 090/474] `base.AssetKind`: Add new enum `NOTAPPLICABLE` and update description `NOTAPPLICABLE` added to `_generic.ASSET_KIND` --- basyx/aas/adapter/_generic.py | 3 ++- basyx/aas/adapter/json/aasJSONSchema.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 982328e..eaaed1a 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -22,7 +22,8 @@ ASSET_KIND: Dict[model.AssetKind, str] = { model.AssetKind.TYPE: 'Type', - model.AssetKind.INSTANCE: 'Instance'} + model.AssetKind.INSTANCE: 'Instance', + model.AssetKind.NOTAPPLICABLE: 'NotApplicable'} QUALIFIER_KIND: Dict[model.QualifierKind, str] = { model.QualifierKind.CONCEPT_QUALIFIER: 'ConceptQualifier', diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 63b23c9..38d3015 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -105,7 +105,7 @@ "$ref": "#/definitions/AssetKind" }, "globalAssetId": { - "$ref": "#/definitions/Reference" + "$ref": "#/definitions/Identifier" }, "specificAssetIds": { "type": "array", From 2f8a7b4c3806889310cc45ceb485d9416191cc8e Mon Sep 17 00:00:00 2001 From: David Niebert Date: Tue, 23 May 2023 11:35:43 +0200 Subject: [PATCH 091/474] Revert unrelated change in `aasJSONSchema` --- basyx/aas/adapter/json/aasJSONSchema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 38d3015..63b23c9 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -105,7 +105,7 @@ "$ref": "#/definitions/AssetKind" }, "globalAssetId": { - "$ref": "#/definitions/Identifier" + "$ref": "#/definitions/Reference" }, "specificAssetIds": { "type": "array", From 2a456a9f1e73940c6e7b9f0932b72aeb77f17085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 31 May 2023 17:10:30 +0200 Subject: [PATCH 092/474] adapter._generic: rename NOTAPPLICABLE to NOT_APPLICABLE --- basyx/aas/adapter/_generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index eaaed1a..cb06901 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -23,7 +23,7 @@ ASSET_KIND: Dict[model.AssetKind, str] = { model.AssetKind.TYPE: 'Type', model.AssetKind.INSTANCE: 'Instance', - model.AssetKind.NOTAPPLICABLE: 'NotApplicable'} + model.AssetKind.NOT_APPLICABLE: 'NotApplicable'} QUALIFIER_KIND: Dict[model.QualifierKind, str] = { model.QualifierKind.CONCEPT_QUALIFIER: 'ConceptQualifier', From f36cfe272da648d8d481b3f62d99b23844be3d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 31 May 2023 17:11:15 +0200 Subject: [PATCH 093/474] adapter: add `NOT_APPLICABLE` AssetKind to schemata --- basyx/aas/adapter/json/aasJSONSchema.json | 3 ++- basyx/aas/adapter/xml/AAS.xsd | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 63b23c9..2088f37 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -126,7 +126,8 @@ "type": "string", "enum": [ "Instance", - "Type" + "Type", + "NotApplicable" ] }, "BasicEventElement": { diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 002126a..eb8244b 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -963,6 +963,7 @@ + From 42c4e39e157fe4970103b564cc638cb0a26157a6 Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Mon, 12 Jun 2023 15:07:25 +0200 Subject: [PATCH 094/474] Add SubmodelElementList to KEY_TYPES dict `model.KeyTypes.SUBMODEL_ELEMENT_LIST` was missed in the KEY_TYPES dict, which is required for serialization --- basyx/aas/adapter/_generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index cb06901..f255bb0 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -62,6 +62,7 @@ model.KeyTypes.RELATIONSHIP_ELEMENT: 'RelationshipElement', model.KeyTypes.SUBMODEL_ELEMENT: 'SubmodelElement', model.KeyTypes.SUBMODEL_ELEMENT_COLLECTION: 'SubmodelElementCollection', + model.KeyTypes.SUBMODEL_ELEMENT_LIST: 'SubmodelElementList', model.KeyTypes.GLOBAL_REFERENCE: 'GlobalReference', model.KeyTypes.FRAGMENT_REFERENCE: 'FragmentReference'} From 5f05c01825aa84901fd412e2de6eaa79eaf2779d Mon Sep 17 00:00:00 2001 From: dxvidnrt <104552105+dxvidnrt@users.noreply.github.com> Date: Sat, 1 Jul 2023 15:45:24 +0200 Subject: [PATCH 095/474] Remove attribute `kind` from `SubmodelElement` `SubmodelElement` does not inherit from `base.HasKind` anymore. Removed all occurences of `kind` in `SubmodelElement` and its subclasses. * Remove attribute `kind` in initializations of objects of type `SubmodelElement`. * Remove check for attribute `kind` in `_helper.py` for all objects of type `SubmodelElement` * Remove attribute `kind` from methods in `json_deserialization.py` that work on classes of type `SubmodelElement` * Remove attribute `kind` from methods in `xml_deserialization.py` that work on classes of type `SubmodelElement` * Remove attribute `kind` from initialized objects of type `SubmodelElement` * Remove attribute `kind` from initialized objects of type `SubmodelElement` in file `example_aas_missing_attributes.py` * Remove check for attribute `kind` in `_check_qualifier_equal()` --------- Co-authored-by: David Niebert Co-authored-by: s-heppner --- .../aas/adapter/json/json_deserialization.py | 39 +++++++------------ basyx/aas/adapter/xml/xml_deserialization.py | 39 +++++++------------ 2 files changed, 27 insertions(+), 51 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 778bdc2..be079ce 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -526,7 +526,6 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'specificAssetIds', dict)) ret = object_class(id_short=_get_ts(dct, "idShort", str), - kind=cls._get_kind(dct), entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], global_asset_id=global_asset_id, specific_asset_id=specific_asset_id) @@ -565,8 +564,7 @@ def _construct_extension(cls, dct: Dict[str, object], object_class=model.Extensi @classmethod def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel) -> model.Submodel: - ret = object_class(id_=_get_ts(dct, 'id', str), - kind=cls._get_kind(dct)) + ret = object_class(id_=_get_ts(dct, 'id', str)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'submodelElements' in dct: for element in _get_ts(dct, "submodelElements", list): @@ -576,7 +574,7 @@ def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel @classmethod def _construct_capability(cls, dct: Dict[str, object], object_class=model.Capability) -> model.Capability: - ret = object_class(id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + ret = object_class(id_short=_get_ts(dct, "idShort", str)) cls._amend_abstract_attributes(ret, dct) return ret @@ -589,8 +587,7 @@ def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=mod observed=cls._construct_model_reference(_get_ts(dct, 'observed', dict), model.Referable), # type: ignore direction=DIRECTION_INVERSE[_get_ts(dct, "direction", str)], - state=STATE_OF_EVENT_INVERSE[_get_ts(dct, "state", str)], - kind=cls._get_kind(dct)) + state=STATE_OF_EVENT_INVERSE[_get_ts(dct, "state", str)]) cls._amend_abstract_attributes(ret, dct) if 'messageTopic' in dct: ret.message_topic = _get_ts(dct, 'messageTopic', str) @@ -606,7 +603,7 @@ def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=mod @classmethod def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operation) -> model.Operation: - ret = object_class(_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + ret = object_class(_get_ts(dct, "idShort", str)) cls._amend_abstract_attributes(ret, dct) # Deserialize variables (they are not Referable, thus we don't @@ -633,8 +630,7 @@ def _construct_relationship_element( # see https://github.com/python/mypy/issues/5374 ret = object_class(id_short=_get_ts(dct, "idShort", str), first=cls._construct_reference(_get_ts(dct, 'first', dict)), - second=cls._construct_reference(_get_ts(dct, 'second', dict)), - kind=cls._get_kind(dct)) + second=cls._construct_reference(_get_ts(dct, 'second', dict))) cls._amend_abstract_attributes(ret, dct) return ret @@ -647,8 +643,7 @@ def _construct_annotated_relationship_element( ret = object_class( id_short=_get_ts(dct, "idShort", str), first=cls._construct_reference(_get_ts(dct, 'first', dict)), - second=cls._construct_reference(_get_ts(dct, 'second', dict)), - kind=cls._get_kind(dct)) + second=cls._construct_reference(_get_ts(dct, 'second', dict))) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'annotations' in dct: for element in _get_ts(dct, 'annotations', list): @@ -660,7 +655,7 @@ def _construct_annotated_relationship_element( def _construct_submodel_element_collection(cls, dct: Dict[str, object], object_class=model.SubmodelElementCollection)\ -> model.SubmodelElementCollection: - ret = object_class(id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + ret = object_class(id_short=_get_ts(dct, "idShort", str)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'value' in dct: for element in _get_ts(dct, "value", list): @@ -685,8 +680,7 @@ def _construct_submodel_element_list(cls, dct: Dict[str, object], object_class=m type_value_list_element=type_value_list_element, order_relevant=order_relevant, semantic_id_list_element=semantic_id_list_element, - value_type_list_element=value_type_list_element, - kind=cls._get_kind(dct)) + value_type_list_element=value_type_list_element) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'value' in dct: for element in _get_ts(dct, 'value', list): @@ -697,8 +691,7 @@ def _construct_submodel_element_list(cls, dct: Dict[str, object], object_class=m @classmethod def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> model.Blob: ret = object_class(id_short=_get_ts(dct, "idShort", str), - content_type=_get_ts(dct, "contentType", str), - kind=cls._get_kind(dct)) + content_type=_get_ts(dct, "contentType", str)) cls._amend_abstract_attributes(ret, dct) if 'value' in dct: ret.value = base64.b64decode(_get_ts(dct, 'value', str)) @@ -708,8 +701,7 @@ def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> mod def _construct_file(cls, dct: Dict[str, object], object_class=model.File) -> model.File: ret = object_class(id_short=_get_ts(dct, "idShort", str), value=None, - content_type=_get_ts(dct, "contentType", str), - kind=cls._get_kind(dct)) + content_type=_get_ts(dct, "contentType", str)) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: ret.value = _get_ts(dct, 'value', str) @@ -726,7 +718,7 @@ def _construct_resource(cls, dct: Dict[str, object], object_class=model.Resource @classmethod def _construct_multi_language_property( cls, dct: Dict[str, object], object_class=model.MultiLanguageProperty) -> model.MultiLanguageProperty: - ret = object_class(id_short=_get_ts(dct, "idShort", str), kind=cls._get_kind(dct)) + ret = object_class(id_short=_get_ts(dct, "idShort", str)) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: ret.value = cls._construct_lang_string_set(_get_ts(dct, 'value', list)) @@ -737,8 +729,7 @@ def _construct_multi_language_property( @classmethod def _construct_property(cls, dct: Dict[str, object], object_class=model.Property) -> model.Property: ret = object_class(id_short=_get_ts(dct, "idShort", str), - value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)], - kind=cls._get_kind(dct)) + value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)],) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) @@ -749,8 +740,7 @@ def _construct_property(cls, dct: Dict[str, object], object_class=model.Property @classmethod def _construct_range(cls, dct: Dict[str, object], object_class=model.Range) -> model.Range: ret = object_class(id_short=_get_ts(dct, "idShort", str), - value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)], - kind=cls._get_kind(dct)) + value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)],) cls._amend_abstract_attributes(ret, dct) if 'min' in dct and dct['min'] is not None: ret.min = model.datatypes.from_xsd(_get_ts(dct, 'min', str), ret.value_type) @@ -762,8 +752,7 @@ def _construct_range(cls, dct: Dict[str, object], object_class=model.Range) -> m def _construct_reference_element( cls, dct: Dict[str, object], object_class=model.ReferenceElement) -> model.ReferenceElement: ret = object_class(id_short=_get_ts(dct, "idShort", str), - value=None, - kind=cls._get_kind(dct)) + value=None) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: ret.value = cls._construct_reference(_get_ts(dct, 'value', dict)) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 10c6228..4a0f918 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -493,8 +493,7 @@ def _construct_relationship_element_internal(cls, element: etree.Element, object relationship_element = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), _child_construct_mandatory(element, NS_AAS + "first", cls.construct_reference), - _child_construct_mandatory(element, NS_AAS + "second", cls.construct_reference), - kind=_get_modeling_kind(element) + _child_construct_mandatory(element, NS_AAS + "second", cls.construct_reference) ) cls._amend_abstract_attributes(relationship_element, element) return relationship_element @@ -726,8 +725,7 @@ def construct_basic_event_element(cls, element: etree.Element, object_class=mode _child_text_mandatory(element, NS_AAS + "idShort"), _child_construct_mandatory(element, NS_AAS + "observed", cls._construct_referable_reference), _child_text_mandatory_mapped(element, NS_AAS + "direction", DIRECTION_INVERSE), - _child_text_mandatory_mapped(element, NS_AAS + "state", STATE_OF_EVENT_INVERSE), - kind=_get_modeling_kind(element) + _child_text_mandatory_mapped(element, NS_AAS + "state", STATE_OF_EVENT_INVERSE) ) message_topic = _get_text_or_none(element.find(NS_AAS + "messageTopic")) if message_topic is not None: @@ -752,8 +750,7 @@ def construct_basic_event_element(cls, element: etree.Element, object_class=mode def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: blob = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), - _child_text_mandatory(element, NS_AAS + "contentType"), - kind=_get_modeling_kind(element) + _child_text_mandatory(element, NS_AAS + "contentType") ) value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: @@ -765,8 +762,7 @@ def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwar def construct_capability(cls, element: etree.Element, object_class=model.Capability, **_kwargs: Any) \ -> model.Capability: capability = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), - kind=_get_modeling_kind(element) + _child_text_mandatory(element, NS_AAS + "idShort") ) cls._amend_abstract_attributes(capability, element) return capability @@ -796,8 +792,7 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ def construct_file(cls, element: etree.Element, object_class=model.File, **_kwargs: Any) -> model.File: file = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), - _child_text_mandatory(element, NS_AAS + "contentType"), - kind=_get_modeling_kind(element) + _child_text_mandatory(element, NS_AAS + "contentType") ) value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: @@ -820,8 +815,7 @@ def construct_resource(cls, element: etree.Element, object_class=model.Resource, def construct_multi_language_property(cls, element: etree.Element, object_class=model.MultiLanguageProperty, **_kwargs: Any) -> model.MultiLanguageProperty: multi_language_property = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), - kind=_get_modeling_kind(element) + _child_text_mandatory(element, NS_AAS + "idShort") ) value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_lang_string_set, cls.failsafe) if value is not None: @@ -836,8 +830,7 @@ def construct_multi_language_property(cls, element: etree.Element, object_class= def construct_operation(cls, element: etree.Element, object_class=model.Operation, **_kwargs: Any) \ -> model.Operation: operation = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), - kind=_get_modeling_kind(element) + _child_text_mandatory(element, NS_AAS + "idShort") ) input_variables = element.find(NS_AAS + "inputVariables") if input_variables is not None: @@ -861,8 +854,7 @@ def construct_operation(cls, element: etree.Element, object_class=model.Operatio def construct_property(cls, element: etree.Element, object_class=model.Property, **_kwargs: Any) -> model.Property: property_ = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), - value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), - kind=_get_modeling_kind(element) + value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: @@ -877,8 +869,7 @@ def construct_property(cls, element: etree.Element, object_class=model.Property, def construct_range(cls, element: etree.Element, object_class=model.Range, **_kwargs: Any) -> model.Range: range_ = object_class( _child_text_mandatory(element, NS_AAS + "idShort"), - value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), - kind=_get_modeling_kind(element) + value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) max_ = _get_text_or_none(element.find(NS_AAS + "max")) if max_ is not None: @@ -893,8 +884,7 @@ def construct_range(cls, element: etree.Element, object_class=model.Range, **_kw def construct_reference_element(cls, element: etree.Element, object_class=model.ReferenceElement, **_kwargs: Any) \ -> model.ReferenceElement: reference_element = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), - kind=_get_modeling_kind(element) + _child_text_mandatory(element, NS_AAS + "idShort") ) value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_reference, cls.failsafe) if value is not None: @@ -911,8 +901,7 @@ def construct_relationship_element(cls, element: etree.Element, object_class=mod def construct_submodel_element_collection(cls, element: etree.Element, object_class=model.SubmodelElementCollection, **_kwargs: Any) -> model.SubmodelElementCollection: collection = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), - kind=_get_modeling_kind(element) + _child_text_mandatory(element, NS_AAS + "idShort") ) if not cls.stripped: value = element.find(NS_AAS + "value") @@ -940,8 +929,7 @@ def construct_submodel_element_list(cls, element: etree.Element, object_class=mo value_type_list_element=_get_text_mapped_or_none(element.find(NS_AAS + "valueTypeListElement"), model.datatypes.XSD_TYPE_CLASSES), order_relevant=_str_to_bool(_get_text_mandatory(order_relevant)) - if order_relevant is not None else True, - kind=_get_modeling_kind(element) + if order_relevant is not None else True ) if not cls.stripped: value = element.find(NS_AAS + "value") @@ -1010,8 +998,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **_kwargs: Any) \ -> model.Submodel: submodel = object_class( - _child_text_mandatory(element, NS_AAS + "id"), - kind=_get_modeling_kind(element) + _child_text_mandatory(element, NS_AAS + "id") ) if not cls.stripped: submodel_elements = element.find(NS_AAS + "submodelElements") From 5b622b8a55762d07285a6c5aa284da1089836034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 27 Jun 2023 18:31:09 +0200 Subject: [PATCH 096/474] adapter.xml.xml_deserialization: except `AASConstraintViolation` in `_failsafe_construct()` Previously, raised `AASConstraintViolation` would immediately abort processing XML data in failsafe mode. --- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 4a0f918..a4b0b0a 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -285,10 +285,10 @@ def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[ return None try: return constructor(element, **kwargs) - except (KeyError, ValueError) as e: + except (KeyError, ValueError, model.AASConstraintViolation) as e: error_message = f"Failed to construct {_element_pretty_identifier(element)} using {constructor.__name__}!" if not failsafe: - raise type(e)(error_message) from e + raise (type(e) if isinstance(e, (KeyError, ValueError)) else ValueError)(error_message) from e error_type = type(e).__name__ cause: Optional[BaseException] = e while cause is not None: From 4ed231f0083e7edc974aa2d3b28494108a0bdcd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 19 Jul 2023 20:59:20 +0200 Subject: [PATCH 097/474] Partially revert "Remove attribute `kind` from `SubmodelElement`" This partially reverts bde7a77a4844fdadc6756bea0d7d8e3d8c9f7b92. The attribute is only removed from `SubmodelElement`s in the spec, but the commit also removes deserialization of the `kind`-Attribute for `Submodels` and some `kind` attributes of `Qualifier`s and `Submodel`s in the examples. --- basyx/aas/adapter/json/json_deserialization.py | 3 ++- basyx/aas/adapter/xml/xml_deserialization.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index be079ce..8b5aeea 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -564,7 +564,8 @@ def _construct_extension(cls, dct: Dict[str, object], object_class=model.Extensi @classmethod def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel) -> model.Submodel: - ret = object_class(id_=_get_ts(dct, 'id', str)) + ret = object_class(id_=_get_ts(dct, 'id', str), + kind=cls._get_kind(dct)) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'submodelElements' in dct: for element in _get_ts(dct, "submodelElements", list): diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index a4b0b0a..c702a0e 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -998,7 +998,8 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **_kwargs: Any) \ -> model.Submodel: submodel = object_class( - _child_text_mandatory(element, NS_AAS + "id") + _child_text_mandatory(element, NS_AAS + "id"), + kind=_get_modeling_kind(element) ) if not cls.stripped: submodel_elements = element.find(NS_AAS + "submodelElements") From 0f683cfeb0a642ce97e7b81faf0079ddf24c1171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 15 May 2023 16:05:10 +0200 Subject: [PATCH 098/474] update XML/JSON schemata, adapter and test files for updated `AssetInformation` attribute --- basyx/aas/adapter/json/aasJSONSchema.json | 11 ++++++++++- basyx/aas/adapter/json/json_deserialization.py | 4 +++- basyx/aas/adapter/json/json_serialization.py | 2 ++ basyx/aas/adapter/xml/AAS.xsd | 17 ++++++++++++++++- basyx/aas/adapter/xml/xml_deserialization.py | 8 +++++--- basyx/aas/adapter/xml/xml_serialization.py | 4 +++- 6 files changed, 39 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 2088f37..2b43585 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -105,7 +105,10 @@ "$ref": "#/definitions/AssetKind" }, "globalAssetId": { - "$ref": "#/definitions/Reference" + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "specificAssetIds": { "type": "array", @@ -114,6 +117,12 @@ }, "minItems": 1 }, + "assetType": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, "defaultThumbnail": { "$ref": "#/definitions/Resource" } diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 8b5aeea..289f66c 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -417,11 +417,13 @@ def _construct_asset_information(cls, dct: Dict[str, object], object_class=model ret = object_class(asset_kind=ASSET_KIND_INVERSE[_get_ts(dct, 'assetKind', str)]) cls._amend_abstract_attributes(ret, dct) if 'globalAssetId' in dct: - ret.global_asset_id = cls._construct_reference(_get_ts(dct, 'globalAssetId', dict)) + ret.global_asset_id = model.Identifier(_get_ts(dct, 'globalAssetId', str)) if 'specificAssetIds' in dct: for desc_data in _get_ts(dct, "specificAssetIds", list): ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) + if 'assetType' in dct: + ret.asset_type = model.Identifier(_get_ts(dct, 'assetType', str)) if 'defaultThumbnail' in dct: ret.default_thumbnail = cls._construct_resource(_get_ts(dct, 'defaultThumbnail', dict)) return ret diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 6f216bb..9f471fd 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -308,6 +308,8 @@ def _asset_information_to_json(cls, obj: model.AssetInformation) -> Dict[str, ob data['globalAssetId'] = obj.global_asset_id if obj.specific_asset_id: data['specificAssetIds'] = list(obj.specific_asset_id) + if obj.asset_type: + data['assetType'] = obj.asset_type if obj.default_thumbnail: data['defaultThumbnail'] = obj.default_thumbnail return data diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index eb8244b..2460459 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -49,7 +49,14 @@ - + + + + + + + + @@ -57,6 +64,14 @@ + + + + + + + + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index c702a0e..d7d8288 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -977,15 +977,17 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. asset_information = object_class( _child_text_mandatory_mapped(element, NS_AAS + "assetKind", ASSET_KIND_INVERSE), ) - global_asset_id = _failsafe_construct(element.find(NS_AAS + "globalAssetId"), - cls.construct_reference, cls.failsafe) + global_asset_id = _get_text_or_none(element.find(NS_AAS + "globalAssetId")) if global_asset_id is not None: - asset_information.global_asset_id = global_asset_id + asset_information.global_asset_id = model.Identifier(global_asset_id) specific_assset_ids = element.find(NS_AAS + "specificAssetIds") if specific_assset_ids is not None: for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", cls.construct_specific_asset_id, cls.failsafe): asset_information.specific_asset_id.add(id) + asset_type = _get_text_or_none(element.find(NS_AAS + "assetType")) + if asset_type is not None: + asset_information.asset_type = model.Identifier(asset_type) thumbnail = _failsafe_construct(element.find(NS_AAS + "defaultThumbnail"), cls.construct_resource, cls.failsafe) if thumbnail is not None: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 76951bb..f007f10 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -342,12 +342,14 @@ def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"ass et_asset_information = abstract_classes_to_xml(tag, obj) et_asset_information.append(_generate_element(name=NS_AAS + "assetKind", text=_generic.ASSET_KIND[obj.asset_kind])) if obj.global_asset_id: - et_asset_information.append(reference_to_xml(obj.global_asset_id, NS_AAS + "globalAssetId")) + et_asset_information.append(_generate_element(name=NS_AAS + "globalAssetId", text=obj.global_asset_id)) if obj.specific_asset_id: et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") for specific_asset_id in obj.specific_asset_id: et_specific_asset_id.append(specific_asset_id_to_xml(specific_asset_id, NS_AAS + "specificAssetId")) et_asset_information.append(et_specific_asset_id) + if obj.asset_type: + et_asset_information.append(_generate_element(name=NS_AAS + "assetType", text=obj.asset_type)) if obj.default_thumbnail: et_asset_information.append(resource_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbnail")) From 8f9a7b9e8c20044f9cb3a12c5e7d3b1a9fa43c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 20 Jul 2023 01:48:53 +0200 Subject: [PATCH 099/474] remove wrapping `Identifier()` around strings These were introduced in past commits for the attributes `asset_type` and `global_asset_id` of `AssetInformation`, but aren't necessary since `Identifier` is implemented as an alias for `str`. --- basyx/aas/adapter/json/json_deserialization.py | 4 ++-- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 289f66c..a9d8fdb 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -417,13 +417,13 @@ def _construct_asset_information(cls, dct: Dict[str, object], object_class=model ret = object_class(asset_kind=ASSET_KIND_INVERSE[_get_ts(dct, 'assetKind', str)]) cls._amend_abstract_attributes(ret, dct) if 'globalAssetId' in dct: - ret.global_asset_id = model.Identifier(_get_ts(dct, 'globalAssetId', str)) + ret.global_asset_id = _get_ts(dct, 'globalAssetId', str) if 'specificAssetIds' in dct: for desc_data in _get_ts(dct, "specificAssetIds", list): ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) if 'assetType' in dct: - ret.asset_type = model.Identifier(_get_ts(dct, 'assetType', str)) + ret.asset_type = _get_ts(dct, 'assetType', str) if 'defaultThumbnail' in dct: ret.default_thumbnail = cls._construct_resource(_get_ts(dct, 'defaultThumbnail', dict)) return ret diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index d7d8288..ecd2d6c 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -979,7 +979,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. ) global_asset_id = _get_text_or_none(element.find(NS_AAS + "globalAssetId")) if global_asset_id is not None: - asset_information.global_asset_id = model.Identifier(global_asset_id) + asset_information.global_asset_id = global_asset_id specific_assset_ids = element.find(NS_AAS + "specificAssetIds") if specific_assset_ids is not None: for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", @@ -987,7 +987,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. asset_information.specific_asset_id.add(id) asset_type = _get_text_or_none(element.find(NS_AAS + "assetType")) if asset_type is not None: - asset_information.asset_type = model.Identifier(asset_type) + asset_information.asset_type = asset_type thumbnail = _failsafe_construct(element.find(NS_AAS + "defaultThumbnail"), cls.construct_resource, cls.failsafe) if thumbnail is not None: From c8cb7652e3f2c1c79dc6348d8b12f053aa35cb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 20 Jul 2023 02:06:30 +0200 Subject: [PATCH 100/474] change type of `Entity.global_asset_id` to `Identifier` --- basyx/aas/adapter/json/aasJSONSchema.json | 5 ++++- basyx/aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/xml/AAS.xsd | 9 ++++++++- basyx/aas/adapter/xml/xml_deserialization.py | 3 +-- basyx/aas/adapter/xml/xml_serialization.py | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 2b43585..8d77c7a 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -483,7 +483,10 @@ "$ref": "#/definitions/EntityType" }, "globalAssetId": { - "$ref": "#/definitions/Reference" + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "specificAssetId": { "$ref": "#/definitions/SpecificAssetId" diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index a9d8fdb..22c3488 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -522,7 +522,7 @@ def _construct_data_specification_iec61360(cls, dct: Dict[str, object], def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> model.Entity: global_asset_id = None if 'globalAssetId' in dct: - global_asset_id = cls._construct_reference(_get_ts(dct, 'globalAssetId', dict)) + global_asset_id = _get_ts(dct, 'globalAssetId', str) specific_asset_id = None if 'specificAssetIds' in dct: specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'specificAssetIds', dict)) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 2460459..d2f6759 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -346,7 +346,14 @@ - + + + + + + + + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index ecd2d6c..6444daa 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -769,8 +769,7 @@ def construct_capability(cls, element: etree.Element, object_class=model.Capabil @classmethod def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_kwargs: Any) -> model.Entity: - global_asset_id = _failsafe_construct(element.find(NS_AAS + "globalAssetId"), - cls.construct_reference, cls.failsafe) + global_asset_id = _get_text_or_none(element.find(NS_AAS + "globalAssetId")) specific_asset_id = _failsafe_construct(element.find(NS_AAS + "specificAssetId"), cls.construct_specific_asset_id, cls.failsafe) entity = object_class( diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index f007f10..51c9c6d 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -827,7 +827,7 @@ def entity_to_xml(obj: model.Entity, et_entity.append(et_statements) et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type])) if obj.global_asset_id: - et_entity.append(reference_to_xml(obj.global_asset_id, NS_AAS + "globalAssetId")) + et_entity.append(_generate_element(NS_AAS + "globalAssetId", text=obj.global_asset_id)) if obj.specific_asset_id: et_entity.append(specific_asset_id_to_xml(obj.specific_asset_id, NS_AAS + "specificAssetId")) return et_entity From b24ee4b1619c1d015847d57a0751063ac3f4477b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 9 Aug 2023 02:49:07 +0200 Subject: [PATCH 101/474] adapter.xml.xml_deserialization: fix pycodestyle warnings Since a recent update, pycodestyle requires whitespaces between the last comma and the backslash at the end of a line, where it is broken. --- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 6444daa..db6ac16 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -50,8 +50,8 @@ import enum from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar -from .._generic import XML_NS_AAS, MODELING_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,\ +from .._generic import XML_NS_AAS, MODELING_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 NS_AAS = XML_NS_AAS From 40acf8d2a7f4d849769d34d762117a64282931ec Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Fri, 11 Aug 2023 14:25:18 +0200 Subject: [PATCH 102/474] model.base: Rename GlobalReference to ExternalReference Version 3.0 of the spec renames `ReferenceTypes/GlobalReference` to `ReferenceTypes/ExternalReference` to avoid confusion with `KeyTypes/GlobalReference`. --- basyx/aas/adapter/_generic.py | 2 +- basyx/aas/adapter/json/json_deserialization.py | 18 +++++++++--------- basyx/aas/adapter/xml/xml_deserialization.py | 16 ++++++++-------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index f255bb0..9ecdd29 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -39,7 +39,7 @@ model.StateOfEvent.OFF: 'off'} REFERENCE_TYPES: Dict[Type[model.Reference], str] = { - model.GlobalReference: 'GlobalReference', + model.ExternalReference: 'ExternalReference', model.ModelReference: 'ModelReference'} KEY_TYPES: Dict[model.KeyTypes, str] = { diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 22c3488..4b5e25c 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -264,8 +264,8 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 model.EmbeddedDataSpecification( - data_specification=cls._construct_global_reference(_get_ts(dspec, 'dataSpecification', - dict)), + data_specification=cls._construct_external_reference(_get_ts(dspec, 'dataSpecification', + dict)), data_specification_content=_get_ts(dspec, 'dataSpecificationContent', model.DataSpecificationContent) # type: ignore ) @@ -304,7 +304,7 @@ def _construct_specific_asset_id(cls, dct: Dict[str, object], object_class=model # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable return object_class(name=_get_ts(dct, 'name', str), value=_get_ts(dct, 'value', str), - external_subject_id=cls._construct_global_reference( + external_subject_id=cls._construct_external_reference( _get_ts(dct, 'externalSubjectId', dict)), semantic_id=cls._construct_reference(_get_ts(dct, 'semanticId', dict)) if 'semanticId' in dct else None, @@ -318,16 +318,16 @@ def _construct_reference(cls, dct: Dict[str, object]) -> model.Reference: reference_type: Type[model.Reference] = REFERENCE_TYPES_INVERSE[_get_ts(dct, 'type', str)] if reference_type is model.ModelReference: return cls._construct_model_reference(dct, model.Referable) # type: ignore - elif reference_type is model.GlobalReference: - return cls._construct_global_reference(dct) + elif reference_type is model.ExternalReference: + return cls._construct_external_reference(dct) raise ValueError(f"Unsupported reference type {reference_type}!") @classmethod - def _construct_global_reference(cls, dct: Dict[str, object], object_class=model.GlobalReference)\ - -> model.GlobalReference: + def _construct_external_reference(cls, dct: Dict[str, object], object_class=model.ExternalReference)\ + -> model.ExternalReference: reference_type: Type[model.Reference] = REFERENCE_TYPES_INVERSE[_get_ts(dct, 'type', str)] - if reference_type is not model.GlobalReference: - raise ValueError(f"Expected a reference of type {model.GlobalReference}, got {reference_type}!") + if reference_type is not model.ExternalReference: + raise ValueError(f"Expected a reference of type {model.ExternalReference}, got {reference_type}!") keys = [cls._construct_key(key_data) for key_data in _get_ts(dct, "keys", list)] return object_class(tuple(keys), cls._construct_reference(_get_ts(dct, 'referredSemanticId', dict)) if 'referredSemanticId' in dct else None) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index db6ac16..1a8beca 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -546,7 +546,7 @@ def construct_reference(cls, element: etree.Element, namespace: str = NS_AAS, ** reference_type: Type[model.Reference] = _child_text_mandatory_mapped(element, NS_AAS + "type", REFERENCE_TYPES_INVERSE) references: Dict[Type[model.Reference], Callable[..., model.Reference]] = { - model.GlobalReference: cls.construct_global_reference, + model.ExternalReference: cls.construct_external_reference, model.ModelReference: cls.construct_model_reference } if reference_type not in references: @@ -554,10 +554,10 @@ def construct_reference(cls, element: etree.Element, namespace: str = NS_AAS, ** return references[reference_type](element, namespace=namespace, **kwargs) @classmethod - def construct_global_reference(cls, element: etree.Element, namespace: str = NS_AAS, - object_class=model.GlobalReference, **_kwargs: Any) \ - -> model.GlobalReference: - _expect_reference_type(element, model.GlobalReference) + def construct_external_reference(cls, element: etree.Element, namespace: str = NS_AAS, + object_class=model.ExternalReference, **_kwargs: Any) \ + -> model.ExternalReference: + _expect_reference_type(element, model.ExternalReference) return object_class(cls._construct_key_tuple(element, namespace=namespace), _failsafe_construct(element.find(NS_AAS + "referredSemanticId"), cls.construct_reference, cls.failsafe, namespace=namespace)) @@ -964,7 +964,7 @@ def construct_specific_asset_id(cls, element: etree.Element, object_class=model. # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable return object_class( external_subject_id=_child_construct_mandatory(element, NS_AAS + "externalSubjectId", - cls.construct_global_reference), + cls.construct_external_reference), name=_get_text_or_none(element.find(NS_AAS + "name")), value=_get_text_or_none(element.find(NS_AAS + "value")), semantic_id=_failsafe_construct(element.find(NS_AAS + "semanticId"), cls.construct_reference, cls.failsafe) @@ -1056,7 +1056,7 @@ def construct_embedded_data_specification(cls, element: etree.Element, object_cl logger.warning(f"{_element_pretty_identifier(data_specification_content)} has more than one " "data specification, using the first one...") embedded_data_specification = object_class( - _child_construct_mandatory(element, NS_AAS + "dataSpecification", cls.construct_global_reference), + _child_construct_mandatory(element, NS_AAS + "dataSpecification", cls.construct_external_reference), _failsafe_construct_mandatory(data_specification_content[0], cls.construct_data_specification_content) ) cls._amend_abstract_attributes(embedded_data_specification, element) @@ -1324,7 +1324,7 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool elif construct == XMLConstructables.MODEL_REFERENCE: constructor = decoder_.construct_model_reference elif construct == XMLConstructables.GLOBAL_REFERENCE: - constructor = decoder_.construct_global_reference + constructor = decoder_.construct_external_reference elif construct == XMLConstructables.ADMINISTRATIVE_INFORMATION: constructor = decoder_.construct_administrative_information elif construct == XMLConstructables.QUALIFIER: From b0e872ae345b2e721a6a2ffdf642a0b1ccdadec3 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Fri, 11 Aug 2023 14:54:22 +0200 Subject: [PATCH 103/474] Update JSON and XSD schemata --- basyx/aas/adapter/json/aasJSONSchema.json | 2 +- basyx/aas/adapter/xml/AAS.xsd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 8d77c7a..c4b9722 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -1059,7 +1059,7 @@ "ReferenceTypes": { "type": "string", "enum": [ - "GlobalReference", + "ExternalReference", "ModelReference" ] }, diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index d2f6759..f85ae2c 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -1116,7 +1116,7 @@ - + From 9cca9670398ebf9be2325b39a322f6d590209d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 21 Aug 2023 00:02:52 +0200 Subject: [PATCH 104/474] adapter: add new `AdministrativeInformation` attributes --- basyx/aas/adapter/json/aasJSONSchema.json | 9 +++++++++ basyx/aas/adapter/json/json_deserialization.py | 4 ++++ basyx/aas/adapter/json/json_serialization.py | 4 ++++ basyx/aas/adapter/xml/AAS.xsd | 9 +++++++++ basyx/aas/adapter/xml/xml_deserialization.py | 6 +++++- basyx/aas/adapter/xml/xml_serialization.py | 4 ++++ 6 files changed, 35 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index c4b9722..3355cb3 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -45,6 +45,15 @@ "revision": { "type": "string", "minLength": 1 + }, + "creator": { + "$ref": "#/definitions/Reference" + }, + "templateId": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" } } } diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 4b5e25c..d0b4193 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -357,6 +357,10 @@ def _construct_administrative_information( ret.revision = _get_ts(dct, 'revision', str) elif 'revision' in dct: logger.warning("Ignoring 'revision' attribute of AdministrativeInformation object due to missing 'version'") + if 'creator' in dct: + ret.creator = cls._construct_reference(_get_ts(dct, 'creator', dict)) + if 'templateId' in dct: + ret.template_id = _get_ts(dct, 'templateId', str) return ret @classmethod diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 9f471fd..f93faa2 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -187,6 +187,10 @@ def _administrative_information_to_json(cls, obj: model.AdministrativeInformatio data['version'] = obj.version if obj.revision: data['revision'] = obj.revision + if obj.creator: + data['creator'] = obj.creator + if obj.template_id: + data['templateId'] = obj.template_id return data @classmethod diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index f85ae2c..745b21d 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -17,6 +17,15 @@ + + + + + + + + + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 1a8beca..4903506 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -601,8 +601,12 @@ def construct_administrative_information(cls, element: etree.Element, object_cla **_kwargs: Any) -> model.AdministrativeInformation: administrative_information = object_class( revision=_get_text_or_none(element.find(NS_AAS + "revision")), - version=_get_text_or_none(element.find(NS_AAS + "version")) + version=_get_text_or_none(element.find(NS_AAS + "version")), + template_id=_get_text_or_none(element.find(NS_AAS + "templateId")) ) + creator = _failsafe_construct(element.find(NS_AAS + "creator"), cls.construct_reference, cls.failsafe) + if creator is not None: + administrative_information.creator = creator cls._amend_abstract_attributes(administrative_information, element) return administrative_information diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 51c9c6d..987cc1e 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -184,6 +184,10 @@ def administrative_information_to_xml(obj: model.AdministrativeInformation, et_administration.append(_generate_element(name=NS_AAS + "version", text=obj.version)) if obj.revision: et_administration.append(_generate_element(name=NS_AAS + "revision", text=obj.revision)) + if obj.creator: + et_administration.append(reference_to_xml(obj.creator, tag=NS_AAS + "creator")) + if obj.template_id: + et_administration.append(_generate_element(name=NS_AAS + "templateId", text=obj.template_id)) return et_administration From 1fcaaa4507bf5c2ba307a3358dacf854fab45813 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Thu, 24 Aug 2023 09:31:01 +0200 Subject: [PATCH 105/474] model.base: Rename ModelingKind to ModellingKind (#104) In version 3.0 the Enum ModelingKind has been renamed to ModellingKind. This renames the class definition and all its occurences in code and documentation. --- basyx/aas/adapter/_generic.py | 8 ++++---- basyx/aas/adapter/json/aasJSONSchema.json | 4 ++-- basyx/aas/adapter/json/json_deserialization.py | 15 +++++++-------- basyx/aas/adapter/json/json_serialization.py | 4 ++-- basyx/aas/adapter/xml/AAS.xsd | 4 ++-- basyx/aas/adapter/xml/xml_deserialization.py | 18 +++++++++--------- basyx/aas/adapter/xml/xml_serialization.py | 4 ++-- 7 files changed, 28 insertions(+), 29 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 9ecdd29..6bd0779 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -16,9 +16,9 @@ XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" -MODELING_KIND: Dict[model.ModelingKind, str] = { - model.ModelingKind.TEMPLATE: 'Template', - model.ModelingKind.INSTANCE: 'Instance'} +MODELLING_KIND: Dict[model.ModellingKind, str] = { + model.ModellingKind.TEMPLATE: 'Template', + model.ModellingKind.INSTANCE: 'Instance'} ASSET_KIND: Dict[model.AssetKind, str] = { model.AssetKind.TYPE: 'Type', @@ -92,7 +92,7 @@ model.base.IEC61360LevelType.MAX: 'max', } -MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} +MODELLING_KIND_INVERSE: Dict[str, model.ModellingKind] = {v: k for k, v in MODELLING_KIND.items()} ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} QUALIFIER_KIND_INVERSE: Dict[str, model.QualifierKind] = {v: k for k, v in QUALIFIER_KIND.items()} DIRECTION_INVERSE: Dict[str, model.Direction] = {v: k for k, v in DIRECTION.items()} diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 3355cb3..697dc06 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -663,7 +663,7 @@ "type": "object", "properties": { "kind": { - "$ref": "#/definitions/ModelingKind" + "$ref": "#/definitions/ModellingKind" } } }, @@ -811,7 +811,7 @@ "SubmodelElementList" ] }, - "ModelingKind": { + "ModellingKind": { "type": "string", "enum": [ "Instance", diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index d0b4193..ee36df8 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.json.json_deserialization: @@ -37,8 +36,8 @@ from typing import Dict, Callable, TypeVar, Type, List, IO, Optional, Set from basyx.aas import model -from .._generic import MODELING_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,\ +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 logger = logging.getLogger(__name__) @@ -276,14 +275,14 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None obj.extension.add(cls._construct_extension(extension)) @classmethod - def _get_kind(cls, dct: Dict[str, object]) -> model.ModelingKind: + def _get_kind(cls, dct: Dict[str, object]) -> model.ModellingKind: """ Utility method to get the kind of an HasKind object from its JSON representation. :param dct: The object's dict representation from JSON :return: The object's `kind` value """ - return MODELING_KIND_INVERSE[_get_ts(dct, "kind", str)] if 'kind' in dct else model.ModelingKind.INSTANCE + return MODELLING_KIND_INVERSE[_get_ts(dct, "kind", str)] if 'kind' in dct else model.ModellingKind.INSTANCE # ############################################################################# # Helper Constructor Methods starting from here diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index f93faa2..ef02698 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -145,8 +145,8 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: if obj.supplemental_semantic_id: data['supplementalSemanticIds'] = list(obj.supplemental_semantic_id) if isinstance(obj, model.HasKind): - if obj.kind is model.ModelingKind.TEMPLATE: - data['kind'] = _generic.MODELING_KIND[obj.kind] + if obj.kind is model.ModellingKind.TEMPLATE: + data['kind'] = _generic.MODELLING_KIND[obj.kind] if isinstance(obj, model.Qualifiable) and not cls.stripped: if obj.qualifier: data['qualifiers'] = list(obj.qualifier) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 745b21d..32b0080 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -535,7 +535,7 @@ - + @@ -1110,7 +1110,7 @@ - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 4903506..3fc1601 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -50,9 +50,9 @@ import enum from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar -from .._generic import XML_NS_AAS, MODELING_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 +from .._generic import XML_NS_AAS, 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 NS_AAS = XML_NS_AAS @@ -389,15 +389,15 @@ def _child_text_mandatory_mapped(parent: etree.Element, child_tag: str, dct: Dic return _get_text_mandatory_mapped(_get_child_mandatory(parent, child_tag), dct) -def _get_modeling_kind(element: etree.Element) -> model.ModelingKind: +def _get_kind(element: etree.Element) -> model.ModellingKind: """ - Returns the modeling kind of an element with the default value INSTANCE, if none specified. + Returns the modelling kind of an element with the default value INSTANCE, if none specified. :param element: The xml element. - :return: The modeling kind of the element. + :return: The modelling kind of the element. """ - modeling_kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), MODELING_KIND_INVERSE) - return modeling_kind if modeling_kind is not None else model.ModelingKind.INSTANCE + modelling_kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), MODELLING_KIND_INVERSE) + return modelling_kind if modelling_kind is not None else model.ModellingKind.INSTANCE def _expect_reference_type(element: etree.Element, expected_type: Type[model.Reference]) -> None: @@ -1004,7 +1004,7 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, -> model.Submodel: submodel = object_class( _child_text_mandatory(element, NS_AAS + "id"), - kind=_get_modeling_kind(element) + kind=_get_kind(element) ) if not cls.stripped: submodel_elements = element.find(NS_AAS + "submodelElements") diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 987cc1e..3cb75a1 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -102,10 +102,10 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: elm.append(administrative_information_to_xml(obj.administration)) elm.append(_generate_element(name=NS_AAS + "id", text=obj.id)) if isinstance(obj, model.HasKind): - if obj.kind is model.ModelingKind.TEMPLATE: + if obj.kind is model.ModellingKind.TEMPLATE: elm.append(_generate_element(name=NS_AAS + "kind", text="Template")) else: - # then modeling-kind is Instance + # then modelling-kind is Instance elm.append(_generate_element(name=NS_AAS + "kind", text="Instance")) if isinstance(obj, model.HasSemantics): if obj.semantic_id: From 6d4a0e0024c67cc759d8a3e8d49d5cc55cf2f617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 22 Aug 2023 19:34:50 +0200 Subject: [PATCH 106/474] adapter.json: improve `LangStringSet` json serialization Since `LangStringSet` was changed to be an own class in 553d0e18f0a59e98fb23042b2dcf31b616d129f2, we can check for a `LangStringSet` via `isinstance()` now and simplify the JSON serialization. --- basyx/aas/adapter/json/json_serialization.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index ef02698..2682b1d 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -78,6 +78,7 @@ def default(self, obj: object) -> object: model.Extension: self._extension_to_json, model.File: self._file_to_json, model.Key: self._key_to_json, + model.LangStringSet: self._lang_string_set_to_json, model.MultiLanguageProperty: self._multi_language_property_to_json, model.Operation: self._operation_to_json, model.OperationVariable: self._operation_variable_to_json, @@ -124,11 +125,11 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: if obj.id_short: data['idShort'] = obj.id_short if obj.display_name: - data['displayName'] = cls._lang_string_set_to_json(obj.display_name) + data['displayName'] = obj.display_name if obj.category: data['category'] = obj.category if obj.description: - data['description'] = cls._lang_string_set_to_json(obj.description) + data['description'] = obj.description try: ref_type = next(iter(t for t in inspect.getmro(type(obj)) if t in model.KEY_TYPES_CLASSES)) except StopIteration as e: @@ -342,14 +343,14 @@ def _data_specification_iec61360_to_json( """ data_spec: Dict[str, object] = { 'modelType': 'DataSpecificationIEC61360', - 'preferredName': cls._lang_string_set_to_json(obj.preferred_name) + 'preferredName': obj.preferred_name } if obj.data_type is not None: data_spec['dataType'] = _generic.IEC61360_DATA_TYPES[obj.data_type] if obj.definition is not None: - data_spec['definition'] = cls._lang_string_set_to_json(obj.definition) + data_spec['definition'] = obj.definition if obj.short_name is not None: - data_spec['shortName'] = cls._lang_string_set_to_json(obj.short_name) + data_spec['shortName'] = obj.short_name if obj.unit is not None: data_spec['unit'] = obj.unit if obj.unit_id is not None: @@ -380,7 +381,7 @@ def _data_specification_physical_unit_to_json( 'modelType': 'DataSpecificationPhysicalUnit', 'unitName': obj.unit_name, 'unitSymbol': obj.unit_symbol, - 'definition': cls._lang_string_set_to_json(obj.definition) + 'definition': obj.definition } if obj.si_notation is not None: data_spec['siNotation'] = obj.si_notation @@ -475,7 +476,7 @@ def _multi_language_property_to_json(cls, obj: model.MultiLanguageProperty) -> D """ data = cls._abstract_classes_to_json(obj) if obj.value: - data['value'] = cls._lang_string_set_to_json(obj.value) + data['value'] = obj.value if obj.value_id: data['valueId'] = obj.value_id return data From e690afdfac9a566ca141f9fe96b543ea4ae14130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 22 Aug 2023 21:45:31 +0200 Subject: [PATCH 107/474] adapter: adjust for the new `ConstrainedLangStringSet`s This commit changes the XML/JSON (de-)serialization adapter and corresponding schemata such that they are compatible with the new `ConstrainedLangStringSet` types. --- basyx/aas/adapter/json/aasJSONSchema.json | 110 +++++++++++++++--- .../aas/adapter/json/json_deserialization.py | 28 +++-- basyx/aas/adapter/xml/AAS.xsd | 87 +++++++++++--- basyx/aas/adapter/xml/xml_deserialization.py | 85 ++++++++++---- basyx/aas/adapter/xml/xml_serialization.py | 11 +- 5 files changed, 249 insertions(+), 72 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 697dc06..e5e2716 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -31,6 +31,24 @@ "SubmodelElementList" ] }, + "AbstractLangString": { + "type": "object", + "properties": { + "language": { + "type": "string", + "pattern": "^(([a-zA-Z]{2,3}(-[a-zA-Z]{3}(-[a-zA-Z]{3}){2})?|[a-zA-Z]{4}|[a-zA-Z]{5,8})(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-(([a-zA-Z0-9]){5,8}|[0-9]([a-zA-Z0-9]){3}))*(-[0-9A-WY-Za-wy-z](-([a-zA-Z0-9]){2,8})+)*(-[xX](-([a-zA-Z0-9]){1,8})+)?|[xX](-([a-zA-Z0-9]){1,8})+|((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)))$" + }, + "text": { + "type": "string", + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + } + }, + "required": [ + "language", + "text" + ] + }, "AdministrativeInformation": { "allOf": [ { @@ -263,14 +281,14 @@ "preferredName": { "type": "array", "items": { - "$ref": "#/definitions/LangString" + "$ref": "#/definitions/LangStringPreferredNameTypeIec61360" }, "minItems": 1 }, "shortName": { "type": "array", "items": { - "$ref": "#/definitions/LangString" + "$ref": "#/definitions/LangStringShortNameTypeIec61360" }, "minItems": 1 }, @@ -295,7 +313,7 @@ "definition": { "type": "array", "items": { - "$ref": "#/definitions/LangString" + "$ref": "#/definitions/LangStringDefinitionTypeIec61360" }, "minItems": 1 }, @@ -337,7 +355,7 @@ "definition": { "type": "array", "items": { - "$ref": "#/definitions/LangString" + "$ref": "#/definitions/LangStringDefinitionTypeIec61360" }, "minItems": 1 }, @@ -748,20 +766,74 @@ "SubmodelElementList" ] }, - "LangString": { - "type": "object", - "properties": { - "language": { - "type": "string", - "pattern": "^(([a-zA-Z]{2,3}(-[a-zA-Z]{3}(-[a-zA-Z]{3}){2})?|[a-zA-Z]{4}|[a-zA-Z]{5,8})(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-(([a-zA-Z0-9]){5,8}|[0-9]([a-zA-Z0-9]){3}))*(-[0-9A-WY-Za-wy-z](-([a-zA-Z0-9]){2,8})+)*(-[xX](-([a-zA-Z0-9]){1,8})+)?|[xX](-([a-zA-Z0-9]){1,8})+|((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)))$" + "LangStringDefinitionTypeIec61360": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" }, - "text": { - "type": "string" + { + "properties": { + "text": { + "maxLength": 1023 + } + } + } + ] + }, + "LangStringNameType": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 128 + } + } + } + ] + }, + "LangStringPreferredNameTypeIec61360": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 255 + } + } + } + ] + }, + "LangStringShortNameTypeIec61360": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 18 + } + } + } + ] + }, + "LangStringTextType": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 1023 + } + } } - }, - "required": [ - "language", - "text" ] }, "LevelType": { @@ -828,7 +900,7 @@ "value": { "type": "array", "items": { - "$ref": "#/definitions/LangString" + "$ref": "#/definitions/LangStringTextType" }, "minItems": 1 }, @@ -1004,14 +1076,14 @@ "displayName": { "type": "array", "items": { - "$ref": "#/definitions/LangString" + "$ref": "#/definitions/LangStringNameType" }, "minItems": 1 }, "description": { "type": "array", "items": { - "$ref": "#/definitions/LangString" + "$ref": "#/definitions/LangStringTextType" }, "minItems": 1 }, diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index ee36df8..df93146 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -48,6 +48,7 @@ # ############################################################################# T = TypeVar('T') +LSS = TypeVar('LSS', bound=model.LangStringSet) def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: @@ -235,9 +236,11 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None if 'category' in dct: obj.category = _get_ts(dct, 'category', str) if 'displayName' in dct: - obj.display_name = cls._construct_lang_string_set(_get_ts(dct, 'displayName', list)) + obj.display_name = cls._construct_lang_string_set(_get_ts(dct, 'displayName', list), + model.MultiLanguageNameType) if 'description' in dct: - obj.description = cls._construct_lang_string_set(_get_ts(dct, 'description', list)) + obj.description = cls._construct_lang_string_set(_get_ts(dct, 'description', list), + model.MultiLanguageTextType) if isinstance(obj, model.Identifiable): if 'idShort' in dct: obj.id_short = _get_ts(dct, 'idShort', str) @@ -371,19 +374,19 @@ def _construct_operation_variable( return ret @classmethod - def _construct_lang_string_set(cls, lst: List[Dict[str, object]]) -> Optional[model.LangStringSet]: + def _construct_lang_string_set(cls, lst: List[Dict[str, object]], object_class: Type[LSS]) -> LSS: ret = {} for desc in lst: try: ret[_get_ts(desc, 'language', str)] = _get_ts(desc, 'text', str) except (KeyError, TypeError) as e: - error_message = "Error while trying to convert JSON object into LangString: {} >>> {}".format( - e, pprint.pformat(desc, depth=2, width=2 ** 14, compact=True)) + error_message = "Error while trying to convert JSON object into {}: {} >>> {}".format( + object_class.__name__, e, pprint.pformat(desc, depth=2, width=2 ** 14, compact=True)) if cls.failsafe: logger.error(error_message, exc_info=e) else: raise type(e)(error_message) from e - return model.LangStringSet(ret) + return object_class(ret) @classmethod def _construct_value_list(cls, dct: Dict[str, object], value_format: model.DataTypeDefXsd) -> model.ValueList: @@ -464,7 +467,7 @@ def _construct_data_specification_physical_unit(cls, dct: Dict[str, object], ret = object_class( unit_name=_get_ts(dct, 'unitName', str), unit_symbol=_get_ts(dct, 'unitSymbol', str), - definition=cls._construct_lang_string_set(_get_ts(dct, 'definition', list)) + definition=cls._construct_lang_string_set(_get_ts(dct, 'definition', list), model.DefinitionTypeIEC61360) ) if 'siNotation' in dct: ret.si_notation = _get_ts(dct, 'siNotation', str) @@ -492,13 +495,16 @@ def _construct_data_specification_physical_unit(cls, dct: Dict[str, object], def _construct_data_specification_iec61360(cls, dct: Dict[str, object], object_class=model.base.DataSpecificationIEC61360)\ -> model.base.DataSpecificationIEC61360: - ret = object_class(preferred_name=cls._construct_lang_string_set(_get_ts(dct, 'preferredName', list))) + ret = object_class(preferred_name=cls._construct_lang_string_set(_get_ts(dct, 'preferredName', list), + model.PreferredNameTypeIEC61360)) if 'dataType' in dct: ret.data_type = IEC61360_DATA_TYPES_INVERSE[_get_ts(dct, 'dataType', str)] if 'definition' in dct: - ret.definition = cls._construct_lang_string_set(_get_ts(dct, 'definition', list)) + ret.definition = cls._construct_lang_string_set(_get_ts(dct, 'definition', list), + model.DefinitionTypeIEC61360) if 'shortName' in dct: - ret.short_name = cls._construct_lang_string_set(_get_ts(dct, 'shortName', list)) + ret.short_name = cls._construct_lang_string_set(_get_ts(dct, 'shortName', list), + model.ShortNameTypeIEC61360) if 'unit' in dct: ret.unit = _get_ts(dct, 'unit', str) if 'unitId' in dct: @@ -727,7 +733,7 @@ def _construct_multi_language_property( ret = object_class(id_short=_get_ts(dct, "idShort", str)) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: - ret.value = cls._construct_lang_string_set(_get_ts(dct, 'value', list)) + ret.value = cls._construct_lang_string_set(_get_ts(dct, 'value', list), model.MultiLanguageTextType) if 'valueId' in dct: ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) return ret diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 32b0080..47cf861 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -1,5 +1,23 @@ + + + + + + + + + + + + + + + + + + @@ -183,14 +201,14 @@ - + - + @@ -220,7 +238,7 @@ - + @@ -256,7 +274,7 @@ - + @@ -623,16 +641,29 @@ - + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -641,7 +672,7 @@ - + @@ -768,14 +799,14 @@ - + - + @@ -1265,9 +1296,29 @@ - + + + + + + + + + + + + + + + + + + + + + - + diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 3fc1601..e1d30ef 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -60,6 +60,7 @@ T = TypeVar("T") RE = TypeVar("RE", bound=model.RelationshipElement) +LSS = TypeVar("LSS", bound=model.LangStringSet) def _str_to_bool(string: str) -> bool: @@ -437,14 +438,14 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None """ if isinstance(obj, model.Referable): category = _get_text_or_none(element.find(NS_AAS + "category")) - display_name = _failsafe_construct(element.find(NS_AAS + "displayName"), cls.construct_lang_string_set, - cls.failsafe) + display_name = _failsafe_construct(element.find(NS_AAS + "displayName"), + cls.construct_multi_language_name_type, cls.failsafe) if display_name is not None: obj.display_name = display_name if category is not None: obj.category = category - description = _failsafe_construct(element.find(NS_AAS + "description"), cls.construct_lang_string_set, - cls.failsafe) + description = _failsafe_construct(element.find(NS_AAS + "description"), + cls.construct_multi_language_text_type, cls.failsafe) if description is not None: obj.description = description if isinstance(obj, model.Identifiable): @@ -611,16 +612,42 @@ def construct_administrative_information(cls, element: etree.Element, object_cla return administrative_information @classmethod - def construct_lang_string_set(cls, element: etree.Element, namespace: str = NS_AAS, **_kwargs: Any) \ - -> model.LangStringSet: - """ - This function doesn't support the object_class parameter, because LangStringSet is just a generic type alias. - """ - lss: Dict[str, str] = {} - for lang_string in _get_all_children_expect_tag(element, namespace + "langString", cls.failsafe): - lss[_child_text_mandatory(lang_string, namespace + "language")] = _child_text_mandatory(lang_string, - namespace + "text") - return model.LangStringSet(lss) + def construct_lang_string_set(cls, element: etree.Element, expected_tag: str, object_class: Type[LSS], + **_kwargs: Any) -> LSS: + collected_lang_strings: Dict[str, str] = {} + for lang_string_elem in _get_all_children_expect_tag(element, expected_tag, cls.failsafe): + collected_lang_strings[_child_text_mandatory(lang_string_elem, NS_AAS + "language")] = \ + _child_text_mandatory(lang_string_elem, NS_AAS + "text") + return object_class(collected_lang_strings) + + @classmethod + def construct_multi_language_name_type(cls, element: etree.Element, object_class=model.MultiLanguageNameType, + **kwargs: Any) -> model.MultiLanguageNameType: + return cls.construct_lang_string_set(element, NS_AAS + "langStringNameType", object_class, **kwargs) + + @classmethod + def construct_multi_language_text_type(cls, element: etree.Element, object_class=model.MultiLanguageTextType, + **kwargs: Any) -> model.MultiLanguageTextType: + return cls.construct_lang_string_set(element, NS_AAS + "langStringTextType", object_class, **kwargs) + + @classmethod + def construct_definition_type_iec61360(cls, element: etree.Element, object_class=model.DefinitionTypeIEC61360, + **kwargs: Any) -> model.DefinitionTypeIEC61360: + return cls.construct_lang_string_set(element, NS_AAS + "langStringDefinitionTypeIec61360", object_class, + **kwargs) + + @classmethod + def construct_preferred_name_type_iec61360(cls, element: etree.Element, + object_class=model.PreferredNameTypeIEC61360, + **kwargs: Any) -> model.PreferredNameTypeIEC61360: + return cls.construct_lang_string_set(element, NS_AAS + "langStringPreferredNameTypeIec61360", object_class, + **kwargs) + + @classmethod + def construct_short_name_type_iec61360(cls, element: etree.Element, object_class=model.ShortNameTypeIEC61360, + **kwargs: Any) -> model.ShortNameTypeIEC61360: + return cls.construct_lang_string_set(element, NS_AAS + "langStringShortNameTypeIec61360", object_class, + **kwargs) @classmethod def construct_qualifier(cls, element: etree.Element, object_class=model.Qualifier, **_kwargs: Any) \ @@ -820,7 +847,8 @@ def construct_multi_language_property(cls, element: etree.Element, object_class= multi_language_property = object_class( _child_text_mandatory(element, NS_AAS + "idShort") ) - value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_lang_string_set, cls.failsafe) + value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_multi_language_text_type, + cls.failsafe) if value is not None: multi_language_property.value = value value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) @@ -1088,7 +1116,8 @@ def construct_data_specification_physical_unit(cls, element: etree.Element, -> model.DataSpecificationPhysicalUnit: dspu = object_class(_child_text_mandatory(element, NS_AAS + "unitName"), _child_text_mandatory(element, NS_AAS + "unitSymbol"), - _child_construct_mandatory(element, NS_AAS + "definition", cls.construct_lang_string_set)) + _child_construct_mandatory(element, NS_AAS + "definition", + cls.construct_definition_type_iec61360)) si_notation = _get_text_or_none(element.find(NS_AAS + "siNotation")) if si_notation is not None: dspu.si_notation = si_notation @@ -1126,8 +1155,8 @@ def construct_data_specification_physical_unit(cls, element: etree.Element, def construct_data_specification_iec61360(cls, element: etree.Element, object_class=model.DataSpecificationIEC61360, **_kwargs: Any) -> model.DataSpecificationIEC61360: ds_iec = object_class(_child_construct_mandatory(element, NS_AAS + "preferredName", - cls.construct_lang_string_set)) - short_name = _failsafe_construct(element.find(NS_AAS + "shortName"), cls.construct_lang_string_set, + cls.construct_preferred_name_type_iec61360)) + short_name = _failsafe_construct(element.find(NS_AAS + "shortName"), cls.construct_short_name_type_iec61360, cls.failsafe) if short_name is not None: ds_iec.short_name = short_name @@ -1146,7 +1175,7 @@ def construct_data_specification_iec61360(cls, element: etree.Element, object_cl data_type = _get_text_mapped_or_none(element.find(NS_AAS + "dataType"), IEC61360_DATA_TYPES_INVERSE) if data_type is not None: ds_iec.data_type = data_type - definition = _failsafe_construct(element.find(NS_AAS + "definition"), cls.construct_lang_string_set, + definition = _failsafe_construct(element.find(NS_AAS + "definition"), cls.construct_definition_type_iec61360, cls.failsafe) if definition is not None: ds_iec.definition = definition @@ -1293,7 +1322,11 @@ class XMLConstructables(enum.Enum): DATA_ELEMENT = enum.auto() SUBMODEL_ELEMENT = enum.auto() VALUE_LIST = enum.auto() - LANG_STRING_SET = enum.auto() + MULTI_LANGUAGE_NAME_TYPE = enum.auto() + MULTI_LANGUAGE_TEXT_TYPE = enum.auto() + DEFINITION_TYPE_IEC61360 = enum.auto() + PREFERRED_NAME_TYPE_IEC61360 = enum.auto() + SHORT_NAME_TYPE_IEC61360 = enum.auto() EMBEDDED_DATA_SPECIFICATION = enum.auto() DATA_SPECIFICATION_CONTENT = enum.auto() DATA_SPECIFICATION_IEC61360 = enum.auto() @@ -1379,8 +1412,16 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_value_reference_pair elif construct == XMLConstructables.CONCEPT_DESCRIPTION: constructor = decoder_.construct_concept_description - elif construct == XMLConstructables.LANG_STRING_SET: - constructor = decoder_.construct_lang_string_set + elif construct == XMLConstructables.MULTI_LANGUAGE_NAME_TYPE: + constructor = decoder_.construct_multi_language_name_type + elif construct == XMLConstructables.MULTI_LANGUAGE_TEXT_TYPE: + constructor = decoder_.construct_multi_language_text_type + elif construct == XMLConstructables.DEFINITION_TYPE_IEC61360: + constructor = decoder_.construct_definition_type_iec61360 + elif construct == XMLConstructables.PREFERRED_NAME_TYPE_IEC61360: + constructor = decoder_.construct_preferred_name_type_iec61360 + elif construct == XMLConstructables.SHORT_NAME_TYPE_IEC61360: + constructor = decoder_.construct_short_name_type_iec61360 elif construct == XMLConstructables.EMBEDDED_DATA_SPECIFICATION: constructor = decoder_.construct_embedded_data_specification elif construct == XMLConstructables.DATA_SPECIFICATION_IEC61360: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 3cb75a1..9196137 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -19,7 +19,7 @@ """ from lxml import etree # type: ignore -from typing import Dict, IO, Optional +from typing import Dict, IO, Optional, Type import base64 from basyx.aas import model @@ -161,9 +161,16 @@ def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: :param tag: Namespace+Tag name of the returned XML element. :return: Serialized ElementTree object """ + LANG_STRING_SET_TAGS: Dict[Type[model.LangStringSet], str] = {k: NS_AAS + v for k, v in { + model.MultiLanguageNameType: "langStringNameType", + model.MultiLanguageTextType: "langStringTextType", + model.DefinitionTypeIEC61360: "langStringDefinitionTypeIec61360", + model.PreferredNameTypeIEC61360: "langStringPreferredNameTypeIec61360", + model.ShortNameTypeIEC61360: "langStringShortNameTypeIec61360" + }.items()} et_lss = _generate_element(name=tag) for language, text in obj.items(): - et_ls = _generate_element(name=NS_AAS + "langString") + et_ls = _generate_element(name=LANG_STRING_SET_TAGS[type(obj)]) et_ls.append(_generate_element(name=NS_AAS + "language", text=language)) et_ls.append(_generate_element(name=NS_AAS + "text", text=text)) et_lss.append(et_ls) From 9cf3f899f941a53c21e181ee6aebfb65b12de30d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Sep 2023 16:22:07 +0200 Subject: [PATCH 108/474] adapter.json.json_deserialization: handle `AASConstraintViolation` Previously, `AASConstraintViolation` exception weren't excepted in the json deserialization, breaking failsafe parsing whenever a constraint is violated. This commit ports 2a1bd396ec7ce0763b8c224bad3f627f2861a328 to the json deserialization. --- basyx/aas/adapter/json/json_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index df93146..9683717 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -207,7 +207,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object: # Use constructor function to transform JSON representation into BaSyx Python SDK model object try: return AAS_CLASS_PARSERS[model_type](dct) - except (KeyError, TypeError) as e: + except (KeyError, TypeError, model.AASConstraintViolation) as e: error_message = "Error while trying to convert JSON object into {}: {} >>> {}".format( model_type, e, pprint.pformat(dct, depth=2, width=2**14, compact=True)) if cls.failsafe: @@ -217,7 +217,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object: # constructors for complex objects will skip those items by using _expect_type(). return dct else: - raise type(e)(error_message) from e + raise (type(e) if isinstance(e, (KeyError, TypeError)) else TypeError)(error_message) from e # ################################################################################################## # Utility Methods used in constructor methods to add general attributes (from abstract base classes) From 6139471bd0549a964058f332428496247056d4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 2 Oct 2023 19:26:53 +0200 Subject: [PATCH 109/474] make `id_shorts` optional Since `SubmodelElementLists` require that their children elements don't have `id_shorts`, they have to be made optional for all elements, but required for `NamespaceSets`. --- .../aas/adapter/json/json_deserialization.py | 32 +++++++------- basyx/aas/adapter/xml/xml_deserialization.py | 42 +++++++------------ basyx/aas/adapter/xml/xml_serialization.py | 3 +- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 9683717..0eb7866 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -233,6 +233,8 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None :param dct: The object's dict representation from JSON """ if isinstance(obj, model.Referable): + if 'idShort' in dct: + obj.id_short = _get_ts(dct, 'idShort', str) if 'category' in dct: obj.category = _get_ts(dct, 'category', str) if 'displayName' in dct: @@ -242,8 +244,6 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None obj.description = cls._construct_lang_string_set(_get_ts(dct, 'description', list), model.MultiLanguageTextType) if isinstance(obj, model.Identifiable): - if 'idShort' in dct: - obj.id_short = _get_ts(dct, 'idShort', str) if 'administration' in dct: obj.administration = cls._construct_administrative_information(_get_ts(dct, 'administration', dict)) if isinstance(obj, model.HasSemantics): @@ -536,7 +536,7 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> if 'specificAssetIds' in dct: specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'specificAssetIds', dict)) - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], global_asset_id=global_asset_id, specific_asset_id=specific_asset_id) @@ -586,7 +586,7 @@ def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel @classmethod def _construct_capability(cls, dct: Dict[str, object], object_class=model.Capability) -> model.Capability: - ret = object_class(id_short=_get_ts(dct, "idShort", str)) + ret = object_class(id_short=None) cls._amend_abstract_attributes(ret, dct) return ret @@ -595,7 +595,7 @@ def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=mod -> model.BasicEventElement: # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, observed=cls._construct_model_reference(_get_ts(dct, 'observed', dict), model.Referable), # type: ignore direction=DIRECTION_INVERSE[_get_ts(dct, "direction", str)], @@ -615,7 +615,7 @@ def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=mod @classmethod def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operation) -> model.Operation: - ret = object_class(_get_ts(dct, "idShort", str)) + ret = object_class(None) cls._amend_abstract_attributes(ret, dct) # Deserialize variables (they are not Referable, thus we don't @@ -640,7 +640,7 @@ def _construct_relationship_element( cls, dct: Dict[str, object], object_class=model.RelationshipElement) -> model.RelationshipElement: # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, first=cls._construct_reference(_get_ts(dct, 'first', dict)), second=cls._construct_reference(_get_ts(dct, 'second', dict))) cls._amend_abstract_attributes(ret, dct) @@ -653,7 +653,7 @@ def _construct_annotated_relationship_element( # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 ret = object_class( - id_short=_get_ts(dct, "idShort", str), + id_short=None, first=cls._construct_reference(_get_ts(dct, 'first', dict)), second=cls._construct_reference(_get_ts(dct, 'second', dict))) cls._amend_abstract_attributes(ret, dct) @@ -667,7 +667,7 @@ def _construct_annotated_relationship_element( def _construct_submodel_element_collection(cls, dct: Dict[str, object], object_class=model.SubmodelElementCollection)\ -> model.SubmodelElementCollection: - ret = object_class(id_short=_get_ts(dct, "idShort", str)) + ret = object_class(id_short=None) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'value' in dct: for element in _get_ts(dct, "value", list): @@ -688,7 +688,7 @@ def _construct_submodel_element_list(cls, dct: Dict[str, object], object_class=m if 'semanticIdListElement' in dct else None value_type_list_element = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueTypeListElement', str)]\ if 'valueTypeListElement' in dct else None - ret = object_class(id_short=_get_ts(dct, 'idShort', str), + ret = object_class(id_short=None, type_value_list_element=type_value_list_element, order_relevant=order_relevant, semantic_id_list_element=semantic_id_list_element, @@ -702,7 +702,7 @@ def _construct_submodel_element_list(cls, dct: Dict[str, object], object_class=m @classmethod def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> model.Blob: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, content_type=_get_ts(dct, "contentType", str)) cls._amend_abstract_attributes(ret, dct) if 'value' in dct: @@ -711,7 +711,7 @@ def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> mod @classmethod def _construct_file(cls, dct: Dict[str, object], object_class=model.File) -> model.File: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, value=None, content_type=_get_ts(dct, "contentType", str)) cls._amend_abstract_attributes(ret, dct) @@ -730,7 +730,7 @@ def _construct_resource(cls, dct: Dict[str, object], object_class=model.Resource @classmethod def _construct_multi_language_property( cls, dct: Dict[str, object], object_class=model.MultiLanguageProperty) -> model.MultiLanguageProperty: - ret = object_class(id_short=_get_ts(dct, "idShort", str)) + ret = object_class(id_short=None) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: ret.value = cls._construct_lang_string_set(_get_ts(dct, 'value', list), model.MultiLanguageTextType) @@ -740,7 +740,7 @@ def _construct_multi_language_property( @classmethod def _construct_property(cls, dct: Dict[str, object], object_class=model.Property) -> model.Property: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)],) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: @@ -751,7 +751,7 @@ def _construct_property(cls, dct: Dict[str, object], object_class=model.Property @classmethod def _construct_range(cls, dct: Dict[str, object], object_class=model.Range) -> model.Range: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)],) cls._amend_abstract_attributes(ret, dct) if 'min' in dct and dct['min'] is not None: @@ -763,7 +763,7 @@ def _construct_range(cls, dct: Dict[str, object], object_class=model.Range) -> m @classmethod def _construct_reference_element( cls, dct: Dict[str, object], object_class=model.ReferenceElement) -> model.ReferenceElement: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, value=None) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index e1d30ef..e92b78a 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -437,6 +437,9 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None :return: None """ if isinstance(obj, model.Referable): + id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) + if id_short is not None: + obj.id_short = id_short category = _get_text_or_none(element.find(NS_AAS + "category")) display_name = _failsafe_construct(element.find(NS_AAS + "displayName"), cls.construct_multi_language_name_type, cls.failsafe) @@ -449,9 +452,6 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None if description is not None: obj.description = description if isinstance(obj, model.Identifiable): - id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) - if id_short is not None: - obj.id_short = id_short administration = _failsafe_construct(element.find(NS_AAS + "administration"), cls.construct_administrative_information, cls.failsafe) if administration: @@ -492,7 +492,7 @@ def _construct_relationship_element_internal(cls, element: etree.Element, object to reduce duplicate code """ relationship_element = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, _child_construct_mandatory(element, NS_AAS + "first", cls.construct_reference), _child_construct_mandatory(element, NS_AAS + "second", cls.construct_reference) ) @@ -753,7 +753,7 @@ def construct_annotated_relationship_element(cls, element: etree.Element, def construct_basic_event_element(cls, element: etree.Element, object_class=model.BasicEventElement, **_kwargs: Any) -> model.BasicEventElement: basic_event_element = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, _child_construct_mandatory(element, NS_AAS + "observed", cls._construct_referable_reference), _child_text_mandatory_mapped(element, NS_AAS + "direction", DIRECTION_INVERSE), _child_text_mandatory_mapped(element, NS_AAS + "state", STATE_OF_EVENT_INVERSE) @@ -780,7 +780,7 @@ def construct_basic_event_element(cls, element: etree.Element, object_class=mode @classmethod def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: blob = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, _child_text_mandatory(element, NS_AAS + "contentType") ) value = _get_text_or_none(element.find(NS_AAS + "value")) @@ -792,9 +792,7 @@ def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwar @classmethod def construct_capability(cls, element: etree.Element, object_class=model.Capability, **_kwargs: Any) \ -> model.Capability: - capability = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + capability = object_class(None) cls._amend_abstract_attributes(capability, element) return capability @@ -804,7 +802,7 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ specific_asset_id = _failsafe_construct(element.find(NS_AAS + "specificAssetId"), cls.construct_specific_asset_id, cls.failsafe) entity = object_class( - id_short=_child_text_mandatory(element, NS_AAS + "idShort"), + id_short=None, entity_type=_child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), global_asset_id=global_asset_id, specific_asset_id=specific_asset_id) @@ -821,7 +819,7 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ @classmethod def construct_file(cls, element: etree.Element, object_class=model.File, **_kwargs: Any) -> model.File: file = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, _child_text_mandatory(element, NS_AAS + "contentType") ) value = _get_text_or_none(element.find(NS_AAS + "value")) @@ -844,9 +842,7 @@ def construct_resource(cls, element: etree.Element, object_class=model.Resource, @classmethod def construct_multi_language_property(cls, element: etree.Element, object_class=model.MultiLanguageProperty, **_kwargs: Any) -> model.MultiLanguageProperty: - multi_language_property = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + multi_language_property = object_class(None) value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_multi_language_text_type, cls.failsafe) if value is not None: @@ -860,9 +856,7 @@ def construct_multi_language_property(cls, element: etree.Element, object_class= @classmethod def construct_operation(cls, element: etree.Element, object_class=model.Operation, **_kwargs: Any) \ -> model.Operation: - operation = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + operation = object_class(None) input_variables = element.find(NS_AAS + "inputVariables") if input_variables is not None: for input_variable in _child_construct_multiple(input_variables, NS_AAS + "operationVariable", @@ -884,7 +878,7 @@ def construct_operation(cls, element: etree.Element, object_class=model.Operatio @classmethod def construct_property(cls, element: etree.Element, object_class=model.Property, **_kwargs: Any) -> model.Property: property_ = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) value = _get_text_or_none(element.find(NS_AAS + "value")) @@ -899,7 +893,7 @@ def construct_property(cls, element: etree.Element, object_class=model.Property, @classmethod def construct_range(cls, element: etree.Element, object_class=model.Range, **_kwargs: Any) -> model.Range: range_ = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) max_ = _get_text_or_none(element.find(NS_AAS + "max")) @@ -914,9 +908,7 @@ def construct_range(cls, element: etree.Element, object_class=model.Range, **_kw @classmethod def construct_reference_element(cls, element: etree.Element, object_class=model.ReferenceElement, **_kwargs: Any) \ -> model.ReferenceElement: - reference_element = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + reference_element = object_class(None) value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_reference, cls.failsafe) if value is not None: reference_element.value = value @@ -931,9 +923,7 @@ def construct_relationship_element(cls, element: etree.Element, object_class=mod @classmethod def construct_submodel_element_collection(cls, element: etree.Element, object_class=model.SubmodelElementCollection, **_kwargs: Any) -> model.SubmodelElementCollection: - collection = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + collection = object_class(None) if not cls.stripped: value = element.find(NS_AAS + "value") if value is not None: @@ -953,7 +943,7 @@ def construct_submodel_element_list(cls, element: etree.Element, object_class=mo f"{model.SubmodelElement}, got {type_value_list_element}!") order_relevant = element.find(NS_AAS + "orderRelevant") list_ = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, type_value_list_element, semantic_id_list_element=_failsafe_construct(element.find(NS_AAS + "semanticIdListElement"), cls.construct_reference, cls.failsafe), diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 9196137..cabec85 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -92,7 +92,8 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: if isinstance(obj, model.Referable): if obj.category: elm.append(_generate_element(name=NS_AAS + "category", text=obj.category)) - elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) + if obj.id_short: + elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) if obj.display_name: elm.append(lang_string_set_to_xml(obj.display_name, tag=NS_AAS + "displayName")) if obj.description: From 23671ada5347ac4bd09c2e1cfa9443cbf754e292 Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:13:24 +0200 Subject: [PATCH 110/474] Remove the www. subdomain in AASX namespace (#122) Needed to be compliant to the spec V3.0. Fixes #96 --------- Co-authored-by: s-heppner --- basyx/aas/adapter/aasx.py | 8 ++++---- basyx/aas/adapter/xml/xml_deserialization.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index ada18d8..825312e 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -39,10 +39,10 @@ logger = logging.getLogger(__name__) -RELATIONSHIP_TYPE_AASX_ORIGIN = "http://www.admin-shell.io/aasx/relationships/aasx-origin" -RELATIONSHIP_TYPE_AAS_SPEC = "http://www.admin-shell.io/aasx/relationships/aas-spec" -RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://www.admin-shell.io/aasx/relationships/aas-spec-split" -RELATIONSHIP_TYPE_AAS_SUPL = "http://www.admin-shell.io/aasx/relationships/aas-suppl" +RELATIONSHIP_TYPE_AASX_ORIGIN = "http://admin-shell.io/aasx/relationships/aasx-origin" +RELATIONSHIP_TYPE_AAS_SPEC = "http://admin-shell.io/aasx/relationships/aas-spec" +RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://admin-shell.io/aasx/relationships/aas-spec-split" +RELATIONSHIP_TYPE_AAS_SUPL = "http://admin-shell.io/aasx/relationships/aas-suppl" class AASXReader: diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index e92b78a..1b640ca 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -99,7 +99,7 @@ def _element_pretty_identifier(element: etree.Element) -> str: If the prefix is known, the namespace in the element tag is replaced by the prefix. If additionally also the sourceline is known, is is added as a suffix to name. - For example, instead of "{http://www.admin-shell.io/aas/3/0}assetAdministrationShell" this function would return + For example, instead of "{https://admin-shell.io/aas/3/0}assetAdministrationShell" this function would return "aas:assetAdministrationShell on line $line", if both, prefix and sourceline, are known. :param element: The xml element. From f125d0cc14e56f524a1298175d66e64caf031693 Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:51:48 +0200 Subject: [PATCH 111/474] Remove DataSpecificationPhysicalUnit (#137) As the DataSpecificationPhysicalUnit is not part of V3.0 it was removed from the SDK. Fixes #136 --- basyx/aas/adapter/json/aasJSONSchema.json | 72 ------------- .../aas/adapter/json/json_deserialization.py | 32 ------ basyx/aas/adapter/json/json_serialization.py | 38 ------- basyx/aas/adapter/xml/AAS.xsd | 102 ------------------ basyx/aas/adapter/xml/xml_deserialization.py | 45 -------- basyx/aas/adapter/xml/xml_serialization.py | 41 ------- 6 files changed, 330 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index e5e2716..5e0ca1f 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -337,77 +337,6 @@ } ] }, - "DataSpecificationPhysicalUnit": { - "allOf": [ - { - "$ref": "#/definitions/DataSpecificationContent" - }, - { - "properties": { - "unitName": { - "type": "string", - "minLength": 1 - }, - "unitSymbol": { - "type": "string", - "minLength": 1 - }, - "definition": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringDefinitionTypeIec61360" - }, - "minItems": 1 - }, - "siNotation": { - "type": "string", - "minLength": 1 - }, - "siName": { - "type": "string", - "minLength": 1 - }, - "dinNotation": { - "type": "string", - "minLength": 1 - }, - "eceName": { - "type": "string", - "minLength": 1 - }, - "eceCode": { - "type": "string", - "minLength": 1 - }, - "nistName": { - "type": "string", - "minLength": 1 - }, - "sourceOfDefinition": { - "type": "string", - "minLength": 1 - }, - "conversionFactor": { - "type": "string", - "minLength": 1 - }, - "registrationAuthorityId": { - "type": "string", - "minLength": 1 - }, - "supplier": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "unitName", - "unitSymbol", - "definition" - ] - } - ] - }, "DataTypeDefXsd": { "type": "string", "enum": [ @@ -869,7 +798,6 @@ "Capability", "ConceptDescription", "DataSpecificationIEC61360", - "DataSpecificationPhysicalUnit", "Entity", "File", "MultiLanguageProperty", diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 0eb7866..cfdafe1 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -187,7 +187,6 @@ def object_hook(cls, dct: Dict[str, object]) -> object: 'Range': cls._construct_range, 'ReferenceElement': cls._construct_reference_element, 'DataSpecificationIEC61360': cls._construct_data_specification_iec61360, - 'DataSpecificationPhysicalUnit': cls._construct_data_specification_physical_unit, } # Get modelType and constructor function @@ -460,37 +459,6 @@ def _construct_concept_description(cls, dct: Dict[str, object], object_class=mod ret.is_case_of.add(cls._construct_reference(case_data)) return ret - @classmethod - def _construct_data_specification_physical_unit(cls, dct: Dict[str, object], - object_class=model.base.DataSpecificationPhysicalUnit)\ - -> model.base.DataSpecificationPhysicalUnit: - ret = object_class( - unit_name=_get_ts(dct, 'unitName', str), - unit_symbol=_get_ts(dct, 'unitSymbol', str), - definition=cls._construct_lang_string_set(_get_ts(dct, 'definition', list), model.DefinitionTypeIEC61360) - ) - if 'siNotation' in dct: - ret.si_notation = _get_ts(dct, 'siNotation', str) - if 'siName' in dct: - ret.si_name = _get_ts(dct, 'siName', str) - if 'dinNotation' in dct: - ret.din_notation = _get_ts(dct, 'dinNotation', str) - if 'eceName' in dct: - ret.ece_name = _get_ts(dct, 'eceName', str) - if 'eceCode' in dct: - ret.ece_code = _get_ts(dct, 'eceCode', str) - if 'nistName' in dct: - ret.nist_name = _get_ts(dct, 'nistName', str) - if 'sourceOfDefinition' in dct: - ret.source_of_definition = _get_ts(dct, 'sourceOfDefinition', str) - if 'conversionFactor' in dct: - ret.conversion_factor = _get_ts(dct, 'conversionFactor', str) - if 'registrationAuthorityId' in dct: - ret.registration_authority_id = _get_ts(dct, 'registrationAuthorityId', str) - if 'supplier' in dct: - ret.supplier = _get_ts(dct, 'supplier', str) - return ret - @classmethod def _construct_data_specification_iec61360(cls, dct: Dict[str, object], object_class=model.base.DataSpecificationIEC61360)\ diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 2682b1d..21e8ddc 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -73,7 +73,6 @@ def default(self, obj: object) -> object: model.Capability: self._capability_to_json, model.ConceptDescription: self._concept_description_to_json, model.DataSpecificationIEC61360: self._data_specification_iec61360_to_json, - model.DataSpecificationPhysicalUnit: self._data_specification_physical_unit_to_json, model.Entity: self._entity_to_json, model.Extension: self._extension_to_json, model.File: self._file_to_json, @@ -368,43 +367,6 @@ def _data_specification_iec61360_to_json( data_spec['levelType'] = {v: k in obj.level_types for k, v in _generic.IEC61360_LEVEL_TYPES.items()} return data_spec - @classmethod - def _data_specification_physical_unit_to_json( - cls, obj: model.base.DataSpecificationPhysicalUnit) -> Dict[str, object]: - """ - serialization of an object from class DataSpecificationPhysicalUnit to json - - :param obj: object of class DataSpecificationPhysicalUnit - :return: dict with the serialized attributes of this object - """ - data_spec: Dict[str, object] = { - 'modelType': 'DataSpecificationPhysicalUnit', - 'unitName': obj.unit_name, - 'unitSymbol': obj.unit_symbol, - 'definition': obj.definition - } - if obj.si_notation is not None: - data_spec['siNotation'] = obj.si_notation - if obj.si_name is not None: - data_spec['siName'] = obj.si_name - if obj.din_notation is not None: - data_spec['dinNotation'] = obj.din_notation - if obj.ece_name is not None: - data_spec['eceName'] = obj.ece_name - if obj.ece_code is not None: - data_spec['eceCode'] = obj.ece_code - if obj.nist_name is not None: - data_spec['nistName'] = obj.nist_name - if obj.source_of_definition is not None: - data_spec['sourceOfDefinition'] = obj.source_of_definition - if obj.conversion_factor is not None: - data_spec['conversionFactor'] = obj.conversion_factor - if obj.registration_authority_id is not None: - data_spec['registrationAuthorityId'] = obj.registration_authority_id - if obj.supplier is not None: - data_spec['supplier'] = obj.supplier - return data_spec - @classmethod def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell) -> Dict[str, object]: """ diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 47cf861..1c9a6e2 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -192,7 +192,6 @@ - @@ -254,102 +253,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1226,11 +1129,6 @@ - - - - - diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 1b640ca..7b3ea83 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1094,53 +1094,11 @@ def construct_data_specification_content(cls, element: etree.Element, **kwargs: data_specification_contents: Dict[str, Callable[..., model.DataSpecificationContent]] = \ {NS_AAS + k: v for k, v in { "dataSpecificationIec61360": cls.construct_data_specification_iec61360, - "dataSpecificationPhysicalUnit": cls.construct_data_specification_physical_unit, }.items()} if element.tag not in data_specification_contents: raise KeyError(f"{_element_pretty_identifier(element)} is not a valid DataSpecificationContent!") return data_specification_contents[element.tag](element, **kwargs) - @classmethod - def construct_data_specification_physical_unit(cls, element: etree.Element, - object_class=model.DataSpecificationPhysicalUnit, **_kwargs: Any) \ - -> model.DataSpecificationPhysicalUnit: - dspu = object_class(_child_text_mandatory(element, NS_AAS + "unitName"), - _child_text_mandatory(element, NS_AAS + "unitSymbol"), - _child_construct_mandatory(element, NS_AAS + "definition", - cls.construct_definition_type_iec61360)) - si_notation = _get_text_or_none(element.find(NS_AAS + "siNotation")) - if si_notation is not None: - dspu.si_notation = si_notation - si_name = _get_text_or_none(element.find(NS_AAS + "siName")) - if si_name is not None: - dspu.si_name = si_name - din_notation = _get_text_or_none(element.find(NS_AAS + "dinNotation")) - if din_notation is not None: - dspu.din_notation = din_notation - ece_name = _get_text_or_none(element.find(NS_AAS + "eceName")) - if ece_name is not None: - dspu.ece_name = ece_name - ece_code = _get_text_or_none(element.find(NS_AAS + "eceCode")) - if ece_code is not None: - dspu.ece_code = ece_code - nist_name = _get_text_or_none(element.find(NS_AAS + "nistName")) - if nist_name is not None: - dspu.nist_name = nist_name - source_of_definition = _get_text_or_none(element.find(NS_AAS + "sourceOfDefinition")) - if source_of_definition is not None: - dspu.source_of_definition = source_of_definition - conversion_factor = _get_text_or_none(element.find(NS_AAS + "conversionFactor")) - if conversion_factor is not None: - dspu.conversion_factor = conversion_factor - registration_authority_id = _get_text_or_none(element.find(NS_AAS + "registrationAuthorityId")) - if registration_authority_id is not None: - dspu.registration_authority_id = registration_authority_id - supplier = _get_text_or_none(element.find(NS_AAS + "supplier")) - if supplier is not None: - dspu.supplier = supplier - cls._amend_abstract_attributes(dspu, element) - return dspu - @classmethod def construct_data_specification_iec61360(cls, element: etree.Element, object_class=model.DataSpecificationIEC61360, **_kwargs: Any) -> model.DataSpecificationIEC61360: @@ -1320,7 +1278,6 @@ class XMLConstructables(enum.Enum): EMBEDDED_DATA_SPECIFICATION = enum.auto() DATA_SPECIFICATION_CONTENT = enum.auto() DATA_SPECIFICATION_IEC61360 = enum.auto() - DATA_SPECIFICATION_PHYSICAL_UNIT = enum.auto() def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool = True, stripped: bool = False, @@ -1416,8 +1373,6 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_embedded_data_specification elif construct == XMLConstructables.DATA_SPECIFICATION_IEC61360: constructor = decoder_.construct_data_specification_iec61360 - elif construct == XMLConstructables.DATA_SPECIFICATION_PHYSICAL_UNIT: - constructor = decoder_.construct_data_specification_physical_unit # the following constructors decide which constructor to call based on the elements tag elif construct == XMLConstructables.DATA_ELEMENT: constructor = decoder_.construct_data_element diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index cabec85..0b0cf17 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -413,8 +413,6 @@ def data_specification_content_to_xml(obj: model.DataSpecificationContent, et_data_specification_content = abstract_classes_to_xml(tag, obj) if isinstance(obj, model.DataSpecificationIEC61360): et_data_specification_content.append(data_specification_iec61360_to_xml(obj)) - elif isinstance(obj, model.DataSpecificationPhysicalUnit): - et_data_specification_content.append(data_specification_physical_unit_to_xml(obj)) else: raise TypeError(f"Serialization of {obj.__class__} to XML is not supported!") return et_data_specification_content @@ -464,45 +462,6 @@ def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, return et_data_specification_iec61360 -def data_specification_physical_unit_to_xml(obj: model.DataSpecificationPhysicalUnit, - tag: str = NS_AAS+"dataSpecificationPhysicalUnit") -> etree.Element: - """ - Serialization of objects of class :class:`~aas.model.base.DataSpecificationPhysicalUnit` to XML - - :param obj: Object of class :class:`~aas.model.base.DataSpecificationPhysicalUnit` - :param tag: Namespace+Tag of the ElementTree object. Default is "aas:dataSpecificationPhysicalUnit" - :return: Serialized ElementTree object - """ - et_data_specification_physical_unit = abstract_classes_to_xml(tag, obj) - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "unitName", text=obj.unit_name)) - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "unitSymbol", text=obj.unit_symbol)) - et_data_specification_physical_unit.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition")) - if obj.si_notation is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "siNotation", text=obj.si_notation)) - if obj.si_name is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "siName", text=obj.si_name)) - if obj.din_notation is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "dinNotation", text=obj.din_notation)) - if obj.ece_name is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "eceName", text=obj.ece_name)) - if obj.ece_code is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "eceCode", text=obj.ece_code)) - if obj.nist_name is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "nistName", text=obj.nist_name)) - if obj.source_of_definition is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "sourceOfDefinition", - text=obj.source_of_definition)) - if obj.conversion_factor is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "conversionFactor", - text=obj.conversion_factor)) - if obj.registration_authority_id is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "registrationAuthorityId", - text=obj.registration_authority_id)) - if obj.supplier is not None: - et_data_specification_physical_unit.append(_generate_element(NS_AAS + "supplier", text=obj.supplier)) - return et_data_specification_physical_unit - - def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, tag: str = NS_AAS+"assetAdministrationShell") -> etree.Element: """ From 503d58486ecf80c22c24c205342df877e4f7fb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 4 Oct 2023 17:33:04 +0200 Subject: [PATCH 112/474] generate unique id_shorts for `SubmodelElementList`-children Constraint AASd-120 requires direct children of a `SubmodelElementList` to have id_short=None. On the contrary, `SubmodelElementList` must be a Namespace, since children of Lists must still be referable via References, and also must be allowed to reference their parent, which is expected to be a Namespace. Since id_short=None must hold for all direct children, they lack a unique identifying attribute, that can be used to refer to an item. However, this is required for a Namespace. Thus, we had two options for implementing this: - Refactor a lot of the model.base module such that `SubmodelElementLists` are considered Namespaces - Generate a unique id_short for every direct children of a `SubmodelElementList` whenever it is added. Since the first alternative would require a distinction for `SubmodelElementList` in all places where a `Namespace` is used, we decided on the second alternative. This commit implements the generation of unique id_shorts via the `item_id_set_hook`, that was recently added to `NamespaceSet` and `OrderedNamespaceSet`. It is called for every added SubmodelElement. Furthermore, the `item_id_del_hook` is called for every removed SubmodelElement and used to remove the generated id_short again. This aside, the examples and unit tests are also adjusted such that the id_short is removed for all direct children of `SubmodelElementList`. Furthermore, a test for `AASd-120` is added. The AASDataChecker is adjusted to skip the comparison of id_short for direct children of `SubmodelElementList`, since these are generated and thus never the same now. For the same reason, the XML/JSON serialisation is adjusted to skip serialising the id_short if direct children of a `SubmodelElementList`. --- basyx/aas/adapter/json/json_serialization.py | 2 +- basyx/aas/adapter/xml/xml_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 21e8ddc..811de0d 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -121,7 +121,7 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: ] if isinstance(obj, model.Referable): - if obj.id_short: + if obj.id_short and not isinstance(obj.parent, model.SubmodelElementList): data['idShort'] = obj.id_short if obj.display_name: data['displayName'] = obj.display_name diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 0b0cf17..b7da708 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -92,7 +92,7 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: if isinstance(obj, model.Referable): if obj.category: elm.append(_generate_element(name=NS_AAS + "category", text=obj.category)) - if obj.id_short: + if obj.id_short and not isinstance(obj.parent, model.SubmodelElementList): elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) if obj.display_name: elm.append(lang_string_set_to_xml(obj.display_name, tag=NS_AAS + "displayName")) From ce2d55fe7e1bd29faa645e6a8d5bae1b2a074e8c Mon Sep 17 00:00:00 2001 From: zrgt Date: Wed, 18 Oct 2023 12:31:20 +0200 Subject: [PATCH 113/474] Set `SpecificAssetId.external_subject_id` as optional according to the spec --- basyx/aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/json/json_serialization.py | 3 ++- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- basyx/aas/adapter/xml/xml_serialization.py | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index cfdafe1..6129821 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -306,7 +306,7 @@ def _construct_specific_asset_id(cls, dct: Dict[str, object], object_class=model return object_class(name=_get_ts(dct, 'name', str), value=_get_ts(dct, 'value', str), external_subject_id=cls._construct_external_reference( - _get_ts(dct, 'externalSubjectId', dict)), + _get_ts(dct, 'externalSubjectId', dict)) if 'externalSubjectId' in dct else None, semantic_id=cls._construct_reference(_get_ts(dct, 'semanticId', dict)) if 'semanticId' in dct else None, supplemental_semantic_id=[ diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 811de0d..08933c7 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -295,7 +295,8 @@ def _specific_asset_id_to_json(cls, obj: model.SpecificAssetId) -> Dict[str, obj data = cls._abstract_classes_to_json(obj) data['name'] = obj.name data['value'] = obj.value - data['externalSubjectId'] = obj.external_subject_id + if obj.external_subject_id: + data['externalSubjectId'] = obj.external_subject_id return data @classmethod diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 7b3ea83..f5aabff 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -985,10 +985,10 @@ def construct_specific_asset_id(cls, element: etree.Element, object_class=model. **_kwargs: Any) -> model.SpecificAssetId: # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable return object_class( - external_subject_id=_child_construct_mandatory(element, NS_AAS + "externalSubjectId", - cls.construct_external_reference), name=_get_text_or_none(element.find(NS_AAS + "name")), value=_get_text_or_none(element.find(NS_AAS + "value")), + external_subject_id=_failsafe_construct(element.find(NS_AAS + "externalSubjectId"), + cls.construct_external_reference, cls.failsafe), semantic_id=_failsafe_construct(element.find(NS_AAS + "semanticId"), cls.construct_reference, cls.failsafe) ) diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index b7da708..3134869 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -338,7 +338,8 @@ def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "sp et_asset_information = abstract_classes_to_xml(tag, obj) et_asset_information.append(_generate_element(name=NS_AAS + "name", text=obj.name)) et_asset_information.append(_generate_element(name=NS_AAS + "value", text=obj.value)) - et_asset_information.append(reference_to_xml(obj.external_subject_id, NS_AAS + "externalSubjectId")) + if obj.external_subject_id: + et_asset_information.append(reference_to_xml(obj.external_subject_id, NS_AAS + "externalSubjectId")) return et_asset_information From eb5c0868b69411a3a912b108e970e728b2de8faa Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 16 Oct 2023 17:03:02 +0200 Subject: [PATCH 114/474] Use correct modelName 'DataSpecificationIec61360' --- basyx/aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/json/json_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 6129821..107d49e 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -186,7 +186,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object: 'Property': cls._construct_property, 'Range': cls._construct_range, 'ReferenceElement': cls._construct_reference_element, - 'DataSpecificationIEC61360': cls._construct_data_specification_iec61360, + 'DataSpecificationIec61360': cls._construct_data_specification_iec61360, } # Get modelType and constructor function diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 08933c7..ca34e2b 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -342,7 +342,7 @@ def _data_specification_iec61360_to_json( :return: dict with the serialized attributes of this object """ data_spec: Dict[str, object] = { - 'modelType': 'DataSpecificationIEC61360', + 'modelType': 'DataSpecificationIec61360', 'preferredName': obj.preferred_name } if obj.data_type is not None: From 0c9aff83475d9699ec34238635945f657e17ec34 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 16 Oct 2023 22:27:26 +0200 Subject: [PATCH 115/474] Set typehint of EmbeddedDataSpecification.data_specification Set to Reference instead of ExternalReference according to specs --- basyx/aas/adapter/json/json_deserialization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 107d49e..01aff02 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -265,8 +265,7 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 model.EmbeddedDataSpecification( - data_specification=cls._construct_external_reference(_get_ts(dspec, 'dataSpecification', - dict)), + data_specification=cls._construct_reference(_get_ts(dspec, 'dataSpecification', dict)), data_specification_content=_get_ts(dspec, 'dataSpecificationContent', model.DataSpecificationContent) # type: ignore ) From 05d42821ba793744e1a7c8424b73790c87453200 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 16 Oct 2023 23:50:38 +0200 Subject: [PATCH 116/474] Fix capitalization of "DataSpecificationIec61360" --- basyx/aas/adapter/json/aasJSONSchema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 5e0ca1f..feb41a2 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -271,7 +271,7 @@ "modelType" ] }, - "DataSpecificationIEC61360": { + "DataSpecificationIec61360": { "allOf": [ { "$ref": "#/definitions/DataSpecificationContent" @@ -797,7 +797,7 @@ "Blob", "Capability", "ConceptDescription", - "DataSpecificationIEC61360", + "DataSpecificationIec61360", "Entity", "File", "MultiLanguageProperty", From 19397adb6ca58872c8d6c7c83feca60a1c2d6f44 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 17 Oct 2023 14:33:31 +0200 Subject: [PATCH 117/474] Update IEC61360_DATA_TYPES --- basyx/aas/adapter/_generic.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 6bd0779..5fd80b8 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -74,15 +74,22 @@ model.base.IEC61360DataType.DATE: 'DATE', model.base.IEC61360DataType.STRING: 'STRING', model.base.IEC61360DataType.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE', + model.base.IEC61360DataType.INTEGER_MEASURE: 'INTEGER_MEASURE', + model.base.IEC61360DataType.INTEGER_COUNT: 'INTEGER_COUNT', + model.base.IEC61360DataType.INTEGER_CURRENCY: 'INTEGER_CURRENCY', model.base.IEC61360DataType.REAL_MEASURE: 'REAL_MEASURE', model.base.IEC61360DataType.REAL_COUNT: 'REAL_COUNT', model.base.IEC61360DataType.REAL_CURRENCY: 'REAL_CURRENCY', model.base.IEC61360DataType.BOOLEAN: 'BOOLEAN', - model.base.IEC61360DataType.URL: 'URL', + model.base.IEC61360DataType.IRI: 'IRI', + model.base.IEC61360DataType.IRDI: 'IRDI', model.base.IEC61360DataType.RATIONAL: 'RATIONAL', model.base.IEC61360DataType.RATIONAL_MEASURE: 'RATIONAL_MEASURE', model.base.IEC61360DataType.TIME: 'TIME', model.base.IEC61360DataType.TIMESTAMP: 'TIMESTAMP', + model.base.IEC61360DataType.HTML: 'HTML', + model.base.IEC61360DataType.BLOB: 'BLOB', + model.base.IEC61360DataType.FILE: 'FILE', } IEC61360_LEVEL_TYPES: Dict[model.base.IEC61360LevelType, str] = { From cb971b517bec7c5b84a3bcbadaaabb3e3d04235e Mon Sep 17 00:00:00 2001 From: zrgt Date: Wed, 18 Oct 2023 01:18:07 +0200 Subject: [PATCH 118/474] Fix the implementation of DataSpecificationIEC61360 - Update IEC61360_DATA_TYPES from the Spec - Add checking of value_type string: 1 model.ValueList: + def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList: ret: model.ValueList = set() for element in _get_ts(dct, 'valueReferencePairs', list): try: - ret.add(cls._construct_value_reference_pair(element, value_format=value_format)) + ret.add(cls._construct_value_reference_pair(element)) except (KeyError, TypeError) as e: error_message = "Error while trying to convert JSON object into ValueReferencePair: {} >>> {}".format( e, pprint.pformat(element, depth=2, width=2 ** 14, compact=True)) @@ -402,11 +402,10 @@ def _construct_value_list(cls, dct: Dict[str, object], value_format: model.DataT return ret @classmethod - def _construct_value_reference_pair(cls, dct: Dict[str, object], value_format: model.DataTypeDefXsd, + def _construct_value_reference_pair(cls, dct: Dict[str, object], object_class=model.ValueReferencePair) -> model.ValueReferencePair: - return object_class(value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_format), - value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict)), - value_type=value_format) + return object_class(value=_get_ts(dct, 'value', str), + value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict))) # ############################################################################# # Direct Constructor Methods (for classes with `modelType`) starting from here @@ -481,11 +480,11 @@ def _construct_data_specification_iec61360(cls, dct: Dict[str, object], if 'symbol' in dct: ret.symbol = _get_ts(dct, 'symbol', str) if 'valueFormat' in dct: - ret.value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueFormat', str)] + ret.value_format = _get_ts(dct, 'valueFormat', str) if 'valueList' in dct: - ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict), value_format=ret.value_format) + ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict)) if 'value' in dct: - ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_format) + ret.value = _get_ts(dct, 'value', str) if 'valueId' in dct: ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) if 'levelType' in dct: diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index ca34e2b..8e41a3d 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -359,11 +359,12 @@ def _data_specification_iec61360_to_json( data_spec['sourceOfDefinition'] = obj.source_of_definition if obj.symbol is not None: data_spec['symbol'] = obj.symbol - data_spec['valueFormat'] = model.datatypes.XSD_TYPE_NAMES[obj.value_format] + if obj.value_format is not None: + data_spec['valueFormat'] = obj.value_format if obj.value_list is not None: data_spec['valueList'] = cls._value_list_to_json(obj.value_list) if obj.value is not None: - data_spec['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None + data_spec['value'] = obj.value if obj.level_types: data_spec['levelType'] = {v: k in obj.level_types for k, v in _generic.IEC61360_LEVEL_TYPES.items()} return data_spec diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index f5aabff..92f766b 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1034,18 +1034,13 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, return submodel @classmethod - def construct_value_reference_pair(cls, element: etree.Element, value_format: model.DataTypeDefXsd, - object_class=model.ValueReferencePair, **_kwargs: Any) \ - -> model.ValueReferencePair: - return object_class( - model.datatypes.from_xsd(_child_text_mandatory(element, NS_AAS + "value"), value_format), - _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference), - value_format - ) + def construct_value_reference_pair(cls, element: etree.Element, object_class=model.ValueReferencePair + , **_kwargs: Any) -> model.ValueReferencePair: + return object_class(_child_text_mandatory(element, NS_AAS + "value"), + _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference)) @classmethod - def construct_value_list(cls, element: etree.Element, value_format: model.DataTypeDefXsd, **_kwargs: Any) \ - -> model.ValueList: + def construct_value_list(cls, element: etree.Element, **_kwargs: Any) -> model.ValueList: """ This function doesn't support the object_class parameter, because ValueList is just a generic type alias. """ @@ -1053,7 +1048,7 @@ def construct_value_list(cls, element: etree.Element, value_format: model.DataTy return set( _child_construct_multiple(_get_child_mandatory(element, NS_AAS + "valueReferencePairs"), NS_AAS + "valueReferencePair", cls.construct_value_reference_pair, - cls.failsafe, value_format=value_format) + cls.failsafe) ) @classmethod @@ -1127,16 +1122,15 @@ def construct_data_specification_iec61360(cls, element: etree.Element, object_cl cls.failsafe) if definition is not None: ds_iec.definition = definition - value_format = _get_text_mapped_or_none(element.find(NS_AAS + "valueFormat"), model.datatypes.XSD_TYPE_CLASSES) + value_format = _get_text_or_none(element.find(NS_AAS + "valueFormat")) if value_format is not None: ds_iec.value_format = value_format - value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe, - value_format=ds_iec.value_format) + value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe) if value_list is not None: ds_iec.value_list = value_list value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None and value_format is not None: - ds_iec.value = model.datatypes.from_xsd(value, value_format) + ds_iec.value = value level_type = element.find(NS_AAS + "levelType") if level_type is not None: for child in level_type: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 3134869..55446da 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -297,7 +297,7 @@ def value_reference_pair_to_xml(obj: model.ValueReferencePair, """ et_vrp = _generate_element(tag) # TODO: value_type isn't used at all by _value_to_xml(), thus we can ignore the type here for now - et_vrp.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore + et_vrp.append(_generate_element(NS_AAS+"value", text=obj.value)) # type: ignore et_vrp.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) return et_vrp @@ -446,15 +446,15 @@ def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, text=_generic.IEC61360_DATA_TYPES[obj.data_type])) if obj.definition is not None: et_data_specification_iec61360.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition")) - et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat", - text=model.datatypes.XSD_TYPE_NAMES[obj.value_format])) + + if obj.value_format is not None: + et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat", text=obj.value_format)) # this can be either None or an empty set, both of which are equivalent to the bool false # thus we don't check 'is not None' for this property if obj.value_list: et_data_specification_iec61360.append(value_list_to_xml(obj.value_list)) if obj.value is not None: - et_data_specification_iec61360.append(_generate_element(NS_AAS + "value", - text=model.datatypes.xsd_repr(obj.value))) + et_data_specification_iec61360.append(_generate_element(NS_AAS + "value", text=obj.value)) if obj.level_types: et_level_types = _generate_element(NS_AAS + "levelType") for k, v in _generic.IEC61360_LEVEL_TYPES.items(): From 9285e9bc1928757e50281af0291b46e79af15f44 Mon Sep 17 00:00:00 2001 From: zrgt Date: Wed, 18 Oct 2023 11:54:41 +0200 Subject: [PATCH 119/474] Fix pycodestyle issue --- basyx/aas/adapter/json/json_deserialization.py | 3 +-- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index a4e3c21..6365315 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -423,8 +423,7 @@ def _construct_asset_information(cls, dct: Dict[str, object], object_class=model ret.global_asset_id = _get_ts(dct, 'globalAssetId', str) if 'specificAssetIds' in dct: for desc_data in _get_ts(dct, "specificAssetIds", list): - ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, - model.SpecificAssetId)) + ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) if 'assetType' in dct: ret.asset_type = _get_ts(dct, 'assetType', str) if 'defaultThumbnail' in dct: diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 92f766b..3edbc57 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1034,8 +1034,8 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, return submodel @classmethod - def construct_value_reference_pair(cls, element: etree.Element, object_class=model.ValueReferencePair - , **_kwargs: Any) -> model.ValueReferencePair: + def construct_value_reference_pair(cls, element: etree.Element, object_class=model.ValueReferencePair, + **_kwargs: Any) -> model.ValueReferencePair: return object_class(_child_text_mandatory(element, NS_AAS + "value"), _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference)) From 31eb2dd2b76131435b981d02a2c783be44a5e2dc Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 27 Oct 2023 21:19:24 +0200 Subject: [PATCH 120/474] Fix Entity.specificAssetIds in schemas --- basyx/aas/adapter/json/aasJSONSchema.json | 8 ++++++-- basyx/aas/adapter/xml/AAS.xsd | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index feb41a2..9aacb90 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -444,8 +444,12 @@ "maxLength": 2000, "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, - "specificAssetId": { - "$ref": "#/definitions/SpecificAssetId" + "specificAssetIds": { + "type": "array", + "items": { + "$ref": "#/definitions/SpecificAssetId" + }, + "minItems": 1 } }, "required": [ diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 1c9a6e2..76c1fd5 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -284,7 +284,13 @@ - + + + + + + + From de2a8a17e9cdeb684d2bd8414ef4a5404e97ce68 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 27 Oct 2023 21:31:31 +0200 Subject: [PATCH 121/474] - Set `Entity.specific_asset_id` as `Iterable[SpecificAssetId]`, because the spec has changed - Add check of constraint AASd-014 for Entity, see https://rwth-iat.github.io/aas-specs/AASiD/AASiD_1_Metamodel/index.html#Entity - Add check of constraint AASd-131 for AssetInformation, see https://rwth-iat.github.io/aas-specs/AASiD/AASiD_1_Metamodel/index.html#AssetInformation - Refactor de-/serialization of Entity - Refactor deserialization of AssetInformation because of check of constraint AASd-131 --- .../aas/adapter/json/json_deserialization.py | 19 +++++++++---- basyx/aas/adapter/json/json_serialization.py | 2 +- basyx/aas/adapter/xml/xml_deserialization.py | 28 +++++++++++-------- basyx/aas/adapter/xml/xml_serialization.py | 5 +++- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 6365315..d261873 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -417,13 +417,19 @@ def _construct_value_reference_pair(cls, dct: Dict[str, object], @classmethod def _construct_asset_information(cls, dct: Dict[str, object], object_class=model.AssetInformation)\ -> model.AssetInformation: - ret = object_class(asset_kind=ASSET_KIND_INVERSE[_get_ts(dct, 'assetKind', str)]) - cls._amend_abstract_attributes(ret, dct) + global_asset_id = None if 'globalAssetId' in dct: - ret.global_asset_id = _get_ts(dct, 'globalAssetId', str) + global_asset_id = _get_ts(dct, 'globalAssetId', str) + specific_asset_id = set() if 'specificAssetIds' in dct: for desc_data in _get_ts(dct, "specificAssetIds", list): - ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) + specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) + + ret = object_class(asset_kind=ASSET_KIND_INVERSE[_get_ts(dct, 'assetKind', str)], + global_asset_id=global_asset_id, + specific_asset_id=specific_asset_id) + cls._amend_abstract_attributes(ret, dct) + if 'assetType' in dct: ret.asset_type = _get_ts(dct, 'assetType', str) if 'defaultThumbnail' in dct: @@ -497,9 +503,10 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> global_asset_id = None if 'globalAssetId' in dct: global_asset_id = _get_ts(dct, 'globalAssetId', str) - specific_asset_id = None + specific_asset_id = set() if 'specificAssetIds' in dct: - specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'specificAssetIds', dict)) + for desc_data in _get_ts(dct, "specificAssetIds", list): + specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) ret = object_class(id_short=None, entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 8e41a3d..4704f08 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -632,7 +632,7 @@ def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]: if obj.global_asset_id: data['globalAssetId'] = obj.global_asset_id if obj.specific_asset_id: - data['specificAssetIds'] = obj.specific_asset_id + data['specificAssetIds'] = list(obj.specific_asset_id) return data @classmethod diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 3edbc57..3ebb7f9 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -798,13 +798,17 @@ def construct_capability(cls, element: etree.Element, object_class=model.Capabil @classmethod def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_kwargs: Any) -> model.Entity: - global_asset_id = _get_text_or_none(element.find(NS_AAS + "globalAssetId")) - specific_asset_id = _failsafe_construct(element.find(NS_AAS + "specificAssetId"), - cls.construct_specific_asset_id, cls.failsafe) + specific_asset_id = set() + specific_assset_ids = element.find(NS_AAS + "specificAssetIds") + if specific_assset_ids is not None: + for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", + cls.construct_specific_asset_id, cls.failsafe): + specific_asset_id.add(id) + entity = object_class( id_short=None, entity_type=_child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), - global_asset_id=global_asset_id, + global_asset_id=_get_text_or_none(element.find(NS_AAS + "globalAssetId")), specific_asset_id=specific_asset_id) if not cls.stripped: @@ -995,17 +999,19 @@ def construct_specific_asset_id(cls, element: etree.Element, object_class=model. @classmethod def construct_asset_information(cls, element: etree.Element, object_class=model.AssetInformation, **_kwargs: Any) \ -> model.AssetInformation: - asset_information = object_class( - _child_text_mandatory_mapped(element, NS_AAS + "assetKind", ASSET_KIND_INVERSE), - ) - global_asset_id = _get_text_or_none(element.find(NS_AAS + "globalAssetId")) - if global_asset_id is not None: - asset_information.global_asset_id = global_asset_id + specific_asset_id = set() specific_assset_ids = element.find(NS_AAS + "specificAssetIds") if specific_assset_ids is not None: for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", cls.construct_specific_asset_id, cls.failsafe): - asset_information.specific_asset_id.add(id) + specific_asset_id.add(id) + + asset_information = object_class( + _child_text_mandatory_mapped(element, NS_AAS + "assetKind", ASSET_KIND_INVERSE), + global_asset_id=_get_text_or_none(element.find(NS_AAS + "globalAssetId")), + specific_asset_id=specific_asset_id, + ) + asset_type = _get_text_or_none(element.find(NS_AAS + "assetType")) if asset_type is not None: asset_information.asset_type = asset_type diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 55446da..0ba5c86 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -801,7 +801,10 @@ def entity_to_xml(obj: model.Entity, if obj.global_asset_id: et_entity.append(_generate_element(NS_AAS + "globalAssetId", text=obj.global_asset_id)) if obj.specific_asset_id: - et_entity.append(specific_asset_id_to_xml(obj.specific_asset_id, NS_AAS + "specificAssetId")) + et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") + for specific_asset_id in obj.specific_asset_id: + et_specific_asset_id.append(specific_asset_id_to_xml(specific_asset_id, NS_AAS + "specificAssetId")) + et_entity.append(et_specific_asset_id) return et_entity From 4d4960dd70c5bf317b6fc9e30918443b5c033368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 3 Nov 2023 17:18:53 +0100 Subject: [PATCH 122/474] make `Operation` a `Namespace` This commit makes `Operation` inherit from `UniqueIdShortNamespace`, to implement Constraint AASd-134: For an Operation, the idShort of all inputVariable/value, outputVariable/value, and inoutputVariable/value shall be unique. In the DotAAS spec, the attributes `inputVariable`, `outputVariable` and `inoutputVariable` of `Operation` are defined to be a collection of `OperationVariable` instances, which themselves just contain a single `SubmodelElement`. Thus, the `OperationVariable` isn't really required for `Operation`, as the `Operation` can just contain the `SubmodelElements` directly, without an unnecessary wrapper. This makes `Operation` less tedious to use and also allows us to use normal `NamespaceSets` for the 3 attributes, which together with the `UniqueIdShortNamespace` ensure, that the `idShort` of all contained `SubmodelElements` is unique across all 3 attributes. Aside this, the examples are updated since `SubmodelElements` as children of an `Operation` are now linked to the parent. This prevents us from reusing other `SubmodelElements` as `OperationVariables` as it was done previously, since each `SubmodelElement` can only have one parent. Fix #146 #148 --- .../aas/adapter/json/json_deserialization.py | 12 +++-- basyx/aas/adapter/json/json_serialization.py | 27 +++++----- basyx/aas/adapter/xml/xml_deserialization.py | 53 ++++++++----------- basyx/aas/adapter/xml/xml_serialization.py | 35 ++++++------ 4 files changed, 57 insertions(+), 70 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index d261873..262aebe 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -364,12 +364,14 @@ def _construct_administrative_information( return ret @classmethod - def _construct_operation_variable( - cls, dct: Dict[str, object], object_class=model.OperationVariable) -> model.OperationVariable: + def _construct_operation_variable(cls, dct: Dict[str, object]) -> model.SubmodelElement: + """ + Since we don't implement `OperationVariable`, this constructor discards the wrapping `OperationVariable` object + and just returns the contained :class:`~aas.model.submodel.SubmodelElement`. + """ # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - ret = object_class(value=_get_ts(dct, 'value', model.SubmodelElement)) # type: ignore - return ret + return _get_ts(dct, 'value', model.SubmodelElement) # type: ignore @classmethod def _construct_lang_string_set(cls, lst: List[Dict[str, object]], object_class: Type[LSS]) -> LSS: @@ -597,7 +599,7 @@ def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operati if json_name in dct: for variable_data in _get_ts(dct, json_name, list): try: - target.append(cls._construct_operation_variable(variable_data)) + target.add(cls._construct_operation_variable(variable_data)) except (KeyError, TypeError) as e: error_message = "Error while trying to convert JSON object into {} of {}: {}".format( json_name, ret, pprint.pformat(variable_data, depth=2, width=2 ** 14, compact=True)) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 4704f08..14c3932 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -80,7 +80,6 @@ def default(self, obj: object) -> object: model.LangStringSet: self._lang_string_set_to_json, model.MultiLanguageProperty: self._multi_language_property_to_json, model.Operation: self._operation_to_json, - model.OperationVariable: self._operation_variable_to_json, model.Property: self._property_to_json, model.Qualifier: self._qualifier_to_json, model.Range: self._range_to_json, @@ -577,16 +576,17 @@ def _annotated_relationship_element_to_json(cls, obj: model.AnnotatedRelationshi return data @classmethod - def _operation_variable_to_json(cls, obj: model.OperationVariable) -> Dict[str, object]: + def _operation_variable_to_json(cls, obj: model.SubmodelElement) -> Dict[str, object]: """ - serialization of an object from class OperationVariable to json + serialization of an object from class SubmodelElement to a json OperationVariable representation + Since we don't implement the `OperationVariable` class, which is just a wrapper for a single + :class:`~aas.model.submodel.SubmodelElement`, elements are serialized as the `value` attribute of an + `operationVariable` object. - :param obj: object of class OperationVariable - :return: dict with the serialized attributes of this object + :param obj: object of class `SubmodelElement` + :return: `OperationVariable` wrapper containing the serialized `SubmodelElement` """ - data = cls._abstract_classes_to_json(obj) - data['value'] = obj.value - return data + return {'value': obj} @classmethod def _operation_to_json(cls, obj: model.Operation) -> Dict[str, object]: @@ -597,12 +597,11 @@ def _operation_to_json(cls, obj: model.Operation) -> Dict[str, object]: :return: dict with the serialized attributes of this object """ data = cls._abstract_classes_to_json(obj) - if obj.input_variable: - data['inputVariables'] = list(obj.input_variable) - if obj.output_variable: - data['outputVariables'] = list(obj.output_variable) - if obj.in_output_variable: - data['inoutputVariables'] = list(obj.in_output_variable) + for tag, nss in (('inputVariables', obj.input_variable), + ('outputVariables', obj.output_variable), + ('inoutputVariables', obj.in_output_variable)): + if nss: + data[tag] = [cls._operation_variable_to_json(obj) for obj in nss] return data @classmethod diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 3ebb7f9..78e470d 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -534,6 +534,20 @@ def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ # see https://github.com/python/mypy/issues/5374 return cls.construct_model_reference_expect_type(element, model.Referable, **kwargs) # type: ignore + @classmethod + def _construct_operation_variable(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement: + """ + Since we don't implement `OperationVariable`, this constructor discards the wrapping `aas:operationVariable` + and `aas:value` and just returns the contained :class:`~aas.model.submodel.SubmodelElement`. + """ + value = _get_child_mandatory(element, NS_AAS + "value") + if len(value) == 0: + raise KeyError(f"{_element_pretty_identifier(value)} has no submodel element!") + if len(value) > 1: + logger.warning(f"{_element_pretty_identifier(value)} has more than one submodel element, " + "using the first one...") + return cls.construct_submodel_element(value[0], **kwargs) + @classmethod def construct_key(cls, element: etree.Element, object_class=model.Key, **_kwargs: Any) \ -> model.Key: @@ -723,19 +737,6 @@ def construct_data_element(cls, element: etree.Element, abstract_class_name: str raise KeyError(_element_pretty_identifier(element) + f" is not a valid {abstract_class_name}!") return data_elements[element.tag](element, **kwargs) - @classmethod - def construct_operation_variable(cls, element: etree.Element, object_class=model.OperationVariable, - **_kwargs: Any) -> model.OperationVariable: - value = _get_child_mandatory(element, NS_AAS + "value") - if len(value) == 0: - raise KeyError(f"{_element_pretty_identifier(value)} has no submodel element!") - if len(value) > 1: - logger.warning(f"{_element_pretty_identifier(value)} has more than one submodel element, " - "using the first one...") - return object_class( - _failsafe_construct_mandatory(value[0], cls.construct_submodel_element) - ) - @classmethod def construct_annotated_relationship_element(cls, element: etree.Element, object_class=model.AnnotatedRelationshipElement, **_kwargs: Any) \ @@ -861,21 +862,14 @@ def construct_multi_language_property(cls, element: etree.Element, object_class= def construct_operation(cls, element: etree.Element, object_class=model.Operation, **_kwargs: Any) \ -> model.Operation: operation = object_class(None) - input_variables = element.find(NS_AAS + "inputVariables") - if input_variables is not None: - for input_variable in _child_construct_multiple(input_variables, NS_AAS + "operationVariable", - cls.construct_operation_variable, cls.failsafe): - operation.input_variable.append(input_variable) - output_variables = element.find(NS_AAS + "outputVariables") - if output_variables is not None: - for output_variable in _child_construct_multiple(output_variables, NS_AAS + "operationVariable", - cls.construct_operation_variable, cls.failsafe): - operation.output_variable.append(output_variable) - in_output_variables = element.find(NS_AAS + "inoutputVariables") - if in_output_variables is not None: - for in_output_variable in _child_construct_multiple(in_output_variables, NS_AAS + "operationVariable", - cls.construct_operation_variable, cls.failsafe): - operation.in_output_variable.append(in_output_variable) + for tag, target in ((NS_AAS + "inputVariables", operation.input_variable), + (NS_AAS + "outputVariables", operation.output_variable), + (NS_AAS + "inoutputVariables", operation.in_output_variable)): + variables = element.find(tag) + if variables is not None: + for var in _child_construct_multiple(variables, NS_AAS + "operationVariable", + cls._construct_operation_variable, cls.failsafe): + target.add(var) cls._amend_abstract_attributes(operation, element) return operation @@ -1243,7 +1237,6 @@ class XMLConstructables(enum.Enum): ADMINISTRATIVE_INFORMATION = enum.auto() QUALIFIER = enum.auto() SECURITY = enum.auto() - OPERATION_VARIABLE = enum.auto() ANNOTATED_RELATIONSHIP_ELEMENT = enum.auto() BASIC_EVENT_ELEMENT = enum.auto() BLOB = enum.auto() @@ -1313,8 +1306,6 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_administrative_information elif construct == XMLConstructables.QUALIFIER: constructor = decoder_.construct_qualifier - elif construct == XMLConstructables.OPERATION_VARIABLE: - constructor = decoder_.construct_operation_variable elif construct == XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT: constructor = decoder_.construct_annotated_relationship_element elif construct == XMLConstructables.BASIC_EVENT_ELEMENT: diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 0ba5c86..c5d4546 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -726,18 +726,20 @@ def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElemen return et_annotated_relationship_element -def operation_variable_to_xml(obj: model.OperationVariable, - tag: str = NS_AAS+"operationVariable") -> etree.Element: +def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"operationVariable") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.OperationVariable` to XML + Serialization of :class:`~aas.model.submodel.SubmodelElement` to the XML OperationVariable representation + Since we don't implement the `OperationVariable` class, which is just a wrapper for a single + :class:`~aas.model.submodel.SubmodelElement`, elements are serialized as the `aas:value` child of an + `aas:operationVariable` element. - :param obj: Object of class :class:`~aas.model.submodel.OperationVariable` + :param obj: Object of class :class:`~aas.model.submodel.SubmodelElement` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operationVariable" :return: Serialized ElementTree object """ et_operation_variable = _generate_element(tag) et_value = _generate_element(NS_AAS+"value") - et_value.append(submodel_element_to_xml(obj.value)) + et_value.append(submodel_element_to_xml(obj)) et_operation_variable.append(et_value) return et_operation_variable @@ -752,21 +754,14 @@ def operation_to_xml(obj: model.Operation, :return: Serialized ElementTree object """ et_operation = abstract_classes_to_xml(tag, obj) - if obj.input_variable: - et_input_variables = _generate_element(NS_AAS+"inputVariables") - for input_ov in obj.input_variable: - et_input_variables.append(operation_variable_to_xml(input_ov, NS_AAS+"operationVariable")) - et_operation.append(et_input_variables) - if obj.output_variable: - et_output_variables = _generate_element(NS_AAS+"outputVariables") - for output_ov in obj.output_variable: - et_output_variables.append(operation_variable_to_xml(output_ov, NS_AAS+"operationVariable")) - et_operation.append(et_output_variables) - if obj.in_output_variable: - et_inoutput_variables = _generate_element(NS_AAS+"inoutputVariables") - for in_out_ov in obj.in_output_variable: - et_inoutput_variables.append(operation_variable_to_xml(in_out_ov, NS_AAS+"operationVariable")) - et_operation.append(et_inoutput_variables) + for tag, nss in ((NS_AAS+"inputVariables", obj.input_variable), + (NS_AAS+"outputVariables", obj.output_variable), + (NS_AAS+"inoutputVariables", obj.in_output_variable)): + if nss: + et_variables = _generate_element(tag) + for submodel_element in nss: + et_variables.append(operation_variable_to_xml(submodel_element)) + et_operation.append(et_variables) return et_operation From f7eee9fca066972c98fbdb265b20d0d21d9c219d Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 14 Nov 2023 15:36:46 +0100 Subject: [PATCH 123/474] adapter.json: Update Schema to comply with v3.0 Please note, that according to [#72], we decided not to include the `File` pattern, since the specification is wrong at this. [#72](https://github.com/eclipse-basyx/basyx-python-sdk/issues/72) --- basyx/aas/adapter/json/aasJSONSchema.json | 379 ++++++++++++++++++---- 1 file changed, 313 insertions(+), 66 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 9aacb90..39c59b4 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -7,7 +7,7 @@ "$ref": "#/definitions/Environment" } ], - "$id": "https://admin-shell.io/aas/3/0/RC02", + "$id": "https://admin-shell.io/aas/3/0", "definitions": { "AasSubmodelElements": { "type": "string", @@ -58,11 +58,33 @@ "properties": { "version": { "type": "string", - "minLength": 1 + "allOf": [ + { + "minLength": 1, + "maxLength": 4 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^(0|[1-9][0-9]*)$" + } + ] }, "revision": { "type": "string", - "minLength": 1 + "allOf": [ + { + "minLength": 1, + "maxLength": 4 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^(0|[1-9][0-9]*)$" + } + ] }, "creator": { "$ref": "#/definitions/Reference" @@ -80,16 +102,19 @@ "AnnotatedRelationshipElement": { "allOf": [ { - "$ref": "#/definitions/RelationshipElement" + "$ref": "#/definitions/RelationshipElement_abstract" }, { "properties": { "annotations": { "type": "array", "items": { - "$ref": "#/definitions/DataElement" + "$ref": "#/definitions/DataElement_choice" }, "minItems": 1 + }, + "modelType": { + "const": "AnnotatedRelationshipElement" } } } @@ -117,6 +142,9 @@ "$ref": "#/definitions/Reference" }, "minItems": 1 + }, + "modelType": { + "const": "AssetAdministrationShell" } }, "required": [ @@ -162,8 +190,8 @@ "type": "string", "enum": [ "Instance", - "Type", - "NotApplicable" + "NotApplicable", + "Type" ] }, "BasicEventElement": { @@ -184,22 +212,27 @@ }, "messageTopic": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 255, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "messageBroker": { "$ref": "#/definitions/Reference" }, "lastUpdate": { "type": "string", - "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|[+-]00:00)$" + "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" }, "minInterval": { "type": "string", - "pattern": "^P(([0-9]+Y|[0-9]+Y[0-9]+M|[0-9]+Y[0-9]+M[0-9]+D|[0-9]+Y[0-9]+D|[0-9]+M|[0-9]+M[0-9]+D|[0-9]+D)(T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))?|T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))$" + "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" }, "maxInterval": { "type": "string", - "pattern": "^P(([0-9]+Y|[0-9]+Y[0-9]+M|[0-9]+Y[0-9]+M[0-9]+D|[0-9]+Y[0-9]+D|[0-9]+M|[0-9]+M[0-9]+D|[0-9]+D)(T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))?|T([0-9]+H[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+H[0-9]+(\\.[0-9]+)?S|[0-9]+H|[0-9]+H[0-9]+M|[0-9]+M[0-9]+(\\.[0-9]+)?S|[0-9]+M|[0-9]+(\\.[0-9]+)?S))$" + "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" + }, + "modelType": { + "const": "BasicEventElement" } }, "required": [ @@ -223,8 +256,21 @@ }, "contentType": { "type": "string", - "minLength": 1, - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \t]*;[ \t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\t !#-\\[\\]-~]|[\\x80-\\xff])|\\\\([\t !-~]|[\\x80-\\xff]))*\"))*$" + "allOf": [ + { + "minLength": 1, + "maxLength": 100 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" + } + ] + }, + "modelType": { + "const": "Blob" } }, "required": [ @@ -234,7 +280,18 @@ ] }, "Capability": { - "$ref": "#/definitions/SubmodelElement" + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "modelType": { + "const": "Capability" + } + } + } + ] }, "ConceptDescription": { "allOf": [ @@ -252,6 +309,9 @@ "$ref": "#/definitions/Reference" }, "minItems": 1 + }, + "modelType": { + "const": "ConceptDescription" } } } @@ -260,6 +320,28 @@ "DataElement": { "$ref": "#/definitions/SubmodelElement" }, + "DataElement_choice": { + "oneOf": [ + { + "$ref": "#/definitions/Blob" + }, + { + "$ref": "#/definitions/File" + }, + { + "$ref": "#/definitions/MultiLanguageProperty" + }, + { + "$ref": "#/definitions/Property" + }, + { + "$ref": "#/definitions/Range" + }, + { + "$ref": "#/definitions/ReferenceElement" + } + ] + }, "DataSpecificationContent": { "type": "object", "properties": { @@ -271,6 +353,13 @@ "modelType" ] }, + "DataSpecificationContent_choice": { + "oneOf": [ + { + "$ref": "#/definitions/DataSpecificationIec61360" + } + ] + }, "DataSpecificationIec61360": { "allOf": [ { @@ -294,21 +383,24 @@ }, "unit": { "type": "string", - "minLength": 1 + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "unitId": { "$ref": "#/definitions/Reference" }, "sourceOfDefinition": { "type": "string", - "minLength": 1 + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "symbol": { "type": "string", - "minLength": 1 + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "dataType": { - "$ref": "#/definitions/DataTypeIEC61360" + "$ref": "#/definitions/DataTypeIec61360" }, "definition": { "type": "array", @@ -319,16 +411,23 @@ }, "valueFormat": { "type": "string", - "minLength": 1 + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "valueList": { "$ref": "#/definitions/ValueList" }, "value": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "levelType": { "$ref": "#/definitions/LevelType" + }, + "modelType": { + "const": "DataSpecificationIec61360" } }, "required": [ @@ -371,11 +470,10 @@ "xs:unsignedByte", "xs:unsignedInt", "xs:unsignedLong", - "xs:unsignedShort", - "xs:yearMonthDuration" + "xs:unsignedShort" ] }, - "DataTypeIEC61360": { + "DataTypeIec61360": { "type": "string", "enum": [ "BLOB", @@ -413,7 +511,7 @@ "$ref": "#/definitions/Reference" }, "dataSpecificationContent": { - "$ref": "#/definitions/DataSpecificationContent" + "$ref": "#/definitions/DataSpecificationContent_choice" } }, "required": [ @@ -431,7 +529,7 @@ "statements": { "type": "array", "items": { - "$ref": "#/definitions/SubmodelElement" + "$ref": "#/definitions/SubmodelElement_choice" }, "minItems": 1 }, @@ -450,6 +548,9 @@ "$ref": "#/definitions/SpecificAssetId" }, "minItems": 1 + }, + "modelType": { + "const": "Entity" } }, "required": [ @@ -511,18 +612,20 @@ }, "topic": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 255, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "subjectId": { "$ref": "#/definitions/Reference" }, "timeStamp": { "type": "string", - "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)Z$" + "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" }, "payload": { "type": "string", - "minLength": 1 + "contentEncoding": "base64" } }, "required": [ @@ -540,7 +643,9 @@ "properties": { "name": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 128, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "valueType": { "$ref": "#/definitions/DataTypeDefXsd" @@ -570,14 +675,25 @@ { "properties": { "value": { - "type": "string", - "minLength": 1, - "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$" + "type": "string" }, "contentType": { "type": "string", - "minLength": 1, - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \t]*;[ \t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\t !#-\\[\\]-~]|[\\x80-\\xff])|\\\\([\t !-~]|[\\x80-\\xff]))*\"))*$" + "allOf": [ + { + "minLength": 1, + "maxLength": 100 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" + } + ] + }, + "modelType": { + "const": "File" } }, "required": [ @@ -645,7 +761,9 @@ }, "id": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" } }, "required": [ @@ -662,7 +780,9 @@ }, "value": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" } }, "required": [ @@ -838,6 +958,9 @@ }, "valueId": { "$ref": "#/definitions/Reference" + }, + "modelType": { + "const": "MultiLanguageProperty" } } } @@ -870,6 +993,9 @@ "$ref": "#/definitions/OperationVariable" }, "minItems": 1 + }, + "modelType": { + "const": "Operation" } } } @@ -879,7 +1005,7 @@ "type": "object", "properties": { "value": { - "$ref": "#/definitions/SubmodelElement" + "$ref": "#/definitions/SubmodelElement_choice" } }, "required": [ @@ -901,6 +1027,9 @@ }, "valueId": { "$ref": "#/definitions/Reference" + }, + "modelType": { + "const": "Property" } }, "required": [ @@ -939,7 +1068,9 @@ }, "type": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 128, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "valueType": { "$ref": "#/definitions/DataTypeDefXsd" @@ -981,6 +1112,9 @@ }, "max": { "type": "string" + }, + "modelType": { + "const": "Range" } }, "required": [ @@ -998,12 +1132,24 @@ "properties": { "category": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 128, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "idShort": { "type": "string", - "maxLength": 128, - "pattern": "^[a-zA-Z][a-zA-Z0-9_]+$" + "allOf": [ + { + "minLength": 1, + "maxLength": 128 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$" + } + ] }, "displayName": { "type": "array", @@ -1019,10 +1165,6 @@ }, "minItems": 1 }, - "checksum": { - "type": "string", - "minLength": 1 - }, "modelType": { "$ref": "#/definitions/ModelType" } @@ -1064,6 +1206,9 @@ "properties": { "value": { "$ref": "#/definitions/Reference" + }, + "modelType": { + "const": "ReferenceElement" } } } @@ -1077,6 +1222,20 @@ ] }, "RelationshipElement": { + "allOf": [ + { + "$ref": "#/definitions/RelationshipElement_abstract" + }, + { + "properties": { + "modelType": { + "const": "RelationshipElement" + } + } + } + ] + }, + "RelationshipElement_abstract": { "allOf": [ { "$ref": "#/definitions/SubmodelElement" @@ -1097,18 +1256,48 @@ } ] }, + "RelationshipElement_choice": { + "oneOf": [ + { + "$ref": "#/definitions/RelationshipElement" + }, + { + "$ref": "#/definitions/AnnotatedRelationshipElement" + } + ] + }, "Resource": { "type": "object", "properties": { "path": { "type": "string", - "minLength": 1, - "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$" + "allOf": [ + { + "minLength": 1, + "maxLength": 2000 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$" + } + ] }, "contentType": { "type": "string", - "minLength": 1, - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \t]*;[ \t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\t !#-\\[\\]-~]|[\\x80-\\xff])|\\\\([\t !-~]|[\\x80-\\xff]))*\"))*$" + "allOf": [ + { + "minLength": 1, + "maxLength": 100 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" + } + ] } }, "required": [ @@ -1124,11 +1313,15 @@ "properties": { "name": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 64, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "value": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "externalSubjectId": { "$ref": "#/definitions/Reference" @@ -1136,8 +1329,7 @@ }, "required": [ "name", - "value", - "externalSubjectId" + "value" ] } ] @@ -1171,9 +1363,12 @@ "submodelElements": { "type": "array", "items": { - "$ref": "#/definitions/SubmodelElement" + "$ref": "#/definitions/SubmodelElement_choice" }, "minItems": 1 + }, + "modelType": { + "const": "Submodel" } } } @@ -1184,9 +1379,6 @@ { "$ref": "#/definitions/Referable" }, - { - "$ref": "#/definitions/HasKind" - }, { "$ref": "#/definitions/HasSemantics" }, @@ -1208,9 +1400,12 @@ "value": { "type": "array", "items": { - "$ref": "#/definitions/SubmodelElement" + "$ref": "#/definitions/SubmodelElement_choice" }, "minItems": 1 + }, + "modelType": { + "const": "SubmodelElementCollection" } } } @@ -1226,13 +1421,6 @@ "orderRelevant": { "type": "boolean" }, - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement" - }, - "minItems": 1 - }, "semanticIdListElement": { "$ref": "#/definitions/Reference" }, @@ -1241,6 +1429,16 @@ }, "valueTypeListElement": { "$ref": "#/definitions/DataTypeDefXsd" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement_choice" + }, + "minItems": 1 + }, + "modelType": { + "const": "SubmodelElementList" } }, "required": [ @@ -1249,6 +1447,52 @@ } ] }, + "SubmodelElement_choice": { + "oneOf": [ + { + "$ref": "#/definitions/RelationshipElement" + }, + { + "$ref": "#/definitions/AnnotatedRelationshipElement" + }, + { + "$ref": "#/definitions/BasicEventElement" + }, + { + "$ref": "#/definitions/Blob" + }, + { + "$ref": "#/definitions/Capability" + }, + { + "$ref": "#/definitions/Entity" + }, + { + "$ref": "#/definitions/File" + }, + { + "$ref": "#/definitions/MultiLanguageProperty" + }, + { + "$ref": "#/definitions/Operation" + }, + { + "$ref": "#/definitions/Property" + }, + { + "$ref": "#/definitions/Range" + }, + { + "$ref": "#/definitions/ReferenceElement" + }, + { + "$ref": "#/definitions/SubmodelElementCollection" + }, + { + "$ref": "#/definitions/SubmodelElementList" + } + ] + }, "ValueList": { "type": "object", "properties": { @@ -1268,7 +1512,10 @@ "type": "object", "properties": { "value": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, "valueId": { "$ref": "#/definitions/Reference" From 95da6a1bf770ab28e0422057c21e793254266750 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Wed, 15 Nov 2023 10:37:51 +0100 Subject: [PATCH 124/474] adapter.json: Remove deprecated XSD DataTypes from Schema This removes the two XSD datatypes - `xs:dateTimeStamp` - `xs:dayTimeDuration` from the `DataTypeDefXsd` in the JSON schema, as they were removed from the specification in v3.0 --- basyx/aas/adapter/json/aasJSONSchema.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index 39c59b4..f48db4d 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -445,8 +445,6 @@ "xs:byte", "xs:date", "xs:dateTime", - "xs:dateTimeStamp", - "xs:dayTimeDuration", "xs:decimal", "xs:double", "xs:duration", From b780470036c6abe013a6bd5fba98353629ae0ba1 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 14 Nov 2023 15:58:49 +0100 Subject: [PATCH 125/474] adapter.xml: Update XSD Schema --- basyx/aas/adapter/xml/AAS.xsd | 120 +++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 54 deletions(-) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 76c1fd5..8cac50c 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -18,20 +18,33 @@ + + + + + + + + + + + + + @@ -112,6 +125,7 @@ + @@ -119,21 +133,21 @@ - + - + - + @@ -148,6 +162,7 @@ + @@ -249,7 +264,14 @@ - + + + + + + + + @@ -338,6 +360,7 @@ + @@ -345,17 +368,11 @@ - - - - - - - - + + @@ -365,6 +382,7 @@ + @@ -382,6 +400,7 @@ + @@ -390,6 +409,7 @@ + @@ -467,21 +487,7 @@ - - - - - - - - - - - - - - @@ -526,6 +532,7 @@ + @@ -545,6 +552,7 @@ + @@ -671,6 +679,7 @@ + @@ -694,13 +703,15 @@ + - + + @@ -719,13 +730,6 @@ - - - - - - - @@ -788,6 +792,7 @@ + @@ -796,6 +801,7 @@ + @@ -808,6 +814,7 @@ + @@ -815,10 +822,11 @@ + - + @@ -905,7 +913,14 @@ - + + + + + + + + @@ -942,9 +957,9 @@ + - @@ -955,23 +970,20 @@ - - - - + - - - + + - + + + + + - - - @@ -1011,30 +1023,30 @@ - - - + + + - + - + @@ -1321,4 +1333,4 @@ - \ No newline at end of file + From 7637c21dd7bef2b3689dd60b08c10a357c67126d Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 14 Nov 2023 16:30:04 +0100 Subject: [PATCH 126/474] adapter.xml: Update XSD of `valueDataType` and `Extension` This commit remanes `valueDataType_t` from the XSD to `valueDataType`. Furthermore, it adds a missing `` tag around the `refersTo` References of `Extension`. --- basyx/aas/adapter/xml/AAS.xsd | 31 ++++++++++++++-------- basyx/aas/adapter/xml/xml_serialization.py | 8 +++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 8cac50c..eadb28d 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -387,8 +387,14 @@ - - + + + + + + + + @@ -637,7 +643,7 @@ - + @@ -684,7 +690,7 @@ - + @@ -692,8 +698,8 @@ - - + + @@ -1087,11 +1093,14 @@ - - - - - + + + + + + + + diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index c5d4546..7751598 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -277,9 +277,11 @@ def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etr text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) if obj.value: et_extension.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore # (value_type could be None) - for refers_to in obj.refers_to: - et_extension.append(reference_to_xml(refers_to, NS_AAS+"refersTo")) - + if obj.refers_to: + refers_to = _generate_element(NS_AAS+"refersTo") + for reference in obj.refers_to: + refers_to.append(reference_to_xml(reference, NS_AAS+"reference")) + et_extension.append(refers_to) return et_extension From e52d099e1b5542272aeb13b22698bb848f8cb57c Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 14 Nov 2023 16:33:06 +0100 Subject: [PATCH 127/474] adapter.xml: Update `levelType` XSD --- basyx/aas/adapter/xml/AAS.xsd | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index eadb28d..36e4816 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -589,6 +589,14 @@ + + + + + + + + @@ -1055,19 +1063,6 @@ - - - - - - - - - - - - - @@ -1246,6 +1241,11 @@ + + + + + From cc8a3d81eecb27f42eb26f691eff626a4882e4de Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 14 Nov 2023 16:46:01 +0100 Subject: [PATCH 128/474] adapter.xml: Change order of `SubmodelElementList` objects The current order of the elements in `SubmodelElementList` was wrong. This updates the order. --- basyx/aas/adapter/xml/AAS.xsd | 6 +++--- basyx/aas/adapter/xml/xml_serialization.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 36e4816..bb31c37 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -884,6 +884,9 @@ + + + @@ -891,9 +894,6 @@ - - - diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 7751598..f3c9380 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -679,11 +679,6 @@ def submodel_element_list_to_xml(obj: model.SubmodelElementList, tag: str = NS_AAS+"submodelElementList") -> etree.Element: et_submodel_element_list = abstract_classes_to_xml(tag, obj) et_submodel_element_list.append(_generate_element(NS_AAS + "orderRelevant", boolean_to_xml(obj.order_relevant))) - if len(obj.value) > 0: - et_value = _generate_element(NS_AAS + "value") - for se in obj.value: - et_value.append(submodel_element_to_xml(se)) - et_submodel_element_list.append(et_value) if obj.semantic_id_list_element is not None: et_submodel_element_list.append(reference_to_xml(obj.semantic_id_list_element, NS_AAS + "semanticIdListElement")) @@ -692,6 +687,11 @@ def submodel_element_list_to_xml(obj: model.SubmodelElementList, if obj.value_type_list_element is not None: et_submodel_element_list.append(_generate_element(NS_AAS + "valueTypeListElement", model.datatypes.XSD_TYPE_NAMES[obj.value_type_list_element])) + if len(obj.value) > 0: + et_value = _generate_element(NS_AAS + "value") + for se in obj.value: + et_value.append(submodel_element_to_xml(se)) + et_submodel_element_list.append(et_value) return et_submodel_element_list From caa3f6a9ec70890bb92a1f8b8bdcf5711c24a71f Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 14 Nov 2023 17:24:40 +0100 Subject: [PATCH 129/474] adapter.xml: Remove `` from `SubmodelElement`s in XSD Version 3.0 of the spec removes the attribute `kind` from `SubmodelElement`s. While we already implemented this, it was still missing in the XSD Schema, as well as the examples. This commit fixes that. --- basyx/aas/adapter/xml/AAS.xsd | 1 - 1 file changed, 1 deletion(-) diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index bb31c37..25d7a52 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -862,7 +862,6 @@ - From aeef31f8a5c44682a95393bbebac52026161599d Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Wed, 15 Nov 2023 09:46:42 +0100 Subject: [PATCH 130/474] adapter.xml: Fix deserialization for `Extension` Currently, the XML deserialization missed the `` wrapper around the single references inside `Extension.refers_to`. This commit fixes that. --- basyx/aas/adapter/xml/xml_deserialization.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 78e470d..eea8696 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -693,8 +693,10 @@ def construct_extension(cls, element: etree.Element, object_class=model.Extensio value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: extension.value = model.datatypes.from_xsd(value, extension.value_type) - extension.refers_to = _failsafe_construct_multiple(element.findall(NS_AAS + "refersTo"), - cls._construct_referable_reference, cls.failsafe) + extension.refers_to = _failsafe_construct_multiple( + element.find(NS_AAS + "refersTo").findall(NS_AAS + "reference"), + cls._construct_referable_reference, cls.failsafe + ) cls._amend_abstract_attributes(extension, element) return extension From c1aa8fc99ff9460f33c633967e4ce4289ce27018 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Wed, 15 Nov 2023 10:10:18 +0100 Subject: [PATCH 131/474] model.Extension: Change refers_to to be of type `Set[Reference]` Currently, `Extension.refers_to` is declared as a `Iterable[Reference]`. This implies, that we can not necessarily check, whether or not the attribute is empty or not. This creates a problem with the XML serialization, since the `` element should only appear if there is at least one `Reference` inside. This commit changes the `Extension.refers_to` to be a set of `Reference`s, as well as adapting a more clear check whether or not the attribute is empty in `adapter.xml.xml_serialization`. --- basyx/aas/adapter/xml/xml_deserialization.py | 9 +++++---- basyx/aas/adapter/xml/xml_serialization.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index eea8696..76d07e1 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -693,10 +693,11 @@ def construct_extension(cls, element: etree.Element, object_class=model.Extensio value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: extension.value = model.datatypes.from_xsd(value, extension.value_type) - extension.refers_to = _failsafe_construct_multiple( - element.find(NS_AAS + "refersTo").findall(NS_AAS + "reference"), - cls._construct_referable_reference, cls.failsafe - ) + refers_to = element.find(NS_AAS + "refersTo") + if refers_to is not None: + for ref in _child_construct_multiple(refers_to, NS_AAS + "reference", cls._construct_referable_reference, + cls.failsafe): + extension.refers_to.add(ref) cls._amend_abstract_attributes(extension, element) return extension diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index f3c9380..106535a 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -277,7 +277,7 @@ def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etr text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) if obj.value: et_extension.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore # (value_type could be None) - if obj.refers_to: + if len(obj.refers_to) > 0: refers_to = _generate_element(NS_AAS+"refersTo") for reference in obj.refers_to: refers_to.append(reference_to_xml(reference, NS_AAS+"reference")) From 4dc224582cbed56f864764d7c6d1653fec44804c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 16 Nov 2023 19:33:48 +0100 Subject: [PATCH 132/474] adapter: fix deserialization of `Extension` --- basyx/aas/adapter/json/json_deserialization.py | 4 ++-- basyx/aas/adapter/xml/xml_deserialization.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 262aebe..6524f4b 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -543,8 +543,8 @@ def _construct_extension(cls, dct: Dict[str, object], object_class=model.Extensi if 'value' in dct: ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) if 'refersTo' in dct: - ret.refers_to = [cls._construct_model_reference(refers_to, model.Referable) # type: ignore - for refers_to in _get_ts(dct, 'refersTo', list)] + ret.refers_to = {cls._construct_model_reference(refers_to, model.Referable) # type: ignore + for refers_to in _get_ts(dct, 'refersTo', list)} return ret @classmethod diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 76d07e1..7ae5ff7 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -479,9 +479,10 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None cls.construct_embedded_data_specification, cls.failsafe): obj.embedded_data_specifications.append(eds) if isinstance(obj, model.HasExtension) and not cls.stripped: - extension_elem = element.find(NS_AAS + "extension") + extension_elem = element.find(NS_AAS + "extensions") if extension_elem is not None: - for extension in _failsafe_construct_multiple(extension_elem, cls.construct_extension, cls.failsafe): + for extension in _child_construct_multiple(extension_elem, NS_AAS + "extension", + cls.construct_extension, cls.failsafe): obj.extension.add(extension) @classmethod From 143049d58a001c8c65487114c7192a55f7792ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 19 Dec 2023 13:17:49 +0100 Subject: [PATCH 133/474] adapter.xml: verify declared namespaces before deserializing Previously, if the elements of an XML document are part of an unknown namespace, this would lead to cryptic error messages such as: Unexpected top-level list aas:assetAdministrationShells on line 3 where, the correct expected element is indeed aas:assetAdministrationShells, leaving the user wondering about what could possibly be wrong. The only difference is the namespace, which isn't part of the error message, because it gets replaced by the prefix. To improve the error messages in this case, a check that compares the namespaces declared on the document against the ones required by the deserialization, and errors if a required namespace isn't declared. Partially fix https://github.com/eclipse-basyx/basyx-python-sdk/issues/190 --- basyx/aas/adapter/xml/xml_deserialization.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 7ae5ff7..c479acb 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -50,11 +50,12 @@ import enum from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar -from .._generic import XML_NS_AAS, MODELLING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_TYPES_INVERSE, \ +from .._generic import XML_NS_MAP, XML_NS_AAS, 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 NS_AAS = XML_NS_AAS +REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]} logger = logging.getLogger(__name__) @@ -1197,13 +1198,22 @@ def _parse_xml_document(file: IO, failsafe: bool = True, **parser_kwargs: Any) - parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, **parser_kwargs) try: - return etree.parse(file, parser).getroot() + root = etree.parse(file, parser).getroot() except etree.XMLSyntaxError as e: if failsafe: logger.error(e) return None raise e + missing_namespaces: Set[str] = REQUIRED_NAMESPACES - set(root.nsmap.values()) + if missing_namespaces: + error_message = f"The following required namespaces are not declared: {' | '.join(missing_namespaces)}" \ + + " - Is the input document of an older version?" + if not failsafe: + raise KeyError(error_message) + logger.error(error_message) + return root + def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFromXmlDecoder]]) \ -> Type[AASFromXmlDecoder]: From b7120f1bb2d11fb88f2e4c1c63a654540a629408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 19 Dec 2023 13:37:39 +0100 Subject: [PATCH 134/474] adapter.xml: improve element formatting in error messages Previously, the namespace of an element would always be replaced by its prefix if the prefix is known. However, this turned out to mask errors in case the namespace is different from the one used by our SDK. Thus, the function `_element_pretty_identifier()` is adjusted such that it only replaces the namespace if it matches one of the namespaces known to our SDK. Partially fix https://github.com/eclipse-basyx/basyx-python-sdk/issues/190 See also: 79a86352bb52b1528a9bf9c860331915b4db7556 --- basyx/aas/adapter/xml/xml_deserialization.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index c479acb..b342d07 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -99,7 +99,7 @@ def _element_pretty_identifier(element: etree.Element) -> str: Returns a pretty element identifier for a given XML element. If the prefix is known, the namespace in the element tag is replaced by the prefix. - If additionally also the sourceline is known, is is added as a suffix to name. + If additionally also the sourceline is known, it is added as a suffix to name. For example, instead of "{https://admin-shell.io/aas/3/0}assetAdministrationShell" this function would return "aas:assetAdministrationShell on line $line", if both, prefix and sourceline, are known. @@ -108,7 +108,11 @@ def _element_pretty_identifier(element: etree.Element) -> str: """ identifier = element.tag if element.prefix is not None: - identifier = element.prefix + ":" + element.tag.split("}")[1] + # Only replace the namespace by the prefix if it matches our known namespaces, + # so the replacement by the prefix doesn't mask errors such as incorrect namespaces. + namespace, tag = element.tag.split("}", 1) + if namespace[1:] in XML_NS_MAP.values(): + identifier = element.prefix + ":" + tag if element.sourceline is not None: identifier += f" on line {element.sourceline}" return identifier From 717d89645e6bfe6c04869edb543e04c471784635 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Wed, 20 Dec 2023 18:59:14 +0100 Subject: [PATCH 135/474] fixed half the broken doc links --- basyx/aas/adapter/aasx.py | 60 ++++++------- .../aas/adapter/json/json_deserialization.py | 10 +-- basyx/aas/adapter/json/json_serialization.py | 2 +- basyx/aas/adapter/xml/xml_deserialization.py | 12 +-- basyx/aas/adapter/xml/xml_serialization.py | 90 +++++++++---------- 5 files changed, 87 insertions(+), 87 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 825312e..c8dd7b8 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -119,10 +119,10 @@ def read_into(self, object_store: model.AbstractObjectStore, This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - `object_store`. While doing so, it searches all parsed :class:`Submodels ` for - :class:`~aas.model.submodel.File` objects to extract the supplementary + `object_store`. While doing so, it searches all parsed :class:`Submodels ` for + :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced supplementary files are added to the given `file_store` and the - :class:`~aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file + :class:`~basyx.aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file to allow for robust resolution the file within the `file_store` later. @@ -131,10 +131,10 @@ def read_into(self, object_store: model.AbstractObjectStore, :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the embedded supplementary files to :param override_existing: If `True`, existing objects in the object store are overridden with objects from the - AASX that have the same :class:`~aas.model.base.Identifier`. Default behavior is to skip those objects from + AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects from the AASX. - :return: A set of the :class:`Identifiers ` of all - :class:`~aas.model.base.Identifiable` objects parsed from the AASX file + :return: A set of the :class:`Identifiers ` of all + :class:`~basyx.aas.model.base.Identifiable` objects parsed from the AASX file """ # Find AASX-Origin part core_rels = self.reader.get_related_parts_by_type() @@ -321,18 +321,18 @@ def write_aas(self, write_json: bool = False) -> None: """ Convenience method to write one or more - :class:`AssetAdministrationShells ` with all included and referenced + :class:`AssetAdministrationShells ` with all included and referenced objects to the AASX package according to the part name conventions from DotAAS. - This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve the + This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve the AASs from the given object_store. - :class:`References ` to :class:`Submodels ` and - :class:`ConceptDescriptions ` (via semanticId attributes) are also + :class:`References ` to :class:`Submodels ` and + :class:`ConceptDescriptions ` (via semanticId attributes) are also resolved using the `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` or `/aasx/data.json` in the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". - Supplementary files which are referenced by a :class:`~aas.model.submodel.File` object in any of the - :class:`Submodels ` are also added to the AASX + Supplementary files which are referenced by a :class:`~basyx.aas.model.submodel.File` object in any of the + :class:`Submodels ` are also added to the AASX package. This method uses `write_all_aas_objects()` to write the AASX part. @@ -346,21 +346,21 @@ def write_aas(self, To write multiple Asset Administration Shells to a single AASX package file, call this method once, passing a list of AAS Identifiers to the `aas_ids` parameter. - :param aas_ids: :class:`~aas.model.base.Identifier` or Iterable of - :class:`Identifiers ` of the AAS(s) to be written to the AASX file + :param aas_ids: :class:`~basyx.aas.model.base.Identifier` or Iterable of + :class:`Identifiers ` of the AAS(s) to be written to the AASX file :param object_store: :class:`ObjectStore ` to retrieve the - :class:`~aas.model.base.Identifiable` AAS objects (:class:`~aas.model.aas.AssetAdministrationShell`, - :class:`~aas.model.concept.ConceptDescription` and :class:`~aas.model.submodel.Submodel`) from + :class:`~basyx.aas.model.base.Identifiable` AAS objects (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, + :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.SubmodelElement`) from :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve - supplementary files from, which are referenced by :class:`~aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~aas.model.submodel.Submodel` + supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects + :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.SubmodelElement` in the AASX package file instead of XML parts. Defaults to `False`. :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable - :class:`Submodels ` and - :class:`ConceptDescriptions ` are skipped, logging a warning/info + :class:`Submodels ` and + :class:`ConceptDescriptions ` are skipped, logging a warning/info message) :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another - :class:`~aas.model.base.Identifiable` object) + :class:`~basyx.aas.model.base.Identifiable` object) """ if isinstance(aas_ids, model.Identifier): aas_ids = (aas_ids,) @@ -426,10 +426,10 @@ def write_aas_objects(self, """ A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - This method takes the AAS's :class:`~aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given + This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given object_store. If the list of written objects includes :class:`aas.model.submodel.Submodel` objects, Supplementary files which are - referenced by :class:`~aas.model.submodel.File` objects within + referenced by :class:`~basyx.aas.model.submodel.File` objects within those submodels, are also added to the AASX package. .. attention:: @@ -440,12 +440,12 @@ def write_aas_objects(self, :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. '.json' if `write_json` else '.xml'). - :param object_ids: A list of :class:`Identifiers ` of the objects to be written to - the AASX package. Only these :class:`~aas.model.base.Identifiable` objects (and included - :class:`~aas.model.base.Referable` objects) are written to the package. - :param object_store: The objects store to retrieve the :class:`~aas.model.base.Identifiable` objects from + :param object_ids: A list of :class:`Identifiers ` of the objects to be written to + the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included + :class:`~basyx.aas.model.base.Referable` objects) are written to the package. + :param object_store: The objects store to retrieve the :class:`~basyx.aas.model.base.Identifiable` objects from :param file_store: The :class:`SupplementaryFileContainer ` - to retrieve supplementary files from (if there are any :class:`~aas.model.submodel.File` + to retrieve supplementary files from (if there are any :class:`~basyx.aas.model.submodel.File` objects within the written objects. :param write_json: If `True`, the part is written as a JSON file instead of an XML file. Defaults to `False`. :param split_part: If `True`, no aas-spec relationship is added from the aasx-origin to this part. You must make @@ -484,7 +484,7 @@ def write_all_aas_objects(self, This method takes an :class:`ObjectStore ` and writes all contained objects into an "aas_env" part in the AASX package. If the ObjectStore includes :class:`~aas.model.submodel.Submodel` objects, supplementary files which are - referenced by :class:`~aas.model.submodel.File` objects + referenced by :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the `file_store` and added to the AASX package. .. attention:: @@ -710,7 +710,7 @@ class AbstractSupplementaryFileContainer(metaclass=abc.ABCMeta): Supplementary files may be PDF files or other binary or textual files, referenced in a File object of an AAS by their name. They are used to provide associated documents without embedding their contents (as - :class:`~aas.model.submodel.Blob` object) in the AAS. + :class:`~basyx.aas.model.submodel.Blob` object) in the AAS. A SupplementaryFileContainer keeps track of the name and content_type (MIME type) for each file. Additionally it allows to resolve name conflicts by comparing the files' contents and providing an alternative name for a dissimilar diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 6524f4b..436a6e5 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -21,7 +21,7 @@ complete AAS JSON file, reads its contents and stores the objects in the provided :class:`~aas.model.provider.AbstractObjectStore`. :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file` is a wrapper for this function. Instead of storing the objects in a given :class:`~aas.model.provider.AbstractObjectStore`, -it returns a :class:`~aas.model.provider.DictObjectStore` containing parsed objects. +it returns a :class:`~basyx.aas.model.provider.DictObjectStore` containing parsed objects. The deserialization is performed in a bottom-up approach: The `object_hook()` method gets called for every parsed JSON object (as dict) and checks for existence of the `modelType` attribute. If it is present, the `AAS_CLASS_PARSERS` dict @@ -367,7 +367,7 @@ def _construct_administrative_information( def _construct_operation_variable(cls, dct: Dict[str, object]) -> model.SubmodelElement: """ Since we don't implement `OperationVariable`, this constructor discards the wrapping `OperationVariable` object - and just returns the contained :class:`~aas.model.submodel.SubmodelElement`. + and just returns the contained :class:`~basyx.aas.model.submodel.SubmodelElement`. """ # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 @@ -814,7 +814,7 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r 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 - :return: A set of :class:`Identifiers ` that were added to object_store + :return: A set of :class:`Identifiers ` that were added to object_store """ ret: Set[model.Identifier] = set() decoder_ = _select_decoder(failsafe, stripped, decoder) @@ -867,12 +867,12 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r def read_aas_json_file(file: IO, **kwargs) -> model.DictObjectStore[model.Identifiable]: """ A wrapper of :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects in an - empty :class:`~aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as + empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into`. :param file: A filename or file-like object to read the JSON-serialized data from :param kwargs: Keyword arguments passed to :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into` - :return: A :class:`~aas.model.provider.DictObjectStore` containing all AAS objects from the JSON file + :return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the JSON file """ object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() read_aas_json_file_into(object_store, file, **kwargs) diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 14c3932..d3b6b65 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -580,7 +580,7 @@ def _operation_variable_to_json(cls, obj: model.SubmodelElement) -> Dict[str, ob """ serialization of an object from class SubmodelElement to a json OperationVariable representation Since we don't implement the `OperationVariable` class, which is just a wrapper for a single - :class:`~aas.model.submodel.SubmodelElement`, elements are serialized as the `value` attribute of an + :class:`~basyx.aas.model.submodel.SubmodelElement`, elements are serialized as the `value` attribute of an `operationVariable` object. :param obj: object of class `SubmodelElement` diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index b342d07..3965840 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -17,7 +17,7 @@ - :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` constructs all elements of an XML document and stores them in a given :class:`ObjectStore ` - :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file` constructs all elements of an XML document and returns - them in a :class:`~aas.model.provider.DictObjectStore` + them in a :class:`~basyx.aas.model.provider.DictObjectStore` These functions take a decoder class as keyword argument, which allows parsing in failsafe (default) or non-failsafe mode. Parsing stripped elements - used in the HTTP adapter - is also possible. It is also possible to subclass the @@ -544,7 +544,7 @@ def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ def _construct_operation_variable(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement: """ Since we don't implement `OperationVariable`, this constructor discards the wrapping `aas:operationVariable` - and `aas:value` and just returns the contained :class:`~aas.model.submodel.SubmodelElement`. + and `aas:value` and just returns the contained :class:`~basyx.aas.model.submodel.SubmodelElement`. """ value = _get_child_mandatory(element, NS_AAS + "value") if len(value) == 0: @@ -1408,7 +1408,7 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif into a given :class:`ObjectStore `. :param object_store: The :class:`ObjectStore ` in which the - :class:`~aas.model.base.Identifiable` objects should be stored + :class:`~basyx.aas.model.base.Identifiable` objects should be stored :param file: A filename or file-like object to read the XML-serialized data from :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. @@ -1421,7 +1421,7 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif This parameter is ignored if a decoder class is specified. :param decoder: The decoder class used to decode the XML elements :param parser_kwargs: Keyword arguments passed to the XMLParser constructor - :return: A set of :class:`Identifiers ` that were added to object_store + :return: A set of :class:`Identifiers ` that were added to object_store """ ret: Set[model.Identifier] = set() @@ -1475,12 +1475,12 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif def read_aas_xml_file(file: IO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]: """ A wrapper of :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an - empty :class:`~aas.model.provider.DictObjectStore`. This function supports + empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`. :param file: A filename or file-like object to read the XML-serialized data from :param kwargs: Keyword arguments passed to :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` - :return: A :class:`~aas.model.provider.DictObjectStore` containing all AAS objects from the XML file + :return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the XML file """ object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() read_aas_xml_file_into(object_store, file, **kwargs) diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 106535a..656d65e 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -181,9 +181,9 @@ def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: def administrative_information_to_xml(obj: model.AdministrativeInformation, tag: str = NS_AAS+"administration") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.AdministrativeInformation` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.AdministrativeInformation` to XML - :param obj: Object of class :class:`~aas.model.base.AdministrativeInformation` + :param obj: Object of class :class:`~basyx.aas.model.base.AdministrativeInformation` :param tag: Namespace+Tag of the serialized element. Default is "aas:administration" :return: Serialized ElementTree object """ @@ -201,9 +201,9 @@ def administrative_information_to_xml(obj: model.AdministrativeInformation, def data_element_to_xml(obj: model.DataElement) -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.DataElement` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.DataElement` to XML - :param obj: Object of class :class:`~aas.model.submodel.DataElement` + :param obj: Object of class :class:`~basyx.aas.model.submodel.DataElement` :return: Serialized ElementTree element """ if isinstance(obj, model.MultiLanguageProperty): @@ -222,9 +222,9 @@ def data_element_to_xml(obj: model.DataElement) -> etree.Element: def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.Reference` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.Reference` to XML - :param obj: Object of class :class:`~aas.model.base.Reference` + :param obj: Object of class :class:`~basyx.aas.model.base.Reference` :param tag: Namespace+Tag of the returned element. Default is "aas:reference" :return: Serialized ElementTree """ @@ -331,9 +331,9 @@ def value_list_to_xml(obj: model.ValueList, def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "specifidAssetId") \ -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.SpecificAssetId` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.SpecificAssetId` to XML - :param obj: Object of class :class:`~aas.model.base.SpecificAssetId` + :param obj: Object of class :class:`~basyx.aas.model.base.SpecificAssetId` :param tag: Namespace+Tag of the ElementTree object. Default is "aas:identifierKeyValuePair" :return: Serialized ElementTree object """ @@ -374,9 +374,9 @@ def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"ass def concept_description_to_xml(obj: model.ConceptDescription, tag: str = NS_AAS+"conceptDescription") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.concept.ConceptDescription` to XML + Serialization of objects of class :class:`~basyx.aas.model.concept.ConceptDescription` to XML - :param obj: Object of class :class:`~aas.model.concept.ConceptDescription` + :param obj: Object of class :class:`~basyx.aas.model.concept.ConceptDescription` :param tag: Namespace+Tag of the ElementTree object. Default is "aas:conceptDescription" :return: Serialized ElementTree object """ @@ -468,9 +468,9 @@ def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, tag: str = NS_AAS+"assetAdministrationShell") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.aas.AssetAdministrationShell` to XML + Serialization of objects of class :class:`~basyx.aas.model.aas.AssetAdministrationShell` to XML - :param obj: Object of class :class:`~aas.model.aas.AssetAdministrationShell` + :param obj: Object of class :class:`~basyx.aas.model.aas.AssetAdministrationShell` :param tag: Namespace+Tag of the ElementTree object. Default is "aas:assetAdministrationShell" :return: Serialized ElementTree object """ @@ -493,9 +493,9 @@ def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.SubmodelElement` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElement` to XML - :param obj: Object of class :class:`~aas.model.submodel.SubmodelElement` + :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` :return: Serialized ElementTree object """ if isinstance(obj, model.DataElement): @@ -521,9 +521,9 @@ def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: def submodel_to_xml(obj: model.Submodel, tag: str = NS_AAS+"submodel") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.Submodel` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElement` to XML - :param obj: Object of class :class:`~aas.model.submodel.Submodel` + :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:submodel" :return: Serialized ElementTree object """ @@ -539,9 +539,9 @@ def submodel_to_xml(obj: model.Submodel, def property_to_xml(obj: model.Property, tag: str = NS_AAS+"property") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.Property` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.Property` to XML - :param obj: Object of class :class:`~aas.model.submodel.Property` + :param obj: Object of class :class:`~basyx.aas.model.submodel.Property` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:property" :return: Serialized ElementTree object """ @@ -557,9 +557,9 @@ def property_to_xml(obj: model.Property, def multi_language_property_to_xml(obj: model.MultiLanguageProperty, tag: str = NS_AAS+"multiLanguageProperty") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.MultiLanguageProperty` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.MultiLanguageProperty` to XML - :param obj: Object of class :class:`~aas.model.submodel.MultiLanguageProperty` + :param obj: Object of class :class:`~basyx.aas.model.submodel.MultiLanguageProperty` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:multiLanguageProperty" :return: Serialized ElementTree object """ @@ -574,9 +574,9 @@ def multi_language_property_to_xml(obj: model.MultiLanguageProperty, def range_to_xml(obj: model.Range, tag: str = NS_AAS+"range") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.Range` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.Range` to XML - :param obj: Object of class :class:`~aas.model.submodel.Range` + :param obj: Object of class :class:`~basyx.aas.model.submodel.Range` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:range" :return: Serialized ElementTree object """ @@ -593,9 +593,9 @@ def range_to_xml(obj: model.Range, def blob_to_xml(obj: model.Blob, tag: str = NS_AAS+"blob") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.Blob` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.Blob` to XML - :param obj: Object of class :class:`~aas.model.submodel.Blob` + :param obj: Object of class :class:`~basyx.aas.model.submodel.Blob` :param tag: Namespace+Tag of the serialized element. Default is "blob" :return: Serialized ElementTree object """ @@ -611,9 +611,9 @@ def blob_to_xml(obj: model.Blob, def file_to_xml(obj: model.File, tag: str = NS_AAS+"file") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.File` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.File` to XML - :param obj: Object of class :class:`~aas.model.submodel.File` + :param obj: Object of class :class:`~basyx.aas.model.submodel.File` :param tag: Namespace+Tag of the serialized element. Default is "aas:file" :return: Serialized ElementTree object """ @@ -643,9 +643,9 @@ def resource_to_xml(obj: model.Resource, def reference_element_to_xml(obj: model.ReferenceElement, tag: str = NS_AAS+"referenceElement") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.ReferenceElement` to XMl + Serialization of objects of class :class:`~basyx.aas.model.submodel.ReferenceElement` to XMl - :param obj: Object of class :class:`~aas.model.submodel.ReferenceElement` + :param obj: Object of class :class:`~basyx.aas.model.submodel.ReferenceElement` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:referenceElement" :return: Serialized ElementTree object """ @@ -658,11 +658,11 @@ def reference_element_to_xml(obj: model.ReferenceElement, def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, tag: str = NS_AAS+"submodelElementCollection") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.SubmodelElementCollection` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElementCollection` to XML Note that we do not have parameter "allowDuplicates" in out implementation - :param obj: Object of class :class:`~aas.model.submodel.SubmodelElementCollection` + :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElementCollection` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:submodelElementCollection" :return: Serialized ElementTree object """ @@ -698,9 +698,9 @@ def submodel_element_list_to_xml(obj: model.SubmodelElementList, def relationship_element_to_xml(obj: model.RelationshipElement, tag: str = NS_AAS+"relationshipElement") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.RelationshipElement` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.RelationshipElement` to XML - :param obj: Object of class :class:`~aas.model.submodel.RelationshipElement` + :param obj: Object of class :class:`~basyx.aas.model.submodel.RelationshipElement` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:relationshipElement" :return: Serialized ELementTree object """ @@ -713,9 +713,9 @@ def relationship_element_to_xml(obj: model.RelationshipElement, def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElement, tag: str = NS_AAS+"annotatedRelationshipElement") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.AnnotatedRelationshipElement` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement` to XML - :param obj: Object of class :class:`~aas.model.submodel.AnnotatedRelationshipElement` + :param obj: Object of class :class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement` :param tag: Namespace+Tag of the serialized element (optional): Default is "aas:annotatedRelationshipElement" :return: Serialized ElementTree object """ @@ -730,12 +730,12 @@ def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElemen def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"operationVariable") -> etree.Element: """ - Serialization of :class:`~aas.model.submodel.SubmodelElement` to the XML OperationVariable representation + Serialization of :class:`~basyx.aas.model.submodel.SubmodelElement` to the XML OperationVariable representation Since we don't implement the `OperationVariable` class, which is just a wrapper for a single - :class:`~aas.model.submodel.SubmodelElement`, elements are serialized as the `aas:value` child of an + :class:`~basyx.aas.model.submodel.SubmodelElement`, elements are serialized as the `aas:value` child of an `aas:operationVariable` element. - :param obj: Object of class :class:`~aas.model.submodel.SubmodelElement` + :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operationVariable" :return: Serialized ElementTree object """ @@ -749,9 +749,9 @@ def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"ope def operation_to_xml(obj: model.Operation, tag: str = NS_AAS+"operation") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.Operation` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.Operation` to XML - :param obj: Object of class :class:`~aas.model.submodel.Operation` + :param obj: Object of class :class:`~basyx.aas.model.submodel.Operation` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operation" :return: Serialized ElementTree object """ @@ -770,9 +770,9 @@ def operation_to_xml(obj: model.Operation, def capability_to_xml(obj: model.Capability, tag: str = NS_AAS+"capability") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.Capability` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.Capability` to XML - :param obj: Object of class :class:`~aas.model.submodel.Capability` + :param obj: Object of class :class:`~basyx.aas.model.submodel.Capability` :param tag: Namespace+Tag of the serialized element, default is "aas:capability" :return: Serialized ElementTree object """ @@ -782,9 +782,9 @@ def capability_to_xml(obj: model.Capability, def entity_to_xml(obj: model.Entity, tag: str = NS_AAS+"entity") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.Entity` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.Entity` to XML - :param obj: Object of class :class:`~aas.model.submodel.Entity` + :param obj: Object of class :class:`~basyx.aas.model.submodel.Entity` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:entity" :return: Serialized ElementTree object """ @@ -807,9 +807,9 @@ def entity_to_xml(obj: model.Entity, def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+"basicEventElement") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.submodel.BasicEventElement` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.BasicEventElement` to XML - :param obj: Object of class :class:`~aas.model.submodel.BasicEventElement` + :param obj: Object of class :class:`~basyx.aas.model.submodel.BasicEventElement` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:basicEventElement" :return: Serialized ElementTree object """ From 7ee1d72fc20d83ebf1ce0be26c6fe85edf5d57bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 22 Dec 2023 04:35:11 +0100 Subject: [PATCH 136/474] fix incorrect `Submodel` -> `SubmodelElement` replacements See commit 6699deed1ea5b3555483460d4c62931cece4583b. --- basyx/aas/adapter/aasx.py | 12 ++++++------ basyx/aas/adapter/xml/xml_serialization.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index c8dd7b8..f7d6cda 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -119,7 +119,7 @@ def read_into(self, object_store: model.AbstractObjectStore, This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - `object_store`. While doing so, it searches all parsed :class:`Submodels ` for + `object_store`. While doing so, it searches all parsed :class:`Submodels ` for :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced supplementary files are added to the given `file_store` and the :class:`~basyx.aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file @@ -326,13 +326,13 @@ def write_aas(self, This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve the AASs from the given object_store. - :class:`References ` to :class:`Submodels ` and + :class:`References ` to :class:`Submodels ` and :class:`ConceptDescriptions ` (via semanticId attributes) are also resolved using the `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` or `/aasx/data.json` in the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". Supplementary files which are referenced by a :class:`~basyx.aas.model.submodel.File` object in any of the - :class:`Submodels ` are also added to the AASX + :class:`Submodels ` are also added to the AASX package. This method uses `write_all_aas_objects()` to write the AASX part. @@ -350,13 +350,13 @@ def write_aas(self, :class:`Identifiers ` of the AAS(s) to be written to the AASX file :param object_store: :class:`ObjectStore ` to retrieve the :class:`~basyx.aas.model.base.Identifiable` AAS objects (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, - :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.SubmodelElement`) from + :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.Submodel`) from :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.SubmodelElement` + :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts. Defaults to `False`. :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable - :class:`Submodels ` and + :class:`Submodels ` and :class:`ConceptDescriptions ` are skipped, logging a warning/info message) :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 656d65e..0376b62 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -521,9 +521,9 @@ def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: def submodel_to_xml(obj: model.Submodel, tag: str = NS_AAS+"submodel") -> etree.Element: """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElement` to XML + Serialization of objects of class :class:`~basyx.aas.model.submodel.Submodel` to XML - :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` + :param obj: Object of class :class:`~basyx.aas.model.submodel.Submodel` :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:submodel" :return: Serialized ElementTree object """ From 3476cf63bb0591d3e6ad586903a4eb0218b8b3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 22 Dec 2023 04:57:00 +0100 Subject: [PATCH 137/474] fix pycodestyle warnings --- basyx/aas/adapter/aasx.py | 63 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index f7d6cda..4976a60 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -119,20 +119,19 @@ def read_into(self, object_store: model.AbstractObjectStore, This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - `object_store`. While doing so, it searches all parsed :class:`Submodels ` for - :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary - files. The referenced supplementary files are added to the given `file_store` and the - :class:`~basyx.aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file - to allow for robust resolution the file within the - `file_store` later. + `object_store`. While doing so, it searches all parsed :class:`Submodels ` + for :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced + supplementary files are added to the given `file_store` and the :class:`~basyx.aas.model.submodel.File` + objects' values are updated with the absolute name of the supplementary file to allow for robust resolution the + file within the `file_store` later. :param object_store: An :class:`ObjectStore ` to add the AAS objects from the AASX file to :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the embedded supplementary files to :param override_existing: If `True`, existing objects in the object store are overridden with objects from the - AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects from - the AASX. + AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects + from the AASX. :return: A set of the :class:`Identifiers ` of all :class:`~basyx.aas.model.base.Identifiable` objects parsed from the AASX file """ @@ -321,19 +320,18 @@ def write_aas(self, write_json: bool = False) -> None: """ Convenience method to write one or more - :class:`AssetAdministrationShells ` with all included and referenced - objects to the AASX package according to the part name conventions from DotAAS. - - This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve the - AASs from the given object_store. - :class:`References ` to :class:`Submodels ` and - :class:`ConceptDescriptions ` (via semanticId attributes) are also - resolved using the - `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` or `/aasx/data.json` in - the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". - Supplementary files which are referenced by a :class:`~basyx.aas.model.submodel.File` object in any of the - :class:`Submodels ` are also added to the AASX - package. + :class:`AssetAdministrationShells ` with all included + and referenced objects to the AASX package according to the part name conventions from DotAAS. + + This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve + the AASs from the given object_store. + :class:`References ` to :class:`Submodels ` + and :class:`ConceptDescriptions ` (via semanticId attributes) are + also resolved using the `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` + or `/aasx/data.json` in the AASX package, compliant to the convention presented in + "Details of the Asset Administration Shell". Supplementary files which are referenced by a + :class:`~basyx.aas.model.submodel.File` object in any of the + :class:`Submodels ` are also added to the AASX package. This method uses `write_all_aas_objects()` to write the AASX part. @@ -349,16 +347,18 @@ def write_aas(self, :param aas_ids: :class:`~basyx.aas.model.base.Identifier` or Iterable of :class:`Identifiers ` of the AAS(s) to be written to the AASX file :param object_store: :class:`ObjectStore ` to retrieve the - :class:`~basyx.aas.model.base.Identifiable` AAS objects (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, + :class:`~basyx.aas.model.base.Identifiable` AAS objects + (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.Submodel`) from :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.Submodel` - in the AASX package file instead of XML parts. Defaults to `False`. + :param write_json: If `True`, JSON parts are created for the AAS and each + :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts. + Defaults to `False`. :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable :class:`Submodels ` and - :class:`ConceptDescriptions ` are skipped, logging a warning/info - message) + :class:`ConceptDescriptions ` are skipped, logging a + warning/info message) :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another :class:`~basyx.aas.model.base.Identifiable` object) """ @@ -426,10 +426,9 @@ def write_aas_objects(self, """ A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as `aas_id`) to retrieve it from the given - object_store. If the list - of written objects includes :class:`aas.model.submodel.Submodel` objects, Supplementary files which are - referenced by :class:`~basyx.aas.model.submodel.File` objects within + This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as `aas_id`) to retrieve it + from the given object_store. If the list of written objects includes :class:`aas.model.submodel.Submodel` + objects, Supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within those submodels, are also added to the AASX package. .. attention:: @@ -440,8 +439,8 @@ def write_aas_objects(self, :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. '.json' if `write_json` else '.xml'). - :param object_ids: A list of :class:`Identifiers ` of the objects to be written to - the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included + :param object_ids: A list of :class:`Identifiers ` of the objects to be written + to the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included :class:`~basyx.aas.model.base.Referable` objects) are written to the package. :param object_store: The objects store to retrieve the :class:`~basyx.aas.model.base.Identifiable` objects from :param file_store: The :class:`SupplementaryFileContainer ` From 875cb7020c6a7c45931c6a5ddf9b52d6ab872f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 23 Dec 2023 01:28:28 +0100 Subject: [PATCH 138/474] massive docstring overhaul This fixes missing references, improves the layout, removes outdated information in some places, and more. --- basyx/aas/adapter/aasx.py | 100 ++++---- basyx/aas/adapter/json/__init__.py | 17 +- .../aas/adapter/json/json_deserialization.py | 90 ++++---- basyx/aas/adapter/json/json_serialization.py | 43 ++-- basyx/aas/adapter/xml/__init__.py | 4 +- basyx/aas/adapter/xml/xml_deserialization.py | 41 ++-- basyx/aas/adapter/xml/xml_serialization.py | 216 +++++++++--------- 7 files changed, 258 insertions(+), 253 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 4976a60..fe7e173 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -10,13 +10,13 @@ Functionality for reading and writing AASX files according to "Details of the Asset Administration Shell Part 1 V2.0", section 7. -The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the `pyecma376_2` library +The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the ``pyecma376_2`` library for low level OPC reading and writing. It currently supports all required features except for embedded digital signatures. Writing and reading of AASX packages is performed through the :class:`~.AASXReader` and :class:`~.AASXWriter` classes. Each instance of these classes wraps an existing AASX file resp. a file to be created and allows to read/write the -included AAS objects into/form :class:`ObjectStores `. +included AAS objects into/form :class:`ObjectStores `. For handling of embedded supplementary files, this module provides the :class:`~.AbstractSupplementaryFileContainer` class interface and the :class:`~.DictSupplementaryFileContainer` implementation. @@ -64,7 +64,7 @@ def __init__(self, file: Union[os.PathLike, str, IO]): """ Open an AASX reader for the given filename or file handle - The given file is opened as OPC ZIP package. Make sure to call `AASXReader.close()` after reading the file + The given file is opened as OPC ZIP package. Make sure to call ``AASXReader.close()`` after reading the file contents to close the underlying ZIP file reader. You may also use the AASXReader as a context manager to ensure closing under any circumstances. @@ -92,7 +92,7 @@ def get_thumbnail(self) -> Optional[bytes]: Retrieve the packages thumbnail image The thumbnail image file is read into memory and returned as bytes object. You may use some python image library - for further processing or conversion, e.g. `pillow`: + for further processing or conversion, e.g. ``pillow``: .. code-block:: python @@ -115,21 +115,21 @@ def read_into(self, object_store: model.AbstractObjectStore, override_existing: bool = False) -> Set[model.Identifier]: """ Read the contents of the AASX package and add them into a given - :class:`ObjectStore ` + :class:`ObjectStore ` This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - `object_store`. While doing so, it searches all parsed :class:`Submodels ` + ``object_store``. While doing so, it searches all parsed :class:`Submodels ` for :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced - supplementary files are added to the given `file_store` and the :class:`~basyx.aas.model.submodel.File` + supplementary files are added to the given ``file_store`` and the :class:`~basyx.aas.model.submodel.File` objects' values are updated with the absolute name of the supplementary file to allow for robust resolution the - file within the `file_store` later. + file within the ``file_store`` later. - :param object_store: An :class:`ObjectStore ` to add the AAS objects - from the AASX file to + :param object_store: An :class:`ObjectStore ` to add the AAS + objects from the AASX file to :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the embedded supplementary files to - :param override_existing: If `True`, existing objects in the object store are overridden with objects from the + :param override_existing: If ``True``, existing objects in the object store are overridden with objects from the AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects from the AASX. :return: A set of the :class:`Identifiers ` of all @@ -176,8 +176,8 @@ def _read_aas_part_into(self, part_name: str, """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. - This method primarily checks for duplicate objects. It uses `_parse_aas_parse()` to do the actual parsing and - `_collect_supplementary_files()` for supplementary file processing of non-duplicate objects. + This method primarily checks for duplicate objects. It uses ``_parse_aas_parse()`` to do the actual parsing and + ``_collect_supplementary_files()`` for supplementary file processing of non-duplicate objects. :param part_name: The OPC part name to read :param object_store: An ObjectStore to add the AAS objects from the AASX file to @@ -290,7 +290,7 @@ def __init__(self, file: Union[os.PathLike, str, IO]): """ Create a new AASX package in the given file and open the AASXWriter to add contents to the package. - Make sure to call `AASXWriter.close()` after writing all contents to write the aas-spec relationships for all + Make sure to call ``AASXWriter.close()`` after writing all contents to write the aas-spec relationships for all AAS parts to the file and close the underlying ZIP file writer. You may also use the AASXWriter as a context manager to ensure closing under any circumstances. @@ -323,38 +323,38 @@ def write_aas(self, :class:`AssetAdministrationShells ` with all included and referenced objects to the AASX package according to the part name conventions from DotAAS. - This method takes the AASs' :class:`Identifiers ` (as `aas_ids`) to retrieve - the AASs from the given object_store. + This method takes the AASs' :class:`Identifiers ` (as ``aas_ids``) to retrieve + the AASs from the given ``object_store``. :class:`References ` to :class:`Submodels ` and :class:`ConceptDescriptions ` (via semanticId attributes) are - also resolved using the `object_store`. All of these objects are written to an aas-spec part `/aasx/data.xml` - or `/aasx/data.json` in the AASX package, compliant to the convention presented in + also resolved using the ``object_store``. All of these objects are written to an aas-spec part + ``/aasx/data.xml`` or ``/aasx/data.json`` in the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". Supplementary files which are referenced by a :class:`~basyx.aas.model.submodel.File` object in any of the :class:`Submodels ` are also added to the AASX package. - This method uses `write_all_aas_objects()` to write the AASX part. + This method uses :meth:`write_all_aas_objects` to write the AASX part. .. attention:: - This method **must only be used once** on a single AASX package. Otherwise, the `/aasx/data.json` - (or `...xml`) part would be written twice to the package, hiding the first part and possibly causing + This method **must only be used once** on a single AASX package. Otherwise, the ``/aasx/data.json`` + (or ``...xml``) part would be written twice to the package, hiding the first part and possibly causing problems when reading the package. To write multiple Asset Administration Shells to a single AASX package file, call this method once, passing - a list of AAS Identifiers to the `aas_ids` parameter. + a list of AAS Identifiers to the ``aas_ids`` parameter. :param aas_ids: :class:`~basyx.aas.model.base.Identifier` or Iterable of :class:`Identifiers ` of the AAS(s) to be written to the AASX file - :param object_store: :class:`ObjectStore ` to retrieve the + :param object_store: :class:`ObjectStore ` to retrieve the :class:`~basyx.aas.model.base.Identifiable` AAS objects (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.Submodel`) from - :param file_store: :class:`SupplementaryFileContainer <~.AbstractSupplementaryFileContainer>` to retrieve + :param file_store: :class:`SupplementaryFileContainer ` to retrieve supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects - :param write_json: If `True`, JSON parts are created for the AAS and each + :param write_json: If ``True``, JSON parts are created for the AAS and each :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts. - Defaults to `False`. + Defaults to ``False``. :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable :class:`Submodels ` and :class:`ConceptDescriptions ` are skipped, logging a @@ -426,29 +426,31 @@ def write_aas_objects(self, """ A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as `aas_id`) to retrieve it - from the given object_store. If the list of written objects includes :class:`aas.model.submodel.Submodel` + This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as ``aas_id``) to retrieve it + from the given object_store. If the list of written objects includes :class:`~basyx.aas.model.submodel.Submodel` objects, Supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within those submodels, are also added to the AASX package. .. attention:: - You must make sure to call this method or `write_all_aas_objects` only once per unique `part_name` on a - single package instance. + You must make sure to call this method or :meth:`write_all_aas_objects` only once per unique ``part_name`` + on a single package instance. :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. - '.json' if `write_json` else '.xml'). + '.json' if ``write_json`` else '.xml'). :param object_ids: A list of :class:`Identifiers ` of the objects to be written to the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included :class:`~basyx.aas.model.base.Referable` objects) are written to the package. :param object_store: The objects store to retrieve the :class:`~basyx.aas.model.base.Identifiable` objects from - :param file_store: The :class:`SupplementaryFileContainer ` + :param file_store: The + :class:`SupplementaryFileContainer ` to retrieve supplementary files from (if there are any :class:`~basyx.aas.model.submodel.File` objects within the written objects. - :param write_json: If `True`, the part is written as a JSON file instead of an XML file. Defaults to `False`. - :param split_part: If `True`, no aas-spec relationship is added from the aasx-origin to this part. You must make - sure to reference it via a aas-spec-split relationship from another aas-spec part + :param write_json: If ``True``, the part is written as a JSON file instead of an XML file. Defaults to + ``False``. + :param split_part: If ``True``, no aas-spec relationship is added from the aasx-origin to this part. You must + make sure to reference it via a aas-spec-split relationship from another aas-spec part :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object part to be written, in addition to the aas-suppl relationships which are created automatically. """ @@ -477,26 +479,26 @@ def write_all_aas_objects(self, split_part: bool = False, additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: """ - Write all AAS objects in a given :class:`ObjectStore ` to an XML or - JSON part in the AASX package and add the referenced supplementary files to the package. + Write all AAS objects in a given :class:`ObjectStore ` to an XML + or JSON part in the AASX package and add the referenced supplementary files to the package. - This method takes an :class:`ObjectStore ` and writes all contained - objects into an "aas_env" part in the AASX package. If - the ObjectStore includes :class:`~aas.model.submodel.Submodel` objects, supplementary files which are - referenced by :class:`~basyx.aas.model.submodel.File` objects - within those Submodels, are fetched from the `file_store` and added to the AASX package. + This method takes an :class:`ObjectStore ` and writes all + contained objects into an ``aas_env`` part in the AASX package. If the ObjectStore includes + :class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by + :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store`` + and added to the AASX package. .. attention:: - You must make sure to call this method only once per unique `part_name` on a single package instance. + You must make sure to call this method only once per unique ``part_name`` on a single package instance. :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. - '.json' if `write_json` else '.xml'). + '.json' if ``write_json`` else '.xml'). :param objects: The objects to be written to the AASX package. Only these Identifiable objects (and included Referable objects) are written to the package. - :param file_store: The SupplementaryFileContainer to retrieve supplementary files from (if there are any `File` - objects within the written objects. + :param file_store: The SupplementaryFileContainer to retrieve supplementary files from (if there are any + ``File`` objects within the written objects. :param write_json: If True, the part is written as a JSON file instead of an XML file. Defaults to False. :param split_part: If True, no aas-spec relationship is added from the aasx-origin to this part. You must make sure to reference it via a aas-spec-split relationship from another aas-spec part @@ -614,7 +616,7 @@ def _write_aasx_origin_relationships(self): """ Helper function to write aas-spec relationships of the aasx-origin part. - This method uses the list of aas-spec parts in `_aas_part_names`. It should be called just before closing the + This method uses the list of aas-spec parts in ``_aas_part_names``. It should be called just before closing the file to make sure all aas-spec parts of the package have already been written. """ # Add relationships from AASX-origin part to AAS parts @@ -729,8 +731,8 @@ def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: :param name: The file's proposed name. Should start with a '/'. Should not contain URI-encoded '/' or '\' :param file: A binary file-like opened for reading the file contents :param content_type: The file's content_type - :return: The file name as stored in the SupplementaryFileContainer. Typically `name` or a modified version of - `name` to resolve conflicts. + :return: The file name as stored in the SupplementaryFileContainer. Typically ``name`` or a modified version of + ``name`` to resolve conflicts. """ pass # pragma: no cover diff --git a/basyx/aas/adapter/json/__init__.py b/basyx/aas/adapter/json/__init__.py index d469468..740b0fb 100644 --- a/basyx/aas/adapter/json/__init__.py +++ b/basyx/aas/adapter/json/__init__.py @@ -4,16 +4,17 @@ This package contains functionality for serialization and deserialization of BaSyx Python SDK objects into/from JSON. :ref:`json_serialization `: The module offers a function to write an ObjectStore to a -given file and therefore defines the custom JSONEncoder :class:`~.aas.adapter.json.json_serialization.AASToJsonEncoder` -which handles encoding of all BaSyx Python SDK objects and their attributes by converting them into standard python -objects. +given file and therefore defines the custom JSONEncoder +:class:`~basyx.aas.adapter.json.json_serialization.AASToJsonEncoder` which handles encoding of all BaSyx Python SDK +objects and their attributes by converting them into standard python objects. :ref:`json_deserialization `: The module implements custom JSONDecoder classes -:class:`~aas.adapter.json.json_deserialization.AASFromJsonDecoder` and -:class:`~aas.adapter.json.json_deserialization.StrictAASFromJsonDecoder`, that — when used with Python's `json` -module — detect AAS objects in the parsed JSON and convert them into the corresponding BaSyx Python SDK object. -A function :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file` is provided to read all AAS objects -within a JSON file and return them as BaSyx Python SDK objectstore. +:class:`~basyx.aas.adapter.json.json_deserialization.AASFromJsonDecoder` and +:class:`~basyx.aas.adapter.json.json_deserialization.StrictAASFromJsonDecoder`, that — when used with Python's +:mod:`json` module — detect AAS objects in the parsed JSON and convert them into the corresponding BaSyx Python SDK +object. A function :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file` is provided to read all +AAS objects within a JSON file and return them as BaSyx Python SDK +:class:`ObjectStore `. """ import os.path diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 436a6e5..1e3aecb 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -10,24 +10,24 @@ Module for deserializing Asset Administration Shell data from the official JSON format The module provides custom JSONDecoder classes :class:`~.AASFromJsonDecoder` and :class:`~.StrictAASFromJsonDecoder` to -be used with the Python standard `json` module. - -Furthermore it provides two classes :class:`~aas.adapter.json.json_deserialization.StrippedAASFromJsonDecoder` and -:class:`~aas.adapter.json.json_deserialization.StrictStrippedAASFromJsonDecoder` for parsing stripped JSON objects, -which are used in the http adapter (see https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91). -The classes contain a custom :meth:`~aas.adapter.json.json_deserialization.AASFromJsonDecoder.object_hook` function -to detect encoded AAS objects within the JSON data and convert them to BaSyx Python SDK objects while parsing. -Additionally, there's the :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into` function, that takes a -complete AAS JSON file, reads its contents and stores the objects in the provided -:class:`~aas.model.provider.AbstractObjectStore`. :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file` is -a wrapper for this function. Instead of storing the objects in a given :class:`~aas.model.provider.AbstractObjectStore`, +be used with the Python standard :mod:`json` module. + +Furthermore it provides two classes :class:`~basyx.aas.adapter.json.json_deserialization.StrippedAASFromJsonDecoder` and +:class:`~basyx.aas.adapter.json.json_deserialization.StrictStrippedAASFromJsonDecoder` for parsing stripped +JSON objects, which are used in the http adapter (see https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91). +The classes contain a custom :meth:`~basyx.aas.adapter.json.json_deserialization.AASFromJsonDecoder.object_hook` +function to detect encoded AAS objects within the JSON data and convert them to BaSyx Python SDK objects while parsing. +Additionally, there's the :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into` function, that +takes a complete AAS JSON file, reads its contents and stores the objects in the provided +:class:`~basyx.aas.model.provider.AbstractObjectStore`. :meth:`read_aas_json_file` is a wrapper for this function. +Instead of storing the objects in a given :class:`~basyx.aas.model.provider.AbstractObjectStore`, it returns a :class:`~basyx.aas.model.provider.DictObjectStore` containing parsed objects. -The deserialization is performed in a bottom-up approach: The `object_hook()` method gets called for every parsed JSON -object (as dict) and checks for existence of the `modelType` attribute. If it is present, the `AAS_CLASS_PARSERS` dict -defines, which of the constructor methods of the class is to be used for converting the dict into an object. Embedded -objects that should have a `modelType` themselves are expected to be converted already. Other embedded objects are -converted using a number of helper constructor methods. +The deserialization is performed in a bottom-up approach: The ``object_hook()`` method gets called for every parsed JSON +object (as dict) and checks for existence of the ``modelType`` attribute. If it is present, the ``AAS_CLASS_PARSERS`` +dict defines, which of the constructor methods of the class is to be used for converting the dict into an object. +Embedded objects that should have a ``modelType`` themselves are expected to be converted already. +Other embedded objects are converted using a number of helper constructor methods. """ import base64 import json @@ -101,7 +101,7 @@ def _expect_type(object_: object, type_: Type, context: str, failsafe: bool) -> class AASFromJsonDecoder(json.JSONDecoder): """ - Custom JSONDecoder class to use the `json` module for deserializing Asset Administration Shell data from the + Custom JSONDecoder class to use the :mod:`json` module for deserializing Asset Administration Shell data from the official JSON format The class contains a custom :meth:`~.AASFromJsonDecoder.object_hook` function to detect encoded AAS objects within @@ -113,17 +113,17 @@ class AASFromJsonDecoder(json.JSONDecoder): data = json.loads(json_string, cls=AASFromJsonDecoder) - The `object_hook` function uses a set of `_construct_*()` methods, one for each + The ``object_hook`` function uses a set of ``_construct_*()`` methods, one for each AAS object type to transform the JSON objects in to BaSyx Python SDK objects. These constructor methods are divided - into two parts: "Helper Constructor Methods", that are used to construct AAS object types without a `modelType` + into two parts: "Helper Constructor Methods", that are used to construct AAS object types without a ``modelType`` attribute as embedded objects within other AAS objects, and "Direct Constructor Methods" for AAS object types *with* - `modelType` attribute. The former are called from other constructor methods or utility methods based on the expected - type of an attribute, the latter are called directly from the `object_hook()` function based on the `modelType` - attribute. + ``modelType`` attribute. The former are called from other constructor methods or utility methods based on the + expected type of an attribute, the latter are called directly from the ``object_hook()`` function based on the + ``modelType`` attribute. This class may be subclassed to override some of the constructor functions, e.g. to construct objects of specialized - subclasses of the BaSyx Python SDK object classes instead of these normal classes from the `model` package. To - simplify this tasks, (nearly) all the constructor methods take a parameter `object_type` defaulting to the normal + subclasses of the BaSyx Python SDK object classes instead of these normal classes from the ``model`` package. To + simplify this tasks, (nearly) all the constructor methods take a parameter ``object_type`` defaulting to the normal BaSyx Python SDK object class, that can be overridden in a derived function: .. code-block:: python @@ -139,11 +139,11 @@ def _construct_submodel(cls, dct, object_class=EnhancedSubmodel): return super()._construct_submodel(dct, object_class=object_class) - :cvar failsafe: If `True` (the default), don't raise Exceptions for missing attributes and wrong types, but instead - skip defective objects and use logger to output warnings. Use StrictAASFromJsonDecoder for a + :cvar failsafe: If ``True`` (the default), don't raise Exceptions for missing attributes and wrong types, but + instead skip defective objects and use logger to output warnings. Use StrictAASFromJsonDecoder for a non-failsafe version. - :cvar stripped: If `True`, the JSON objects will be parsed in a stripped manner, excluding some attributes. - Defaults to `False`. + :cvar stripped: If ``True``, the JSON objects will be parsed in a stripped manner, excluding some attributes. + Defaults to ``False``. See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 """ failsafe = True @@ -160,10 +160,10 @@ def object_hook(cls, dct: Dict[str, object]) -> object: return dct # 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 + # ``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 + # 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]] = { 'AssetAdministrationShell': cls._construct_asset_administration_shell, @@ -281,7 +281,7 @@ def _get_kind(cls, dct: Dict[str, object]) -> model.ModellingKind: Utility method to get the kind of an HasKind object from its JSON representation. :param dct: The object's dict representation from JSON - :return: The object's `kind` value + :return: The object's ``kind`` value """ return MODELLING_KIND_INVERSE[_get_ts(dct, "kind", str)] if 'kind' in dct else model.ModellingKind.INSTANCE @@ -366,8 +366,8 @@ def _construct_administrative_information( @classmethod def _construct_operation_variable(cls, dct: Dict[str, object]) -> model.SubmodelElement: """ - Since we don't implement `OperationVariable`, this constructor discards the wrapping `OperationVariable` object - and just returns the contained :class:`~basyx.aas.model.submodel.SubmodelElement`. + Since we don't implement ``OperationVariable``, this constructor discards the wrapping ``OperationVariable`` + object and just returns the contained :class:`~basyx.aas.model.submodel.SubmodelElement`. """ # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 @@ -750,7 +750,7 @@ class StrictAASFromJsonDecoder(AASFromJsonDecoder): A strict version of the AASFromJsonDecoder class for deserializing Asset Administration Shell data from the official JSON format - This version has set `failsafe = False`, which will lead to Exceptions raised for every missing attribute or wrong + This version has set ``failsafe = False``, which will lead to Exceptions raised for every missing attribute or wrong object type. """ failsafe = False @@ -776,8 +776,8 @@ def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFr Returns the correct decoder based on the parameters failsafe and stripped. If a decoder class is given, failsafe and stripped are ignored. - :param failsafe: If `True`, a failsafe decoder is selected. Ignored if a decoder class is specified. - :param stripped: If `True`, a decoder for parsing stripped JSON objects is selected. Ignored if a decoder class is + :param failsafe: If ``True``, a failsafe decoder is selected. Ignored if a decoder class is specified. + :param stripped: If ``True``, a decoder for parsing stripped JSON objects is selected. Ignored if a decoder class is specified. :param decoder: Is returned, if specified. :return: An :class:`~.AASFromJsonDecoder` (sub)class. @@ -801,16 +801,16 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 into a given object store. - :param object_store: The :class:`ObjectStore ` in which the identifiable - objects should be stored + :param object_store: The :class:`ObjectStore ` in which the + identifiable objects should be stored :param file: A file-like object to read the JSON-serialized data from :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. - This parameter is ignored if replace_existing is `True`. - :param failsafe: If `True`, the document is parsed in a failsafe way: Missing attributes and elements are logged + This parameter is ignored if replace_existing is ``True``. + :param failsafe: If ``True``, the document is parsed in a failsafe way: Missing attributes and elements are logged instead of causing exceptions. Defect objects are skipped. This parameter is ignored if a decoder class is specified. - :param stripped: If `True`, stripped JSON objects are parsed. + :param stripped: If ``True``, stripped JSON objects are parsed. 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 @@ -866,12 +866,12 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r def read_aas_json_file(file: IO, **kwargs) -> model.DictObjectStore[model.Identifiable]: """ - A wrapper of :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects in an - empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as - :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into`. + A wrapper of :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects + in an empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as + :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into`. :param file: A filename or file-like object to read the JSON-serialized data from - :param kwargs: Keyword arguments passed to :meth:`~aas.adapter.json.json_deserialization.read_aas_json_file_into` + :param kwargs: Keyword arguments passed to :meth:`read_aas_json_file_into` :return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the JSON file """ object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index d3b6b65..08ba971 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -10,21 +10,21 @@ Module for serializing Asset Administration Shell objects to the official JSON format -The module provides an custom JSONEncoder classes :class:`~.AASToJsonEncoder` and :class:`~.AASToJsonEncoderStripped` -to be used with the Python standard `json` module. While the former serializes objects as defined in the specification, -the latter serializes stripped objects, excluding some attributes +The module provides an custom JSONEncoder classes :class:`AASToJsonEncoder` and :class:`StrippedAASToJsonEncoder` +to be used with the Python standard :mod:`json` module. While the former serializes objects as defined in the +specification, the latter serializes stripped objects, excluding some attributes (see https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91). Each class contains a custom :meth:`~.AASToJsonEncoder.default` function which converts BaSyx Python SDK objects to simple python types for an automatic JSON serialization. -To simplify the usage of this module, the :meth:`~.write_aas_json_file` and :meth:`~.object_store_to_json` are provided. -The former is used to serialize a given :class:`~aas.model.provider.AbstractObjectStore` to a file, while the latter -serializes the object store to a string and returns it. +To simplify the usage of this module, the :meth:`write_aas_json_file` and :meth:`object_store_to_json` are provided. +The former is used to serialize a given :class:`~basyx.aas.model.provider.AbstractObjectStore` to a file, while the +latter serializes the object store to a string and returns it. The serialization is performed in an iterative approach: The :meth:`~.AASToJsonEncoder.default` function gets called for every object and checks if an object is an BaSyx Python SDK object. In this case, it calls a special function for the respective BaSyx Python SDK class which converts the object (but not the contained objects) into a simple Python dict, which is serializable. Any contained BaSyx Python SDK objects are included into the dict as they are to be converted -later on. The special helper function :meth:`~.AASToJsonEncoder._abstract_classes_to_json` is called by most of the +later on. The special helper function ``_abstract_classes_to_json`` is called by most of the conversion functions to handle all the attributes of abstract base classes. """ import base64 @@ -38,10 +38,10 @@ class AASToJsonEncoder(json.JSONEncoder): """ - Custom JSON Encoder class to use the `json` module for serializing Asset Administration Shell data into the + Custom JSON Encoder class to use the :mod:`json` module for serializing Asset Administration Shell data into the official JSON format - The class overrides the `default()` method to transform BaSyx Python SDK objects into dicts that may be serialized + The class overrides the ``default()`` method to transform BaSyx Python SDK objects into dicts that may be serialized by the standard encode method. Typical usage: @@ -51,14 +51,14 @@ class AASToJsonEncoder(json.JSONEncoder): json_string = json.dumps(data, cls=AASToJsonEncoder) :cvar stripped: If True, the JSON objects will be serialized in a stripped manner, excluding some attributes. - Defaults to `False`. + Defaults to ``False``. See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 """ stripped = False def default(self, obj: object) -> object: """ - The overwritten `default` method for `json.JSONEncoder` + The overwritten ``default`` method for :class:`json.JSONEncoder` :param obj: The object to serialize to json :return: The serialized object @@ -579,12 +579,13 @@ def _annotated_relationship_element_to_json(cls, obj: model.AnnotatedRelationshi def _operation_variable_to_json(cls, obj: model.SubmodelElement) -> Dict[str, object]: """ serialization of an object from class SubmodelElement to a json OperationVariable representation - Since we don't implement the `OperationVariable` class, which is just a wrapper for a single - :class:`~basyx.aas.model.submodel.SubmodelElement`, elements are serialized as the `value` attribute of an - `operationVariable` object. + Since we don't implement the ``OperationVariable`` class, which is just a wrapper for a single + :class:`~basyx.aas.model.submodel.SubmodelElement`, elements are serialized as the ``value`` attribute of an + ``operationVariable`` object. - :param obj: object of class `SubmodelElement` - :return: `OperationVariable` wrapper containing the serialized `SubmodelElement` + :param obj: object of class :class:`~basyx.aas.model.submodel.SubmodelElement` + :return: ``OperationVariable`` wrapper containing the serialized + :class:`~basyx.aas.model.submodel.SubmodelElement` """ return {'value': obj} @@ -719,13 +720,13 @@ def object_store_to_json(data: model.AbstractObjectStore, stripped: bool = False Create a json serialization of a set of AAS objects according to 'Details of the Asset Administration Shell', chapter 5.5 - :param data: :class:`ObjectStore ` which contains different objects of the - AAS meta model which should be serialized to a JSON file + :param data: :class:`ObjectStore ` which contains different objects of + the AAS meta model which should be serialized to a JSON file :param stripped: If true, objects are serialized to stripped json objects. See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 This parameter is ignored if an encoder class is specified. :param encoder: The encoder class used to encode the JSON objects - :param kwargs: Additional keyword arguments to be passed to `json.dumps()` + :param kwargs: Additional keyword arguments to be passed to :func:`json.dumps` """ encoder_ = _select_encoder(stripped, encoder) # serialize object to json @@ -739,8 +740,8 @@ def write_aas_json_file(file: IO, data: model.AbstractObjectStore, stripped: boo Administration Shell', chapter 5.5 :param file: A file-like object to write the JSON-serialized data to - :param data: :class:`ObjectStore ` which contains different objects of the - AAS meta model which should be serialized to a JSON file + :param data: :class:`ObjectStore ` which contains different objects of + the AAS meta model which should be serialized to a JSON file :param stripped: If `True`, objects are serialized to stripped json objects. See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 This parameter is ignored if an encoder class is specified. diff --git a/basyx/aas/adapter/xml/__init__.py b/basyx/aas/adapter/xml/__init__.py index 43e333c..714c806 100644 --- a/basyx/aas/adapter/xml/__init__.py +++ b/basyx/aas/adapter/xml/__init__.py @@ -4,10 +4,10 @@ This package contains functionality for serialization and deserialization of BaSyx Python SDK objects into/from XML. :ref:`xml_serialization `: The module offers a function to write an -:class:`ObjectStore ` to a given file. +:class:`ObjectStore ` to a given file. :ref:`xml_deserialization `: The module offers a function to create an -:class:`ObjectStore ` from a given xml document. +:class:`ObjectStore ` from a given xml document. """ import os.path diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 3965840..6cabeff 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -12,12 +12,11 @@ This module provides the following functions for parsing XML documents: -- :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_element` constructs a single object from an XML document - containing a single element -- :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` constructs all elements of an XML document and - stores them in a given :class:`ObjectStore ` -- :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file` constructs all elements of an XML document and returns - them in a :class:`~basyx.aas.model.provider.DictObjectStore` +- :func:`read_aas_xml_element` constructs a single object from an XML document containing a single element +- :func:`read_aas_xml_file_into` constructs all elements of an XML document and stores them in a given + :class:`ObjectStore ` +- :func:`read_aas_xml_file` constructs all elements of an XML document and returns them in a + :class:`~basyx.aas.model.provider.DictObjectStore` These functions take a decoder class as keyword argument, which allows parsing in failsafe (default) or non-failsafe mode. Parsing stripped elements - used in the HTTP adapter - is also possible. It is also possible to subclass the @@ -25,8 +24,8 @@ In failsafe mode errors regarding missing attributes and elements or invalid values are caught and logged. In non-failsafe mode any error would abort parsing. -Error handling is done only by `_failsafe_construct()` in this module. Nearly all constructor functions are called -by other constructor functions via `_failsafe_construct()`, so an error chain is constructed in the error case, +Error handling is done only by ``_failsafe_construct()`` in this module. Nearly all constructor functions are called +by other constructor functions via ``_failsafe_construct()``, so an error chain is constructed in the error case, which allows printing stacktrace-like error messages like the following in the error case (in failsafe mode of course): @@ -66,13 +65,13 @@ def _str_to_bool(string: str) -> bool: """ - XML only allows "false" and "true" (case-sensitive) as valid values for a boolean. + XML only allows ``false`` and ``true`` (case-sensitive) as valid values for a boolean. - This function checks the string and raises a ValueError if the string is neither "true" nor "false". + This function checks the string and raises a ValueError if the string is neither ``true`` nor ``false``. - :param string: String representation of a boolean. ("true" or "false") + :param string: String representation of a boolean. (``true`` or ``false``) :return: The respective boolean value. - :raises ValueError: If string is neither "true" nor "false". + :raises ValueError: If string is neither ``true`` nor ``false``. """ if string not in ("true", "false"): raise ValueError(f"{string} is not a valid boolean! Only true and false are allowed.") @@ -425,7 +424,7 @@ class AASFromXmlDecoder: It parses XML documents in a failsafe manner, meaning any errors encountered will be logged and invalid XML elements will be skipped. - Most member functions support the `object_class` parameter. It was introduced so they can be overwritten + Most member functions support the ``object_class`` parameter. It was introduced so they can be overwritten in subclasses, which allows constructing instances of subtypes. """ failsafe = True @@ -543,7 +542,7 @@ def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ @classmethod def _construct_operation_variable(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement: """ - Since we don't implement `OperationVariable`, this constructor discards the wrapping `aas:operationVariable` + Since we don't implement ``OperationVariable``, this constructor discards the wrapping `aas:operationVariable` and `aas:value` and just returns the contained :class:`~basyx.aas.model.submodel.SubmodelElement`. """ value = _get_child_mandatory(element, NS_AAS + "value") @@ -1405,18 +1404,18 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif **parser_kwargs: Any) -> Set[model.Identifier]: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 - into a given :class:`ObjectStore `. + into a given :class:`ObjectStore `. - :param object_store: The :class:`ObjectStore ` in which the + :param object_store: The :class:`ObjectStore ` in which the :class:`~basyx.aas.model.base.Identifiable` objects should be stored :param file: A filename or file-like object to read the XML-serialized data from :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. This parameter is ignored if replace_existing is True. - :param failsafe: If `True`, the document is parsed in a failsafe way: missing attributes and elements are logged + :param failsafe: If ``True``, the document is parsed in a failsafe way: missing attributes and elements are logged instead of causing exceptions. Defect objects are skipped. This parameter is ignored if a decoder class is specified. - :param stripped: If `True`, stripped XML elements are parsed. + :param stripped: If ``True``, stripped XML elements are parsed. 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 XML elements @@ -1474,12 +1473,12 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif def read_aas_xml_file(file: IO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]: """ - A wrapper of :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an + A wrapper of :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports - the same keyword arguments as :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`. + the same keyword arguments as :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`. :param file: A filename or file-like object to read the XML-serialized data from - :param kwargs: Keyword arguments passed to :meth:`~aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` + :param kwargs: Keyword arguments passed to :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` :return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the XML file """ object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 0376b62..c6eb2be 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -11,11 +11,11 @@ How to use: -- For generating an XML-File from a :class:`~aas.model.provider.AbstractObjectStore`, check out the function - :meth:`~aas.adapter.xml.xml_serialization.write_aas_xml_file`. +- For generating an XML-File from a :class:`~basyx.aas.model.provider.AbstractObjectStore`, check out the function + :func:`write_aas_xml_file`. - For serializing any object to an XML fragment, that fits the XML specification from 'Details of the - Asset Administration Shell', chapter 5.4, check out `_to_xml()`. These functions return - an :class:`xml.etree.ElementTree.Element` object to be serialized into XML. + Asset Administration Shell', chapter 5.4, check out ``_to_xml()``. These functions return + an :class:`~lxml.etree.Element` object to be serialized into XML. """ from lxml import etree # type: ignore @@ -36,12 +36,12 @@ def _generate_element(name: str, text: Optional[str] = None, attributes: Optional[Dict] = None) -> etree.Element: """ - generate an ElementTree.Element object + generate an :class:`~lxml.etree.Element` object :param name: namespace+tag_name of the element :param text: Text of the element. Default is None - :param attributes: Attributes of the elements in form of a dict {"attribute_name": "attribute_content"} - :return: ElementTree.Element object + :param attributes: Attributes of the elements in form of a dict ``{"attribute_name": "attribute_content"}`` + :return: :class:`~lxml.etree.Element` object """ et_element = etree.Element(name) if text: @@ -56,8 +56,8 @@ def boolean_to_xml(obj: bool) -> str: """ Serialize a boolean to XML - :param obj: Boolean (`True`, `False`) - :return: String in the XML accepted form (`'true'`, `'false'`) + :param obj: Boolean (``True``, ``False``) + :return: String in the XML accepted form (``true``, ``false``) """ if obj: return "true" @@ -72,7 +72,7 @@ def boolean_to_xml(obj: bool) -> str: def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: """ - Generates an XML element and adds attributes of abstract base classes of `obj`. + Generates an XML element and adds attributes of abstract base classes of ``obj``. If the object obj is inheriting from any abstract AAS class, this function adds all the serialized information of those abstract classes to the generated element. @@ -140,12 +140,12 @@ def _value_to_xml(value: model.ValueDataType, value_type: model.DataTypeDefXsd, tag: str = NS_AAS+"value") -> etree.Element: """ - Serialization of objects of class ValueDataType to XML + Serialization of objects of :class:`~basyx.aas.model.base.ValueDataType` to XML - :param value: model.ValueDataType object - :param value_type: Corresponding model.DataTypeDefXsd - :param tag: tag of the serialized ValueDataType object - :return: Serialized ElementTree.Element object + :param value: :class:`~basyx.aas.model.base.ValueDataType` object + :param value_type: Corresponding :class:`~basyx.aas.model.base.DataTypeDefXsd` + :param tag: tag of the serialized :class:`~basyx.aas.model.base.ValueDataType` object + :return: Serialized :class:`~lxml.etree.Element` object """ # todo: add "{NS_XSI+"type": "xs:"+model.datatypes.XSD_TYPE_NAMES[value_type]}" as attribute, if the schema allows # it @@ -156,11 +156,11 @@ def _value_to_xml(value: model.ValueDataType, def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.LangStringSet` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.LangStringSet` to XML - :param obj: Object of class :class:`~aas.model.base.LangStringSet` + :param obj: Object of class :class:`~basyx.aas.model.base.LangStringSet` :param tag: Namespace+Tag name of the returned XML element. - :return: Serialized ElementTree object + :return: Serialized :class:`~lxml.etree.Element` object """ LANG_STRING_SET_TAGS: Dict[Type[model.LangStringSet], str] = {k: NS_AAS + v for k, v in { model.MultiLanguageNameType: "langStringNameType", @@ -184,8 +184,8 @@ def administrative_information_to_xml(obj: model.AdministrativeInformation, Serialization of objects of class :class:`~basyx.aas.model.base.AdministrativeInformation` to XML :param obj: Object of class :class:`~basyx.aas.model.base.AdministrativeInformation` - :param tag: Namespace+Tag of the serialized element. Default is "aas:administration" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element. Default is ``aas:administration`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_administration = abstract_classes_to_xml(tag, obj) if obj.version: @@ -204,7 +204,7 @@ def data_element_to_xml(obj: model.DataElement) -> etree.Element: Serialization of objects of class :class:`~basyx.aas.model.submodel.DataElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.DataElement` - :return: Serialized ElementTree element + :return: Serialized :class:`~lxml.etree.Element` object """ if isinstance(obj, model.MultiLanguageProperty): return multi_language_property_to_xml(obj) @@ -225,8 +225,8 @@ def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etr Serialization of objects of class :class:`~basyx.aas.model.base.Reference` to XML :param obj: Object of class :class:`~basyx.aas.model.base.Reference` - :param tag: Namespace+Tag of the returned element. Default is "aas:reference" - :return: Serialized ElementTree + :param tag: Namespace+Tag of the returned element. Default is ``aas:reference`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_reference = _generate_element(tag) et_reference.append(_generate_element(NS_AAS + "type", text=_generic.REFERENCE_TYPES[obj.__class__])) @@ -245,11 +245,11 @@ def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etr def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.Qualifier` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.Qualifier` to XML - :param obj: Object of class :class:`~aas.model.base.Qualifier` - :param tag: Namespace+Tag of the serialized ElementTree object. Default is "aas:qualifier" - :return: Serialized ElementTreeObject + :param obj: Object of class :class:`~basyx.aas.model.base.Qualifier` + :param tag: Namespace+Tag of the serialized ElementTree object. Default is ``aas:qualifier`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_qualifier = abstract_classes_to_xml(tag, obj) et_qualifier.append(_generate_element(NS_AAS + "kind", text=_generic.QUALIFIER_KIND[obj.kind])) @@ -264,11 +264,11 @@ def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etr def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.Extension` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.Extension` to XML - :param obj: Object of class :class:`~aas.model.base.Extension` - :param tag: Namespace+Tag of the serialized ElementTree object. Default is "aas:extension" - :return: Serialized ElementTreeObject + :param obj: Object of class :class:`~basyx.aas.model.base.Extension` + :param tag: Namespace+Tag of the serialized ElementTree object. Default is ``aas:extension`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_extension = abstract_classes_to_xml(tag, obj) et_extension.append(_generate_element(NS_AAS + "name", text=obj.name)) @@ -288,14 +288,11 @@ def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etr def value_reference_pair_to_xml(obj: model.ValueReferencePair, tag: str = NS_AAS+"valueReferencePair") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.ValueReferencePair` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.ValueReferencePair` to XML - todo: couldn't find it in the official schema, so guessing how to implement serialization - check namespace, tag and correct serialization - - :param obj: Object of class :class:`~aas.model.base.ValueReferencePair` - :param tag: Namespace+Tag of the serialized element. Default is "aas:valueReferencePair" - :return: Serialized ElementTree object + :param obj: Object of class :class:`~basyx.aas.model.base.ValueReferencePair` + :param tag: Namespace+Tag of the serialized element. Default is ``aas:valueReferencePair`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_vrp = _generate_element(tag) # TODO: value_type isn't used at all by _value_to_xml(), thus we can ignore the type here for now @@ -307,13 +304,13 @@ def value_reference_pair_to_xml(obj: model.ValueReferencePair, def value_list_to_xml(obj: model.ValueList, tag: str = NS_AAS+"valueList") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.ValueList` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.ValueList` to XML todo: couldn't find it in the official schema, so guessing how to implement serialization - :param obj: Object of class :class:`~aas.model.base.ValueList` - :param tag: Namespace+Tag of the serialized element. Default is "aas:valueList" - :return: Serialized ElementTree object + :param obj: Object of class :class:`~basyx.aas.model.base.ValueList` + :param tag: Namespace+Tag of the serialized element. Default is ``aas:valueList`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_value_list = _generate_element(tag) et_value_reference_pairs = _generate_element(NS_AAS+"valueReferencePairs") @@ -334,8 +331,8 @@ def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "sp Serialization of objects of class :class:`~basyx.aas.model.base.SpecificAssetId` to XML :param obj: Object of class :class:`~basyx.aas.model.base.SpecificAssetId` - :param tag: Namespace+Tag of the ElementTree object. Default is "aas:identifierKeyValuePair" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:identifierKeyValuePair`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_asset_information = abstract_classes_to_xml(tag, obj) et_asset_information.append(_generate_element(name=NS_AAS + "name", text=obj.name)) @@ -348,11 +345,11 @@ def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "sp def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"assetInformation") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.aas.AssetInformation` to XML + Serialization of objects of class :class:`~basyx.aas.model.aas.AssetInformation` to XML - :param obj: Object of class :class:`~aas.model.aas.AssetInformation` - :param tag: Namespace+Tag of the ElementTree object. Default is "aas:assetInformation" - :return: Serialized ElementTree object + :param obj: Object of class :class:`~basyx.aas.model.aas.AssetInformation` + :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:assetInformation`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_asset_information = abstract_classes_to_xml(tag, obj) et_asset_information.append(_generate_element(name=NS_AAS + "assetKind", text=_generic.ASSET_KIND[obj.asset_kind])) @@ -377,8 +374,8 @@ def concept_description_to_xml(obj: model.ConceptDescription, Serialization of objects of class :class:`~basyx.aas.model.concept.ConceptDescription` to XML :param obj: Object of class :class:`~basyx.aas.model.concept.ConceptDescription` - :param tag: Namespace+Tag of the ElementTree object. Default is "aas:conceptDescription" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:conceptDescription`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_concept_description = abstract_classes_to_xml(tag, obj) if obj.is_case_of: @@ -392,11 +389,11 @@ def concept_description_to_xml(obj: model.ConceptDescription, def embedded_data_specification_to_xml(obj: model.EmbeddedDataSpecification, tag: str = NS_AAS+"embeddedDataSpecification") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.EmbeddedDataSpecification` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.EmbeddedDataSpecification` to XML - :param obj: Object of class :class:`~aas.model.base.EmbeddedDataSpecification` - :param tag: Namespace+Tag of the ElementTree object. Default is "aas:embeddedDataSpecification" - :return: Serialized ElementTree object + :param obj: Object of class :class:`~basyx.aas.model.base.EmbeddedDataSpecification` + :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:embeddedDataSpecification`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_embedded_data_specification = abstract_classes_to_xml(tag, obj) et_embedded_data_specification.append(reference_to_xml(obj.data_specification, tag=NS_AAS + "dataSpecification")) @@ -407,11 +404,11 @@ def embedded_data_specification_to_xml(obj: model.EmbeddedDataSpecification, def data_specification_content_to_xml(obj: model.DataSpecificationContent, tag: str = NS_AAS+"dataSpecificationContent") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.DataSpecificationContent` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.DataSpecificationContent` to XML - :param obj: Object of class :class:`~aas.model.base.DataSpecificationContent` - :param tag: Namespace+Tag of the ElementTree object. Default is "aas:dataSpecificationContent" - :return: Serialized ElementTree object + :param obj: Object of class :class:`~basyx.aas.model.base.DataSpecificationContent` + :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:dataSpecificationContent`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_data_specification_content = abstract_classes_to_xml(tag, obj) if isinstance(obj, model.DataSpecificationIEC61360): @@ -424,11 +421,11 @@ def data_specification_content_to_xml(obj: model.DataSpecificationContent, def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, tag: str = NS_AAS+"dataSpecificationIec61360") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.DataSpecificationIEC61360` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.DataSpecificationIEC61360` to XML - :param obj: Object of class :class:`~aas.model.base.DataSpecificationIEC61360` - :param tag: Namespace+Tag of the ElementTree object. Default is "aas:dataSpecificationIec61360" - :return: Serialized ElementTree object + :param obj: Object of class :class:`~basyx.aas.model.base.DataSpecificationIEC61360` + :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:dataSpecificationIec61360`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_data_specification_iec61360 = abstract_classes_to_xml(tag, obj) et_data_specification_iec61360.append(lang_string_set_to_xml(obj.preferred_name, NS_AAS + "preferredName")) @@ -471,8 +468,8 @@ def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, Serialization of objects of class :class:`~basyx.aas.model.aas.AssetAdministrationShell` to XML :param obj: Object of class :class:`~basyx.aas.model.aas.AssetAdministrationShell` - :param tag: Namespace+Tag of the ElementTree object. Default is "aas:assetAdministrationShell" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:assetAdministrationShell`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_aas = abstract_classes_to_xml(tag, obj) if obj.derived_from: @@ -496,7 +493,7 @@ def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` - :return: Serialized ElementTree object + :return: Serialized :class:`~lxml.etree.Element` object """ if isinstance(obj, model.DataElement): return data_element_to_xml(obj) @@ -524,8 +521,8 @@ def submodel_to_xml(obj: model.Submodel, Serialization of objects of class :class:`~basyx.aas.model.submodel.Submodel` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Submodel` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:submodel" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodel`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_submodel = abstract_classes_to_xml(tag, obj) if obj.submodel_element: @@ -542,8 +539,8 @@ def property_to_xml(obj: model.Property, Serialization of objects of class :class:`~basyx.aas.model.submodel.Property` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Property` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:property" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:property`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_property = abstract_classes_to_xml(tag, obj) et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) @@ -560,8 +557,8 @@ def multi_language_property_to_xml(obj: model.MultiLanguageProperty, Serialization of objects of class :class:`~basyx.aas.model.submodel.MultiLanguageProperty` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.MultiLanguageProperty` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:multiLanguageProperty" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:multiLanguageProperty`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_multi_language_property = abstract_classes_to_xml(tag, obj) if obj.value: @@ -577,8 +574,8 @@ def range_to_xml(obj: model.Range, Serialization of objects of class :class:`~basyx.aas.model.submodel.Range` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Range` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:range" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:range`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_range = abstract_classes_to_xml(tag, obj) et_range.append(_generate_element(name=NS_AAS + "valueType", @@ -596,8 +593,8 @@ def blob_to_xml(obj: model.Blob, Serialization of objects of class :class:`~basyx.aas.model.submodel.Blob` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Blob` - :param tag: Namespace+Tag of the serialized element. Default is "blob" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element. Default is ``aas:blob`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_blob = abstract_classes_to_xml(tag, obj) et_value = etree.Element(NS_AAS + "value") @@ -614,8 +611,8 @@ def file_to_xml(obj: model.File, Serialization of objects of class :class:`~basyx.aas.model.submodel.File` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.File` - :param tag: Namespace+Tag of the serialized element. Default is "aas:file" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element. Default is ``aas:file`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_file = abstract_classes_to_xml(tag, obj) if obj.value: @@ -627,11 +624,11 @@ def file_to_xml(obj: model.File, def resource_to_xml(obj: model.Resource, tag: str = NS_AAS+"resource") -> etree.Element: """ - Serialization of objects of class :class:`~aas.model.base.Resource` to XML + Serialization of objects of class :class:`~basyx.aas.model.base.Resource` to XML - :param obj: Object of class :class:`~aas.model.base.Resource` - :param tag: Namespace+Tag of the serialized element. Default is "aas:resource" - :return: Serialized ElementTree object + :param obj: Object of class :class:`~basyx.aas.model.base.Resource` + :param tag: Namespace+Tag of the serialized element. Default is ``aas:resource`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_resource = abstract_classes_to_xml(tag, obj) et_resource.append(_generate_element(NS_AAS + "path", text=obj.path)) @@ -646,8 +643,8 @@ def reference_element_to_xml(obj: model.ReferenceElement, Serialization of objects of class :class:`~basyx.aas.model.submodel.ReferenceElement` to XMl :param obj: Object of class :class:`~basyx.aas.model.submodel.ReferenceElement` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:referenceElement" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:referenceElement`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_reference_element = abstract_classes_to_xml(tag, obj) if obj.value: @@ -660,11 +657,9 @@ def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, """ Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElementCollection` to XML - Note that we do not have parameter "allowDuplicates" in out implementation - :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElementCollection` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:submodelElementCollection" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodelElementCollection`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_submodel_element_collection = abstract_classes_to_xml(tag, obj) if obj.value: @@ -677,6 +672,13 @@ def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, def submodel_element_list_to_xml(obj: model.SubmodelElementList, tag: str = NS_AAS+"submodelElementList") -> etree.Element: + """ + Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElementList` to XML + + :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElementList` + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodelElementList`` + :return: Serialized :class:`~lxml.etree.Element` object + """ et_submodel_element_list = abstract_classes_to_xml(tag, obj) et_submodel_element_list.append(_generate_element(NS_AAS + "orderRelevant", boolean_to_xml(obj.order_relevant))) if obj.semantic_id_list_element is not None: @@ -701,8 +703,8 @@ def relationship_element_to_xml(obj: model.RelationshipElement, Serialization of objects of class :class:`~basyx.aas.model.submodel.RelationshipElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.RelationshipElement` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:relationshipElement" - :return: Serialized ELementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:relationshipElement`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_relationship_element = abstract_classes_to_xml(tag, obj) et_relationship_element.append(reference_to_xml(obj.first, NS_AAS+"first")) @@ -716,8 +718,8 @@ def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElemen Serialization of objects of class :class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement` - :param tag: Namespace+Tag of the serialized element (optional): Default is "aas:annotatedRelationshipElement" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional): Default is ``aas:annotatedRelationshipElement`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_annotated_relationship_element = relationship_element_to_xml(obj, tag) if obj.annotation: @@ -731,13 +733,13 @@ def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElemen def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"operationVariable") -> etree.Element: """ Serialization of :class:`~basyx.aas.model.submodel.SubmodelElement` to the XML OperationVariable representation - Since we don't implement the `OperationVariable` class, which is just a wrapper for a single - :class:`~basyx.aas.model.submodel.SubmodelElement`, elements are serialized as the `aas:value` child of an - `aas:operationVariable` element. + Since we don't implement the ``OperationVariable`` class, which is just a wrapper for a single + :class:`~basyx.aas.model.submodel.SubmodelElement`, elements are serialized as the ``aas:value`` child of an + ``aas:operationVariable`` element. :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operationVariable" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:operationVariable`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_operation_variable = _generate_element(tag) et_value = _generate_element(NS_AAS+"value") @@ -752,8 +754,8 @@ def operation_to_xml(obj: model.Operation, Serialization of objects of class :class:`~basyx.aas.model.submodel.Operation` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Operation` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operation" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:operation`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_operation = abstract_classes_to_xml(tag, obj) for tag, nss in ((NS_AAS+"inputVariables", obj.input_variable), @@ -773,8 +775,8 @@ def capability_to_xml(obj: model.Capability, Serialization of objects of class :class:`~basyx.aas.model.submodel.Capability` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Capability` - :param tag: Namespace+Tag of the serialized element, default is "aas:capability" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element, default is ``aas:capability`` + :return: Serialized :class:`~lxml.etree.Element` object """ return abstract_classes_to_xml(tag, obj) @@ -785,8 +787,8 @@ def entity_to_xml(obj: model.Entity, Serialization of objects of class :class:`~basyx.aas.model.submodel.Entity` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Entity` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:entity" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:entity`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_entity = abstract_classes_to_xml(tag, obj) if obj.statement: @@ -810,8 +812,8 @@ def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+" Serialization of objects of class :class:`~basyx.aas.model.submodel.BasicEventElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.BasicEventElement` - :param tag: Namespace+Tag of the serialized element (optional). Default is "aas:basicEventElement" - :return: Serialized ElementTree object + :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:basicEventElement`` + :return: Serialized :class:`~lxml.etree.Element` object """ et_basic_event_element = abstract_classes_to_xml(tag, obj) et_basic_event_element.append(reference_to_xml(obj.observed, NS_AAS+"observed")) @@ -846,9 +848,9 @@ def write_aas_xml_file(file: IO, Administration Shell', chapter 5.4 :param file: A file-like object to write the XML-serialized data to - :param data: :class:`ObjectStore ` which contains different objects of the - AAS meta model which should be serialized to an XML file - :param kwargs: Additional keyword arguments to be passed to `tree.write()` + :param data: :class:`ObjectStore ` which contains different objects of + the AAS meta model which should be serialized to an XML file + :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree.ElementTree.write` """ # separate different kind of objects asset_administration_shells = [] From 3afea3aa7751261c47d449ae9a8f6a89293f8e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 14 Jan 2024 22:11:00 +0100 Subject: [PATCH 139/474] adapter.aasx: improve error messages A `FileNotFoundError` is no longer converted to a `ValueError`, but re-raised instead. Furthermore, the message of the `ValueError` now contains more information as to why a file is not a valid OPC package. Fix #221 --- basyx/aas/adapter/aasx.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index fe7e173..7bb78e3 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -69,13 +69,16 @@ def __init__(self, file: Union[os.PathLike, str, IO]): closing under any circumstances. :param file: A filename, file path or an open file-like object in binary mode + :raises FileNotFoundError: If the file does not exist :raises ValueError: If the file is not a valid OPC zip package """ try: logger.debug("Opening {} as AASX pacakge for reading ...".format(file)) self.reader = pyecma376_2.ZipPackageReader(file) + except FileNotFoundError: + raise except Exception as e: - raise ValueError("{} is not a valid ECMA376-2 (OPC) file".format(file)) from e + raise ValueError("{} is not a valid ECMA376-2 (OPC) file: {}".format(file, e)) from e def get_core_properties(self) -> pyecma376_2.OPCCoreProperties: """ From 781b81476fa1456b5f5cfe1e81b4ad5c6a3571de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 5 Jun 2020 15:39:25 +0200 Subject: [PATCH 140/474] adapter.http: add first working draft --- basyx/aas/adapter/http.py | 142 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 basyx/aas/adapter/http.py diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py new file mode 100644 index 0000000..e722409 --- /dev/null +++ b/basyx/aas/adapter/http.py @@ -0,0 +1,142 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import base64 +from binascii import Error as Base64Error +from werkzeug.datastructures import MIMEAccept, Headers +from werkzeug.exceptions import BadRequest, HTTPException, NotAcceptable, NotImplemented +from werkzeug.http import parse_accept_header +from werkzeug.routing import Map, Rule, Submount +from werkzeug.wrappers import Request, Response + +from .. import model +from ._generic import IDENTIFIER_TYPES_INVERSE +from .json.json_serialization import asset_administration_shell_to_json + +from typing import Dict, Iterable, Optional, Type + + +class JsonResponse(Response): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, content_type="application/json") + + +class XmlResponse(Response): + def __init__(self, *args, content_type="application/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + +class XmlResponseAlt(XmlResponse): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, content_type="text/xml") + + + """ + A mapping of supported content types to their respective ResponseType. + The first content type in this dict will be preferred if the requester doesn't specify preferred content types using + and HTTP-"Accept" header. + """ +RESPONSE_TYPES = { + "application/json": JsonResponse, +# "application/xml": XmlResponse, +# "text/xml": XmlResponseAlt +} + + +class WSGIApp: + def __init__(self, object_store: model.AbstractObjectStore): + self.object_store: model.AbstractObjectStore = object_store + self.url_map: Map = Map([ + Submount("/api/v1", [ + # TODO: custom decoder for base64 + Rule("/shells//aas", methods=["GET"], endpoint=self.get_aas), + Rule("/shells//abc") # no endpoint => 501 not implemented + ]) + ]) + + def __call__(self, environ, start_response): + response = self.handle_request(Request(environ)) + return response(environ, start_response) + + @classmethod + def preferred_content_type(cls, headers: Headers, content_types: Iterable[str]) -> Optional[str]: + accept_str: Optional[str] = headers.get("accept") + if accept_str is None: + # return first content type in case accept http header is not specified + return next(iter(content_types)) + accept: MIMEAccept = parse_accept_header(accept_str, MIMEAccept) + return accept.best_match(content_types) + + @classmethod + def base64_param(cls, args: Dict[str, str], param: str) -> str: + try: + b64decoded = base64.b64decode(args[param]) + except Base64Error: + raise BadRequest(f"URL-Parameter '{param}' with value '{args[param]}' is not a valid base64 string!") + try: + return b64decoded.decode("utf-8") + except UnicodeDecodeError: + raise BadRequest(f"URL-Parameter '{param}' with base64 decoded value '{b64decoded!r}' is not valid utf-8!") + + @classmethod + def identifier_from_param(cls, args: Dict[str, str], param: str) -> model.Identifier: + id_type, id_ = cls.base64_param(args, param).split(":", 1) + try: + return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type]) + except KeyError: + raise BadRequest(f"'{id_type}' is not a valid identifier type!") + + # this is not used yet + @classmethod + def mandatory_request_param(cls, request: Request, param: str) -> str: + try: + return request.args[param] + except KeyError: + raise BadRequest(f"Parameter '{param}' is mandatory") + + def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: + identifiable = self.object_store.get(identifier) + if not isinstance(identifiable, type_): + raise BadRequest(f"Object specified by id {identifier} is of unexpected type {type(identifiable)}! " + f"Expected type: {type_}") + return identifiable + + def handle_request(self, request: Request): + adapter = self.url_map.bind_to_environ(request.environ) + # determine response content type + # TODO: implement xml responses + content_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) + if content_type is None: + return NotAcceptable(f"This server supports the following content types: " + + ", ".join(RESPONSE_TYPES.keys())) + response_type = RESPONSE_TYPES[content_type] + try: + endpoint, values = adapter.match() + if endpoint is None: + return NotImplemented("This route is not yet implemented.") + endpoint(request, values, response_type) + except HTTPException as e: + # raised error leaving this function => 500 + return e + + # http api issues (depth parameter, repository interface (id encoding)) + def get_aas(self, request: Request, args: Dict[str, str], response_type: Type[Response]): + # TODO: depth parameter + aas_id = self.identifier_from_param(args, "aas_id") + aas = self.get_obj_ts(aas_id, model.AssetAdministrationShell) + # TODO: encode with xml for xml responses + return response_type(asset_administration_shell_to_json(aas)) + + +if __name__ == "__main__": + from werkzeug.serving import run_simple + from aas.examples.data.example_aas import create_full_example + run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From d744e4837bae2613aa532006619003e5c11af5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 7 Jun 2020 20:15:50 +0200 Subject: [PATCH 141/474] adapter.http: fix codestyle --- basyx/aas/adapter/http.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e722409..3cd4329 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -39,15 +39,15 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs, content_type="text/xml") - """ - A mapping of supported content types to their respective ResponseType. - The first content type in this dict will be preferred if the requester doesn't specify preferred content types using - and HTTP-"Accept" header. - """ +""" +A mapping of supported content types to their respective ResponseType. +The first content type in this dict will be preferred if the requester doesn't specify preferred content types using +and HTTP-"Accept" header. +""" RESPONSE_TYPES = { "application/json": JsonResponse, -# "application/xml": XmlResponse, -# "text/xml": XmlResponseAlt + # "application/xml": XmlResponse, + # "text/xml": XmlResponseAlt } From 356289b0121d503c3fcb3f5c95d53695c7f98828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 14 Jun 2020 14:12:17 +0200 Subject: [PATCH 142/474] adapter.http: serialize response data in xml/json response classes adapter.http: add custom identifier converter for werkzeug adapter.http: restructure routing map, add first submodel route adapter.http: refine imports --- basyx/aas/adapter/http.py | 143 ++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 54 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 3cd4329..211f427 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -9,91 +9,123 @@ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. -import base64 -from binascii import Error as Base64Error -from werkzeug.datastructures import MIMEAccept, Headers -from werkzeug.exceptions import BadRequest, HTTPException, NotAcceptable, NotImplemented -from werkzeug.http import parse_accept_header -from werkzeug.routing import Map, Rule, Submount +import json +from lxml import etree # type: ignore +import werkzeug +from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented +from werkzeug.routing import Rule, Submount from werkzeug.wrappers import Request, Response from .. import model from ._generic import IDENTIFIER_TYPES_INVERSE -from .json.json_serialization import asset_administration_shell_to_json +from .json import json_serialization +from .xml import xml_serialization from typing import Dict, Iterable, Optional, Type -class JsonResponse(Response): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs, content_type="application/json") +def xml_element_to_str(element: etree.Element) -> str: + # namespaces will just get assigned a prefix like nsX, where X is a positive integer + # "aas" would be a better prefix for the AAS namespace + # TODO: find a way to specify a namespace map when serializing + return etree.tostring(element, xml_declaration=True, encoding="utf-8") + + +class APIResponse(Response): + def __init__(self, data, *args, **kwargs): + super().__init__(*args, **kwargs) + if isinstance(data, model.AssetAdministrationShell): + self.data = self.serialize_aas(data) + elif isinstance(data, model.Submodel): + self.data = self.serialize_sm(data) + # TODO: encode non-data responses with json/xml as well (e.g. results and errors) + + def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: + pass + + def serialize_sm(self, aas: model.Submodel) -> str: + pass + + +class JsonResponse(APIResponse): + def __init__(self, *args, content_type="application/json", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: + return json.dumps(aas, cls=json_serialization.AASToJsonEncoder) + def serialize_sm(self, sm: model.Submodel) -> str: + return json.dumps(sm, cls=json_serialization.AASToJsonEncoder) -class XmlResponse(Response): + +class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) + def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: + return xml_element_to_str(xml_serialization.asset_administration_shell_to_xml(aas)) + + def serialize_sm(self, sm: model.Submodel) -> str: + return xml_element_to_str(xml_serialization.submodel_to_xml(sm)) + class XmlResponseAlt(XmlResponse): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs, content_type="text/xml") + def __init__(self, *args, content_type="text/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) """ A mapping of supported content types to their respective ResponseType. -The first content type in this dict will be preferred if the requester doesn't specify preferred content types using -and HTTP-"Accept" header. +The first content type in this dict will be preferred if the requester +doesn't specify preferred content types using the HTTP Accept header. """ RESPONSE_TYPES = { "application/json": JsonResponse, - # "application/xml": XmlResponse, - # "text/xml": XmlResponseAlt + "application/xml": XmlResponse, + "text/xml": XmlResponseAlt } +class IdentifierConverter(werkzeug.routing.PathConverter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def to_python(self, value) -> model.Identifier: + id_type, id_ = super().to_python(value).split(":", 1) + try: + return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type]) + except KeyError: + raise BadRequest(f"'{id_type}' is not a valid identifier type!") + + class WSGIApp: def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store - self.url_map: Map = Map([ + self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - # TODO: custom decoder for base64 - Rule("/shells//aas", methods=["GET"], endpoint=self.get_aas), - Rule("/shells//abc") # no endpoint => 501 not implemented + Submount("/shells/", [ + Rule("/aas", methods=["GET"], endpoint=self.get_aas) + ]), + Submount("/submodels/", [ + Rule("/submodel", methods=["GET"], endpoint=self.get_sm) + ]) ]) - ]) + ], converters={"identifier": IdentifierConverter}) def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) return response(environ, start_response) @classmethod - def preferred_content_type(cls, headers: Headers, content_types: Iterable[str]) -> Optional[str]: + def preferred_content_type(cls, headers: werkzeug.datastructures.Headers, content_types: Iterable[str]) \ + -> Optional[str]: accept_str: Optional[str] = headers.get("accept") if accept_str is None: # return first content type in case accept http header is not specified return next(iter(content_types)) - accept: MIMEAccept = parse_accept_header(accept_str, MIMEAccept) + accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) return accept.best_match(content_types) - @classmethod - def base64_param(cls, args: Dict[str, str], param: str) -> str: - try: - b64decoded = base64.b64decode(args[param]) - except Base64Error: - raise BadRequest(f"URL-Parameter '{param}' with value '{args[param]}' is not a valid base64 string!") - try: - return b64decoded.decode("utf-8") - except UnicodeDecodeError: - raise BadRequest(f"URL-Parameter '{param}' with base64 decoded value '{b64decoded!r}' is not valid utf-8!") - - @classmethod - def identifier_from_param(cls, args: Dict[str, str], param: str) -> model.Identifier: - id_type, id_ = cls.base64_param(args, param).split(":", 1) - try: - return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type]) - except KeyError: - raise BadRequest(f"'{id_type}' is not a valid identifier type!") - # this is not used yet @classmethod def mandatory_request_param(cls, request: Request, param: str) -> str: @@ -105,14 +137,12 @@ def mandatory_request_param(cls, request: Request, param: str) -> str: def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): - raise BadRequest(f"Object specified by id {identifier} is of unexpected type {type(identifiable)}! " - f"Expected type: {type_}") + raise NotFound(f"No '{type_.__name__}' with id '{identifier}' found!") return identifiable def handle_request(self, request: Request): adapter = self.url_map.bind_to_environ(request.environ) # determine response content type - # TODO: implement xml responses content_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) if content_type is None: return NotAcceptable(f"This server supports the following content types: " @@ -122,18 +152,23 @@ def handle_request(self, request: Request): endpoint, values = adapter.match() if endpoint is None: return NotImplemented("This route is not yet implemented.") - endpoint(request, values, response_type) - except HTTPException as e: - # raised error leaving this function => 500 + return endpoint(request, values, response_type) + # any raised error that leaves this function will cause a 500 internal server error + # so catch raised http exceptions and return them + # TODO: apply response types to http exceptions + except werkzeug.exceptions.HTTPException as e: return e # http api issues (depth parameter, repository interface (id encoding)) - def get_aas(self, request: Request, args: Dict[str, str], response_type: Type[Response]): + def get_aas(self, request: Request, args: Dict, response_type: Type[APIResponse]) -> APIResponse: + # TODO: depth parameter + aas = self.get_obj_ts(args["aas_id"], model.AssetAdministrationShell) + return response_type(aas) + + def get_sm(self, request: Request, args: Dict, response_type: Type[APIResponse]) -> APIResponse: # TODO: depth parameter - aas_id = self.identifier_from_param(args, "aas_id") - aas = self.get_obj_ts(aas_id, model.AssetAdministrationShell) - # TODO: encode with xml for xml responses - return response_type(asset_administration_shell_to_json(aas)) + sm = self.get_obj_ts(args["sm_id"], model.Submodel) + return response_type(sm) if __name__ == "__main__": From a271ecf37750851266bc041de515c90cf13582bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 2 Jul 2020 13:35:30 +0200 Subject: [PATCH 143/474] adapter.http: move to folder with seperate files adapter._generic: add identifier URI encode/decode functions adapter.http: make api routes return ResponseData instead of the Response directly adapter.http: add Result + Message types + factory function --- basyx/aas/adapter/_generic.py | 16 +++ basyx/aas/adapter/http.py | 177 ----------------------------- basyx/aas/adapter/http/__init__.py | 12 ++ basyx/aas/adapter/http/__main__.py | 17 +++ basyx/aas/adapter/http/response.py | 115 +++++++++++++++++++ basyx/aas/adapter/http/wsgi.py | 110 ++++++++++++++++++ 6 files changed, 270 insertions(+), 177 deletions(-) delete mode 100644 basyx/aas/adapter/http.py create mode 100644 basyx/aas/adapter/http/__init__.py create mode 100644 basyx/aas/adapter/http/__main__.py create mode 100644 basyx/aas/adapter/http/response.py create mode 100644 basyx/aas/adapter/http/wsgi.py diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 34c3412..a65cf54 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -11,6 +11,7 @@ from typing import Dict, Type from basyx.aas import model +import urllib.parse # XML Namespace definition XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} @@ -113,3 +114,18 @@ KEY_TYPES_CLASSES_INVERSE: Dict[model.KeyTypes, Type[model.Referable]] = \ {v: k for k, v in model.KEY_TYPES_CLASSES.items()} + + +def identifier_uri_encode(id_: model.Identifier) -> str: + return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") + + +def identifier_uri_decode(id_str: str) -> model.Identifier: + try: + id_type_str, id_ = id_str.split(":", 1) + except ValueError as e: + raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") + id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) + if id_type is None: + raise ValueError(f"Identifier Type '{id_type_str}' is invalid") + return model.Identifier(urllib.parse.unquote(id_), id_type) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py deleted file mode 100644 index 211f427..0000000 --- a/basyx/aas/adapter/http.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -import json -from lxml import etree # type: ignore -import werkzeug -from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented -from werkzeug.routing import Rule, Submount -from werkzeug.wrappers import Request, Response - -from .. import model -from ._generic import IDENTIFIER_TYPES_INVERSE -from .json import json_serialization -from .xml import xml_serialization - -from typing import Dict, Iterable, Optional, Type - - -def xml_element_to_str(element: etree.Element) -> str: - # namespaces will just get assigned a prefix like nsX, where X is a positive integer - # "aas" would be a better prefix for the AAS namespace - # TODO: find a way to specify a namespace map when serializing - return etree.tostring(element, xml_declaration=True, encoding="utf-8") - - -class APIResponse(Response): - def __init__(self, data, *args, **kwargs): - super().__init__(*args, **kwargs) - if isinstance(data, model.AssetAdministrationShell): - self.data = self.serialize_aas(data) - elif isinstance(data, model.Submodel): - self.data = self.serialize_sm(data) - # TODO: encode non-data responses with json/xml as well (e.g. results and errors) - - def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: - pass - - def serialize_sm(self, aas: model.Submodel) -> str: - pass - - -class JsonResponse(APIResponse): - def __init__(self, *args, content_type="application/json", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: - return json.dumps(aas, cls=json_serialization.AASToJsonEncoder) - - def serialize_sm(self, sm: model.Submodel) -> str: - return json.dumps(sm, cls=json_serialization.AASToJsonEncoder) - - -class XmlResponse(APIResponse): - def __init__(self, *args, content_type="application/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: - return xml_element_to_str(xml_serialization.asset_administration_shell_to_xml(aas)) - - def serialize_sm(self, sm: model.Submodel) -> str: - return xml_element_to_str(xml_serialization.submodel_to_xml(sm)) - - -class XmlResponseAlt(XmlResponse): - def __init__(self, *args, content_type="text/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - -""" -A mapping of supported content types to their respective ResponseType. -The first content type in this dict will be preferred if the requester -doesn't specify preferred content types using the HTTP Accept header. -""" -RESPONSE_TYPES = { - "application/json": JsonResponse, - "application/xml": XmlResponse, - "text/xml": XmlResponseAlt -} - - -class IdentifierConverter(werkzeug.routing.PathConverter): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def to_python(self, value) -> model.Identifier: - id_type, id_ = super().to_python(value).split(":", 1) - try: - return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type]) - except KeyError: - raise BadRequest(f"'{id_type}' is not a valid identifier type!") - - -class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore): - self.object_store: model.AbstractObjectStore = object_store - self.url_map = werkzeug.routing.Map([ - Submount("/api/v1", [ - Submount("/shells/", [ - Rule("/aas", methods=["GET"], endpoint=self.get_aas) - ]), - Submount("/submodels/", [ - Rule("/submodel", methods=["GET"], endpoint=self.get_sm) - ]) - ]) - ], converters={"identifier": IdentifierConverter}) - - def __call__(self, environ, start_response): - response = self.handle_request(Request(environ)) - return response(environ, start_response) - - @classmethod - def preferred_content_type(cls, headers: werkzeug.datastructures.Headers, content_types: Iterable[str]) \ - -> Optional[str]: - accept_str: Optional[str] = headers.get("accept") - if accept_str is None: - # return first content type in case accept http header is not specified - return next(iter(content_types)) - accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) - return accept.best_match(content_types) - - # this is not used yet - @classmethod - def mandatory_request_param(cls, request: Request, param: str) -> str: - try: - return request.args[param] - except KeyError: - raise BadRequest(f"Parameter '{param}' is mandatory") - - def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: - identifiable = self.object_store.get(identifier) - if not isinstance(identifiable, type_): - raise NotFound(f"No '{type_.__name__}' with id '{identifier}' found!") - return identifiable - - def handle_request(self, request: Request): - adapter = self.url_map.bind_to_environ(request.environ) - # determine response content type - content_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) - if content_type is None: - return NotAcceptable(f"This server supports the following content types: " - + ", ".join(RESPONSE_TYPES.keys())) - response_type = RESPONSE_TYPES[content_type] - try: - endpoint, values = adapter.match() - if endpoint is None: - return NotImplemented("This route is not yet implemented.") - return endpoint(request, values, response_type) - # any raised error that leaves this function will cause a 500 internal server error - # so catch raised http exceptions and return them - # TODO: apply response types to http exceptions - except werkzeug.exceptions.HTTPException as e: - return e - - # http api issues (depth parameter, repository interface (id encoding)) - def get_aas(self, request: Request, args: Dict, response_type: Type[APIResponse]) -> APIResponse: - # TODO: depth parameter - aas = self.get_obj_ts(args["aas_id"], model.AssetAdministrationShell) - return response_type(aas) - - def get_sm(self, request: Request, args: Dict, response_type: Type[APIResponse]) -> APIResponse: - # TODO: depth parameter - sm = self.get_obj_ts(args["sm_id"], model.Submodel) - return response_type(sm) - - -if __name__ == "__main__": - from werkzeug.serving import run_simple - from aas.examples.data.example_aas import create_full_example - run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/basyx/aas/adapter/http/__init__.py b/basyx/aas/adapter/http/__init__.py new file mode 100644 index 0000000..27808a8 --- /dev/null +++ b/basyx/aas/adapter/http/__init__.py @@ -0,0 +1,12 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from .wsgi import WSGIApp diff --git a/basyx/aas/adapter/http/__main__.py b/basyx/aas/adapter/http/__main__.py new file mode 100644 index 0000000..59bf98b --- /dev/null +++ b/basyx/aas/adapter/http/__main__.py @@ -0,0 +1,17 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from werkzeug.serving import run_simple + +from aas.examples.data.example_aas import create_full_example +from . import WSGIApp + +run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py new file mode 100644 index 0000000..196976d --- /dev/null +++ b/basyx/aas/adapter/http/response.py @@ -0,0 +1,115 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import abc +import enum +import json +from lxml import etree # type: ignore +import werkzeug.exceptions +from werkzeug.wrappers import Response + +from aas import model +from aas.adapter.json import json_serialization +from aas.adapter.xml import xml_serialization + +from typing import Dict, List, Optional, Type, Union + + +@enum.unique +class MessageType(enum.Enum): + UNSPECIFIED = enum.auto() + DEBUG = enum.auto() + INFORMATION = enum.auto() + WARNING = enum.auto() + ERROR = enum.auto() + FATAL = enum.auto() + EXCEPTION = enum.auto() + + +class Message: + def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNSPECIFIED): + self.code = code + self.text = text + self.message_type = message_type + + +class Result: + def __init__(self, success: bool, is_exception: bool, messages: List[Message]): + self.success = success + self.is_exception = is_exception + self.messages = messages + + +ResponseDataType = Union[Result, model.Referable, List[model.Referable]] + + +class ResponseData(BaseException): + def __init__(self, data: ResponseDataType, http_status_code: int = 200): + self.data = data + self.http_status_code = 200 + + +class APIResponse(abc.ABC, Response): + def __init__(self, data: ResponseData, *args, **kwargs): + super().__init__(*args, **kwargs) + self.status_code = data.http_status_code + self.data = self.serialize(data.data) + + @abc.abstractmethod + def serialize(self, data: ResponseDataType) -> str: + pass + + +class JsonResponse(APIResponse): + def __init__(self, *args, content_type="application/json", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize(self, data: ResponseDataType) -> str: + return json.dumps(data, cls=json_serialization.AASToJsonEncoder) + + +class XmlResponse(APIResponse): + def __init__(self, *args, content_type="application/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize(self, data: ResponseDataType) -> str: + return "" + + +class XmlResponseAlt(XmlResponse): + def __init__(self, *args, content_type="text/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + +""" +A mapping of supported content types to their respective ResponseType. +The first content type in this dict will be preferred if the requester +doesn't specify preferred content types using the HTTP Accept header. +""" +RESPONSE_TYPES: Dict[str, Type[APIResponse]] = { + "application/json": JsonResponse, + "application/xml": XmlResponse, + "text/xml": XmlResponseAlt +} + + +def create_result_response(code: str, text: str, message_type: MessageType, http_status_code: int = 200, + success: bool = False, is_exception: bool = False) -> ResponseData: + message = Message(code, text, message_type) + result = Result(success, is_exception, [message]) + return ResponseData(result, http_status_code) + + +def xml_element_to_str(element: etree.Element) -> str: + # namespaces will just get assigned a prefix like nsX, where X is a positive integer + # "aas" would be a better prefix for the AAS namespace + # TODO: find a way to specify a namespace map when serializing + return etree.tostring(element, xml_declaration=True, encoding="utf-8") diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py new file mode 100644 index 0000000..6762169 --- /dev/null +++ b/basyx/aas/adapter/http/wsgi.py @@ -0,0 +1,110 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + + +import werkzeug +from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented +from werkzeug.routing import Rule, Submount +from werkzeug.wrappers import Request + +from aas import model + +from .response import RESPONSE_TYPES, MessageType, ResponseData, create_result_response +from .._generic import identifier_uri_decode, identifier_uri_encode + +from typing import Dict, Iterable, Optional, Type + + +class IdentifierConverter(werkzeug.routing.UnicodeConverter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def to_url(self, value: model.Identifier) -> str: + return super().to_url(identifier_uri_encode(value)) + + def to_python(self, value: str) -> model.Identifier: + try: + return identifier_uri_decode(super().to_python(value)) + except ValueError as e: + raise BadRequest(str(e)) + + +class WSGIApp: + def __init__(self, object_store: model.AbstractObjectStore): + self.object_store: model.AbstractObjectStore = object_store + self.url_map = werkzeug.routing.Map([ + Submount("/api/v1.0", [ + Submount("/shells/", [ + Rule("/aas", methods=["GET"], endpoint=self.get_aas) + ]), + Submount("/submodels/", [ + Rule("/submodel", methods=["GET"], endpoint=self.get_sm) + ]) + ]) + ], converters={"identifier": IdentifierConverter}) + + def __call__(self, environ, start_response): + response = self.handle_request(Request(environ)) + return response(environ, start_response) + + @classmethod + def preferred_content_type(cls, headers: werkzeug.datastructures.Headers, content_types: Iterable[str]) \ + -> Optional[str]: + accept_str: Optional[str] = headers.get("accept") + if accept_str is None: + # return first content type in case accept http header is not specified + return next(iter(content_types)) + accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) + return accept.best_match(content_types) + + # this is not used yet + @classmethod + def mandatory_request_param(cls, request: Request, param: str) -> str: + req_param = request.args.get(param) + if req_param is None: + raise create_result_response("mandatory_param_missing", f"Parameter '{param}' is mandatory", + MessageType.ERROR, 400) + return req_param + + def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: + identifiable = self.object_store.get(identifier) + if not isinstance(identifiable, type_): + raise create_result_response("identifier_not_found", f"No {type_.__name__} with {identifier} found!", + MessageType.ERROR, 404) + return identifiable + + def handle_request(self, request: Request): + adapter = self.url_map.bind_to_environ(request.environ) + # determine response content type + preferred_response_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) + if preferred_response_type is None: + return NotAcceptable(f"This server supports the following content types: " + + ", ".join(RESPONSE_TYPES.keys())) + response_type = RESPONSE_TYPES[preferred_response_type] + try: + endpoint, values = adapter.match() + if endpoint is None: + return NotImplemented("This route is not yet implemented.") + return response_type(endpoint(request, values)) + # any raised error that leaves this function will cause a 500 internal server error + # so catch raised http exceptions and return them + except werkzeug.exceptions.HTTPException as e: + return e + except ResponseData as rd: + return response_type(rd) + + def get_aas(self, request: Request, url_args: Dict) -> ResponseData: + # TODO: depth parameter + return ResponseData(self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + + def get_sm(self, request: Request, url_args: Dict) -> ResponseData: + # TODO: depth parameter + return ResponseData(self.get_obj_ts(url_args["sm_id"], model.Submodel)) From bfe93817ff9662f51f68908f19c4ac5ad3e2d057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 2 Jul 2020 20:31:02 +0200 Subject: [PATCH 144/474] adapter.http: make http endpoints return a response directly --- basyx/aas/adapter/http/response.py | 65 +++++++++++++++--------------- basyx/aas/adapter/http/wsgi.py | 57 +++++++++++++------------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py index 196976d..1d131ab 100644 --- a/basyx/aas/adapter/http/response.py +++ b/basyx/aas/adapter/http/response.py @@ -14,7 +14,7 @@ import json from lxml import etree # type: ignore import werkzeug.exceptions -from werkzeug.wrappers import Response +from werkzeug.wrappers import Request, Response from aas import model from aas.adapter.json import json_serialization @@ -48,23 +48,16 @@ def __init__(self, success: bool, is_exception: bool, messages: List[Message]): self.messages = messages -ResponseDataType = Union[Result, model.Referable, List[model.Referable]] - - -class ResponseData(BaseException): - def __init__(self, data: ResponseDataType, http_status_code: int = 200): - self.data = data - self.http_status_code = 200 +ResponseData = Union[Result, model.Referable, List[model.Referable]] class APIResponse(abc.ABC, Response): def __init__(self, data: ResponseData, *args, **kwargs): super().__init__(*args, **kwargs) - self.status_code = data.http_status_code - self.data = self.serialize(data.data) + self.data = self.serialize(data) @abc.abstractmethod - def serialize(self, data: ResponseDataType) -> str: + def serialize(self, data: ResponseData) -> str: pass @@ -72,7 +65,7 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseDataType) -> str: + def serialize(self, data: ResponseData) -> str: return json.dumps(data, cls=json_serialization.AASToJsonEncoder) @@ -80,7 +73,7 @@ class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseDataType) -> str: + def serialize(self, data: ResponseData) -> str: return "" @@ -89,27 +82,35 @@ def __init__(self, *args, content_type="text/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) -""" -A mapping of supported content types to their respective ResponseType. -The first content type in this dict will be preferred if the requester -doesn't specify preferred content types using the HTTP Accept header. -""" -RESPONSE_TYPES: Dict[str, Type[APIResponse]] = { - "application/json": JsonResponse, - "application/xml": XmlResponse, - "text/xml": XmlResponseAlt -} - - -def create_result_response(code: str, text: str, message_type: MessageType, http_status_code: int = 200, - success: bool = False, is_exception: bool = False) -> ResponseData: - message = Message(code, text, message_type) - result = Result(success, is_exception, [message]) - return ResponseData(result, http_status_code) - - def xml_element_to_str(element: etree.Element) -> str: # namespaces will just get assigned a prefix like nsX, where X is a positive integer # "aas" would be a better prefix for the AAS namespace # TODO: find a way to specify a namespace map when serializing return etree.tostring(element, xml_declaration=True, encoding="utf-8") + + +def get_response_type(request: Request) -> Type[APIResponse]: + response_types: Dict[str, Type[APIResponse]] = { + "application/json": JsonResponse, + "application/xml": XmlResponse, + "text/xml": XmlResponseAlt + } + accept_str: Optional[str] = request.headers.get("accept") + if accept_str is None: + # default to json in case unspecified + return JsonResponse + accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) + mime_type = accept.best_match(response_types) + if mime_type is None: + raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " + + ", ".join(response_types.keys())) + return response_types[mime_type] + + +def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ + -> APIResponse: + success: bool = exception.code < 400 if exception.code is not None else False + message_type = MessageType.INFORMATION if success else MessageType.ERROR + message = Message(type(exception).__name__, exception.description if exception.description is not None else "", + message_type) + return response_type(Result(success, not success, [message])) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py index 6762169..9e4a581 100644 --- a/basyx/aas/adapter/http/wsgi.py +++ b/basyx/aas/adapter/http/wsgi.py @@ -10,6 +10,7 @@ # specific language governing permissions and limitations under the License. +import urllib.parse import werkzeug from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented from werkzeug.routing import Rule, Submount @@ -17,12 +18,27 @@ from aas import model -from .response import RESPONSE_TYPES, MessageType, ResponseData, create_result_response -from .._generic import identifier_uri_decode, identifier_uri_encode +from .response import APIResponse, get_response_type, http_exception_to_response +from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE from typing import Dict, Iterable, Optional, Type +def identifier_uri_encode(id_: model.Identifier) -> str: + return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") + + +def identifier_uri_decode(id_str: str) -> model.Identifier: + try: + id_type_str, id_ = id_str.split(":", 1) + except ValueError as e: + raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") + id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) + if id_type is None: + raise ValueError(f"Identifier Type '{id_type_str}' is invalid") + return model.Identifier(urllib.parse.unquote(id_), id_type) + + class IdentifierConverter(werkzeug.routing.UnicodeConverter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -55,56 +71,39 @@ def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) return response(environ, start_response) - @classmethod - def preferred_content_type(cls, headers: werkzeug.datastructures.Headers, content_types: Iterable[str]) \ - -> Optional[str]: - accept_str: Optional[str] = headers.get("accept") - if accept_str is None: - # return first content type in case accept http header is not specified - return next(iter(content_types)) - accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) - return accept.best_match(content_types) - # this is not used yet @classmethod def mandatory_request_param(cls, request: Request, param: str) -> str: req_param = request.args.get(param) if req_param is None: - raise create_result_response("mandatory_param_missing", f"Parameter '{param}' is mandatory", - MessageType.ERROR, 400) + raise BadRequest(f"Parameter '{param}' is mandatory") return req_param def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): - raise create_result_response("identifier_not_found", f"No {type_.__name__} with {identifier} found!", - MessageType.ERROR, 404) + raise NotFound(f"No {type_.__name__} with {identifier} found!") return identifiable def handle_request(self, request: Request): adapter = self.url_map.bind_to_environ(request.environ) # determine response content type - preferred_response_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) - if preferred_response_type is None: - return NotAcceptable(f"This server supports the following content types: " - + ", ".join(RESPONSE_TYPES.keys())) - response_type = RESPONSE_TYPES[preferred_response_type] try: endpoint, values = adapter.match() if endpoint is None: return NotImplemented("This route is not yet implemented.") - return response_type(endpoint(request, values)) + return endpoint(request, values) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them except werkzeug.exceptions.HTTPException as e: - return e - except ResponseData as rd: - return response_type(rd) + return http_exception_to_response(e, get_response_type(request)) - def get_aas(self, request: Request, url_args: Dict) -> ResponseData: + def get_aas(self, request: Request, url_args: Dict) -> APIResponse: # TODO: depth parameter - return ResponseData(self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + response = get_response_type(request) + return response(self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) - def get_sm(self, request: Request, url_args: Dict) -> ResponseData: + def get_sm(self, request: Request, url_args: Dict) -> APIResponse: # TODO: depth parameter - return ResponseData(self.get_obj_ts(url_args["sm_id"], model.Submodel)) + response = get_response_type(request) + return response(self.get_obj_ts(url_args["sm_id"], model.Submodel)) From fad7a416c8c1d1b4f36a9bb1996efe80df7f9139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 16 Jul 2020 12:44:17 +0200 Subject: [PATCH 145/474] adapter.http: add json/xml result serialization adapter.http: add request body parsing adapter.http: implement all remaining aas routes --- basyx/aas/adapter/http/response.py | 125 +++++++++++-- basyx/aas/adapter/http/wsgi.py | 278 +++++++++++++++++++++++++++-- 2 files changed, 371 insertions(+), 32 deletions(-) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py index 1d131ab..7564784 100644 --- a/basyx/aas/adapter/http/response.py +++ b/basyx/aas/adapter/http/response.py @@ -20,7 +20,7 @@ from aas.adapter.json import json_serialization from aas.adapter.xml import xml_serialization -from typing import Dict, List, Optional, Type, Union +from typing import Dict, List, Sequence, Type, Union @enum.unique @@ -33,12 +33,15 @@ class MessageType(enum.Enum): FATAL = enum.auto() EXCEPTION = enum.auto() + def __str__(self): + return self.name.capitalize() + class Message: def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNSPECIFIED): + self.message_type = message_type self.code = code self.text = text - self.message_type = message_type class Result: @@ -48,16 +51,23 @@ def __init__(self, success: bool, is_exception: bool, messages: List[Message]): self.messages = messages -ResponseData = Union[Result, model.Referable, List[model.Referable]] +# not all sequence types are json serializable, but Sequence is covariant, +# which is necessary for List[Submodel] or List[AssetAdministrationShell] to be valid for List[Referable] +ResponseData = Union[Result, model.Referable, Sequence[model.Referable]] + +ResponseDataInternal = Union[Result, model.Referable, List[model.Referable]] class APIResponse(abc.ABC, Response): def __init__(self, data: ResponseData, *args, **kwargs): super().__init__(*args, **kwargs) + # convert possible sequence types to List (see line 54-55) + if isinstance(data, Sequence): + data = list(data) self.data = self.serialize(data) @abc.abstractmethod - def serialize(self, data: ResponseData) -> str: + def serialize(self, data: ResponseDataInternal) -> str: pass @@ -65,16 +75,17 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseData) -> str: - return json.dumps(data, cls=json_serialization.AASToJsonEncoder) + def serialize(self, data: ResponseDataInternal) -> str: + return json.dumps(data, cls=ResultToJsonEncoder if isinstance(data, Result) + else json_serialization.AASToJsonEncoder) class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseData) -> str: - return "" + def serialize(self, data: ResponseDataInternal) -> str: + return xml_element_to_str(response_data_to_xml(data)) class XmlResponseAlt(XmlResponse): @@ -89,18 +100,101 @@ def xml_element_to_str(element: etree.Element) -> str: return etree.tostring(element, xml_declaration=True, encoding="utf-8") +class ResultToJsonEncoder(json.JSONEncoder): + def default(self, obj: object) -> object: + if isinstance(obj, Result): + return result_to_json(obj) + if isinstance(obj, Message): + return message_to_json(obj) + if isinstance(obj, MessageType): + return str(obj) + return super().default(obj) + + +def result_to_json(result: Result) -> Dict[str, object]: + return { + "success": result.success, + "isException": result.is_exception, + "messages": result.messages + } + + +def message_to_json(message: Message) -> Dict[str, object]: + return { + "messageType": message.message_type, + "code": message.code, + "text": message.text + } + + +def response_data_to_xml(data: ResponseDataInternal) -> etree.Element: + if isinstance(data, Result): + return result_to_xml(data) + if isinstance(data, model.Referable): + return referable_to_xml(data) + if isinstance(data, List): + elements: List[etree.Element] = [referable_to_xml(obj) for obj in data] + wrapper = etree.Element("list") + for elem in elements: + wrapper.append(elem) + return wrapper + + +def referable_to_xml(data: model.Referable) -> etree.Element: + # TODO: maybe support more referables + if isinstance(data, model.AssetAdministrationShell): + return xml_serialization.asset_administration_shell_to_xml(data) + if isinstance(data, model.Submodel): + return xml_serialization.submodel_to_xml(data) + if isinstance(data, model.SubmodelElement): + return xml_serialization.submodel_element_to_xml(data) + if isinstance(data, model.ConceptDictionary): + return xml_serialization.concept_dictionary_to_xml(data) + if isinstance(data, model.ConceptDescription): + return xml_serialization.concept_description_to_xml(data) + if isinstance(data, model.View): + return xml_serialization.view_to_xml(data) + if isinstance(data, model.Asset): + return xml_serialization.asset_to_xml(data) + raise TypeError(f"Referable {data} couldn't be serialized to xml (unsupported type)!") + + +def result_to_xml(result: Result) -> etree.Element: + result_elem = etree.Element("result") + success_elem = etree.Element("success") + success_elem.text = xml_serialization.boolean_to_xml(result.success) + is_exception_elem = etree.Element("isException") + is_exception_elem.text = xml_serialization.boolean_to_xml(result.is_exception) + messages_elem = etree.Element("messages") + for message in result.messages: + messages_elem.append(message_to_xml(message)) + result_elem.append(success_elem) + result_elem.append(is_exception_elem) + result_elem.append(messages_elem) + return result_elem + + +def message_to_xml(message: Message) -> etree.Element: + message_elem = etree.Element("message") + message_type_elem = etree.Element("messageType") + message_type_elem.text = str(message.message_type) + code_elem = etree.Element("code") + code_elem.text = message.code + text_elem = etree.Element("text") + text_elem.text = message.text + message_elem.append(message_type_elem) + message_elem.append(code_elem) + message_elem.append(text_elem) + return message_elem + + def get_response_type(request: Request) -> Type[APIResponse]: response_types: Dict[str, Type[APIResponse]] = { "application/json": JsonResponse, "application/xml": XmlResponse, "text/xml": XmlResponseAlt } - accept_str: Optional[str] = request.headers.get("accept") - if accept_str is None: - # default to json in case unspecified - return JsonResponse - accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) - mime_type = accept.best_match(response_types) + mime_type = request.accept_mimetypes.best_match(response_types) if mime_type is None: raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " + ", ".join(response_types.keys())) @@ -113,4 +207,5 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res message_type = MessageType.INFORMATION if success else MessageType.ERROR message = Message(type(exception).__name__, exception.description if exception.description is not None else "", message_type) - return response_type(Result(success, not success, [message])) + return response_type(Result(success, not success, [message]), status=exception.code, + headers=exception.get_headers()) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py index 9e4a581..455884e 100644 --- a/basyx/aas/adapter/http/wsgi.py +++ b/basyx/aas/adapter/http/wsgi.py @@ -10,18 +10,66 @@ # specific language governing permissions and limitations under the License. +import io +import json +from lxml import etree # type: ignore import urllib.parse import werkzeug -from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented +from werkzeug.exceptions import BadRequest, InternalServerError, NotFound, NotImplemented from werkzeug.routing import Rule, Submount -from werkzeug.wrappers import Request +from werkzeug.wrappers import Request, Response from aas import model - -from .response import APIResponse, get_response_type, http_exception_to_response +from ..xml import xml_deserialization +from ..json import json_deserialization from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE +from .response import get_response_type, http_exception_to_response + +from typing import Dict, Optional, Type + + +def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: + """ + TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent + running out of memory. but it doesn't state how to check the content length + also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema + """ + xml_constructors = { + model.Submodel: xml_deserialization._construct_submodel, + model.View: xml_deserialization._construct_view, + model.ConceptDictionary: xml_deserialization._construct_concept_dictionary, + model.ConceptDescription: xml_deserialization._construct_concept_description, + model.SubmodelElement: xml_deserialization._construct_submodel_element + } + + valid_content_types = ("application/json", "application/xml", "text/xml") + + if request.mimetype not in valid_content_types: + raise werkzeug.exceptions.UnsupportedMediaType(f"Invalid content-type: {request.mimetype}! Supported types: " + + ", ".join(valid_content_types)) + + if request.mimetype == "application/json": + json_data = request.get_data() + try: + rv = json.loads(json_data, cls=json_deserialization.AASFromJsonDecoder) + except json.decoder.JSONDecodeError as e: + raise BadRequest(str(e)) from e + else: + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) + xml_data = io.BytesIO(request.get_data()) + try: + tree = etree.parse(xml_data, parser) + except etree.XMLSyntaxError as e: + raise BadRequest(str(e)) from e + # TODO: check tag of root element + root = tree.getroot() + try: + rv = xml_constructors[expect_type](root, failsafe=False) + except (KeyError, ValueError) as e: + raise BadRequest(xml_deserialization._exception_to_str(e)) from e -from typing import Dict, Iterable, Optional, Type + assert(isinstance(rv, expect_type)) + return rv def identifier_uri_encode(id_: model.Identifier) -> str: @@ -59,10 +107,33 @@ def __init__(self, object_store: model.AbstractObjectStore): self.url_map = werkzeug.routing.Map([ Submount("/api/v1.0", [ Submount("/shells/", [ - Rule("/aas", methods=["GET"], endpoint=self.get_aas) + Rule("/aas", methods=["GET"], endpoint=self.get_aas), + Submount("/aas", [ + Rule("/asset", methods=["GET"], endpoint=self.get_aas_asset), + Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodels), + Rule("/submodels", methods=["PUT"], endpoint=self.put_aas_submodels), + Rule("/views", methods=["GET"], endpoint=self.get_aas_views), + Rule("/views/", methods=["GET"], + endpoint=self.get_aas_views_specific), + Rule("/views/", methods=["DELETE"], + endpoint=self.delete_aas_views_specific), + Rule("/conceptDictionaries", methods=["GET"], endpoint=self.get_aas_concept_dictionaries), + Rule("/conceptDictionaries", methods=["PUT"], endpoint=self.put_aas_concept_dictionaries), + Rule("/conceptDictionaries/", methods=["GET"], + endpoint=self.get_aas_concept_dictionaries_specific), + Rule("/conceptDictionaries/", methods=["DELETE"], + endpoint=self.delete_aas_concept_dictionaries_specific), + Rule("/submodels/", methods=["GET"], + endpoint=self.get_aas_submodels_specific), + Rule("/submodels/", methods=["DELETE"], + endpoint=self.delete_aas_submodels_specific), + ]) ]), - Submount("/submodels/", [ - Rule("/submodel", methods=["GET"], endpoint=self.get_sm) + Submount("/submodels/", [ + Rule("/submodel", methods=["GET"], endpoint=self.get_submodel), + Submount("/submodel", [ + + ]) ]) ]) ], converters={"identifier": IdentifierConverter}) @@ -85,25 +156,198 @@ def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._I raise NotFound(f"No {type_.__name__} with {identifier} found!") return identifiable + def resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: + try: + return reference.resolve(self.object_store) + except (KeyError, TypeError, model.base.UnexpectedTypeError) as e: + raise InternalServerError(xml_deserialization._exception_to_str(e)) from e + def handle_request(self, request: Request): adapter = self.url_map.bind_to_environ(request.environ) - # determine response content type try: endpoint, values = adapter.match() if endpoint is None: - return NotImplemented("This route is not yet implemented.") + raise NotImplemented("This route is not yet implemented.") return endpoint(request, values) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them + except werkzeug.exceptions.NotAcceptable as e: + return e except werkzeug.exceptions.HTTPException as e: - return http_exception_to_response(e, get_response_type(request)) + try: + # get_response_type() may raise a NotAcceptable error, so we have to handle that + return http_exception_to_response(e, get_response_type(request)) + except werkzeug.exceptions.NotAcceptable as e: + return e + + def get_aas(self, request: Request, url_args: Dict) -> Response: + # TODO: depth parameter + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + return response_t(aas) + + def get_aas_asset(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + asset = self.resolve_reference(aas.asset) + asset.update() + return response_t(asset) + + def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: + # TODO: depth parameter + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + submodels = [self.resolve_reference(ref) for ref in aas.submodel] + for submodel in submodels: + submodel.update() + identification_id: Optional[str] = request.args.get("identification.id") + if identification_id is not None: + # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 + submodels = filter(lambda s: identification_id in s.identification.id, submodels) # type: ignore + semantic_id: Optional[str] = request.args.get("semanticId") + if semantic_id is not None: + # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 + submodels = filter(lambda s: s.semantic_id is not None # type: ignore + and len(s.semantic_id.key) > 0 + and semantic_id in s.semantic_id.key[0].value, submodels) # type: ignore + return response_t(list(submodels)) + + def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + new_submodel = parse_request_body(request, model.Submodel) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + current_submodel = None + for s in iter(self.resolve_reference(ref) for ref in aas.submodel): + if s.identification == new_submodel.identification: + current_submodel = s + break + if current_submodel is None: + aas.submodel.add(model.AASReference.from_referable(new_submodel)) + aas.commit() + not_referenced_submodel = self.object_store.get(new_submodel.identification) + assert(isinstance(not_referenced_submodel, model.Submodel)) + current_submodel = not_referenced_submodel + if current_submodel is not None: + self.object_store.discard(current_submodel) + self.object_store.add(new_submodel) + return response_t(new_submodel, status=201) + + def get_aas_views(self, request: Request, url_args: Dict) -> Response: + # TODO: filter parameter + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + if len(aas.view) == 0: + raise NotFound("No views found!") + return response_t(list(aas.view)) + + def put_aas_views(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + new_view = parse_request_body(request, model.View) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + old_view = aas.view.get(new_view.id_short) + if old_view is not None: + aas.view.discard(old_view) + aas.view.add(new_view) + aas.commit() + return response_t(new_view, status=201) - def get_aas(self, request: Request, url_args: Dict) -> APIResponse: + def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + view = aas.view.get(id_short) + if view is None: + raise NotFound(f"No view with idShort '{id_short}' found!") + view.update() + return response_t(view) + + def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + view = aas.view.get(id_short) + if view is None: + raise NotFound(f"No view with idShort '{id_short}' found!") + view.update() + aas.view.remove(view.id_short) + return Response(status=204) + + def get_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: # TODO: depth parameter - response = get_response_type(request) - return response(self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + if len(aas.concept_dictionary) == 0: + raise NotFound("No concept dictionaries found!") + return response_t(list(aas.concept_dictionary)) + + def put_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + new_concept_dictionary = parse_request_body(request, model.ConceptDictionary) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + old_concept_dictionary = aas.concept_dictionary.get(new_concept_dictionary.id_short) + if old_concept_dictionary is not None: + aas.concept_dictionary.discard(old_concept_dictionary) + aas.concept_dictionary.add(new_concept_dictionary) + aas.commit() + return response_t(new_concept_dictionary, status=201) + + def get_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + concept_dictionary = aas.concept_dictionary.get(id_short) + if concept_dictionary is None: + raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") + concept_dictionary.update() + return response_t(concept_dictionary) + + def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + concept_dictionaries = aas.concept_dictionary.get(id_short) + if concept_dictionaries is None: + raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") + concept_dictionaries.update() + aas.view.remove(concept_dictionaries.id_short) + return Response(status=204) + + def get_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + for submodel in iter(self.resolve_reference(ref) for ref in aas.submodel): + submodel.update() + if submodel.id_short == id_short: + return response_t(submodel) + raise NotFound(f"No submodel with idShort '{id_short}' found!") + + def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + for ref in aas.submodel: + submodel = self.resolve_reference(ref) + submodel.update() + if submodel.id_short == id_short: + aas.submodel.discard(ref) + self.object_store.discard(submodel) + return Response(status=204) + raise NotFound(f"No submodel with idShort '{id_short}' found!") - def get_sm(self, request: Request, url_args: Dict) -> APIResponse: + def get_submodel(self, request: Request, url_args: Dict) -> Response: # TODO: depth parameter - response = get_response_type(request) - return response(self.get_obj_ts(url_args["sm_id"], model.Submodel)) + response_t = get_response_type(request) + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(submodel) From cf32e02c94792347ff2ed9f8cb62ecd13465e518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 25 Jul 2020 17:43:10 +0200 Subject: [PATCH 146/474] adapter.http: add more submodel routes --- basyx/aas/adapter/http/response.py | 7 ++- basyx/aas/adapter/http/wsgi.py | 93 ++++++++++++++++++++++++------ 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py index 7564784..f6e47af 100644 --- a/basyx/aas/adapter/http/response.py +++ b/basyx/aas/adapter/http/response.py @@ -203,9 +203,12 @@ def get_response_type(request: Request) -> Type[APIResponse]: def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ -> APIResponse: + headers = exception.get_headers() + location = exception.get_response().location + if location is not None: + headers.append(("Location", location)) success: bool = exception.code < 400 if exception.code is not None else False message_type = MessageType.INFORMATION if success else MessageType.ERROR message = Message(type(exception).__name__, exception.description if exception.description is not None else "", message_type) - return response_type(Result(success, not success, [message]), status=exception.code, - headers=exception.get_headers()) + return response_type(Result(success, not success, [message]), status=exception.code, headers=headers) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py index 455884e..60d6d0e 100644 --- a/basyx/aas/adapter/http/wsgi.py +++ b/basyx/aas/adapter/http/wsgi.py @@ -25,7 +25,7 @@ from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE from .response import get_response_type, http_exception_to_response -from typing import Dict, Optional, Type +from typing import Dict, List, Optional, Type def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: @@ -88,9 +88,6 @@ def identifier_uri_decode(id_str: str) -> model.Identifier: class IdentifierConverter(werkzeug.routing.UnicodeConverter): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def to_url(self, value: model.Identifier) -> str: return super().to_url(identifier_uri_encode(value)) @@ -132,7 +129,12 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels/", [ Rule("/submodel", methods=["GET"], endpoint=self.get_submodel), Submount("/submodel", [ - + Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodelElements", methods=["PUT"], endpoint=self.put_submodel_submodel_elements), + Rule("/submodelElements/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_specific_nested), + Rule("/submodelElements/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_specific) ]) ]) ]) @@ -142,14 +144,6 @@ def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) return response(environ, start_response) - # this is not used yet - @classmethod - def mandatory_request_param(cls, request: Request, param: str) -> str: - req_param = request.args.get(param) - if req_param is None: - raise BadRequest(f"Parameter '{param}' is mandatory") - return req_param - def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): @@ -180,6 +174,7 @@ def handle_request(self, request: Request): except werkzeug.exceptions.NotAcceptable as e: return e + # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict) -> Response: # TODO: depth parameter response_t = get_response_type(request) @@ -213,7 +208,10 @@ def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: submodels = filter(lambda s: s.semantic_id is not None # type: ignore and len(s.semantic_id.key) > 0 and semantic_id in s.semantic_id.key[0].value, submodels) # type: ignore - return response_t(list(submodels)) + submodels_list = list(submodels) + if len(submodels_list) == 0: + raise NotFound("No submodels found!") + return response_t(submodels_list) def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) @@ -314,11 +312,11 @@ def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: D aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] - concept_dictionaries = aas.concept_dictionary.get(id_short) - if concept_dictionaries is None: + concept_dictionary = aas.concept_dictionary.get(id_short) + if concept_dictionary is None: raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") - concept_dictionaries.update() - aas.view.remove(concept_dictionaries.id_short) + concept_dictionary.update() + aas.view.remove(concept_dictionary.id_short) return Response(status=204) def get_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: @@ -345,9 +343,68 @@ def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Res return Response(status=204) raise NotFound(f"No submodel with idShort '{id_short}' found!") + # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict) -> Response: # TODO: depth parameter response_t = get_response_type(request) submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() return response_t(submodel) + + def get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: + # TODO: filter parameter + response_t = get_response_type(request) + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_elements = submodel.submodel_element + semantic_id: Optional[str] = request.args.get("semanticId") + if semantic_id is not None: + # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 + submodel_elements = filter(lambda se: se.semantic_id is not None # type: ignore + and len(se.semantic_id.key) > 0 + and semantic_id in se.semantic_id.key[0].value, submodel_elements # type: ignore + ) + submodel_elements_list = list(submodel_elements) + if len(submodel_elements_list) == 0: + raise NotFound("No submodel elements found!") + return response_t(submodel_elements_list) + + def put_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + new_concept_dictionary = parse_request_body(request, model.SubmodelElement) + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + old_submodel_element = submodel.submodel_element.get(new_concept_dictionary.id_short) + if old_submodel_element is not None: + submodel.submodel_element.discard(old_submodel_element) + submodel.submodel_element.add(new_concept_dictionary) + submodel.commit() + return response_t(new_concept_dictionary, status=201) + + def get_submodel_submodel_element_specific_nested(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + id_shorts: List[str] = url_args["id_shorts"].split("/") + submodel_element: model.SubmodelElement = \ + model.SubmodelElementCollectionUnordered("init_wrapper", submodel.submodel_element) + for id_short in id_shorts: + if not isinstance(submodel_element, model.SubmodelElementCollection): + raise NotFound(f"Nested submodel element {submodel_element} is not a submodel element collection!") + try: + submodel_element = submodel_element.value.get_referable(id_short) + except KeyError: + raise NotFound(f"No nested submodel element with idShort '{id_short}' found!") + submodel_element.update() + return response_t(submodel_element) + + def delete_submodel_submodel_element_specific(self, request: Request, url_args: Dict) -> Response: + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + id_short = url_args["id_short"] + submodel_element = submodel.submodel_element.get(id_short) + if submodel_element is None: + raise NotFound(f"No submodel element with idShort '{id_short}' found!") + submodel_element.update() + submodel.submodel_element.remove(submodel_element.id_short) + return Response(status=204) From 70c3dbd89626fe99281ca1fb3cfe2c094dc49c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 14 Nov 2020 08:40:31 +0100 Subject: [PATCH 147/474] adapter.http: prepare for new api routes from new spec - use stripped object serialization/deserialization - change response format (new spec always returns a result object) --- basyx/aas/adapter/http/response.py | 189 +++++++++++++--------------- basyx/aas/adapter/http/wsgi.py | 192 +++++++++++++---------------- 2 files changed, 175 insertions(+), 206 deletions(-) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py index f6e47af..54071c9 100644 --- a/basyx/aas/adapter/http/response.py +++ b/basyx/aas/adapter/http/response.py @@ -17,14 +17,14 @@ from werkzeug.wrappers import Request, Response from aas import model -from aas.adapter.json import json_serialization -from aas.adapter.xml import xml_serialization +from ..json import StrippedAASToJsonEncoder +from ..xml import xml_serialization -from typing import Dict, List, Sequence, Type, Union +from typing import Dict, Iterable, Optional, Tuple, Type, Union @enum.unique -class MessageType(enum.Enum): +class ErrorType(enum.Enum): UNSPECIFIED = enum.auto() DEBUG = enum.auto() INFORMATION = enum.auto() @@ -37,37 +37,35 @@ def __str__(self): return self.name.capitalize() -class Message: - def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNSPECIFIED): - self.message_type = message_type +class Error: + def __init__(self, code: str, text: str, type_: ErrorType = ErrorType.UNSPECIFIED): + self.type = type_ self.code = code self.text = text -class Result: - def __init__(self, success: bool, is_exception: bool, messages: List[Message]): - self.success = success - self.is_exception = is_exception - self.messages = messages - +ResultData = Union[object, Tuple[object]] -# not all sequence types are json serializable, but Sequence is covariant, -# which is necessary for List[Submodel] or List[AssetAdministrationShell] to be valid for List[Referable] -ResponseData = Union[Result, model.Referable, Sequence[model.Referable]] -ResponseDataInternal = Union[Result, model.Referable, List[model.Referable]] +class Result: + def __init__(self, data: Optional[Union[ResultData, Error]] = None): + self.success: bool = not isinstance(data, Error) + self.data: Optional[ResultData] = None + self.error: Optional[Error] = None + if isinstance(data, Error): + self.error = data + else: + self.data = data class APIResponse(abc.ABC, Response): - def __init__(self, data: ResponseData, *args, **kwargs): + @abc.abstractmethod + def __init__(self, result: Result, *args, **kwargs): super().__init__(*args, **kwargs) - # convert possible sequence types to List (see line 54-55) - if isinstance(data, Sequence): - data = list(data) - self.data = self.serialize(data) + self.data = self.serialize(result) @abc.abstractmethod - def serialize(self, data: ResponseDataInternal) -> str: + def serialize(self, result: Result) -> str: pass @@ -75,17 +73,18 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseDataInternal) -> str: - return json.dumps(data, cls=ResultToJsonEncoder if isinstance(data, Result) - else json_serialization.AASToJsonEncoder) + def serialize(self, result: Result) -> str: + return json.dumps(result, cls=ResultToJsonEncoder) class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseDataInternal) -> str: - return xml_element_to_str(response_data_to_xml(data)) + def serialize(self, result: Result) -> str: + result_elem = result_to_xml(result, nsmap=xml_serialization.NS_MAP) + etree.cleanup_namespaces(result_elem) + return etree.tostring(result_elem, xml_declaration=True, encoding="utf-8") class XmlResponseAlt(XmlResponse): @@ -93,20 +92,13 @@ def __init__(self, *args, content_type="text/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) -def xml_element_to_str(element: etree.Element) -> str: - # namespaces will just get assigned a prefix like nsX, where X is a positive integer - # "aas" would be a better prefix for the AAS namespace - # TODO: find a way to specify a namespace map when serializing - return etree.tostring(element, xml_declaration=True, encoding="utf-8") - - -class ResultToJsonEncoder(json.JSONEncoder): +class ResultToJsonEncoder(StrippedAASToJsonEncoder): def default(self, obj: object) -> object: if isinstance(obj, Result): return result_to_json(obj) - if isinstance(obj, Message): - return message_to_json(obj) - if isinstance(obj, MessageType): + if isinstance(obj, Error): + return error_to_json(obj) + if isinstance(obj, ErrorType): return str(obj) return super().default(obj) @@ -114,78 +106,72 @@ def default(self, obj: object) -> object: def result_to_json(result: Result) -> Dict[str, object]: return { "success": result.success, - "isException": result.is_exception, - "messages": result.messages + "error": result.error, + "data": result.data } -def message_to_json(message: Message) -> Dict[str, object]: +def error_to_json(error: Error) -> Dict[str, object]: return { - "messageType": message.message_type, - "code": message.code, - "text": message.text + "type": error.type, + "code": error.code, + "text": error.text } -def response_data_to_xml(data: ResponseDataInternal) -> etree.Element: - if isinstance(data, Result): - return result_to_xml(data) - if isinstance(data, model.Referable): - return referable_to_xml(data) - if isinstance(data, List): - elements: List[etree.Element] = [referable_to_xml(obj) for obj in data] - wrapper = etree.Element("list") - for elem in elements: - wrapper.append(elem) - return wrapper - - -def referable_to_xml(data: model.Referable) -> etree.Element: - # TODO: maybe support more referables - if isinstance(data, model.AssetAdministrationShell): - return xml_serialization.asset_administration_shell_to_xml(data) - if isinstance(data, model.Submodel): - return xml_serialization.submodel_to_xml(data) - if isinstance(data, model.SubmodelElement): - return xml_serialization.submodel_element_to_xml(data) - if isinstance(data, model.ConceptDictionary): - return xml_serialization.concept_dictionary_to_xml(data) - if isinstance(data, model.ConceptDescription): - return xml_serialization.concept_description_to_xml(data) - if isinstance(data, model.View): - return xml_serialization.view_to_xml(data) - if isinstance(data, model.Asset): - return xml_serialization.asset_to_xml(data) - raise TypeError(f"Referable {data} couldn't be serialized to xml (unsupported type)!") - - -def result_to_xml(result: Result) -> etree.Element: - result_elem = etree.Element("result") +def result_to_xml(result: Result, **kwargs) -> etree.Element: + result_elem = etree.Element("result", **kwargs) success_elem = etree.Element("success") success_elem.text = xml_serialization.boolean_to_xml(result.success) - is_exception_elem = etree.Element("isException") - is_exception_elem.text = xml_serialization.boolean_to_xml(result.is_exception) - messages_elem = etree.Element("messages") - for message in result.messages: - messages_elem.append(message_to_xml(message)) + error_elem = etree.Element("error") + if result.error is not None: + append_error_elements(error_elem, result.error) + data_elem = etree.Element("data") + if result.data is not None: + for element in result_data_to_xml(result.data): + data_elem.append(element) result_elem.append(success_elem) - result_elem.append(is_exception_elem) - result_elem.append(messages_elem) + result_elem.append(error_elem) + result_elem.append(data_elem) return result_elem -def message_to_xml(message: Message) -> etree.Element: - message_elem = etree.Element("message") - message_type_elem = etree.Element("messageType") - message_type_elem.text = str(message.message_type) +def append_error_elements(element: etree.Element, error: Error) -> None: + type_elem = etree.Element("type") + type_elem.text = str(error.type) code_elem = etree.Element("code") - code_elem.text = message.code + code_elem.text = error.code text_elem = etree.Element("text") - text_elem.text = message.text - message_elem.append(message_type_elem) - message_elem.append(code_elem) - message_elem.append(text_elem) - return message_elem + text_elem.text = error.text + element.append(type_elem) + element.append(code_elem) + element.append(text_elem) + + +def result_data_to_xml(data: ResultData) -> Iterable[etree.Element]: + if not isinstance(data, tuple): + data = (data,) + for obj in data: + yield aas_object_to_xml(obj) + + +def aas_object_to_xml(obj: object) -> etree.Element: + if isinstance(obj, model.AssetAdministrationShell): + return xml_serialization.asset_administration_shell_to_xml(obj) + if isinstance(obj, model.Reference): + return xml_serialization.reference_to_xml(obj) + if isinstance(obj, model.View): + return xml_serialization.view_to_xml(obj) + if isinstance(obj, model.Submodel): + return xml_serialization.submodel_to_xml(obj) + # TODO: xml serialization needs a constraint_to_xml() function + if isinstance(obj, model.Qualifier): + return xml_serialization.qualifier_to_xml(obj) + if isinstance(obj, model.Formula): + return xml_serialization.formula_to_xml(obj) + if isinstance(obj, model.SubmodelElement): + return xml_serialization.submodel_element_to_xml(obj) + raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") def get_response_type(request: Request) -> Type[APIResponse]: @@ -207,8 +193,9 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res location = exception.get_response().location if location is not None: headers.append(("Location", location)) - success: bool = exception.code < 400 if exception.code is not None else False - message_type = MessageType.INFORMATION if success else MessageType.ERROR - message = Message(type(exception).__name__, exception.description if exception.description is not None else "", - message_type) - return response_type(Result(success, not success, [message]), status=exception.code, headers=headers) + result = Result() + if exception.code and exception.code >= 400: + error = Error(type(exception).__name__, exception.description if exception.description is not None else "", + ErrorType.ERROR) + result = Result(error) + return response_type(result, status=exception.code, headers=headers) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py index 60d6d0e..de3e8ae 100644 --- a/basyx/aas/adapter/http/wsgi.py +++ b/basyx/aas/adapter/http/wsgi.py @@ -12,7 +12,6 @@ import io import json -from lxml import etree # type: ignore import urllib.parse import werkzeug from werkzeug.exceptions import BadRequest, InternalServerError, NotFound, NotImplemented @@ -20,10 +19,10 @@ from werkzeug.wrappers import Request, Response from aas import model -from ..xml import xml_deserialization -from ..json import json_deserialization +from ..xml import XMLConstructables, read_aas_xml_element +from ..json import StrippedAASFromJsonDecoder from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from .response import get_response_type, http_exception_to_response +from .response import Result, get_response_type, http_exception_to_response from typing import Dict, List, Optional, Type @@ -35,13 +34,15 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema """ xml_constructors = { - model.Submodel: xml_deserialization._construct_submodel, - model.View: xml_deserialization._construct_view, - model.ConceptDictionary: xml_deserialization._construct_concept_dictionary, - model.ConceptDescription: xml_deserialization._construct_concept_description, - model.SubmodelElement: xml_deserialization._construct_submodel_element + model.AASReference: XMLConstructables.AAS_REFERENCE, + model.View: XMLConstructables.VIEW, + model.Constraint: XMLConstructables.CONSTRAINT, + model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT } + if expect_type not in xml_constructors: + raise TypeError(f"Parsing {expect_type} is not supported!") + valid_content_types = ("application/json", "application/xml", "text/xml") if request.mimetype not in valid_content_types: @@ -51,28 +52,19 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m if request.mimetype == "application/json": json_data = request.get_data() try: - rv = json.loads(json_data, cls=json_deserialization.AASFromJsonDecoder) + rv = json.loads(json_data, cls=StrippedAASFromJsonDecoder) except json.decoder.JSONDecodeError as e: raise BadRequest(str(e)) from e else: - parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) xml_data = io.BytesIO(request.get_data()) - try: - tree = etree.parse(xml_data, parser) - except etree.XMLSyntaxError as e: - raise BadRequest(str(e)) from e - # TODO: check tag of root element - root = tree.getroot() - try: - rv = xml_constructors[expect_type](root, failsafe=False) - except (KeyError, ValueError) as e: - raise BadRequest(xml_deserialization._exception_to_str(e)) from e + rv = read_aas_xml_element(xml_data, xml_constructors[expect_type], stripped=True) - assert(isinstance(rv, expect_type)) + assert isinstance(rv, expect_type) return rv def identifier_uri_encode(id_: model.Identifier) -> str: + # TODO: replace urllib with urllib3 if we're using it anyways? return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") @@ -102,40 +94,36 @@ class WSGIApp: def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ - Submount("/api/v1.0", [ - Submount("/shells/", [ - Rule("/aas", methods=["GET"], endpoint=self.get_aas), - Submount("/aas", [ - Rule("/asset", methods=["GET"], endpoint=self.get_aas_asset), - Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodels), - Rule("/submodels", methods=["PUT"], endpoint=self.put_aas_submodels), - Rule("/views", methods=["GET"], endpoint=self.get_aas_views), - Rule("/views/", methods=["GET"], - endpoint=self.get_aas_views_specific), - Rule("/views/", methods=["DELETE"], - endpoint=self.delete_aas_views_specific), - Rule("/conceptDictionaries", methods=["GET"], endpoint=self.get_aas_concept_dictionaries), - Rule("/conceptDictionaries", methods=["PUT"], endpoint=self.put_aas_concept_dictionaries), - Rule("/conceptDictionaries/", methods=["GET"], - endpoint=self.get_aas_concept_dictionaries_specific), - Rule("/conceptDictionaries/", methods=["DELETE"], - endpoint=self.delete_aas_concept_dictionaries_specific), - Rule("/submodels/", methods=["GET"], - endpoint=self.get_aas_submodels_specific), - Rule("/submodels/", methods=["DELETE"], - endpoint=self.delete_aas_submodels_specific), - ]) + Submount("/api/v1", [ + Rule("/aas/", endpoint=self.get_aas), + Submount("/aas/", [ + Rule("/asset", methods=["GET"], endpoint=self.get_aas_asset), + Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodels), + Rule("/submodels", methods=["PUT"], endpoint=self.put_aas_submodels), + Rule("/views", methods=["GET"], endpoint=self.get_aas_views), + Rule("/views/", methods=["GET"], + endpoint=self.get_aas_views_specific), + Rule("/views/", methods=["DELETE"], + endpoint=self.delete_aas_views_specific), + Rule("/conceptDictionaries", methods=["GET"], endpoint=self.get_aas_concept_dictionaries), + Rule("/conceptDictionaries", methods=["PUT"], endpoint=self.put_aas_concept_dictionaries), + Rule("/conceptDictionaries/", methods=["GET"], + endpoint=self.get_aas_concept_dictionaries_specific), + Rule("/conceptDictionaries/", methods=["DELETE"], + endpoint=self.delete_aas_concept_dictionaries_specific), + Rule("/submodels/", methods=["GET"], + endpoint=self.get_aas_submodels_specific), + Rule("/submodels/", methods=["DELETE"], + endpoint=self.delete_aas_submodels_specific), ]), + Rule("/submodels/", endpoint=self.get_submodel), Submount("/submodels/", [ - Rule("/submodel", methods=["GET"], endpoint=self.get_submodel), - Submount("/submodel", [ - Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodelElements", methods=["PUT"], endpoint=self.put_submodel_submodel_elements), - Rule("/submodelElements/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_specific_nested), - Rule("/submodelElements/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_specific) - ]) + Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodelElements", methods=["PUT"], endpoint=self.put_submodel_submodel_elements), + Rule("/submodelElements/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_specific_nested), + Rule("/submodelElements/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_specific) ]) ]) ], converters={"identifier": IdentifierConverter}) @@ -144,17 +132,17 @@ def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) return response(environ, start_response) - def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: + def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): raise NotFound(f"No {type_.__name__} with {identifier} found!") return identifiable - def resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: + def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: try: return reference.resolve(self.object_store) - except (KeyError, TypeError, model.base.UnexpectedTypeError) as e: - raise InternalServerError(xml_deserialization._exception_to_str(e)) from e + except (KeyError, TypeError, model.UnexpectedTypeError) as e: + raise InternalServerError(str(e)) from e def handle_request(self, request: Request): adapter = self.url_map.bind_to_environ(request.environ) @@ -176,26 +164,24 @@ def handle_request(self, request: Request): # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict) -> Response: - # TODO: depth parameter response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - return response_t(aas) + return response_t(Result(aas)) def get_aas_asset(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - asset = self.resolve_reference(aas.asset) + asset = self._resolve_reference(aas.asset) asset.update() - return response_t(asset) + return response_t(Result(asset)) def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: - # TODO: depth parameter response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - submodels = [self.resolve_reference(ref) for ref in aas.submodel] + submodels = [self._resolve_reference(ref) for ref in aas.submodel] for submodel in submodels: submodel.update() identification_id: Optional[str] = request.args.get("identification.id") @@ -211,15 +197,15 @@ def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: submodels_list = list(submodels) if len(submodels_list) == 0: raise NotFound("No submodels found!") - return response_t(submodels_list) + return response_t(Result(submodels_list)) def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) new_submodel = parse_request_body(request, model.Submodel) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() current_submodel = None - for s in iter(self.resolve_reference(ref) for ref in aas.submodel): + for s in iter(self._resolve_reference(ref) for ref in aas.submodel): if s.identification == new_submodel.identification: current_submodel = s break @@ -232,41 +218,40 @@ def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: if current_submodel is not None: self.object_store.discard(current_submodel) self.object_store.add(new_submodel) - return response_t(new_submodel, status=201) + return response_t(Result(new_submodel), status=201) def get_aas_views(self, request: Request, url_args: Dict) -> Response: - # TODO: filter parameter response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) if len(aas.view) == 0: raise NotFound("No views found!") - return response_t(list(aas.view)) + return response_t(Result((aas.view,))) def put_aas_views(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) new_view = parse_request_body(request, model.View) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() old_view = aas.view.get(new_view.id_short) if old_view is not None: aas.view.discard(old_view) aas.view.add(new_view) aas.commit() - return response_t(new_view, status=201) + return response_t(Result(new_view), status=201) def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] view = aas.view.get(id_short) if view is None: raise NotFound(f"No view with idShort '{id_short}' found!") view.update() - return response_t(view) + return response_t(Result(view)) def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] view = aas.view.get(id_short) @@ -277,39 +262,38 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Respons return Response(status=204) def get_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: - # TODO: depth parameter response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() if len(aas.concept_dictionary) == 0: raise NotFound("No concept dictionaries found!") - return response_t(list(aas.concept_dictionary)) + return response_t(Result((aas.concept_dictionary,))) def put_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) new_concept_dictionary = parse_request_body(request, model.ConceptDictionary) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() old_concept_dictionary = aas.concept_dictionary.get(new_concept_dictionary.id_short) if old_concept_dictionary is not None: aas.concept_dictionary.discard(old_concept_dictionary) aas.concept_dictionary.add(new_concept_dictionary) aas.commit() - return response_t(new_concept_dictionary, status=201) + return response_t(Result((new_concept_dictionary,)), status=201) def get_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] concept_dictionary = aas.concept_dictionary.get(id_short) if concept_dictionary is None: raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") concept_dictionary.update() - return response_t(concept_dictionary) + return response_t(Result(concept_dictionary)) def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] concept_dictionary = aas.concept_dictionary.get(id_short) @@ -321,21 +305,21 @@ def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: D def get_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] - for submodel in iter(self.resolve_reference(ref) for ref in aas.submodel): + for submodel in iter(self._resolve_reference(ref) for ref in aas.submodel): submodel.update() if submodel.id_short == id_short: - return response_t(submodel) + return response_t(Result(submodel)) raise NotFound(f"No submodel with idShort '{id_short}' found!") def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] for ref in aas.submodel: - submodel = self.resolve_reference(ref) + submodel = self._resolve_reference(ref) submodel.update() if submodel.id_short == id_short: aas.submodel.discard(ref) @@ -345,16 +329,14 @@ def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Res # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict) -> Response: - # TODO: depth parameter response_t = get_response_type(request) - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - return response_t(submodel) + return response_t(Result(submodel)) def get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: - # TODO: filter parameter response_t = get_response_type(request) - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() submodel_elements = submodel.submodel_element semantic_id: Optional[str] = request.args.get("semanticId") @@ -364,26 +346,26 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Re and len(se.semantic_id.key) > 0 and semantic_id in se.semantic_id.key[0].value, submodel_elements # type: ignore ) - submodel_elements_list = list(submodel_elements) - if len(submodel_elements_list) == 0: + submodel_elements_tuple = (submodel_elements,) + if len(submodel_elements_tuple) == 0: raise NotFound("No submodel elements found!") - return response_t(submodel_elements_list) + return response_t(Result(submodel_elements_tuple)) def put_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) new_concept_dictionary = parse_request_body(request, model.SubmodelElement) - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() old_submodel_element = submodel.submodel_element.get(new_concept_dictionary.id_short) if old_submodel_element is not None: submodel.submodel_element.discard(old_submodel_element) submodel.submodel_element.add(new_concept_dictionary) submodel.commit() - return response_t(new_concept_dictionary, status=201) + return response_t(Result(new_concept_dictionary), status=201) def get_submodel_submodel_element_specific_nested(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() id_shorts: List[str] = url_args["id_shorts"].split("/") submodel_element: model.SubmodelElement = \ @@ -396,10 +378,10 @@ def get_submodel_submodel_element_specific_nested(self, request: Request, url_ar except KeyError: raise NotFound(f"No nested submodel element with idShort '{id_short}' found!") submodel_element.update() - return response_t(submodel_element) + return response_t(Result(submodel_element)) def delete_submodel_submodel_element_specific(self, request: Request, url_args: Dict) -> Response: - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() id_short = url_args["id_short"] submodel_element = submodel.submodel_element.get(id_short) From 4d6f8cdbdbbbacdb31880a5edeeb68be1c53c067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 18 Nov 2020 23:33:16 +0100 Subject: [PATCH 148/474] adapter.http: move code from folder to file http.py adapter.http: remove old routes, add a few new aas and submodel routes --- basyx/aas/adapter/http.py | 398 +++++++++++++++++++++++++++++ basyx/aas/adapter/http/__init__.py | 12 - basyx/aas/adapter/http/__main__.py | 17 -- basyx/aas/adapter/http/response.py | 201 --------------- basyx/aas/adapter/http/wsgi.py | 392 ---------------------------- 5 files changed, 398 insertions(+), 622 deletions(-) create mode 100644 basyx/aas/adapter/http.py delete mode 100644 basyx/aas/adapter/http/__init__.py delete mode 100644 basyx/aas/adapter/http/__main__.py delete mode 100644 basyx/aas/adapter/http/response.py delete mode 100644 basyx/aas/adapter/http/wsgi.py diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py new file mode 100644 index 0000000..1b5fe1b --- /dev/null +++ b/basyx/aas/adapter/http.py @@ -0,0 +1,398 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + + +import abc +import enum +import io +import json +from lxml import etree # type: ignore +import urllib.parse +import werkzeug +from werkzeug.exceptions import BadRequest, Conflict, InternalServerError, NotFound, NotImplemented +from werkzeug.routing import Rule, Submount +from werkzeug.wrappers import Request, Response + +from aas import model +from .xml import XMLConstructables, read_aas_xml_element, xml_serialization +from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder +from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE + +from typing import Dict, Iterable, List, Optional, Tuple, Type, Union + + +@enum.unique +class ErrorType(enum.Enum): + UNSPECIFIED = enum.auto() + DEBUG = enum.auto() + INFORMATION = enum.auto() + WARNING = enum.auto() + ERROR = enum.auto() + FATAL = enum.auto() + EXCEPTION = enum.auto() + + def __str__(self): + return self.name.capitalize() + + +class Error: + def __init__(self, code: str, text: str, type_: ErrorType = ErrorType.UNSPECIFIED): + self.type = type_ + self.code = code + self.text = text + + +ResultData = Union[object, Tuple[object, ...]] + + +class Result: + def __init__(self, data: Optional[Union[ResultData, Error]]): + # the following is True when data is None, which is the expected behavior + self.success: bool = not isinstance(data, Error) + self.data: Optional[ResultData] = None + self.error: Optional[Error] = None + if isinstance(data, Error): + self.error = data + else: + self.data = data + + +class ResultToJsonEncoder(StrippedAASToJsonEncoder): + @classmethod + def _result_to_json(cls, result: Result) -> Dict[str, object]: + return { + "success": result.success, + "error": result.error, + "data": result.data + } + + @classmethod + def _error_to_json(cls, error: Error) -> Dict[str, object]: + return { + "type": error.type, + "code": error.code, + "text": error.text + } + + def default(self, obj: object) -> object: + if isinstance(obj, Result): + return self._result_to_json(obj) + if isinstance(obj, Error): + return self._error_to_json(obj) + if isinstance(obj, ErrorType): + return str(obj) + return super().default(obj) + + +class APIResponse(abc.ABC, werkzeug.wrappers.Response): + @abc.abstractmethod + def __init__(self, result: Result, *args, **kwargs): + super().__init__(*args, **kwargs) + self.data = self.serialize(result) + + @abc.abstractmethod + def serialize(self, result: Result) -> str: + pass + + +class JsonResponse(APIResponse): + def __init__(self, *args, content_type="application/json", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize(self, result: Result) -> str: + return json.dumps(result, cls=ResultToJsonEncoder) + + +class XmlResponse(APIResponse): + def __init__(self, *args, content_type="application/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize(self, result: Result) -> str: + result_elem = result_to_xml(result, nsmap=xml_serialization.NS_MAP) + etree.cleanup_namespaces(result_elem) + return etree.tostring(result_elem, xml_declaration=True, encoding="utf-8") + + +class XmlResponseAlt(XmlResponse): + def __init__(self, *args, content_type="text/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + +def result_to_xml(result: Result, **kwargs) -> etree.Element: + result_elem = etree.Element("result", **kwargs) + success_elem = etree.Element("success") + success_elem.text = xml_serialization.boolean_to_xml(result.success) + if result.error is None: + error_elem = etree.Element("error") + else: + error_elem = error_to_xml(result.error) + data_elem = etree.Element("data") + if result.data is not None: + for element in result_data_to_xml(result.data): + data_elem.append(element) + result_elem.append(success_elem) + result_elem.append(error_elem) + result_elem.append(data_elem) + return result_elem + + +def error_to_xml(error: Error) -> etree.Element: + error_elem = etree.Element("error") + type_elem = etree.Element("type") + type_elem.text = str(error.type) + code_elem = etree.Element("code") + code_elem.text = error.code + text_elem = etree.Element("text") + text_elem.text = error.text + error_elem.append(type_elem) + error_elem.append(code_elem) + error_elem.append(text_elem) + return error_elem + + +def result_data_to_xml(data: ResultData) -> Iterable[etree.Element]: + # for xml we can just append multiple elements to the data element + # so multiple elements will be handled the same as a single element + if not isinstance(data, tuple): + data = (data,) + for obj in data: + yield aas_object_to_xml(obj) + + +def aas_object_to_xml(obj: object) -> etree.Element: + # TODO: a similar function should be implemented in the xml serialization + if isinstance(obj, model.AssetAdministrationShell): + return xml_serialization.asset_administration_shell_to_xml(obj) + if isinstance(obj, model.Reference): + return xml_serialization.reference_to_xml(obj) + if isinstance(obj, model.View): + return xml_serialization.view_to_xml(obj) + if isinstance(obj, model.Submodel): + return xml_serialization.submodel_to_xml(obj) + # TODO: xml serialization needs a constraint_to_xml() function + if isinstance(obj, model.Qualifier): + return xml_serialization.qualifier_to_xml(obj) + if isinstance(obj, model.Formula): + return xml_serialization.formula_to_xml(obj) + if isinstance(obj, model.SubmodelElement): + return xml_serialization.submodel_element_to_xml(obj) + raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") + + +def get_response_type(request: Request) -> Type[APIResponse]: + response_types: Dict[str, Type[APIResponse]] = { + "application/json": JsonResponse, + "application/xml": XmlResponse, + "text/xml": XmlResponseAlt + } + mime_type = request.accept_mimetypes.best_match(response_types) + if mime_type is None: + raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " + + ", ".join(response_types.keys())) + return response_types[mime_type] + + +def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ + -> APIResponse: + headers = exception.get_headers() + location = exception.get_response().location + if location is not None: + headers.append(("Location", location)) + if exception.code and exception.code >= 400: + error = Error(type(exception).__name__, exception.description if exception.description is not None else "", + ErrorType.ERROR) + result = Result(error) + else: + result = Result(None) + return response_type(result, status=exception.code, headers=headers) + + +def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: + """ + TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent + running out of memory. but it doesn't state how to check the content length + also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema + """ + type_constructables_map = { + model.AASReference: XMLConstructables.AAS_REFERENCE, + model.View: XMLConstructables.VIEW, + model.Constraint: XMLConstructables.CONSTRAINT, + model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT + } + + if expect_type not in type_constructables_map: + raise TypeError(f"Parsing {expect_type} is not supported!") + + valid_content_types = ("application/json", "application/xml", "text/xml") + + if request.mimetype not in valid_content_types: + raise werkzeug.exceptions.UnsupportedMediaType(f"Invalid content-type: {request.mimetype}! Supported types: " + + ", ".join(valid_content_types)) + + try: + if request.mimetype == "application/json": + rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) + else: + xml_data = io.BytesIO(request.get_data()) + rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) + except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError) as e: + raise BadRequest(str(e)) from e + + assert isinstance(rv, expect_type) + return rv + + +def identifier_uri_encode(id_: model.Identifier) -> str: + # TODO: replace urllib with urllib3 if we're using it anyways? + return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") + + +def identifier_uri_decode(id_str: str) -> model.Identifier: + try: + id_type_str, id_ = id_str.split(":", 1) + except ValueError: + raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") + id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) + if id_type is None: + raise ValueError(f"Identifier Type '{id_type_str}' is invalid") + return model.Identifier(urllib.parse.unquote(id_), id_type) + + +class IdentifierConverter(werkzeug.routing.UnicodeConverter): + def to_url(self, value: model.Identifier) -> str: + return super().to_url(identifier_uri_encode(value)) + + def to_python(self, value: str) -> model.Identifier: + try: + return identifier_uri_decode(super().to_python(value)) + except ValueError as e: + raise BadRequest(str(e)) + + +class WSGIApp: + def __init__(self, object_store: model.AbstractObjectStore): + self.object_store: model.AbstractObjectStore = object_store + self.url_map = werkzeug.routing.Map([ + Submount("/api/v1", [ + Rule("/aas/", endpoint=self.get_aas), + Submount("/aas/", [ + Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/submodels", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("/submodels/", methods=["GET"], + endpoint=self.get_aas_submodel_refs_specific), + Rule("/submodels/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific), + ]), + Rule("/submodels/", endpoint=self.get_submodel), + ]) + ], converters={"identifier": IdentifierConverter}) + + def __call__(self, environ, start_response): + response = self.handle_request(Request(environ)) + return response(environ, start_response) + + def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: + identifiable = self.object_store.get(identifier) + if not isinstance(identifiable, type_): + raise NotFound(f"No {type_.__name__} with {identifier} found!") + return identifiable + + def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: + try: + return reference.resolve(self.object_store) + except (KeyError, TypeError, model.UnexpectedTypeError) as e: + raise InternalServerError(str(e)) from e + + @classmethod + def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdministrationShell, + sm_identifier: model.Identifier) \ + -> model.AASReference[model.Submodel]: + for sm_ref in aas.submodel: + if sm_ref.get_identifier() == sm_identifier: + return sm_ref + raise NotFound(f"No reference to submodel with {sm_identifier} found!") + + def handle_request(self, request: Request): + adapter = self.url_map.bind_to_environ(request.environ) + try: + endpoint, values = adapter.match() + if endpoint is None: + raise NotImplemented("This route is not yet implemented.") + return endpoint(request, values) + # any raised error that leaves this function will cause a 500 internal server error + # so catch raised http exceptions and return them + except werkzeug.exceptions.NotAcceptable as e: + return e + except werkzeug.exceptions.HTTPException as e: + try: + # get_response_type() may raise a NotAcceptable error, so we have to handle that + return http_exception_to_response(e, get_response_type(request)) + except werkzeug.exceptions.NotAcceptable as e: + return e + + # --------- AAS ROUTES --------- + def get_aas(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + return response_t(Result(aas)) + + def get_aas_submodel_refs(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + return response_t(Result(tuple(aas.submodel))) + + def post_aas_submodel_refs(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = parse_request_body(request, model.AASReference) # type: ignore + assert isinstance(sm_ref, model.AASReference) + if sm_ref in aas.submodel: + raise Conflict(f"{sm_ref!r} already exists!") + # TODO: check if reference references a non-existant submodel? + aas.submodel.add(sm_ref) + aas.commit() + return response_t(Result(sm_ref), status=201) + + def get_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) + return response_t(Result(sm_ref)) + + def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) + # use remove(sm_ref) because it raises a KeyError if sm_ref is not present + # sm_ref must be present because _get_aas_submodel_reference_by_submodel_identifier() found it there + # so if sm_ref is not in aas.submodel, this implementation is bugged and the raised KeyError will result + # in an InternalServerError + aas.submodel.remove(sm_ref) + aas.commit() + return response_t(Result(None)) + + # --------- SUBMODEL ROUTES --------- + def get_submodel(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(Result(submodel)) + + +if __name__ == "__main__": + from werkzeug.serving import run_simple + from aas.examples.data.example_aas import create_full_example + run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/basyx/aas/adapter/http/__init__.py b/basyx/aas/adapter/http/__init__.py deleted file mode 100644 index 27808a8..0000000 --- a/basyx/aas/adapter/http/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -from .wsgi import WSGIApp diff --git a/basyx/aas/adapter/http/__main__.py b/basyx/aas/adapter/http/__main__.py deleted file mode 100644 index 59bf98b..0000000 --- a/basyx/aas/adapter/http/__main__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -from werkzeug.serving import run_simple - -from aas.examples.data.example_aas import create_full_example -from . import WSGIApp - -run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py deleted file mode 100644 index 54071c9..0000000 --- a/basyx/aas/adapter/http/response.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -import abc -import enum -import json -from lxml import etree # type: ignore -import werkzeug.exceptions -from werkzeug.wrappers import Request, Response - -from aas import model -from ..json import StrippedAASToJsonEncoder -from ..xml import xml_serialization - -from typing import Dict, Iterable, Optional, Tuple, Type, Union - - -@enum.unique -class ErrorType(enum.Enum): - UNSPECIFIED = enum.auto() - DEBUG = enum.auto() - INFORMATION = enum.auto() - WARNING = enum.auto() - ERROR = enum.auto() - FATAL = enum.auto() - EXCEPTION = enum.auto() - - def __str__(self): - return self.name.capitalize() - - -class Error: - def __init__(self, code: str, text: str, type_: ErrorType = ErrorType.UNSPECIFIED): - self.type = type_ - self.code = code - self.text = text - - -ResultData = Union[object, Tuple[object]] - - -class Result: - def __init__(self, data: Optional[Union[ResultData, Error]] = None): - self.success: bool = not isinstance(data, Error) - self.data: Optional[ResultData] = None - self.error: Optional[Error] = None - if isinstance(data, Error): - self.error = data - else: - self.data = data - - -class APIResponse(abc.ABC, Response): - @abc.abstractmethod - def __init__(self, result: Result, *args, **kwargs): - super().__init__(*args, **kwargs) - self.data = self.serialize(result) - - @abc.abstractmethod - def serialize(self, result: Result) -> str: - pass - - -class JsonResponse(APIResponse): - def __init__(self, *args, content_type="application/json", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - def serialize(self, result: Result) -> str: - return json.dumps(result, cls=ResultToJsonEncoder) - - -class XmlResponse(APIResponse): - def __init__(self, *args, content_type="application/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - def serialize(self, result: Result) -> str: - result_elem = result_to_xml(result, nsmap=xml_serialization.NS_MAP) - etree.cleanup_namespaces(result_elem) - return etree.tostring(result_elem, xml_declaration=True, encoding="utf-8") - - -class XmlResponseAlt(XmlResponse): - def __init__(self, *args, content_type="text/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - -class ResultToJsonEncoder(StrippedAASToJsonEncoder): - def default(self, obj: object) -> object: - if isinstance(obj, Result): - return result_to_json(obj) - if isinstance(obj, Error): - return error_to_json(obj) - if isinstance(obj, ErrorType): - return str(obj) - return super().default(obj) - - -def result_to_json(result: Result) -> Dict[str, object]: - return { - "success": result.success, - "error": result.error, - "data": result.data - } - - -def error_to_json(error: Error) -> Dict[str, object]: - return { - "type": error.type, - "code": error.code, - "text": error.text - } - - -def result_to_xml(result: Result, **kwargs) -> etree.Element: - result_elem = etree.Element("result", **kwargs) - success_elem = etree.Element("success") - success_elem.text = xml_serialization.boolean_to_xml(result.success) - error_elem = etree.Element("error") - if result.error is not None: - append_error_elements(error_elem, result.error) - data_elem = etree.Element("data") - if result.data is not None: - for element in result_data_to_xml(result.data): - data_elem.append(element) - result_elem.append(success_elem) - result_elem.append(error_elem) - result_elem.append(data_elem) - return result_elem - - -def append_error_elements(element: etree.Element, error: Error) -> None: - type_elem = etree.Element("type") - type_elem.text = str(error.type) - code_elem = etree.Element("code") - code_elem.text = error.code - text_elem = etree.Element("text") - text_elem.text = error.text - element.append(type_elem) - element.append(code_elem) - element.append(text_elem) - - -def result_data_to_xml(data: ResultData) -> Iterable[etree.Element]: - if not isinstance(data, tuple): - data = (data,) - for obj in data: - yield aas_object_to_xml(obj) - - -def aas_object_to_xml(obj: object) -> etree.Element: - if isinstance(obj, model.AssetAdministrationShell): - return xml_serialization.asset_administration_shell_to_xml(obj) - if isinstance(obj, model.Reference): - return xml_serialization.reference_to_xml(obj) - if isinstance(obj, model.View): - return xml_serialization.view_to_xml(obj) - if isinstance(obj, model.Submodel): - return xml_serialization.submodel_to_xml(obj) - # TODO: xml serialization needs a constraint_to_xml() function - if isinstance(obj, model.Qualifier): - return xml_serialization.qualifier_to_xml(obj) - if isinstance(obj, model.Formula): - return xml_serialization.formula_to_xml(obj) - if isinstance(obj, model.SubmodelElement): - return xml_serialization.submodel_element_to_xml(obj) - raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") - - -def get_response_type(request: Request) -> Type[APIResponse]: - response_types: Dict[str, Type[APIResponse]] = { - "application/json": JsonResponse, - "application/xml": XmlResponse, - "text/xml": XmlResponseAlt - } - mime_type = request.accept_mimetypes.best_match(response_types) - if mime_type is None: - raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " - + ", ".join(response_types.keys())) - return response_types[mime_type] - - -def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ - -> APIResponse: - headers = exception.get_headers() - location = exception.get_response().location - if location is not None: - headers.append(("Location", location)) - result = Result() - if exception.code and exception.code >= 400: - error = Error(type(exception).__name__, exception.description if exception.description is not None else "", - ErrorType.ERROR) - result = Result(error) - return response_type(result, status=exception.code, headers=headers) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py deleted file mode 100644 index de3e8ae..0000000 --- a/basyx/aas/adapter/http/wsgi.py +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - - -import io -import json -import urllib.parse -import werkzeug -from werkzeug.exceptions import BadRequest, InternalServerError, NotFound, NotImplemented -from werkzeug.routing import Rule, Submount -from werkzeug.wrappers import Request, Response - -from aas import model -from ..xml import XMLConstructables, read_aas_xml_element -from ..json import StrippedAASFromJsonDecoder -from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from .response import Result, get_response_type, http_exception_to_response - -from typing import Dict, List, Optional, Type - - -def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: - """ - TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent - running out of memory. but it doesn't state how to check the content length - also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema - """ - xml_constructors = { - model.AASReference: XMLConstructables.AAS_REFERENCE, - model.View: XMLConstructables.VIEW, - model.Constraint: XMLConstructables.CONSTRAINT, - model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT - } - - if expect_type not in xml_constructors: - raise TypeError(f"Parsing {expect_type} is not supported!") - - valid_content_types = ("application/json", "application/xml", "text/xml") - - if request.mimetype not in valid_content_types: - raise werkzeug.exceptions.UnsupportedMediaType(f"Invalid content-type: {request.mimetype}! Supported types: " - + ", ".join(valid_content_types)) - - if request.mimetype == "application/json": - json_data = request.get_data() - try: - rv = json.loads(json_data, cls=StrippedAASFromJsonDecoder) - except json.decoder.JSONDecodeError as e: - raise BadRequest(str(e)) from e - else: - xml_data = io.BytesIO(request.get_data()) - rv = read_aas_xml_element(xml_data, xml_constructors[expect_type], stripped=True) - - assert isinstance(rv, expect_type) - return rv - - -def identifier_uri_encode(id_: model.Identifier) -> str: - # TODO: replace urllib with urllib3 if we're using it anyways? - return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") - - -def identifier_uri_decode(id_str: str) -> model.Identifier: - try: - id_type_str, id_ = id_str.split(":", 1) - except ValueError as e: - raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") - id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) - if id_type is None: - raise ValueError(f"Identifier Type '{id_type_str}' is invalid") - return model.Identifier(urllib.parse.unquote(id_), id_type) - - -class IdentifierConverter(werkzeug.routing.UnicodeConverter): - def to_url(self, value: model.Identifier) -> str: - return super().to_url(identifier_uri_encode(value)) - - def to_python(self, value: str) -> model.Identifier: - try: - return identifier_uri_decode(super().to_python(value)) - except ValueError as e: - raise BadRequest(str(e)) - - -class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore): - self.object_store: model.AbstractObjectStore = object_store - self.url_map = werkzeug.routing.Map([ - Submount("/api/v1", [ - Rule("/aas/", endpoint=self.get_aas), - Submount("/aas/", [ - Rule("/asset", methods=["GET"], endpoint=self.get_aas_asset), - Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodels), - Rule("/submodels", methods=["PUT"], endpoint=self.put_aas_submodels), - Rule("/views", methods=["GET"], endpoint=self.get_aas_views), - Rule("/views/", methods=["GET"], - endpoint=self.get_aas_views_specific), - Rule("/views/", methods=["DELETE"], - endpoint=self.delete_aas_views_specific), - Rule("/conceptDictionaries", methods=["GET"], endpoint=self.get_aas_concept_dictionaries), - Rule("/conceptDictionaries", methods=["PUT"], endpoint=self.put_aas_concept_dictionaries), - Rule("/conceptDictionaries/", methods=["GET"], - endpoint=self.get_aas_concept_dictionaries_specific), - Rule("/conceptDictionaries/", methods=["DELETE"], - endpoint=self.delete_aas_concept_dictionaries_specific), - Rule("/submodels/", methods=["GET"], - endpoint=self.get_aas_submodels_specific), - Rule("/submodels/", methods=["DELETE"], - endpoint=self.delete_aas_submodels_specific), - ]), - Rule("/submodels/", endpoint=self.get_submodel), - Submount("/submodels/", [ - Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodelElements", methods=["PUT"], endpoint=self.put_submodel_submodel_elements), - Rule("/submodelElements/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_specific_nested), - Rule("/submodelElements/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_specific) - ]) - ]) - ], converters={"identifier": IdentifierConverter}) - - def __call__(self, environ, start_response): - response = self.handle_request(Request(environ)) - return response(environ, start_response) - - def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: - identifiable = self.object_store.get(identifier) - if not isinstance(identifiable, type_): - raise NotFound(f"No {type_.__name__} with {identifier} found!") - return identifiable - - def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: - try: - return reference.resolve(self.object_store) - except (KeyError, TypeError, model.UnexpectedTypeError) as e: - raise InternalServerError(str(e)) from e - - def handle_request(self, request: Request): - adapter = self.url_map.bind_to_environ(request.environ) - try: - endpoint, values = adapter.match() - if endpoint is None: - raise NotImplemented("This route is not yet implemented.") - return endpoint(request, values) - # any raised error that leaves this function will cause a 500 internal server error - # so catch raised http exceptions and return them - except werkzeug.exceptions.NotAcceptable as e: - return e - except werkzeug.exceptions.HTTPException as e: - try: - # get_response_type() may raise a NotAcceptable error, so we have to handle that - return http_exception_to_response(e, get_response_type(request)) - except werkzeug.exceptions.NotAcceptable as e: - return e - - # --------- AAS ROUTES --------- - def get_aas(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - return response_t(Result(aas)) - - def get_aas_asset(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - asset = self._resolve_reference(aas.asset) - asset.update() - return response_t(Result(asset)) - - def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - submodels = [self._resolve_reference(ref) for ref in aas.submodel] - for submodel in submodels: - submodel.update() - identification_id: Optional[str] = request.args.get("identification.id") - if identification_id is not None: - # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 - submodels = filter(lambda s: identification_id in s.identification.id, submodels) # type: ignore - semantic_id: Optional[str] = request.args.get("semanticId") - if semantic_id is not None: - # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 - submodels = filter(lambda s: s.semantic_id is not None # type: ignore - and len(s.semantic_id.key) > 0 - and semantic_id in s.semantic_id.key[0].value, submodels) # type: ignore - submodels_list = list(submodels) - if len(submodels_list) == 0: - raise NotFound("No submodels found!") - return response_t(Result(submodels_list)) - - def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - new_submodel = parse_request_body(request, model.Submodel) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - current_submodel = None - for s in iter(self._resolve_reference(ref) for ref in aas.submodel): - if s.identification == new_submodel.identification: - current_submodel = s - break - if current_submodel is None: - aas.submodel.add(model.AASReference.from_referable(new_submodel)) - aas.commit() - not_referenced_submodel = self.object_store.get(new_submodel.identification) - assert(isinstance(not_referenced_submodel, model.Submodel)) - current_submodel = not_referenced_submodel - if current_submodel is not None: - self.object_store.discard(current_submodel) - self.object_store.add(new_submodel) - return response_t(Result(new_submodel), status=201) - - def get_aas_views(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - if len(aas.view) == 0: - raise NotFound("No views found!") - return response_t(Result((aas.view,))) - - def put_aas_views(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - new_view = parse_request_body(request, model.View) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - old_view = aas.view.get(new_view.id_short) - if old_view is not None: - aas.view.discard(old_view) - aas.view.add(new_view) - aas.commit() - return response_t(Result(new_view), status=201) - - def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - view = aas.view.get(id_short) - if view is None: - raise NotFound(f"No view with idShort '{id_short}' found!") - view.update() - return response_t(Result(view)) - - def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - view = aas.view.get(id_short) - if view is None: - raise NotFound(f"No view with idShort '{id_short}' found!") - view.update() - aas.view.remove(view.id_short) - return Response(status=204) - - def get_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - if len(aas.concept_dictionary) == 0: - raise NotFound("No concept dictionaries found!") - return response_t(Result((aas.concept_dictionary,))) - - def put_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - new_concept_dictionary = parse_request_body(request, model.ConceptDictionary) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - old_concept_dictionary = aas.concept_dictionary.get(new_concept_dictionary.id_short) - if old_concept_dictionary is not None: - aas.concept_dictionary.discard(old_concept_dictionary) - aas.concept_dictionary.add(new_concept_dictionary) - aas.commit() - return response_t(Result((new_concept_dictionary,)), status=201) - - def get_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - concept_dictionary = aas.concept_dictionary.get(id_short) - if concept_dictionary is None: - raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") - concept_dictionary.update() - return response_t(Result(concept_dictionary)) - - def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - concept_dictionary = aas.concept_dictionary.get(id_short) - if concept_dictionary is None: - raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") - concept_dictionary.update() - aas.view.remove(concept_dictionary.id_short) - return Response(status=204) - - def get_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - for submodel in iter(self._resolve_reference(ref) for ref in aas.submodel): - submodel.update() - if submodel.id_short == id_short: - return response_t(Result(submodel)) - raise NotFound(f"No submodel with idShort '{id_short}' found!") - - def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - for ref in aas.submodel: - submodel = self._resolve_reference(ref) - submodel.update() - if submodel.id_short == id_short: - aas.submodel.discard(ref) - self.object_store.discard(submodel) - return Response(status=204) - raise NotFound(f"No submodel with idShort '{id_short}' found!") - - # --------- SUBMODEL ROUTES --------- - def get_submodel(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - return response_t(Result(submodel)) - - def get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_elements = submodel.submodel_element - semantic_id: Optional[str] = request.args.get("semanticId") - if semantic_id is not None: - # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 - submodel_elements = filter(lambda se: se.semantic_id is not None # type: ignore - and len(se.semantic_id.key) > 0 - and semantic_id in se.semantic_id.key[0].value, submodel_elements # type: ignore - ) - submodel_elements_tuple = (submodel_elements,) - if len(submodel_elements_tuple) == 0: - raise NotFound("No submodel elements found!") - return response_t(Result(submodel_elements_tuple)) - - def put_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - new_concept_dictionary = parse_request_body(request, model.SubmodelElement) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - old_submodel_element = submodel.submodel_element.get(new_concept_dictionary.id_short) - if old_submodel_element is not None: - submodel.submodel_element.discard(old_submodel_element) - submodel.submodel_element.add(new_concept_dictionary) - submodel.commit() - return response_t(Result(new_concept_dictionary), status=201) - - def get_submodel_submodel_element_specific_nested(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - id_shorts: List[str] = url_args["id_shorts"].split("/") - submodel_element: model.SubmodelElement = \ - model.SubmodelElementCollectionUnordered("init_wrapper", submodel.submodel_element) - for id_short in id_shorts: - if not isinstance(submodel_element, model.SubmodelElementCollection): - raise NotFound(f"Nested submodel element {submodel_element} is not a submodel element collection!") - try: - submodel_element = submodel_element.value.get_referable(id_short) - except KeyError: - raise NotFound(f"No nested submodel element with idShort '{id_short}' found!") - submodel_element.update() - return response_t(Result(submodel_element)) - - def delete_submodel_submodel_element_specific(self, request: Request, url_args: Dict) -> Response: - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - id_short = url_args["id_short"] - submodel_element = submodel.submodel_element.get(id_short) - if submodel_element is None: - raise NotFound(f"No submodel element with idShort '{id_short}' found!") - submodel_element.update() - submodel.submodel_element.remove(submodel_element.id_short) - return Response(status=204) From 8fc66ca04a5effbe6a2e7c2130f6454841541f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 19 Nov 2020 03:23:24 +0100 Subject: [PATCH 149/474] adapter.http: add aas.view routes --- basyx/aas/adapter/http.py | 62 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 1b5fe1b..cf545f5 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -290,6 +290,14 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_aas_submodel_refs_specific), Rule("/submodels/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific), + Rule("/views", methods=["GET"], endpoint=self.get_aas_views), + Rule("/views", methods=["POST"], endpoint=self.post_aas_views), + Rule("/views/", methods=["GET"], + endpoint=self.get_aas_views_specific), + Rule("/views/", methods=["PUT"], + endpoint=self.put_aas_views_specific), + Rule("/views/", methods=["DELETE"], + endpoint=self.delete_aas_views_specific) ]), Rule("/submodels/", endpoint=self.get_submodel), ]) @@ -384,6 +392,60 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> aas.commit() return response_t(Result(None)) + def get_aas_views(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + return response_t(Result(tuple(aas.view))) + + def post_aas_views(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + view = parse_request_body(request, model.View) + if view.id_short in aas.view: + raise Conflict(f"View with idShort {view.id_short} already exists!") + aas.view.add(view) + aas.commit() + return response_t(Result(view)) + + def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + view_idshort = url_args["view_idshort"] + view = aas.view.get(view_idshort) + if view is None: + raise NotFound(f"No view with idShort {view_idshort} found!") + # TODO: is view.update() necessary here? + view.update() + return response_t(Result(view)) + + def put_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + view_idshort = url_args["view_idshort"] + view = aas.view.get(view_idshort) + if view is None: + raise NotFound(f"No view with idShort {view_idshort} found!") + new_view = parse_request_body(request, model.View) + if new_view.id_short != view.id_short: + raise BadRequest(f"idShort of new {new_view} doesn't match the old {view}") + aas.view.remove(view) + aas.view.add(new_view) + return response_t(Result(new_view)) + + def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + view_idshort = url_args["view_idshort"] + if view_idshort not in aas.view: + raise NotFound(f"No view with idShort {view_idshort} found!") + aas.view.remove(view_idshort) + return response_t(Result(None)) + # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) From 4fb873caf0db00f5c284109fe0940ecf12a37123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:24:00 +0100 Subject: [PATCH 150/474] adapter.http: remove unused imports --- basyx/aas/adapter/http.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index cf545f5..467672f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -16,7 +16,6 @@ import json from lxml import etree # type: ignore import urllib.parse -import werkzeug from werkzeug.exceptions import BadRequest, Conflict, InternalServerError, NotFound, NotImplemented from werkzeug.routing import Rule, Submount from werkzeug.wrappers import Request, Response @@ -26,7 +25,7 @@ from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Dict, Iterable, List, Optional, Tuple, Type, Union +from typing import Dict, Iterable, Optional, Tuple, Type, Union @enum.unique From efbdaac41d9015bb1eab03d31d97a1ccdec90482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:25:29 +0100 Subject: [PATCH 151/474] adapter.http: use werkzeug instead of urllib to quote and unquote identifiers --- basyx/aas/adapter/http.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 467672f..4282946 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -15,7 +15,7 @@ import io import json from lxml import etree # type: ignore -import urllib.parse +import werkzeug.urls from werkzeug.exceptions import BadRequest, Conflict, InternalServerError, NotFound, NotImplemented from werkzeug.routing import Rule, Submount from werkzeug.wrappers import Request, Response @@ -250,8 +250,7 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m def identifier_uri_encode(id_: model.Identifier) -> str: - # TODO: replace urllib with urllib3 if we're using it anyways? - return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") + return IDENTIFIER_TYPES[id_.id_type] + ":" + werkzeug.urls.url_quote(id_.id, safe="") def identifier_uri_decode(id_str: str) -> model.Identifier: @@ -261,8 +260,8 @@ def identifier_uri_decode(id_str: str) -> model.Identifier: raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) if id_type is None: - raise ValueError(f"Identifier Type '{id_type_str}' is invalid") - return model.Identifier(urllib.parse.unquote(id_), id_type) + raise ValueError(f"IdentifierType '{id_type_str}' is invalid") + return model.Identifier(werkzeug.urls.url_unquote(id_), id_type) class IdentifierConverter(werkzeug.routing.UnicodeConverter): From 452475cfa41abbbd4e2b2bc093bfbd72df3e44bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:29:01 +0100 Subject: [PATCH 152/474] adapter.http: add dirty hack to deserialize json references --- basyx/aas/adapter/http.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 4282946..3403df7 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -239,6 +239,10 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m try: if request.mimetype == "application/json": rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) + # TODO: the following is ugly, but necessary because references aren't self-identified objects + # in the json schema + if expect_type is model.AASReference: + rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) else: xml_data = io.BytesIO(request.get_data()) rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) From ca6721a59147b5acc8af7718d802ec24b8a01023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:35:17 +0100 Subject: [PATCH 153/474] adapter.http: add location header to 201 responses using the werkzeug.routing.MapAdapter url builder --- basyx/aas/adapter/http.py | 52 +++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 3403df7..b881699 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -331,12 +331,12 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi raise NotFound(f"No reference to submodel with {sm_identifier} found!") def handle_request(self, request: Request): - adapter = self.url_map.bind_to_environ(request.environ) + map_adapter = self.url_map.bind_to_environ(request.environ) try: - endpoint, values = adapter.match() + endpoint, values = map_adapter.match() if endpoint is None: raise NotImplemented("This route is not yet implemented.") - return endpoint(request, values) + return endpoint(request, values, map_adapter=map_adapter) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them except werkzeug.exceptions.NotAcceptable as e: @@ -349,39 +349,50 @@ def handle_request(self, request: Request): return e # --------- AAS ROUTES --------- - def get_aas(self, request: Request, url_args: Dict) -> Response: + def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(Result(aas)) - def get_aas_submodel_refs(self, request: Request, url_args: Dict) -> Response: + def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(Result(tuple(aas.submodel))) - def post_aas_submodel_refs(self, request: Request, url_args: Dict) -> Response: + def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: werkzeug.routing.MapAdapter) \ + -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas_identifier = url_args["aas_id"] + aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() sm_ref = parse_request_body(request, model.AASReference) # type: ignore assert isinstance(sm_ref, model.AASReference) + # to give a location header in the response we have to be able to get the submodel identifier from the reference + try: + submodel_identifier = sm_ref.get_identifier() + except ValueError as e: + raise BadRequest(f"Can't resolve submodel identifier for given reference!") from e if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") # TODO: check if reference references a non-existant submodel? aas.submodel.add(sm_ref) aas.commit() - return response_t(Result(sm_ref), status=201) + created_resource_url = map_adapter.build(self.get_aas_submodel_refs_specific, { + "aas_id": aas_identifier, + "sm_id": submodel_identifier + }, force_external=True) + return response_t(Result(sm_ref), status=201, headers={"Location": created_resource_url}) - def get_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> Response: + def get_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) return response_t(Result(sm_ref)) - def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> Response: + def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -394,24 +405,29 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> aas.commit() return response_t(Result(None)) - def get_aas_views(self, request: Request, url_args: Dict) -> Response: + def get_aas_views(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(Result(tuple(aas.view))) - def post_aas_views(self, request: Request, url_args: Dict) -> Response: + def post_aas_views(self, request: Request, url_args: Dict, map_adapter: werkzeug.routing.MapAdapter) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas_identifier = url_args["aas_id"] + aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() view = parse_request_body(request, model.View) if view.id_short in aas.view: raise Conflict(f"View with idShort {view.id_short} already exists!") aas.view.add(view) aas.commit() - return response_t(Result(view)) + created_resource_url = map_adapter.build(self.get_aas_views_specific, { + "aas_id": aas_identifier, + "view_idshort": view.id_short + }, force_external=True) + return response_t(Result(view), status=201, headers={"Location": created_resource_url}) - def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -423,7 +439,7 @@ def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: view.update() return response_t(Result(view)) - def put_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -438,7 +454,7 @@ def put_aas_views_specific(self, request: Request, url_args: Dict) -> Response: aas.view.add(new_view) return response_t(Result(new_view)) - def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -449,7 +465,7 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Respons return response_t(Result(None)) # --------- SUBMODEL ROUTES --------- - def get_submodel(self, request: Request, url_args: Dict) -> Response: + def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() From bed22ffc450b16dac80a1d285aa0d5b5d5dda0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:36:17 +0100 Subject: [PATCH 154/474] adapter.http: raise BadRequest from ValueError in IdentifierConverter --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index b881699..f29f614 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -276,7 +276,7 @@ def to_python(self, value: str) -> model.Identifier: try: return identifier_uri_decode(super().to_python(value)) except ValueError as e: - raise BadRequest(str(e)) + raise BadRequest(str(e)) from e class WSGIApp: From 8791e2eefaaa84592cf50bf4306209ef3dc0494b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 23 Nov 2020 18:23:53 +0100 Subject: [PATCH 155/474] adapter.http: check type of POST'ed references --- basyx/aas/adapter/http.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index f29f614..94aa7fd 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -241,6 +241,8 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema + # TODO: json deserialization automatically creates AASReference[Submodel] this way, so we can't check, + # whether the client posted a submodel reference or not if expect_type is model.AASReference: rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) else: @@ -369,6 +371,8 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: aas.update() sm_ref = parse_request_body(request, model.AASReference) # type: ignore assert isinstance(sm_ref, model.AASReference) + if sm_ref.type is not model.Submodel: + raise BadRequest(f"{sm_ref!r} does not reference a Submodel!") # to give a location header in the response we have to be able to get the submodel identifier from the reference try: submodel_identifier = sm_ref.get_identifier() From 836d1062ff81b15a4278b4abe471504066a7e128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Nov 2020 21:20:13 +0100 Subject: [PATCH 156/474] adapter.http: refactor imports --- basyx/aas/adapter/http.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 94aa7fd..09e8bf9 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -15,9 +15,11 @@ import io import json from lxml import etree # type: ignore +import werkzeug.exceptions +import werkzeug.routing import werkzeug.urls -from werkzeug.exceptions import BadRequest, Conflict, InternalServerError, NotFound, NotImplemented -from werkzeug.routing import Rule, Submount +from werkzeug.exceptions import BadRequest, Conflict, NotFound +from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response from aas import model @@ -91,7 +93,7 @@ def default(self, obj: object) -> object: return super().default(obj) -class APIResponse(abc.ABC, werkzeug.wrappers.Response): +class APIResponse(abc.ABC, Response): @abc.abstractmethod def __init__(self, result: Result, *args, **kwargs): super().__init__(*args, **kwargs) @@ -321,7 +323,7 @@ def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> m try: return reference.resolve(self.object_store) except (KeyError, TypeError, model.UnexpectedTypeError) as e: - raise InternalServerError(str(e)) from e + raise werkzeug.exceptions.InternalServerError(str(e)) from e @classmethod def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdministrationShell, @@ -333,11 +335,11 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi raise NotFound(f"No reference to submodel with {sm_identifier} found!") def handle_request(self, request: Request): - map_adapter = self.url_map.bind_to_environ(request.environ) + map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = map_adapter.match() if endpoint is None: - raise NotImplemented("This route is not yet implemented.") + raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") return endpoint(request, values, map_adapter=map_adapter) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them @@ -363,8 +365,7 @@ def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas.update() return response_t(Result(tuple(aas.submodel))) - def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: werkzeug.routing.MapAdapter) \ - -> Response: + def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) @@ -415,7 +416,7 @@ def get_aas_views(self, request: Request, url_args: Dict, **_kwargs) -> Response aas.update() return response_t(Result(tuple(aas.view))) - def post_aas_views(self, request: Request, url_args: Dict, map_adapter: werkzeug.routing.MapAdapter) -> Response: + def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) From 3361438996af312033c06a521e2ecf17759ea909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Nov 2020 21:24:11 +0100 Subject: [PATCH 157/474] adapter.http: refactor routing Requests to URIs with a trailing slash will now be redirected to the respective URI without trailing slash. e.g. GET /aas/$identifier/ -> GET /aas/$identifier A redirect will only be set if the respective URI without trailing slash exists and the current request method is valid for the new URI. Historically, the trailing slash was only present when the requested resource was a directory. In our case the resources don't work like directories, in the sense, that each resource doesn't even list possible subsequent resources. So because our resources don't behave like directories, they shouldn't have a trailing slash. --- basyx/aas/adapter/http.py | 52 ++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 09e8bf9..8655fe3 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -288,26 +288,32 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - Rule("/aas/", endpoint=self.get_aas), Submount("/aas/", [ - Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/submodels", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("/submodels/", methods=["GET"], - endpoint=self.get_aas_submodel_refs_specific), - Rule("/submodels/", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific), - Rule("/views", methods=["GET"], endpoint=self.get_aas_views), - Rule("/views", methods=["POST"], endpoint=self.post_aas_views), - Rule("/views/", methods=["GET"], - endpoint=self.get_aas_views_specific), - Rule("/views/", methods=["PUT"], - endpoint=self.put_aas_views_specific), - Rule("/views/", methods=["DELETE"], - endpoint=self.delete_aas_views_specific) + Rule("/", methods=["GET"], endpoint=self.get_aas), + Submount("/submodels", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("/", methods=["GET"], + endpoint=self.get_aas_submodel_refs_specific), + Rule("/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific) + ]), + Submount("/views", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_views), + Rule("/", methods=["POST"], endpoint=self.post_aas_views), + Rule("/", methods=["GET"], + endpoint=self.get_aas_views_specific), + Rule("/", methods=["PUT"], + endpoint=self.put_aas_views_specific), + Rule("/", methods=["DELETE"], + endpoint=self.delete_aas_views_specific) + ]) ]), - Rule("/submodels/", endpoint=self.get_submodel), + Submount("/submodels/", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel), + ]) ]) - ], converters={"identifier": IdentifierConverter}) + ], converters={"identifier": IdentifierConverter}, strict_slashes=False) def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) @@ -337,6 +343,18 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: + # redirect requests with a trailing slash to the path without trailing slash + # if the path without trailing slash exists. + # if not, map_adapter.match() will raise NotFound() in both cases + if request.path != "/" and request.path.endswith("/"): + map_adapter.match(request.path[:-1], request.method) + # from werkzeug's internal routing redirection + raise werkzeug.routing.RequestRedirect( + map_adapter.make_redirect_url( + werkzeug.urls.url_quote(request.path[:-1], map_adapter.map.charset, safe="/:|+"), + map_adapter.query_args + ) + ) endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") From 4f0f0e977245766a78a4fcc4760fc0c50f5651f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Nov 2020 21:36:12 +0100 Subject: [PATCH 158/474] adapter.http fix type annotations of parse_request_body() --- basyx/aas/adapter/http.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8655fe3..efcdd17 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -27,7 +27,7 @@ from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Dict, Iterable, Optional, Tuple, Type, Union +from typing import Dict, Iterable, Optional, Tuple, Type, TypeVar, Union @enum.unique @@ -216,7 +216,10 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res return response_type(result, status=exception.code, headers=headers) -def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: +T = TypeVar("T") + + +def parse_request_body(request: Request, expect_type: Type[T]) -> T: """ TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent running out of memory. but it doesn't state how to check the content length @@ -388,8 +391,7 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - sm_ref = parse_request_body(request, model.AASReference) # type: ignore - assert isinstance(sm_ref, model.AASReference) + sm_ref = parse_request_body(request, model.AASReference) if sm_ref.type is not model.Submodel: raise BadRequest(f"{sm_ref!r} does not reference a Submodel!") # to give a location header in the response we have to be able to get the submodel identifier from the reference From f5903e7ad8fbabaef2d0a6301cb45178ba4de852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Nov 2020 21:38:05 +0100 Subject: [PATCH 159/474] adapter.http: remove whitespaces from json responses --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index efcdd17..7dc11ec 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -109,7 +109,7 @@ def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) def serialize(self, result: Result) -> str: - return json.dumps(result, cls=ResultToJsonEncoder) + return json.dumps(result, cls=ResultToJsonEncoder, separators=(",", ":")) class XmlResponse(APIResponse): From 9cc1b6326a8eedf166b251f284495f35b104e55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 25 Nov 2020 19:51:47 +0100 Subject: [PATCH 160/474] adapter.http: cleanup TODO's an remove unnecessary checks --- basyx/aas/adapter/http.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7dc11ec..8525ff5 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -224,6 +224,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent running out of memory. but it doesn't state how to check the content length also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema + In the meeting (25.11.2020) we discussed, this may refer to a reverse proxy in front of this WSGI app, + which should limit the maximum content length. """ type_constructables_map = { model.AASReference: XMLConstructables.AAS_REFERENCE, @@ -246,8 +248,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema - # TODO: json deserialization automatically creates AASReference[Submodel] this way, so we can't check, - # whether the client posted a submodel reference or not + # TODO: json deserialization will always create an AASReference[Submodel], xml deserialization determines + # that automatically if expect_type is model.AASReference: rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) else: @@ -392,8 +394,6 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() sm_ref = parse_request_body(request, model.AASReference) - if sm_ref.type is not model.Submodel: - raise BadRequest(f"{sm_ref!r} does not reference a Submodel!") # to give a location header in the response we have to be able to get the submodel identifier from the reference try: submodel_identifier = sm_ref.get_identifier() @@ -401,7 +401,6 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: raise BadRequest(f"Can't resolve submodel identifier for given reference!") from e if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") - # TODO: check if reference references a non-existant submodel? aas.submodel.add(sm_ref) aas.commit() created_resource_url = map_adapter.build(self.get_aas_submodel_refs_specific, { @@ -460,8 +459,6 @@ def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> view = aas.view.get(view_idshort) if view is None: raise NotFound(f"No view with idShort {view_idshort} found!") - # TODO: is view.update() necessary here? - view.update() return response_t(Result(view)) def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: From 43de4464e6cd4377be98662386c0c44cd7a0b830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 25 Nov 2020 20:15:34 +0100 Subject: [PATCH 161/474] adapter.http: run debug app on example_aas_missing_attributes --- basyx/aas/adapter/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8525ff5..76fb619 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -496,5 +496,6 @@ def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: if __name__ == "__main__": from werkzeug.serving import run_simple - from aas.examples.data.example_aas import create_full_example + # use example_aas_missing_attributes, because the AAS from example_aas has no views + from aas.examples.data.example_aas_missing_attributes import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 722e6b108b3568fcecd5493f0ebf5a09c57b2133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Dec 2020 17:21:28 +0100 Subject: [PATCH 162/474] adapter.http: get root cause in parse_request_body() for better error messages --- basyx/aas/adapter/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 76fb619..0e4bfd0 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -256,6 +256,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: xml_data = io.BytesIO(request.get_data()) rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError) as e: + while e.__cause__ is not None: + e = e.__cause__ raise BadRequest(str(e)) from e assert isinstance(rv, expect_type) From 4a556c9da1ad94e20e08cce2e30955744a93511a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Dec 2020 17:22:40 +0100 Subject: [PATCH 163/474] adapter.http: remove minlength=1 from string url parameters because that's default anyways --- basyx/aas/adapter/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 0e4bfd0..d60c291 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -308,11 +308,11 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/views", [ Rule("/", methods=["GET"], endpoint=self.get_aas_views), Rule("/", methods=["POST"], endpoint=self.post_aas_views), - Rule("/", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.get_aas_views_specific), - Rule("/", methods=["PUT"], + Rule("/", methods=["PUT"], endpoint=self.put_aas_views_specific), - Rule("/", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_aas_views_specific) ]) ]), From 4b6968695c68e732b881ebd369d9784372a5cf72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Dec 2020 17:23:55 +0100 Subject: [PATCH 164/474] adapter.http: rewrite idShort as id_short in error message to stay consistent with the rest of this library --- basyx/aas/adapter/http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d60c291..d3cf63d 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -444,7 +444,7 @@ def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapt aas.update() view = parse_request_body(request, model.View) if view.id_short in aas.view: - raise Conflict(f"View with idShort {view.id_short} already exists!") + raise Conflict(f"View with id_short {view.id_short} already exists!") aas.view.add(view) aas.commit() created_resource_url = map_adapter.build(self.get_aas_views_specific, { @@ -460,7 +460,7 @@ def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> view_idshort = url_args["view_idshort"] view = aas.view.get(view_idshort) if view is None: - raise NotFound(f"No view with idShort {view_idshort} found!") + raise NotFound(f"No view with id_short {view_idshort} found!") return response_t(Result(view)) def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -470,10 +470,10 @@ def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> view_idshort = url_args["view_idshort"] view = aas.view.get(view_idshort) if view is None: - raise NotFound(f"No view with idShort {view_idshort} found!") + raise NotFound(f"No view with id_short {view_idshort} found!") new_view = parse_request_body(request, model.View) if new_view.id_short != view.id_short: - raise BadRequest(f"idShort of new {new_view} doesn't match the old {view}") + raise BadRequest(f"id_short of new {new_view} doesn't match the old {view}") aas.view.remove(view) aas.view.add(new_view) return response_t(Result(new_view)) @@ -484,7 +484,7 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) aas.update() view_idshort = url_args["view_idshort"] if view_idshort not in aas.view: - raise NotFound(f"No view with idShort {view_idshort} found!") + raise NotFound(f"No view with id_short {view_idshort} found!") aas.view.remove(view_idshort) return response_t(Result(None)) From 7d0dd18424582e5ef98edc4b9f0451cdbd7c9876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Dec 2020 17:24:59 +0100 Subject: [PATCH 165/474] adapter.http: add nested submodel elements endpoints add IdShortPathConverter and helper functions --- basyx/aas/adapter/http.py | 135 +++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d3cf63d..522d8d1 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -27,7 +27,7 @@ from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Dict, Iterable, Optional, Tuple, Type, TypeVar, Union +from typing import Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union @enum.unique @@ -290,6 +290,35 @@ def to_python(self, value: str) -> model.Identifier: raise BadRequest(str(e)) from e +def validate_id_short(id_short: str) -> bool: + try: + model.MultiLanguageProperty(id_short) + except ValueError: + return False + return True + + +class IdShortPathConverter(werkzeug.routing.PathConverter): + id_short_prefix = "!" + + def to_url(self, value: List[str]) -> str: + for id_short in value: + if not validate_id_short(id_short): + raise ValueError(f"{id_short} is not a valid id_short!") + return "/".join([self.id_short_prefix + id_short for id_short in value]) + + def to_python(self, value: str) -> List[str]: + id_shorts = super().to_python(value).split("/") + for idx, id_short in enumerate(id_shorts): + if not id_short.startswith(self.id_short_prefix): + raise werkzeug.routing.ValidationError + id_short = id_short[1:] + if not validate_id_short(id_short): + raise BadRequest(f"{id_short} is not a valid id_short!") + id_shorts[idx] = id_short + return id_shorts + + class WSGIApp: def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store @@ -318,9 +347,20 @@ def __init__(self, object_store: model.AbstractObjectStore): ]), Submount("/submodels/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodelElements", methods=["POST"], endpoint=self.post_submodel_submodel_elements), + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_specific_nested), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_specific_nested), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_specific_nested) ]) ]) - ], converters={"identifier": IdentifierConverter}, strict_slashes=False) + ], converters={ + "identifier": IdentifierConverter, + "id_short_path": IdShortPathConverter + }, strict_slashes=False) def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) @@ -347,6 +387,33 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi return sm_ref raise NotFound(f"No reference to submodel with {sm_identifier} found!") + @classmethod + def _get_nested_submodel_element(cls, namespace: model.Namespace, id_shorts: List[str]) -> model.SubmodelElement: + current_namespace: Union[model.Namespace, model.SubmodelElement] = namespace + for id_short in id_shorts: + current_namespace = cls._expect_namespace(current_namespace, id_short) + next_obj = cls._namespace_submodel_element_op(current_namespace, current_namespace.get_referable, id_short) + if not isinstance(next_obj, model.SubmodelElement): + raise werkzeug.exceptions.InternalServerError(f"{next_obj}, child of {current_namespace!r}, " + f"is not a submodel element!") + current_namespace = next_obj + if not isinstance(current_namespace, model.SubmodelElement): + raise TypeError("No id_shorts specified!") + return current_namespace + + @classmethod + def _expect_namespace(cls, obj: object, needle: str) -> model.Namespace: + if not isinstance(obj, model.Namespace): + raise BadRequest(f"{obj!r} is not a namespace, can't locate {needle}!") + return obj + + @classmethod + def _namespace_submodel_element_op(cls, namespace: model.Namespace, op: Callable[[str], T], arg: str) -> T: + try: + return op(arg) + except KeyError as e: + raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e + def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: @@ -495,6 +562,70 @@ def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: submodel.update() return response_t(Result(submodel)) + def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(Result(tuple(submodel.submodel_element))) + + def post_submodel_submodel_elements(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore + if submodel_element.id_short in submodel.submodel_element: + raise Conflict(f"Submodel element with id_short {submodel_element.id_short} already exists!") + submodel.submodel_element.add(submodel_element) + submodel.commit() + created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_specific_nested, { + "submodel_id": submodel_identifier, + "id_shorts": [submodel_element.id_short] + }, force_external=True) + return response_t(Result(submodel_element), status=201, headers={"Location": created_resource_url}) + + def get_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + return response_t(Result(submodel_element)) + + def put_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + new_submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore + mismatch_error_message = f" of new submodel element {new_submodel_element} doesn't not match " \ + f"the current submodel element {submodel_element}" + if not type(submodel_element) is type(new_submodel_element): + raise BadRequest("Type" + mismatch_error_message) + if submodel_element.id_short != new_submodel_element.id_short: + raise BadRequest("id_short" + mismatch_error_message) + submodel_element.update_from(new_submodel_element) + submodel_element.commit() + return response_t(Result(submodel_element)) + + def delete_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + id_shorts: List[str] = url_args["id_shorts"] + parent: model.Namespace = submodel + if len(id_shorts) > 1: + parent = self._expect_namespace( + self._get_nested_submodel_element(submodel, id_shorts[:-1]), + id_shorts[-1] + ) + self._namespace_submodel_element_op(parent, parent.remove_referable, id_shorts[-1]) + return response_t(Result(None)) + if __name__ == "__main__": from werkzeug.serving import run_simple From b949a7c60c1f607d3c43afe5c99ee9847d63da10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 9 Dec 2020 19:38:55 +0100 Subject: [PATCH 166/474] adapter.http: compare id_shorts case-insensitively in PUT routes change type check from `not A is B` to `A is not B` for better readability --- basyx/aas/adapter/http.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 522d8d1..16bf535 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -539,7 +539,8 @@ def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> if view is None: raise NotFound(f"No view with id_short {view_idshort} found!") new_view = parse_request_body(request, model.View) - if new_view.id_short != view.id_short: + # compare id_shorts case-insensitively + if new_view.id_short.lower() != view.id_short.lower(): raise BadRequest(f"id_short of new {new_view} doesn't match the old {view}") aas.view.remove(view) aas.view.add(new_view) @@ -603,9 +604,10 @@ def put_submodel_submodel_elements_specific_nested(self, request: Request, url_a new_submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore mismatch_error_message = f" of new submodel element {new_submodel_element} doesn't not match " \ f"the current submodel element {submodel_element}" - if not type(submodel_element) is type(new_submodel_element): + if type(submodel_element) is not type(new_submodel_element): raise BadRequest("Type" + mismatch_error_message) - if submodel_element.id_short != new_submodel_element.id_short: + # compare id_shorts case-insensitively + if submodel_element.id_short.lower() != new_submodel_element.id_short.lower(): raise BadRequest("id_short" + mismatch_error_message) submodel_element.update_from(new_submodel_element) submodel_element.commit() From 2afc0037acfeced264f788d591a34400744737f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 9 Dec 2020 19:42:48 +0100 Subject: [PATCH 167/474] adapter.http: add nested submodel element endpoints for type-specific attributes like statement, annotation and value --- basyx/aas/adapter/http.py | 59 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 16bf535..e0283c5 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -354,7 +354,26 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_elements_specific_nested), Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_specific_nested) + endpoint=self.delete_submodel_submodel_elements_specific_nested), + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + Rule("//values", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("//values", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("//annotations", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation")), + Rule("//annotations", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation", + request_body_type=model.DataElement)), # type: ignore + Rule("//statements", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, "statement")), + Rule("//statements", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, "statement")) ]) ]) ], converters={ @@ -628,6 +647,44 @@ def delete_submodel_submodel_elements_specific_nested(self, request: Request, ur self._namespace_submodel_element_op(parent, parent.remove_referable, id_shorts[-1]) return response_t(Result(None)) + # --------- SUBMODEL ROUTE FACTORIES --------- + def factory_get_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str) \ + -> Callable[[Request, Dict], Response]: + def route(request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + if not isinstance(submodel_element, type_): + raise BadRequest(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") + return response_t(Result(tuple(getattr(submodel_element, attr)))) + return route + + def factory_post_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str, + request_body_type: Type[model.SubmodelElement] + = model.SubmodelElement) \ + -> Callable[[Request, Dict, MapAdapter], Response]: + def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + id_shorts = url_args["id_shorts"] + submodel_element = self._get_nested_submodel_element(submodel, id_shorts) + if not isinstance(submodel_element, type_): + raise BadRequest(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") + new_submodel_element = parse_request_body(request, request_body_type) + if new_submodel_element.id_short in getattr(submodel_element, attr): + raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") + getattr(submodel_element, attr).add(new_submodel_element) + submodel_element.commit() + created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_specific_nested, { + "submodel_id": submodel_identifier, + "id_shorts": id_shorts + [new_submodel_element.id_short] + }, force_external=True) + return response_t(Result(new_submodel_element), status=201, headers={"Location": created_resource_url}) + return route + if __name__ == "__main__": from werkzeug.serving import run_simple From c79220306d012ce3c9acb370d1bd506ec840b4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 16 Feb 2021 22:09:48 +0100 Subject: [PATCH 168/474] adapter.http: update to V3.0RC01 --- basyx/aas/adapter/http.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e0283c5..93da59a 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -407,8 +407,9 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi raise NotFound(f"No reference to submodel with {sm_identifier} found!") @classmethod - def _get_nested_submodel_element(cls, namespace: model.Namespace, id_shorts: List[str]) -> model.SubmodelElement: - current_namespace: Union[model.Namespace, model.SubmodelElement] = namespace + def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, id_shorts: List[str]) \ + -> model.SubmodelElement: + current_namespace: Union[model.UniqueIdShortNamespace, model.SubmodelElement] = namespace for id_short in id_shorts: current_namespace = cls._expect_namespace(current_namespace, id_short) next_obj = cls._namespace_submodel_element_op(current_namespace, current_namespace.get_referable, id_short) @@ -421,13 +422,14 @@ def _get_nested_submodel_element(cls, namespace: model.Namespace, id_shorts: Lis return current_namespace @classmethod - def _expect_namespace(cls, obj: object, needle: str) -> model.Namespace: - if not isinstance(obj, model.Namespace): + def _expect_namespace(cls, obj: object, needle: str) -> model.UniqueIdShortNamespace: + if not isinstance(obj, model.UniqueIdShortNamespace): raise BadRequest(f"{obj!r} is not a namespace, can't locate {needle}!") return obj @classmethod - def _namespace_submodel_element_op(cls, namespace: model.Namespace, op: Callable[[str], T], arg: str) -> T: + def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, op: Callable[[str], T], arg: str) \ + -> T: try: return op(arg) except KeyError as e: @@ -544,7 +546,7 @@ def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] - view = aas.view.get(view_idshort) + view = aas.view.get("id_short", view_idshort) if view is None: raise NotFound(f"No view with id_short {view_idshort} found!") return response_t(Result(view)) @@ -554,7 +556,7 @@ def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] - view = aas.view.get(view_idshort) + view = aas.view.get("id_short", view_idshort) if view is None: raise NotFound(f"No view with id_short {view_idshort} found!") new_view = parse_request_body(request, model.View) @@ -638,7 +640,7 @@ def delete_submodel_submodel_elements_specific_nested(self, request: Request, ur submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() id_shorts: List[str] = url_args["id_shorts"] - parent: model.Namespace = submodel + parent: model.UniqueIdShortNamespace = submodel if len(id_shorts) > 1: parent = self._expect_namespace( self._get_nested_submodel_element(submodel, id_shorts[:-1]), From bd3943a26bd029b769936727018e5fd916cba828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:13:44 +0100 Subject: [PATCH 169/474] adapter.http: change some response types from BadRequest to UnprocessableEntity --- basyx/aas/adapter/http.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 93da59a..5641720 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -18,7 +18,7 @@ import werkzeug.exceptions import werkzeug.routing import werkzeug.urls -from werkzeug.exceptions import BadRequest, Conflict, NotFound +from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response @@ -258,7 +258,7 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError) as e: while e.__cause__ is not None: e = e.__cause__ - raise BadRequest(str(e)) from e + raise UnprocessableEntity(str(e)) from e assert isinstance(rv, expect_type) return rv @@ -488,7 +488,7 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: try: submodel_identifier = sm_ref.get_identifier() except ValueError as e: - raise BadRequest(f"Can't resolve submodel identifier for given reference!") from e + raise UnprocessableEntity(f"Can't resolve submodel identifier for given reference!") from e if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") aas.submodel.add(sm_ref) @@ -626,10 +626,10 @@ def put_submodel_submodel_elements_specific_nested(self, request: Request, url_a mismatch_error_message = f" of new submodel element {new_submodel_element} doesn't not match " \ f"the current submodel element {submodel_element}" if type(submodel_element) is not type(new_submodel_element): - raise BadRequest("Type" + mismatch_error_message) + raise UnprocessableEntity("Type" + mismatch_error_message) # compare id_shorts case-insensitively if submodel_element.id_short.lower() != new_submodel_element.id_short.lower(): - raise BadRequest("id_short" + mismatch_error_message) + raise UnprocessableEntity("id_short" + mismatch_error_message) submodel_element.update_from(new_submodel_element) submodel_element.commit() return response_t(Result(submodel_element)) @@ -658,7 +658,7 @@ def route(request: Request, url_args: Dict, **_kwargs) -> Response: submodel.update() submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) if not isinstance(submodel_element, type_): - raise BadRequest(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") + raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") return response_t(Result(tuple(getattr(submodel_element, attr)))) return route @@ -674,7 +674,7 @@ def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response id_shorts = url_args["id_shorts"] submodel_element = self._get_nested_submodel_element(submodel, id_shorts) if not isinstance(submodel_element, type_): - raise BadRequest(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") + raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") new_submodel_element = parse_request_body(request, request_body_type) if new_submodel_element.id_short in getattr(submodel_element, attr): raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") From 0f6a0d9d8b8c67b73e18ae1ad5a5febc83dba5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:18:36 +0100 Subject: [PATCH 170/474] adapter.http: refactor submodel element route map --- basyx/aas/adapter/http.py | 54 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 5641720..efa15a8 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -349,31 +349,35 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/submodelElements", methods=["POST"], endpoint=self.post_submodel_submodel_elements), - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_specific_nested), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_specific_nested), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_specific_nested), - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - Rule("//values", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("//values", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("//annotations", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation")), - Rule("//annotations", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation", - request_body_type=model.DataElement)), # type: ignore - Rule("//statements", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, "statement")), - Rule("//statements", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, "statement")) + Submount("/", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_specific_nested), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_specific_nested), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_specific_nested), + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + Rule("/values", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("/values", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("/annotations", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation")), + Rule("/annotations", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation", + request_body_type=model.DataElement)), # type: ignore + Rule("/statements", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, + "statement")), + Rule("/statements", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, + "statement")), + ]) ]) ]) ], converters={ From aa67d996c9854b99192011248c688c72497860df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:52:47 +0100 Subject: [PATCH 171/474] adapter.http: drop support for formulas --- basyx/aas/adapter/http.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index efa15a8..c1d718b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -181,8 +181,6 @@ def aas_object_to_xml(obj: object) -> etree.Element: # TODO: xml serialization needs a constraint_to_xml() function if isinstance(obj, model.Qualifier): return xml_serialization.qualifier_to_xml(obj) - if isinstance(obj, model.Formula): - return xml_serialization.formula_to_xml(obj) if isinstance(obj, model.SubmodelElement): return xml_serialization.submodel_element_to_xml(obj) raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") @@ -230,7 +228,7 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: type_constructables_map = { model.AASReference: XMLConstructables.AAS_REFERENCE, model.View: XMLConstructables.VIEW, - model.Constraint: XMLConstructables.CONSTRAINT, + model.Qualifier: XMLConstructables.QUALIFIER, model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT } From a1627b57017adabe6832d883caa924001b66a22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:54:46 +0100 Subject: [PATCH 172/474] adapter.http: allow changing id_short in PUT routes --- basyx/aas/adapter/http.py | 46 ++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index c1d718b..adcb2c7 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -553,21 +553,26 @@ def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> raise NotFound(f"No view with id_short {view_idshort} found!") return response_t(Result(view)) - def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_aas_views_specific(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas_identifier = url_args["aas_id"] + aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] view = aas.view.get("id_short", view_idshort) if view is None: raise NotFound(f"No view with id_short {view_idshort} found!") new_view = parse_request_body(request, model.View) - # compare id_shorts case-insensitively - if new_view.id_short.lower() != view.id_short.lower(): - raise BadRequest(f"id_short of new {new_view} doesn't match the old {view}") - aas.view.remove(view) - aas.view.add(new_view) - return response_t(Result(new_view)) + # TODO: raise conflict if the following fails + view.update_from(new_view) + view.commit() + if view_idshort.upper() != view.id_short.upper(): + created_resource_url = map_adapter.build(self.put_aas_views_specific, { + "aas_id": aas_identifier, + "view_idshort": view.id_short + }, force_external=True) + return response_t(Result(view), status=201, headers={"Location": created_resource_url}) + return response_t(Result(view)) def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) @@ -617,23 +622,30 @@ def get_submodel_submodel_elements_specific_nested(self, request: Request, url_a submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(Result(submodel_element)) - def put_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, + map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + id_short_path = url_args["id_shorts"] + submodel_element = self._get_nested_submodel_element(submodel, id_short_path) + current_id_short = submodel_element.id_short # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 new_submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - mismatch_error_message = f" of new submodel element {new_submodel_element} doesn't not match " \ - f"the current submodel element {submodel_element}" if type(submodel_element) is not type(new_submodel_element): - raise UnprocessableEntity("Type" + mismatch_error_message) - # compare id_shorts case-insensitively - if submodel_element.id_short.lower() != new_submodel_element.id_short.lower(): - raise UnprocessableEntity("id_short" + mismatch_error_message) + raise UnprocessableEntity(f"Type of new submodel element {new_submodel_element} doesn't not match " + f"the current submodel element {submodel_element}") + # TODO: raise conflict if the following fails submodel_element.update_from(new_submodel_element) submodel_element.commit() + if new_submodel_element.id_short.upper() != current_id_short.upper(): + created_resource_url = map_adapter.build(self.put_submodel_submodel_elements_specific_nested, { + "submodel_id": submodel_identifier, + "id_shorts": id_short_path[:-1] + [submodel_element.id_short] + }, force_external=True) + return response_t(Result(submodel_element), status=201, headers={"Location": created_resource_url}) return response_t(Result(submodel_element)) def delete_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) \ From 3be1814fd9b1b6e9ebde604a404b7f71206042bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:55:52 +0100 Subject: [PATCH 173/474] adapter.http: add constraint routes --- basyx/aas/adapter/http.py | 112 +++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index adcb2c7..97432da 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -375,7 +375,24 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/statements", methods=["POST"], endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, "statement")), - ]) + Rule("/constraints", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/constraints", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]), + Rule("/constraints", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/constraints", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints) ]) ]) ], converters={ @@ -420,9 +437,17 @@ def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, i f"is not a submodel element!") current_namespace = next_obj if not isinstance(current_namespace, model.SubmodelElement): - raise TypeError("No id_shorts specified!") + raise ValueError("No id_shorts specified!") return current_namespace + @classmethod + def _get_submodel_or_nested_submodel_element(cls, submodel: model.Submodel, id_shorts: List[str]) \ + -> Union[model.Submodel, model.SubmodelElement]: + try: + return cls._get_nested_submodel_element(submodel, id_shorts) + except ValueError: + return submodel + @classmethod def _expect_namespace(cls, obj: object, needle: str) -> model.UniqueIdShortNamespace: if not isinstance(obj, model.UniqueIdShortNamespace): @@ -663,6 +688,89 @@ def delete_submodel_submodel_elements_specific_nested(self, request: Request, ur self._namespace_submodel_element_op(parent, parent.remove_referable, id_shorts[-1]) return response_t(Result(None)) + def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) + qualifier_type = url_args.get("qualifier_type") + if qualifier_type is None: + return response_t(Result(tuple(sm_or_se.qualifier))) + try: + return response_t(Result(sm_or_se.get_qualifier_by_type(qualifier_type))) + except KeyError: + raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + + def post_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ + -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + id_shorts: List[str] = url_args.get("id_shorts", []) + sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + qualifier = parse_request_body(request, model.Qualifier) + if ("type", qualifier.type) in sm_or_se.qualifier: + raise Conflict(f"Qualifier with type {qualifier.type} already exists!") + sm_or_se.qualifier.add(qualifier) + sm_or_se.commit() + created_resource_url = map_adapter.build(self.get_submodel_submodel_element_constraints, { + "submodel_id": submodel_identifier, + "id_shorts": id_shorts if len(id_shorts) != 0 else None, + "qualifier_type": qualifier.type + }, force_external=True) + return response_t(Result(qualifier), status=201, headers={"Location": created_resource_url}) + + def put_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ + -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + id_shorts: List[str] = url_args.get("id_shorts", []) + sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + new_qualifier = parse_request_body(request, model.Qualifier) + qualifier_type = url_args["qualifier_type"] + try: + qualifier = sm_or_se.get_qualifier_by_type(qualifier_type) + except KeyError: + raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + if type(qualifier) is not type(new_qualifier): + raise UnprocessableEntity(f"Type of new qualifier {new_qualifier} doesn't not match " + f"the current submodel element {qualifier}") + qualifier_type_changed = qualifier_type != new_qualifier.type + # TODO: have to pass a tuple to __contains__ here. can't this be simplified? + if qualifier_type_changed and ("type", new_qualifier.type) in sm_or_se.qualifier: + raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") + sm_or_se.remove_qualifier_by_type(qualifier.type) + sm_or_se.qualifier.add(new_qualifier) + sm_or_se.commit() + if qualifier_type_changed: + created_resource_url = map_adapter.build(self.get_submodel_submodel_element_constraints, { + "submodel_id": submodel_identifier, + "id_shorts": id_shorts if len(id_shorts) != 0 else None, + "qualifier_type": new_qualifier.type + }, force_external=True) + return response_t(Result(new_qualifier), status=201, headers={"Location": created_resource_url}) + return response_t(Result(new_qualifier)) + + def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + id_shorts: List[str] = url_args.get("id_shorts", []) + sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + qualifier_type = url_args["qualifier_type"] + try: + sm_or_se.remove_qualifier_by_type(qualifier_type) + except KeyError: + raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + sm_or_se.commit() + return response_t(Result(None)) + # --------- SUBMODEL ROUTE FACTORIES --------- def factory_get_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str) \ -> Callable[[Request, Dict], Response]: From 6445e8f090116980163be09fed9cef6568d25406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 22:43:32 +0200 Subject: [PATCH 174/474] adapter.http: make trailing slashes the default http-api-oas: follow suit --- basyx/aas/adapter/http.py | 73 +++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 97432da..09f0a58 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -327,26 +327,26 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("/", methods=["GET"], + Rule("//", methods=["GET"], endpoint=self.get_aas_submodel_refs_specific), - Rule("/", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]), Submount("/views", [ Rule("/", methods=["GET"], endpoint=self.get_aas_views), Rule("/", methods=["POST"], endpoint=self.post_aas_views), - Rule("/", methods=["GET"], + Rule("//", methods=["GET"], endpoint=self.get_aas_views_specific), - Rule("/", methods=["PUT"], + Rule("//", methods=["PUT"], endpoint=self.put_aas_views_specific), - Rule("/", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_views_specific) ]) ]), Submount("/submodels/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodelElements", methods=["POST"], endpoint=self.post_submodel_submodel_elements), + Rule("/submodelElements/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodelElements/", methods=["POST"], endpoint=self.post_submodel_submodel_elements), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_specific_nested), @@ -356,49 +356,52 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.delete_submodel_submodel_elements_specific_nested), # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - Rule("/values", methods=["GET"], + Rule("/values/", methods=["GET"], endpoint=self.factory_get_submodel_submodel_elements_nested_attr( model.SubmodelElementCollection, "value")), # type: ignore - Rule("/values", methods=["POST"], + Rule("/values/", methods=["POST"], endpoint=self.factory_post_submodel_submodel_elements_nested_attr( model.SubmodelElementCollection, "value")), # type: ignore - Rule("/annotations", methods=["GET"], + Rule("/annotations/", methods=["GET"], endpoint=self.factory_get_submodel_submodel_elements_nested_attr( model.AnnotatedRelationshipElement, "annotation")), - Rule("/annotations", methods=["POST"], + Rule("/annotations/", methods=["POST"], endpoint=self.factory_post_submodel_submodel_elements_nested_attr( model.AnnotatedRelationshipElement, "annotation", request_body_type=model.DataElement)), # type: ignore - Rule("/statements", methods=["GET"], + Rule("/statements/", methods=["GET"], endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, "statement")), - Rule("/statements", methods=["POST"], + Rule("/statements/", methods=["POST"], endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, "statement")), - Rule("/constraints", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/constraints", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["GET"], + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) + ]), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["PUT"], + Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_constraints), - ]), - Rule("/constraints", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/constraints", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints) + ]) ]) ]) ], converters={ "identifier": IdentifierConverter, "id_short_path": IdShortPathConverter - }, strict_slashes=False) + }) def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) @@ -465,18 +468,6 @@ def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: - # redirect requests with a trailing slash to the path without trailing slash - # if the path without trailing slash exists. - # if not, map_adapter.match() will raise NotFound() in both cases - if request.path != "/" and request.path.endswith("/"): - map_adapter.match(request.path[:-1], request.method) - # from werkzeug's internal routing redirection - raise werkzeug.routing.RequestRedirect( - map_adapter.make_redirect_url( - werkzeug.urls.url_quote(request.path[:-1], map_adapter.map.charset, safe="/:|+"), - map_adapter.query_args - ) - ) endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") From c349fae325745e79eb7c4f95b026ddfc4ea1bc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:03:53 +0200 Subject: [PATCH 175/474] adapter.http: return json if Accept header is missing or empty --- basyx/aas/adapter/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 09f0a58..c3d7db9 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -192,6 +192,8 @@ def get_response_type(request: Request) -> Type[APIResponse]: "application/xml": XmlResponse, "text/xml": XmlResponseAlt } + if len(request.accept_mimetypes) == 0: + return JsonResponse mime_type = request.accept_mimetypes.best_match(response_types) if mime_type is None: raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " From 0bc0cd3dc3537b8efdd4f2d9c3bde06335309101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:07:41 +0200 Subject: [PATCH 176/474] adapter.http: get root cause only for xml deserializer errors --- basyx/aas/adapter/http.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index c3d7db9..46e0689 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -253,11 +253,17 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: if expect_type is model.AASReference: rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) else: - xml_data = io.BytesIO(request.get_data()) - rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) - except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError) as e: - while e.__cause__ is not None: - e = e.__cause__ + try: + xml_data = io.BytesIO(request.get_data()) + rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) + except (KeyError, ValueError) as e: + # xml deserialization creates an error chain. since we only return one error, return the root cause + f = e + while f.__cause__ is not None: + f = f.__cause__ + raise f from e + except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError, model.AASConstraintViolation) \ + as e: raise UnprocessableEntity(str(e)) from e assert isinstance(rv, expect_type) From f088693247f915cdab9bc9fd0fb2db24e9741c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:08:49 +0200 Subject: [PATCH 177/474] adapter.http: return 422 if parsed object doesn't match the expected type --- basyx/aas/adapter/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 46e0689..0a9e818 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -266,7 +266,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: as e: raise UnprocessableEntity(str(e)) from e - assert isinstance(rv, expect_type) + if not isinstance(rv, expect_type): + raise UnprocessableEntity(f"Object {rv!r} is not of type {expect_type.__name__}!") return rv From 9fe0fbb5e087bdf47f5738e1f598d834481535ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:09:59 +0200 Subject: [PATCH 178/474] adapter.http: fix existance checks --- basyx/aas/adapter/http.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 0a9e818..7605de9 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -558,7 +558,7 @@ def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapt aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() view = parse_request_body(request, model.View) - if view.id_short in aas.view: + if ("id_short", view.id_short) in aas.view: raise Conflict(f"View with id_short {view.id_short} already exists!") aas.view.add(view) aas.commit() @@ -604,7 +604,7 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] - if view_idshort not in aas.view: + if ("id_short", view_idshort) not in aas.view: raise NotFound(f"No view with id_short {view_idshort} found!") aas.view.remove(view_idshort) return response_t(Result(None)) @@ -630,7 +630,7 @@ def post_submodel_submodel_elements(self, request: Request, url_args: Dict, map_ # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - if submodel_element.id_short in submodel.submodel_element: + if ("id_short", submodel_element.id_short) in submodel.submodel_element: raise Conflict(f"Submodel element with id_short {submodel_element.id_short} already exists!") submodel.submodel_element.add(submodel_element) submodel.commit() @@ -740,7 +740,6 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: raise UnprocessableEntity(f"Type of new qualifier {new_qualifier} doesn't not match " f"the current submodel element {qualifier}") qualifier_type_changed = qualifier_type != new_qualifier.type - # TODO: have to pass a tuple to __contains__ here. can't this be simplified? if qualifier_type_changed and ("type", new_qualifier.type) in sm_or_se.qualifier: raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") sm_or_se.remove_qualifier_by_type(qualifier.type) @@ -798,7 +797,7 @@ def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response if not isinstance(submodel_element, type_): raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") new_submodel_element = parse_request_body(request, request_body_type) - if new_submodel_element.id_short in getattr(submodel_element, attr): + if ("id_short", new_submodel_element.id_short) in getattr(submodel_element, attr): raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") getattr(submodel_element, attr).add(new_submodel_element) submodel_element.commit() From 2148056bacd1d8431e11e36cceb3bdae2d319128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:47:40 +0200 Subject: [PATCH 179/474] adapter.http: add missing type annotation --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7605de9..471cb0f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -258,7 +258,7 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) except (KeyError, ValueError) as e: # xml deserialization creates an error chain. since we only return one error, return the root cause - f = e + f: BaseException = e while f.__cause__ is not None: f = f.__cause__ raise f from e From 2fa60ba8f6ff3af4e369c4ddae21918fabf76d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Jul 2021 16:27:26 +0200 Subject: [PATCH 180/474] adapter.http: fix id_short validation --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 471cb0f..392ce3a 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -300,7 +300,7 @@ def to_python(self, value: str) -> model.Identifier: def validate_id_short(id_short: str) -> bool: try: model.MultiLanguageProperty(id_short) - except ValueError: + except model.AASConstraintViolation: return False return True From 9aea9e6440881aa267a998c14a53c195aeb76f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Jul 2021 16:27:55 +0200 Subject: [PATCH 181/474] adapter.http: fix view removal --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 392ce3a..95f768b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -606,7 +606,7 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) view_idshort = url_args["view_idshort"] if ("id_short", view_idshort) not in aas.view: raise NotFound(f"No view with id_short {view_idshort} found!") - aas.view.remove(view_idshort) + aas.view.remove(("id_short", view_idshort)) return response_t(Result(None)) # --------- SUBMODEL ROUTES --------- From ceb67ad34daba295fa44974cef3db15994cb681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Jul 2021 14:28:31 +0200 Subject: [PATCH 182/474] adapter.http: fix NamespaceSet containment checks and removals ... in accordance to 0e343ac0810dead528e6dd43ef3e7752d0387c5f. --- basyx/aas/adapter/http.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 95f768b..b02f4b8 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -558,7 +558,7 @@ def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapt aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() view = parse_request_body(request, model.View) - if ("id_short", view.id_short) in aas.view: + if aas.view.contains_id("id_short", view.id_short): raise Conflict(f"View with id_short {view.id_short} already exists!") aas.view.add(view) aas.commit() @@ -604,9 +604,9 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] - if ("id_short", view_idshort) not in aas.view: + if not aas.view.contains_id("id_short", view_idshort): raise NotFound(f"No view with id_short {view_idshort} found!") - aas.view.remove(("id_short", view_idshort)) + aas.view.remove_by_id("id_short", view_idshort) return response_t(Result(None)) # --------- SUBMODEL ROUTES --------- @@ -630,7 +630,7 @@ def post_submodel_submodel_elements(self, request: Request, url_args: Dict, map_ # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - if ("id_short", submodel_element.id_short) in submodel.submodel_element: + if submodel.submodel_element.contains_id("id_short", submodel_element.id_short): raise Conflict(f"Submodel element with id_short {submodel_element.id_short} already exists!") submodel.submodel_element.add(submodel_element) submodel.commit() @@ -711,7 +711,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier = parse_request_body(request, model.Qualifier) - if ("type", qualifier.type) in sm_or_se.qualifier: + if sm_or_se.qualifier.contains_id("type", qualifier.type): raise Conflict(f"Qualifier with type {qualifier.type} already exists!") sm_or_se.qualifier.add(qualifier) sm_or_se.commit() @@ -740,7 +740,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: raise UnprocessableEntity(f"Type of new qualifier {new_qualifier} doesn't not match " f"the current submodel element {qualifier}") qualifier_type_changed = qualifier_type != new_qualifier.type - if qualifier_type_changed and ("type", new_qualifier.type) in sm_or_se.qualifier: + if qualifier_type_changed and sm_or_se.qualifier.contains_id("type", new_qualifier.type): raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") sm_or_se.remove_qualifier_by_type(qualifier.type) sm_or_se.qualifier.add(new_qualifier) @@ -797,7 +797,7 @@ def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response if not isinstance(submodel_element, type_): raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") new_submodel_element = parse_request_body(request, request_body_type) - if ("id_short", new_submodel_element.id_short) in getattr(submodel_element, attr): + if getattr(submodel_element, attr).contains_id("id_short", new_submodel_element.id_short): raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") getattr(submodel_element, attr).add(new_submodel_element) submodel_element.commit() From e3d990b2765ee0b0cf453980f16f382b28ef7f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 27 Jul 2021 16:48:25 +0200 Subject: [PATCH 183/474] adapter.http: update AAS interface for new api spec update response encoders update response data --- basyx/aas/adapter/http.py | 311 +++++++++++++++++--------------------- 1 file changed, 135 insertions(+), 176 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index b02f4b8..9c6a0ce 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -11,6 +11,7 @@ import abc +import datetime import enum import io import json @@ -24,83 +25,89 @@ from aas import model from .xml import XMLConstructables, read_aas_xml_element, xml_serialization -from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder +from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union +from typing import Callable, Dict, List, Optional, Type, TypeVar, Union + + +# TODO: support the path/reference/etc. parameter @enum.unique -class ErrorType(enum.Enum): - UNSPECIFIED = enum.auto() - DEBUG = enum.auto() - INFORMATION = enum.auto() +class MessageType(enum.Enum): + UNDEFINED = enum.auto() + INFO = enum.auto() WARNING = enum.auto() ERROR = enum.auto() - FATAL = enum.auto() EXCEPTION = enum.auto() def __str__(self): return self.name.capitalize() -class Error: - def __init__(self, code: str, text: str, type_: ErrorType = ErrorType.UNSPECIFIED): - self.type = type_ +class Message: + def __init__(self, code: str, text: str, type_: MessageType = MessageType.UNDEFINED, + timestamp: Optional[datetime.datetime] = None): self.code = code self.text = text - - -ResultData = Union[object, Tuple[object, ...]] + self.messageType = type_ + self.timestamp = timestamp if timestamp is not None else datetime.datetime.utcnow() class Result: - def __init__(self, data: Optional[Union[ResultData, Error]]): - # the following is True when data is None, which is the expected behavior - self.success: bool = not isinstance(data, Error) - self.data: Optional[ResultData] = None - self.error: Optional[Error] = None - if isinstance(data, Error): - self.error = data - else: - self.data = data + def __init__(self, success: bool, messages: Optional[List[Message]] = None): + if messages is None: + messages = [] + self.success: bool = success + self.messages: List[Message] = messages -class ResultToJsonEncoder(StrippedAASToJsonEncoder): +class ResultToJsonEncoder(AASToJsonEncoder): @classmethod def _result_to_json(cls, result: Result) -> Dict[str, object]: return { "success": result.success, - "error": result.error, - "data": result.data + "messages": result.messages } @classmethod - def _error_to_json(cls, error: Error) -> Dict[str, object]: + def _message_to_json(cls, message: Message) -> Dict[str, object]: return { - "type": error.type, - "code": error.code, - "text": error.text + "messageType": message.messageType, + "text": message.text, + "code": message.code, + "timestamp": message.timestamp.isoformat() } def default(self, obj: object) -> object: if isinstance(obj, Result): return self._result_to_json(obj) - if isinstance(obj, Error): - return self._error_to_json(obj) - if isinstance(obj, ErrorType): + if isinstance(obj, Message): + return self._message_to_json(obj) + if isinstance(obj, MessageType): return str(obj) return super().default(obj) +class StrippedResultToJsonEncoder(ResultToJsonEncoder): + stripped = True + + +ResponseData = Union[Result, object, List[object]] + + class APIResponse(abc.ABC, Response): @abc.abstractmethod - def __init__(self, result: Result, *args, **kwargs): + def __init__(self, obj: Optional[ResponseData] = None, stripped: bool = False, *args, **kwargs): super().__init__(*args, **kwargs) - self.data = self.serialize(result) + if obj is None: + self.status_code = 204 + else: + self.data = self.serialize(obj, stripped) @abc.abstractmethod - def serialize(self, result: Result) -> str: + def serialize(self, obj: ResponseData, stripped: bool) -> str: pass @@ -108,18 +115,33 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, result: Result) -> str: - return json.dumps(result, cls=ResultToJsonEncoder, separators=(",", ":")) + def serialize(self, obj: ResponseData, stripped: bool) -> str: + return json.dumps(obj, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, + separators=(",", ":")) class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, result: Result) -> str: - result_elem = result_to_xml(result, nsmap=xml_serialization.NS_MAP) - etree.cleanup_namespaces(result_elem) - return etree.tostring(result_elem, xml_declaration=True, encoding="utf-8") + def serialize(self, obj: ResponseData, stripped: bool) -> str: + # TODO: xml serialization doesn't support stripped objects + if isinstance(obj, Result): + response_elem = result_to_xml(obj, nsmap=xml_serialization.NS_MAP) + etree.cleanup_namespaces(response_elem) + else: + if isinstance(obj, list): + response_elem = etree.Element("list", nsmap=xml_serialization.NS_MAP) + for obj in obj: + response_elem.append(aas_object_to_xml(obj)) + etree.cleanup_namespaces(response_elem) + else: + # dirty hack to be able to use the namespace prefixes defined in xml_serialization.NS_MAP + parent = etree.Element("parent", nsmap=xml_serialization.NS_MAP) + response_elem = aas_object_to_xml(obj) + parent.append(response_elem) + etree.cleanup_namespaces(parent) + return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") class XmlResponseAlt(XmlResponse): @@ -131,51 +153,41 @@ def result_to_xml(result: Result, **kwargs) -> etree.Element: result_elem = etree.Element("result", **kwargs) success_elem = etree.Element("success") success_elem.text = xml_serialization.boolean_to_xml(result.success) - if result.error is None: - error_elem = etree.Element("error") - else: - error_elem = error_to_xml(result.error) - data_elem = etree.Element("data") - if result.data is not None: - for element in result_data_to_xml(result.data): - data_elem.append(element) + messages_elem = etree.Element("messages") + for message in result.messages: + messages_elem.append(message_to_xml(message)) + result_elem.append(success_elem) - result_elem.append(error_elem) - result_elem.append(data_elem) + result_elem.append(messages_elem) return result_elem -def error_to_xml(error: Error) -> etree.Element: - error_elem = etree.Element("error") - type_elem = etree.Element("type") - type_elem.text = str(error.type) - code_elem = etree.Element("code") - code_elem.text = error.code +def message_to_xml(message: Message) -> etree.Element: + message_elem = etree.Element("message") + message_type_elem = etree.Element("messageType") + message_type_elem.text = str(message.messageType) text_elem = etree.Element("text") - text_elem.text = error.text - error_elem.append(type_elem) - error_elem.append(code_elem) - error_elem.append(text_elem) - return error_elem - + text_elem.text = message.text + code_elem = etree.Element("code") + code_elem.text = message.code + timestamp_elem = etree.Element("timestamp") + timestamp_elem.text = message.timestamp.isoformat() -def result_data_to_xml(data: ResultData) -> Iterable[etree.Element]: - # for xml we can just append multiple elements to the data element - # so multiple elements will be handled the same as a single element - if not isinstance(data, tuple): - data = (data,) - for obj in data: - yield aas_object_to_xml(obj) + message_elem.append(message_type_elem) + message_elem.append(text_elem) + message_elem.append(code_elem) + message_elem.append(timestamp_elem) + return message_elem def aas_object_to_xml(obj: object) -> etree.Element: # TODO: a similar function should be implemented in the xml serialization if isinstance(obj, model.AssetAdministrationShell): return xml_serialization.asset_administration_shell_to_xml(obj) + if isinstance(obj, model.AssetInformation): + return xml_serialization.asset_information_to_xml(obj) if isinstance(obj, model.Reference): return xml_serialization.reference_to_xml(obj) - if isinstance(obj, model.View): - return xml_serialization.view_to_xml(obj) if isinstance(obj, model.Submodel): return xml_serialization.submodel_to_xml(obj) # TODO: xml serialization needs a constraint_to_xml() function @@ -208,14 +220,18 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res if location is not None: headers.append(("Location", location)) if exception.code and exception.code >= 400: - error = Error(type(exception).__name__, exception.description if exception.description is not None else "", - ErrorType.ERROR) - result = Result(error) + message = Message(type(exception).__name__, exception.description if exception.description is not None else "", + MessageType.ERROR) + result = Result(False, [message]) else: - result = Result(None) + result = Result(False) return response_type(result, status=exception.code, headers=headers) +def is_stripped_request(request: Request) -> bool: + return request.args.get("level") == "core" + + T = TypeVar("T") @@ -228,6 +244,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: which should limit the maximum content length. """ type_constructables_map = { + model.AssetAdministrationShell: XMLConstructables.ASSET_ADMINISTRATION_SHELL, + model.AssetInformation: XMLConstructables.ASSET_INFORMATION, model.AASReference: XMLConstructables.AAS_REFERENCE, model.View: XMLConstructables.VIEW, model.Qualifier: XMLConstructables.QUALIFIER, @@ -245,17 +263,22 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: try: if request.mimetype == "application/json": - rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) + decoder: Type[StrictAASFromJsonDecoder] = StrictStrippedAASFromJsonDecoder if is_stripped_request(request) \ + else StrictAASFromJsonDecoder + rv = json.loads(request.get_data(), cls=decoder) # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema # TODO: json deserialization will always create an AASReference[Submodel], xml deserialization determines # that automatically if expect_type is model.AASReference: - rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) + rv = decoder._construct_aas_reference(rv, model.Submodel) + elif expect_type is model.AssetInformation: + rv = decoder._construct_asset_information(rv, model.AssetInformation) else: try: xml_data = io.BytesIO(request.get_data()) - rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) + rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], + stripped=is_stripped_request(request), failsafe=False) except (KeyError, ValueError) as e: # xml deserialization creates an error chain. since we only return one error, return the root cause f: BaseException = e @@ -331,25 +354,17 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - Submount("/aas/", [ + Submount("/aas//aas", [ Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/assetInformation", methods=["PUT"], endpoint=self.put_aas_asset_information), Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["GET"], - endpoint=self.get_aas_submodel_refs_specific), + Rule("//", methods=["PUT"], + endpoint=self.put_aas_submodel_refs), Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) - ]), - Submount("/views", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_views), - Rule("/", methods=["POST"], endpoint=self.post_aas_views), - Rule("//", methods=["GET"], - endpoint=self.get_aas_views_specific), - Rule("//", methods=["PUT"], - endpoint=self.put_aas_views_specific), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_views_specific) ]) ]), Submount("/submodels/", [ @@ -497,117 +512,61 @@ def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - return response_t(Result(aas)) + return response_t(aas, stripped=is_stripped_request(request)) - def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - return response_t(Result(tuple(aas.submodel))) - - def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) - aas_identifier = url_args["aas_id"] - aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) - aas.update() - sm_ref = parse_request_body(request, model.AASReference) - # to give a location header in the response we have to be able to get the submodel identifier from the reference - try: - submodel_identifier = sm_ref.get_identifier() - except ValueError as e: - raise UnprocessableEntity(f"Can't resolve submodel identifier for given reference!") from e - if sm_ref in aas.submodel: - raise Conflict(f"{sm_ref!r} already exists!") - aas.submodel.add(sm_ref) + aas_new = parse_request_body(request, model.AssetAdministrationShell) + aas.update_from(aas_new) aas.commit() - created_resource_url = map_adapter.build(self.get_aas_submodel_refs_specific, { - "aas_id": aas_identifier, - "sm_id": submodel_identifier - }, force_external=True) - return response_t(Result(sm_ref), status=201, headers={"Location": created_resource_url}) + return response_t() - def get_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) - return response_t(Result(sm_ref)) + return response_t(aas.asset_information) - def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) - # use remove(sm_ref) because it raises a KeyError if sm_ref is not present - # sm_ref must be present because _get_aas_submodel_reference_by_submodel_identifier() found it there - # so if sm_ref is not in aas.submodel, this implementation is bugged and the raised KeyError will result - # in an InternalServerError - aas.submodel.remove(sm_ref) + aas.asset_information = parse_request_body(request, model.AssetInformation) aas.commit() - return response_t(Result(None)) + return response_t() - def get_aas_views(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - return response_t(Result(tuple(aas.view))) + return response_t(list(aas.submodel)) - def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + def put_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - view = parse_request_body(request, model.View) - if aas.view.contains_id("id_short", view.id_short): - raise Conflict(f"View with id_short {view.id_short} already exists!") - aas.view.add(view) + sm_ref = parse_request_body(request, model.AASReference) + if sm_ref in aas.submodel: + raise Conflict(f"{sm_ref!r} already exists!") + aas.submodel.add(sm_ref) aas.commit() - created_resource_url = map_adapter.build(self.get_aas_views_specific, { - "aas_id": aas_identifier, - "view_idshort": view.id_short - }, force_external=True) - return response_t(Result(view), status=201, headers={"Location": created_resource_url}) - - def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - view_idshort = url_args["view_idshort"] - view = aas.view.get("id_short", view_idshort) - if view is None: - raise NotFound(f"No view with id_short {view_idshort} found!") - return response_t(Result(view)) + return response_t(sm_ref, status=201) - def put_aas_views_specific(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) - aas_identifier = url_args["aas_id"] - aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) - aas.update() - view_idshort = url_args["view_idshort"] - view = aas.view.get("id_short", view_idshort) - if view is None: - raise NotFound(f"No view with id_short {view_idshort} found!") - new_view = parse_request_body(request, model.View) - # TODO: raise conflict if the following fails - view.update_from(new_view) - view.commit() - if view_idshort.upper() != view.id_short.upper(): - created_resource_url = map_adapter.build(self.put_aas_views_specific, { - "aas_id": aas_identifier, - "view_idshort": view.id_short - }, force_external=True) - return response_t(Result(view), status=201, headers={"Location": created_resource_url}) - return response_t(Result(view)) - - def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - view_idshort = url_args["view_idshort"] - if not aas.view.contains_id("id_short", view_idshort): - raise NotFound(f"No view with id_short {view_idshort} found!") - aas.view.remove_by_id("id_short", view_idshort) - return response_t(Result(None)) + sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) + # use remove(sm_ref) because it raises a KeyError if sm_ref is not present + # sm_ref must be present because _get_aas_submodel_reference_by_submodel_identifier() found it there + # so if sm_ref is not in aas.submodel, this implementation is bugged and the raised KeyError will result + # in an InternalServerError + aas.submodel.remove(sm_ref) + aas.commit() + return response_t() # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: From c46e4ab0c9448632097f77d6f9f8b63725b3585e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 28 Jul 2021 03:15:24 +0200 Subject: [PATCH 184/474] adapter.http: update id_short_path and Identifier converters --- basyx/aas/adapter/http.py | 68 ++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 9c6a0ce..935b065 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -11,6 +11,8 @@ import abc +import base64 +import binascii import datetime import enum import io @@ -294,58 +296,50 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: return rv -def identifier_uri_encode(id_: model.Identifier) -> str: - return IDENTIFIER_TYPES[id_.id_type] + ":" + werkzeug.urls.url_quote(id_.id, safe="") - - -def identifier_uri_decode(id_str: str) -> model.Identifier: - try: - id_type_str, id_ = id_str.split(":", 1) - except ValueError: - raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") - id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) - if id_type is None: - raise ValueError(f"IdentifierType '{id_type_str}' is invalid") - return model.Identifier(werkzeug.urls.url_unquote(id_), id_type) - - class IdentifierConverter(werkzeug.routing.UnicodeConverter): + encoding = "utf-8" + def to_url(self, value: model.Identifier) -> str: - return super().to_url(identifier_uri_encode(value)) + return super().to_url(base64.urlsafe_b64encode((IDENTIFIER_TYPES[value.id_type] + ":" + value.id) + .encode(self.encoding))) def to_python(self, value: str) -> model.Identifier: + value = super().to_python(value) try: - return identifier_uri_decode(super().to_python(value)) - except ValueError as e: - raise BadRequest(str(e)) from e - + decoded = base64.urlsafe_b64decode(super().to_python(value)).decode(self.encoding) + except binascii.Error: + raise BadRequest(f"Encoded identifier {value} is invalid base64url!") + except UnicodeDecodeError: + raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + id_type_str, id_ = decoded.split(":", 1) + try: + return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type_str]) + except KeyError: + raise BadRequest(f"{id_type_str} is not a valid identifier type!") -def validate_id_short(id_short: str) -> bool: - try: - model.MultiLanguageProperty(id_short) - except model.AASConstraintViolation: - return False - return True +class IdShortPathConverter(werkzeug.routing.UnicodeConverter): + id_short_sep = "." -class IdShortPathConverter(werkzeug.routing.PathConverter): - id_short_prefix = "!" + @classmethod + def validate_id_short(cls, id_short: str) -> bool: + try: + model.MultiLanguageProperty(id_short) + except model.AASConstraintViolation: + return False + return True def to_url(self, value: List[str]) -> str: for id_short in value: - if not validate_id_short(id_short): + if not self.validate_id_short(id_short): raise ValueError(f"{id_short} is not a valid id_short!") - return "/".join([self.id_short_prefix + id_short for id_short in value]) + return super().to_url(".".join(id_short for id_short in value)) def to_python(self, value: str) -> List[str]: - id_shorts = super().to_python(value).split("/") - for idx, id_short in enumerate(id_shorts): - if not id_short.startswith(self.id_short_prefix): - raise werkzeug.routing.ValidationError - id_short = id_short[1:] - if not validate_id_short(id_short): + id_shorts = super().to_python(value).split(self.id_short_sep) + for id_short in id_shorts: + if not self.validate_id_short(id_short): raise BadRequest(f"{id_short} is not a valid id_short!") - id_shorts[idx] = id_short return id_shorts From e77e0c3d3214fc60ef50c51be46f7b4b5de6838f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 28 Jul 2021 03:16:26 +0200 Subject: [PATCH 185/474] adapter.http: update submodel interface --- basyx/aas/adapter/http.py | 170 ++++++++++++++++++-------------------- 1 file changed, 80 insertions(+), 90 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 935b065..42ed80d 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -348,7 +348,7 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - Submount("/aas//aas", [ + Submount("/shells//aas", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), @@ -361,38 +361,50 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.delete_aas_submodel_refs_specific) ]) ]), - Submount("/submodels/", [ + Submount("/submodels//submodel", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/submodelElements/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodelElements/", methods=["POST"], endpoint=self.post_submodel_submodel_elements), - Submount("/", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_specific_nested), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_specific_nested), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_specific_nested), - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - Rule("/values/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("/values/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("/annotations/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation")), - Rule("/annotations/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation", - request_body_type=model.DataElement)), # type: ignore - Rule("/statements/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, - "statement")), - Rule("/statements/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, - "statement")), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Submount("/submodelElements", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Submount("/", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + Rule("/values/", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("/values/", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("/annotations/", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation")), + Rule("/annotations/", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation", + request_body_type=model.DataElement)), # type: ignore + Rule("/statements/", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, + "statement")), + Rule("/statements/", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, + "statement")), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) + ]), Submount("/constraints", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), @@ -403,16 +415,6 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("//", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_constraints), ]) - ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), ]) ]) ]) @@ -512,8 +514,7 @@ def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - aas_new = parse_request_body(request, model.AssetAdministrationShell) - aas.update_from(aas_new) + aas.update_from(parse_request_body(request, model.AssetAdministrationShell)) aas.commit() return response_t() @@ -564,82 +565,71 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - return response_t(Result(submodel)) + return response_t(submodel, stripped=is_stripped_request(request)) - def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - return response_t(Result(tuple(submodel.submodel_element))) + submodel.update_from(parse_request_body(request, model.Submodel)) + submodel.commit() + return response_t() - def post_submodel_submodel_elements(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent, semanticId, parentPath parameters response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - if submodel.submodel_element.contains_id("id_short", submodel_element.id_short): - raise Conflict(f"Submodel element with id_short {submodel_element.id_short} already exists!") - submodel.submodel_element.add(submodel_element) - submodel.commit() - created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_specific_nested, { - "submodel_id": submodel_identifier, - "id_shorts": [submodel_element.id_short] - }, force_external=True) - return response_t(Result(submodel_element), status=201, headers={"Location": created_resource_url}) + return response_t(list(submodel.submodel_element)) - def get_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) - return response_t(Result(submodel_element)) + return response_t(submodel_element, stripped=is_stripped_request(request)) - def put_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, - map_adapter: MapAdapter) -> Response: + def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content parameter response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] submodel = self._get_obj_ts(submodel_identifier, model.Submodel) submodel.update() id_short_path = url_args["id_shorts"] - submodel_element = self._get_nested_submodel_element(submodel, id_short_path) - current_id_short = submodel_element.id_short - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - new_submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - if type(submodel_element) is not type(new_submodel_element): - raise UnprocessableEntity(f"Type of new submodel element {new_submodel_element} doesn't not match " - f"the current submodel element {submodel_element}") - # TODO: raise conflict if the following fails - submodel_element.update_from(new_submodel_element) + parent = self._expect_namespace( + self._get_nested_submodel_element(submodel, url_args["id_shorts"][:-1]), + id_short_path[-1] + ) + try: + submodel_element = parent.get_referable(id_short_path[-1]) + except KeyError: + # TODO: add new submodel element here, currently impossible + raise NotImplementedError("Adding submodel elements is currently unsupported!") + # return response_t(new_submodel_element, status=201) + # TODO: what if only data elements are allowed as children? + submodel_element.update_from(parse_request_body(request, model.SubmodelElement)) submodel_element.commit() - if new_submodel_element.id_short.upper() != current_id_short.upper(): - created_resource_url = map_adapter.build(self.put_submodel_submodel_elements_specific_nested, { - "submodel_id": submodel_identifier, - "id_shorts": id_short_path[:-1] + [submodel_element.id_short] - }, force_external=True) - return response_t(Result(submodel_element), status=201, headers={"Location": created_resource_url}) - return response_t(Result(submodel_element)) + return response_t() - def delete_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) \ + def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - id_shorts: List[str] = url_args["id_shorts"] + id_short_path: List[str] = url_args["id_shorts"] parent: model.UniqueIdShortNamespace = submodel - if len(id_shorts) > 1: + if len(id_short_path) > 1: parent = self._expect_namespace( - self._get_nested_submodel_element(submodel, id_shorts[:-1]), - id_shorts[-1] + self._get_nested_submodel_element(submodel, id_short_path[:-1]), + id_short_path[-1] ) - self._namespace_submodel_element_op(parent, parent.remove_referable, id_shorts[-1]) - return response_t(Result(None)) + self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) + return response_t() def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: From c91e8b43cdcde74adfb983d75f1b6a637ad5833e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:13:16 +0200 Subject: [PATCH 186/474] adapter.http: remove hardcoded dot as IdShortPath separator --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 42ed80d..1431ede 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -333,7 +333,7 @@ def to_url(self, value: List[str]) -> str: for id_short in value: if not self.validate_id_short(id_short): raise ValueError(f"{id_short} is not a valid id_short!") - return super().to_url(".".join(id_short for id_short in value)) + return super().to_url(self.id_short_sep.join(id_short for id_short in value)) def to_python(self, value: str) -> List[str]: id_shorts = super().to_python(value).split(self.id_short_sep) From 63a1bd04e1070b93cfc1aa1bb408298c1747b0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:40:52 +0200 Subject: [PATCH 187/474] adapter.http: allow adding submodel elements --- basyx/aas/adapter/http.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 1431ede..453c2dc 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -605,14 +605,16 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg self._get_nested_submodel_element(submodel, url_args["id_shorts"][:-1]), id_short_path[-1] ) + # 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) # type: ignore try: submodel_element = parent.get_referable(id_short_path[-1]) except KeyError: - # TODO: add new submodel element here, currently impossible - raise NotImplementedError("Adding submodel elements is currently unsupported!") - # return response_t(new_submodel_element, status=201) - # TODO: what if only data elements are allowed as children? - submodel_element.update_from(parse_request_body(request, model.SubmodelElement)) + parent.add_referable(new_submodel_element) + new_submodel_element.commit() + return response_t(new_submodel_element, status=201) + submodel_element.update_from(new_submodel_element) submodel_element.commit() return response_t() From fe021fc5cb8e7f9704bdaeb06fdfb32cba2c8caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:41:49 +0200 Subject: [PATCH 188/474] adapter.http: adjust submodel routes for new response type --- basyx/aas/adapter/http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 453c2dc..591bb4c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -641,9 +641,9 @@ def get_submodel_submodel_element_constraints(self, request: Request, url_args: sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: - return response_t(Result(tuple(sm_or_se.qualifier))) + return response_t(list(sm_or_se.qualifier)) try: - return response_t(Result(sm_or_se.get_qualifier_by_type(qualifier_type))) + return response_t(sm_or_se.get_qualifier_by_type(qualifier_type)) except KeyError: raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") @@ -665,7 +665,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: "id_shorts": id_shorts if len(id_shorts) != 0 else None, "qualifier_type": qualifier.type }, force_external=True) - return response_t(Result(qualifier), status=201, headers={"Location": created_resource_url}) + return response_t(qualifier, status=201, headers={"Location": created_resource_url}) def put_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ -> Response: @@ -696,8 +696,8 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: "id_shorts": id_shorts if len(id_shorts) != 0 else None, "qualifier_type": new_qualifier.type }, force_external=True) - return response_t(Result(new_qualifier), status=201, headers={"Location": created_resource_url}) - return response_t(Result(new_qualifier)) + return response_t(new_qualifier, status=201, headers={"Location": created_resource_url}) + return response_t(new_qualifier) def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: From 811a6aceecdeba3c0fa348a13f9fe5a80e3cc829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:19:33 +0200 Subject: [PATCH 189/474] adapter.http: use example without missing attributes (contains no views) because the view routes have been removed --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 591bb4c..d30c553 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -757,5 +757,5 @@ def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response if __name__ == "__main__": from werkzeug.serving import run_simple # use example_aas_missing_attributes, because the AAS from example_aas has no views - from aas.examples.data.example_aas_missing_attributes import create_full_example + from aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 1ab37f559da3a70dd449c702f19fae3d1d195392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:29:35 +0200 Subject: [PATCH 190/474] adapter.http: make json/xml decoder interface independent of the request body add aas/submodel repository interface --- basyx/aas/adapter/http.py | 349 +++++++++++++++++++++----------------- 1 file changed, 197 insertions(+), 152 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d30c553..d908fd2 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -30,7 +30,7 @@ from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Callable, Dict, List, Optional, Type, TypeVar, Union +from typing import Callable, Dict, Iterator, List, Optional, Type, TypeVar, Union # TODO: support the path/reference/etc. parameter @@ -237,63 +237,105 @@ def is_stripped_request(request: Request) -> bool: T = TypeVar("T") -def parse_request_body(request: Request, expect_type: Type[T]) -> T: - """ - TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent - running out of memory. but it doesn't state how to check the content length - also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema - In the meeting (25.11.2020) we discussed, this may refer to a reverse proxy in front of this WSGI app, - which should limit the maximum content length. - """ +class HTTPApiDecoder: + # these are the types we can construct (well, only the ones we need) type_constructables_map = { model.AssetAdministrationShell: XMLConstructables.ASSET_ADMINISTRATION_SHELL, model.AssetInformation: XMLConstructables.ASSET_INFORMATION, model.AASReference: XMLConstructables.AAS_REFERENCE, - model.View: XMLConstructables.VIEW, + model.IdentifierKeyValuePair: XMLConstructables.IDENTIFIER_KEY_VALUE_PAIR, model.Qualifier: XMLConstructables.QUALIFIER, model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT } - if expect_type not in type_constructables_map: - raise TypeError(f"Parsing {expect_type} is not supported!") - - valid_content_types = ("application/json", "application/xml", "text/xml") + @classmethod + def check_type_supportance(cls, type_: type): + if type_ not in cls.type_constructables_map: + raise TypeError(f"Parsing {type_} is not supported!") - if request.mimetype not in valid_content_types: - raise werkzeug.exceptions.UnsupportedMediaType(f"Invalid content-type: {request.mimetype}! Supported types: " - + ", ".join(valid_content_types)) + @classmethod + def assert_type(cls, obj: object, type_: Type[T]) -> T: + if not isinstance(obj, type_): + raise UnprocessableEntity(f"Object {obj!r} is not of type {type_.__name__}!") + return obj - try: - if request.mimetype == "application/json": - decoder: Type[StrictAASFromJsonDecoder] = StrictStrippedAASFromJsonDecoder if is_stripped_request(request) \ - else StrictAASFromJsonDecoder - rv = json.loads(request.get_data(), cls=decoder) + @classmethod + def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: + cls.check_type_supportance(expect_type) + decoder: Type[StrictAASFromJsonDecoder] = StrictStrippedAASFromJsonDecoder if stripped \ + else StrictAASFromJsonDecoder + try: + parsed = json.loads(data, cls=decoder) + if not isinstance(parsed, list): + if not expect_single: + raise UnprocessableEntity(f"Expected List[{expect_type.__name__}], got {parsed!r}!") + parsed = [parsed] + elif expect_single: + raise UnprocessableEntity(f"Expected a single object of type {expect_type.__name__}, got {parsed!r}!") # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema # TODO: json deserialization will always create an AASReference[Submodel], xml deserialization determines # that automatically + constructor: Optional[Callable[..., T]] = None + args = [] if expect_type is model.AASReference: - rv = decoder._construct_aas_reference(rv, model.Submodel) + constructor = decoder._construct_aas_reference # type: ignore + args.append(model.Submodel) elif expect_type is model.AssetInformation: - rv = decoder._construct_asset_information(rv, model.AssetInformation) - else: - try: - xml_data = io.BytesIO(request.get_data()) - rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], - stripped=is_stripped_request(request), failsafe=False) - except (KeyError, ValueError) as e: - # xml deserialization creates an error chain. since we only return one error, return the root cause - f: BaseException = e - while f.__cause__ is not None: - f = f.__cause__ - raise f from e - except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError, model.AASConstraintViolation) \ - as e: - raise UnprocessableEntity(str(e)) from e - - if not isinstance(rv, expect_type): - raise UnprocessableEntity(f"Object {rv!r} is not of type {expect_type.__name__}!") - return rv + constructor = decoder._construct_asset_information # type: ignore + elif expect_type is model.IdentifierKeyValuePair: + constructor = decoder._construct_identifier_key_value_pair # type: ignore + + if constructor is not None: + # construct elements that aren't self-identified + return [constructor(obj, *args) for obj in parsed] + + except (KeyError, ValueError, TypeError, json.JSONDecodeError, model.AASConstraintViolation) as e: + raise UnprocessableEntity(str(e)) from e + + return [cls.assert_type(obj, expect_type) for obj in parsed] + + @classmethod + def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: + return cls.json_list(data, expect_type, stripped, True)[0] + + @classmethod + def xml(cls, data: bytes, expect_type: Type[T], stripped: bool) -> T: + cls.check_type_supportance(expect_type) + try: + xml_data = io.BytesIO(data) + rv = read_aas_xml_element(xml_data, cls.type_constructables_map[expect_type], + stripped=stripped, failsafe=False) + except (KeyError, ValueError) as e: + # xml deserialization creates an error chain. since we only return one error, return the root cause + f: BaseException = e + while f.__cause__ is not None: + f = f.__cause__ + raise UnprocessableEntity(str(f)) from e + except (etree.XMLSyntaxError, model.AASConstraintViolation) as e: + raise UnprocessableEntity(str(e)) from e + return cls.assert_type(rv, expect_type) + + @classmethod + def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> T: + """ + TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent + running out of memory. but it doesn't state how to check the content length + also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json + schema + In the meeting (25.11.2020) we discussed, this may refer to a reverse proxy in front of this WSGI app, + which should limit the maximum content length. + """ + valid_content_types = ("application/json", "application/xml", "text/xml") + + if request.mimetype not in valid_content_types: + raise werkzeug.exceptions.UnsupportedMediaType( + f"Invalid content-type: {request.mimetype}! Supported types: " + + ", ".join(valid_content_types)) + + if request.mimetype == "application/json": + return cls.json(request.get_data(), expect_type, stripped) + return cls.xml(request.get_data(), expect_type, stripped) class IdentifierConverter(werkzeug.routing.UnicodeConverter): @@ -348,72 +390,70 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - Submount("/shells//aas", [ - Rule("/", methods=["GET"], endpoint=self.get_aas), - Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/assetInformation", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/submodels", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("//", methods=["PUT"], - endpoint=self.put_aas_submodel_refs), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific) + Submount("/shells", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_all), + Submount("/", [ + Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/", methods=["DELETE"], endpoint=self.delete_aas), + Submount("/aas", [ + Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/assetInformation", methods=["PUT"], endpoint=self.put_aas_asset_information), + Submount("/submodels", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("//", methods=["PUT"], + endpoint=self.put_aas_submodel_refs), + Rule("//", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific) + ]) + ]) ]) ]), - Submount("/submodels//submodel", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Submount("/submodelElements", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Submount("/", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_id_short_path), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_id_short_path), - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - Rule("/values/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("/values/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("/annotations/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation")), - Rule("/annotations/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation", - request_body_type=model.DataElement)), # type: ignore - Rule("/statements/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, - "statement")), - Rule("/statements/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, - "statement")), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), + Submount("/submodels", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_all), + Submount("/", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Submount("/submodel", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Submount("/submodelElements", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Submount("/", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + Submount("/constraints", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) + ]), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) ]) - ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), ]) ]) ]) @@ -433,6 +473,11 @@ def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._ raise NotFound(f"No {type_.__name__} with {identifier} found!") 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 + def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: try: return reference.resolve(self.object_store) @@ -503,18 +548,39 @@ def handle_request(self, request: Request): except werkzeug.exceptions.NotAcceptable as e: return e + # ------ AAS REPO ROUTES ------- + def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) + id_short = request.args.get("idShort") + if id_short is not None: + aas = filter(lambda shell: shell.id_short == id_short, aas) + asset_ids = request.args.get("assetIds") + if asset_ids is not None: + kv_pairs = HTTPApiDecoder.json_list(asset_ids, model.IdentifierKeyValuePair, False, False) + # TODO: it's currently unclear how to filter with these IdentifierKeyValuePairs + return response_t(list(aas)) + + def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + return response_t() + # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content parameter response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(aas, stripped=is_stripped_request(request)) def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content parameter response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - aas.update_from(parse_request_body(request, model.AssetAdministrationShell)) + aas.update_from(HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, + is_stripped_request(request))) aas.commit() return response_t() @@ -528,7 +594,7 @@ def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - aas.asset_information = parse_request_body(request, model.AssetInformation) + aas.asset_information = HTTPApiDecoder.request_body(request, model.AssetInformation, False) aas.commit() return response_t() @@ -543,7 +609,7 @@ def put_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - sm_ref = parse_request_body(request, model.AASReference) + sm_ref = HTTPApiDecoder.request_body(request, model.AASReference, is_stripped_request(request)) if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") aas.submodel.add(sm_ref) @@ -563,6 +629,22 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** aas.commit() return response_t() + # ------ SUBMODEL REPO ROUTES ------- + def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + id_short = request.args.get("idShort") + if id_short is not None: + submodels = filter(lambda sm: sm.id_short == id_short, submodels) + # TODO: filter by semantic id + # semantic_id = request.args.get("semanticId") + return response_t(list(submodels)) + + def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.Submodel)) + return response_t() + # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters @@ -575,12 +657,13 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - submodel.update_from(parse_request_body(request, model.Submodel)) + submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request))) submodel.commit() return response_t() def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent, semanticId, parentPath parameters + # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec + # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() @@ -595,7 +678,7 @@ def get_submodel_submodel_elements_id_short_path(self, request: Request, url_arg return response_t(submodel_element, stripped=is_stripped_request(request)) def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content parameter + # TODO: support content, extent parameter response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] submodel = self._get_obj_ts(submodel_identifier, model.Submodel) @@ -655,7 +738,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: submodel.update() id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) - qualifier = parse_request_body(request, model.Qualifier) + qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) if sm_or_se.qualifier.contains_id("type", qualifier.type): raise Conflict(f"Qualifier with type {qualifier.type} already exists!") sm_or_se.qualifier.add(qualifier) @@ -675,7 +758,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: submodel.update() id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) - new_qualifier = parse_request_body(request, model.Qualifier) + new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) qualifier_type = url_args["qualifier_type"] try: qualifier = sm_or_se.get_qualifier_by_type(qualifier_type) @@ -713,45 +796,7 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg except KeyError: raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") sm_or_se.commit() - return response_t(Result(None)) - - # --------- SUBMODEL ROUTE FACTORIES --------- - def factory_get_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str) \ - -> Callable[[Request, Dict], Response]: - def route(request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) - if not isinstance(submodel_element, type_): - raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") - return response_t(Result(tuple(getattr(submodel_element, attr)))) - return route - - def factory_post_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str, - request_body_type: Type[model.SubmodelElement] - = model.SubmodelElement) \ - -> Callable[[Request, Dict, MapAdapter], Response]: - def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() - id_shorts = url_args["id_shorts"] - submodel_element = self._get_nested_submodel_element(submodel, id_shorts) - if not isinstance(submodel_element, type_): - raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") - new_submodel_element = parse_request_body(request, request_body_type) - if getattr(submodel_element, attr).contains_id("id_short", new_submodel_element.id_short): - raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") - getattr(submodel_element, attr).add(new_submodel_element) - submodel_element.commit() - created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_specific_nested, { - "submodel_id": submodel_identifier, - "id_shorts": id_shorts + [new_submodel_element.id_short] - }, force_external=True) - return response_t(Result(new_submodel_element), status=201, headers={"Location": created_resource_url}) - return route + return response_t() if __name__ == "__main__": From efb111c59bb249194a259cfcd967bf2867e34f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 20 Sep 2021 18:00:17 +0200 Subject: [PATCH 191/474] adapter.http: add missing stripped parameter to HTTPApiDecoder.request_body() call --- basyx/aas/adapter/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d908fd2..22dd4c1 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -690,7 +690,8 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg ) # 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) # type: ignore + new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, + is_stripped_request(request)) # type: ignore try: submodel_element = parent.get_referable(id_short_path[-1]) except KeyError: From db75c711de6a6c24f78570a793ccee5d60cb364a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 21 Sep 2021 18:16:06 +0200 Subject: [PATCH 192/474] adapter.http: update to Review3 add Base64UrlJsonConverter for json objects encoded as base64url as part of the url support deserializing submodels in HTTPApiDecoder add POST routes, remove 'create' functionality from PUT routes --- basyx/aas/adapter/http.py | 160 +++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 47 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 22dd4c1..436fd43 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -245,6 +245,7 @@ class HTTPApiDecoder: model.AASReference: XMLConstructables.AAS_REFERENCE, model.IdentifierKeyValuePair: XMLConstructables.IDENTIFIER_KEY_VALUE_PAIR, model.Qualifier: XMLConstructables.QUALIFIER, + model.Submodel: XMLConstructables.SUBMODEL, model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT } @@ -338,6 +339,35 @@ def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> return cls.xml(request.get_data(), expect_type, stripped) +class Base64UrlJsonConverter(werkzeug.routing.UnicodeConverter): + encoding = "utf-8" + + def __init__(self, url_map, t: str): + super().__init__(url_map) + self.type: type + if t == "AASReference": + self.type = model.AASReference + else: + raise ValueError(f"invalid value t={t}") + + def to_url(self, value: object) -> str: + return super().to_url(base64.urlsafe_b64encode(json.dumps(value, cls=AASToJsonEncoder).encode(self.encoding))) + + def to_python(self, value: str) -> object: + value = super().to_python(value) + try: + decoded = base64.urlsafe_b64decode(super().to_python(value)).decode(self.encoding) + except binascii.Error: + raise BadRequest(f"Encoded json object {value} is invalid base64url!") + except UnicodeDecodeError: + raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + + try: + return HTTPApiDecoder.json(decoded, self.type, False) + except json.JSONDecodeError: + raise BadRequest(f"{decoded} is not a valid json string!") + + class IdentifierConverter(werkzeug.routing.UnicodeConverter): encoding = "utf-8" @@ -392,6 +422,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/api/v1", [ Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), + Rule("/", methods=["POST"], endpoint=self.post_aas), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), @@ -399,13 +430,12 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/aas", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/assetInformation", methods=["PUT"], endpoint=self.put_aas_asset_information), + Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("//", methods=["PUT"], - endpoint=self.put_aas_submodel_refs), - Rule("//", methods=["DELETE"], + Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]) ]) @@ -413,6 +443,7 @@ def __init__(self, object_store: model.AbstractObjectStore): ]), Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_all), + Rule("/", methods=["POST"], endpoint=self.post_submodel), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), @@ -420,11 +451,15 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodel", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Submount("/submodelElements", [ + Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_elements_id_short_path), Rule("/", methods=["DELETE"], @@ -442,17 +477,17 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.delete_submodel_submodel_element_constraints), ]) ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), - ]) + ]), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), ]) ]) ]) @@ -460,7 +495,8 @@ def __init__(self, object_store: model.AbstractObjectStore): ]) ], converters={ "identifier": IdentifierConverter, - "id_short_path": IdShortPathConverter + "id_short_path": IdShortPathConverter, + "base64url_json": Base64UrlJsonConverter }) def __call__(self, environ, start_response): @@ -484,15 +520,6 @@ def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> m except (KeyError, TypeError, model.UnexpectedTypeError) as e: raise werkzeug.exceptions.InternalServerError(str(e)) from e - @classmethod - def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdministrationShell, - sm_identifier: model.Identifier) \ - -> model.AASReference[model.Submodel]: - for sm_ref in aas.submodel: - if sm_ref.get_identifier() == sm_identifier: - return sm_ref - raise NotFound(f"No reference to submodel with {sm_identifier} found!") - @classmethod def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, id_shorts: List[str]) \ -> model.SubmodelElement: @@ -561,6 +588,19 @@ def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: it's currently unclear how to filter with these IdentifierKeyValuePairs return response_t(list(aas)) + def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + aas = HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, False) + try: + self.object_store.add(aas) + except KeyError as e: + raise Conflict(f"AssetAdministrationShell with Identifier {aas.identification} already exists!") from e + aas.commit() + created_resource_url = map_adapter.build(self.get_aas, { + "aas_id": aas.identification + }, force_external=True) + return response_t(aas, status=201, headers={"Location": created_resource_url}) + def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) @@ -604,12 +644,12 @@ def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas.update() return response_t(list(aas.submodel)) - def put_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - sm_ref = HTTPApiDecoder.request_body(request, model.AASReference, is_stripped_request(request)) + sm_ref = HTTPApiDecoder.request_body(request, model.AASReference, False) if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") aas.submodel.add(sm_ref) @@ -620,14 +660,12 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) - # use remove(sm_ref) because it raises a KeyError if sm_ref is not present - # sm_ref must be present because _get_aas_submodel_reference_by_submodel_identifier() found it there - # so if sm_ref is not in aas.submodel, this implementation is bugged and the raised KeyError will result - # in an InternalServerError - aas.submodel.remove(sm_ref) - aas.commit() - return response_t() + for sm_ref in aas.submodel: + if sm_ref == url_args["submodel_ref"]: + aas.submodel.remove(sm_ref) + aas.commit() + return response_t() + raise NotFound(f"The AAS {aas!r} doesn't have the reference {url_args['submodel_ref']!r}!") # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -640,6 +678,19 @@ def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Respo # semantic_id = request.args.get("semanticId") return response_t(list(submodels)) + def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) + try: + self.object_store.add(submodel) + except KeyError as e: + raise Conflict(f"Submodel with Identifier {submodel.identification} already exists!") from e + submodel.commit() + created_resource_url = map_adapter.build(self.get_submodel, { + "submodel_id": submodel.identification + }, force_external=True) + return response_t(submodel, status=201, headers={"Location": created_resource_url}) + def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.Submodel)) @@ -677,27 +728,42 @@ def get_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(submodel_element, stripped=is_stripped_request(request)) - def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] submodel = self._get_obj_ts(submodel_identifier, model.Submodel) submodel.update() - id_short_path = url_args["id_shorts"] - parent = self._expect_namespace( - self._get_nested_submodel_element(submodel, url_args["id_shorts"][:-1]), - id_short_path[-1] - ) + id_short_path = url_args.get("id_shorts", []) + parent = self._get_submodel_or_nested_submodel_element(submodel, id_short_path) + if not isinstance(parent, model.UniqueIdShortNamespace): + raise BadRequest(f"{parent!r} is not a namespace, can't add child submodel element!") # 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 try: - submodel_element = parent.get_referable(id_short_path[-1]) - except KeyError: parent.add_referable(new_submodel_element) - new_submodel_element.commit() - return response_t(new_submodel_element, status=201) + except KeyError: + raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " + f"within {parent}!") + created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { + "submodel_id": submodel.identification, + "id_shorts": id_short_path + [new_submodel_element.id_short] + }, force_external=True) + return response_t(new_submodel_element, status=201, headers={"Location": created_resource_url}) + + def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameter + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + 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 submodel_element.update_from(new_submodel_element) submodel_element.commit() return response_t() From c4854d63b5aa07e11e65e1996069106219235ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:42:22 +0100 Subject: [PATCH 193/474] adapter._generic: remove `identifier_uri_(de/en)code` functions --- basyx/aas/adapter/_generic.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index a65cf54..34c3412 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -11,7 +11,6 @@ from typing import Dict, Type from basyx.aas import model -import urllib.parse # XML Namespace definition XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} @@ -114,18 +113,3 @@ KEY_TYPES_CLASSES_INVERSE: Dict[model.KeyTypes, Type[model.Referable]] = \ {v: k for k, v in model.KEY_TYPES_CLASSES.items()} - - -def identifier_uri_encode(id_: model.Identifier) -> str: - return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") - - -def identifier_uri_decode(id_str: str) -> model.Identifier: - try: - id_type_str, id_ = id_str.split(":", 1) - except ValueError as e: - raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") - id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) - if id_type is None: - raise ValueError(f"Identifier Type '{id_type_str}' is invalid") - return model.Identifier(urllib.parse.unquote(id_), id_type) From bf7ae09f4e45ea91fd8696bdf3aa354baa916ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:47:34 +0100 Subject: [PATCH 194/474] adapter.http: update for changes made in the last 2 years This commit consists mostly of renamed variables, but also other stuff like the identifiers, which consisted of an IdentifierType and the actual Identifier previously. --- basyx/aas/adapter/http.py | 59 ++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 436fd43..2385f74 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -25,10 +25,10 @@ from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response -from aas import model +from basyx.aas import model +from ._generic import XML_NS_MAP from .xml import XMLConstructables, read_aas_xml_element, xml_serialization from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE from typing import Callable, Dict, Iterator, List, Optional, Type, TypeVar, Union @@ -129,17 +129,17 @@ def __init__(self, *args, content_type="application/xml", **kwargs): def serialize(self, obj: ResponseData, stripped: bool) -> str: # TODO: xml serialization doesn't support stripped objects if isinstance(obj, Result): - response_elem = result_to_xml(obj, nsmap=xml_serialization.NS_MAP) + response_elem = result_to_xml(obj, nsmap=XML_NS_MAP) etree.cleanup_namespaces(response_elem) else: if isinstance(obj, list): - response_elem = etree.Element("list", nsmap=xml_serialization.NS_MAP) + response_elem = etree.Element("list", nsmap=XML_NS_MAP) for obj in obj: response_elem.append(aas_object_to_xml(obj)) etree.cleanup_namespaces(response_elem) else: # dirty hack to be able to use the namespace prefixes defined in xml_serialization.NS_MAP - parent = etree.Element("parent", nsmap=xml_serialization.NS_MAP) + parent = etree.Element("parent", nsmap=XML_NS_MAP) response_elem = aas_object_to_xml(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) @@ -242,8 +242,8 @@ class HTTPApiDecoder: type_constructables_map = { model.AssetAdministrationShell: XMLConstructables.ASSET_ADMINISTRATION_SHELL, model.AssetInformation: XMLConstructables.ASSET_INFORMATION, - model.AASReference: XMLConstructables.AAS_REFERENCE, - model.IdentifierKeyValuePair: XMLConstructables.IDENTIFIER_KEY_VALUE_PAIR, + model.ModelReference: XMLConstructables.MODEL_REFERENCE, + model.SpecificAssetId: XMLConstructables.SPECIFIC_ASSET_ID, model.Qualifier: XMLConstructables.QUALIFIER, model.Submodel: XMLConstructables.SUBMODEL, model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT @@ -279,13 +279,13 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool # that automatically constructor: Optional[Callable[..., T]] = None args = [] - if expect_type is model.AASReference: + if expect_type is model.ModelReference: constructor = decoder._construct_aas_reference # type: ignore args.append(model.Submodel) elif expect_type is model.AssetInformation: constructor = decoder._construct_asset_information # type: ignore - elif expect_type is model.IdentifierKeyValuePair: - constructor = decoder._construct_identifier_key_value_pair # type: ignore + elif expect_type is model.SpecificAssetId: + constructor = decoder._construct_specific_asset_id # type: ignore if constructor is not None: # construct elements that aren't self-identified @@ -346,7 +346,7 @@ def __init__(self, url_map, t: str): super().__init__(url_map) self.type: type if t == "AASReference": - self.type = model.AASReference + self.type = model.ModelReference else: raise ValueError(f"invalid value t={t}") @@ -372,8 +372,7 @@ class IdentifierConverter(werkzeug.routing.UnicodeConverter): encoding = "utf-8" def to_url(self, value: model.Identifier) -> str: - return super().to_url(base64.urlsafe_b64encode((IDENTIFIER_TYPES[value.id_type] + ":" + value.id) - .encode(self.encoding))) + return super().to_url(base64.urlsafe_b64encode(value.encode(self.encoding))) def to_python(self, value: str) -> model.Identifier: value = super().to_python(value) @@ -383,11 +382,7 @@ def to_python(self, value: str) -> model.Identifier: raise BadRequest(f"Encoded identifier {value} is invalid base64url!") except UnicodeDecodeError: raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") - id_type_str, id_ = decoded.split(":", 1) - try: - return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type_str]) - except KeyError: - raise BadRequest(f"{id_type_str} is not a valid identifier type!") + return decoded class IdShortPathConverter(werkzeug.routing.UnicodeConverter): @@ -514,7 +509,7 @@ def _get_all_obj_of_type(self, type_: Type[model.provider._IT]) -> Iterator[mode if isinstance(obj, type_): yield obj - def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: + def _resolve_reference(self, reference: model.ModelReference[model.base._RT]) -> model.base._RT: try: return reference.resolve(self.object_store) except (KeyError, TypeError, model.UnexpectedTypeError) as e: @@ -584,8 +579,8 @@ def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: aas = filter(lambda shell: shell.id_short == id_short, aas) asset_ids = request.args.get("assetIds") if asset_ids is not None: - kv_pairs = HTTPApiDecoder.json_list(asset_ids, model.IdentifierKeyValuePair, False, False) - # TODO: it's currently unclear how to filter with these IdentifierKeyValuePairs + spec_asset_ids = HTTPApiDecoder.json_list(asset_ids, model.SpecificAssetId, False, False) + # TODO: it's currently unclear how to filter with these SpecificAssetIds return response_t(list(aas)) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: @@ -594,10 +589,10 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> try: self.object_store.add(aas) except KeyError as e: - raise Conflict(f"AssetAdministrationShell with Identifier {aas.identification} already exists!") from e + raise Conflict(f"AssetAdministrationShell with Identifier {aas.id} already exists!") from e aas.commit() created_resource_url = map_adapter.build(self.get_aas, { - "aas_id": aas.identification + "aas_id": aas.id }, force_external=True) return response_t(aas, status=201, headers={"Location": created_resource_url}) @@ -649,7 +644,7 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - sm_ref = HTTPApiDecoder.request_body(request, model.AASReference, False) + 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) @@ -684,10 +679,10 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte try: self.object_store.add(submodel) except KeyError as e: - raise Conflict(f"Submodel with Identifier {submodel.identification} already exists!") from e + raise Conflict(f"Submodel with Identifier {submodel.id} already exists!") from e submodel.commit() created_resource_url = map_adapter.build(self.get_submodel, { - "submodel_id": submodel.identification + "submodel_id": submodel.id }, force_external=True) return response_t(submodel, status=201, headers={"Location": created_resource_url}) @@ -740,15 +735,15 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar raise BadRequest(f"{parent!r} is not a namespace, can't add child submodel element!") # 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 + new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, # type: ignore + is_stripped_request(request)) try: parent.add_referable(new_submodel_element) except KeyError: raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!") created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { - "submodel_id": submodel.identification, + "submodel_id": submodel.id, "id_shorts": id_short_path + [new_submodel_element.id_short] }, force_external=True) return response_t(new_submodel_element, status=201, headers={"Location": created_resource_url}) @@ -762,8 +757,8 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg 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 + new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, # type: ignore + is_stripped_request(request)) submodel_element.update_from(new_submodel_element) submodel_element.commit() return response_t() @@ -869,5 +864,5 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg if __name__ == "__main__": from werkzeug.serving import run_simple # use example_aas_missing_attributes, because the AAS from example_aas has no views - from aas.examples.data.example_aas import create_full_example + from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 35253e7a5edc0d0d1d2543587f3ae25070f5a0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:52:06 +0100 Subject: [PATCH 195/474] adapter.http: ignore the type of some imports to make `mypy` happy --- basyx/aas/adapter/http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 2385f74..c7a3a9f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -18,12 +18,12 @@ import io import json from lxml import etree # type: ignore -import werkzeug.exceptions -import werkzeug.routing -import werkzeug.urls +import werkzeug.exceptions # type: ignore +import werkzeug.routing # type: ignore +import werkzeug.urls # type: ignore from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount -from werkzeug.wrappers import Request, Response +from werkzeug.wrappers import Request, Response # type: ignore from basyx.aas import model from ._generic import XML_NS_MAP @@ -862,7 +862,7 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg if __name__ == "__main__": - from werkzeug.serving import run_simple + from werkzeug.serving import run_simple # type: ignore # use example_aas_missing_attributes, because the AAS from example_aas has no views from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From f3d85edff41a891cd7a4773b51001aefeb3c7a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:53:28 +0100 Subject: [PATCH 196/474] adapter.http: remove an outdated comment Views were removed from the spec and we're no longer using `example_aas_missing_attributes()` anyway. --- basyx/aas/adapter/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index c7a3a9f..99064b6 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -863,6 +863,5 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg if __name__ == "__main__": from werkzeug.serving import run_simple # type: ignore - # use example_aas_missing_attributes, because the AAS from example_aas has no views from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From b98b47d11e7b17912b728a53f630bf94dc99a716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 23:18:33 +0100 Subject: [PATCH 197/474] adapter.http: rename occurances of `AASReference` to `ModelReference` --- basyx/aas/adapter/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 99064b6..fb631a2 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -275,7 +275,7 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool raise UnprocessableEntity(f"Expected a single object of type {expect_type.__name__}, got {parsed!r}!") # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema - # TODO: json deserialization will always create an AASReference[Submodel], xml deserialization determines + # TODO: json deserialization will always create an ModelReference[Submodel], xml deserialization determines # that automatically constructor: Optional[Callable[..., T]] = None args = [] @@ -345,7 +345,7 @@ class Base64UrlJsonConverter(werkzeug.routing.UnicodeConverter): def __init__(self, url_map, t: str): super().__init__(url_map) self.type: type - if t == "AASReference": + if t == "ModelReference": self.type = model.ModelReference else: raise ValueError(f"invalid value t={t}") @@ -430,7 +430,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]) ]) From 271e119b58084cb7f5712be4c5e65a361d2a17de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 19:56:08 +0100 Subject: [PATCH 198/474] adapter.http: allow typechecking werkzeug imports --- basyx/aas/adapter/http.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index fb631a2..65ce5d5 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -9,6 +9,9 @@ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. +# TODO: remove this once the werkzeug type annotations have been fixed +# https://github.com/pallets/werkzeug/issues/2836 +# mypy: disable-error-code="arg-type" import abc import base64 @@ -17,20 +20,21 @@ import enum import io import json + from lxml import etree # type: ignore -import werkzeug.exceptions # type: ignore -import werkzeug.routing # type: ignore -import werkzeug.urls # type: ignore +import werkzeug.exceptions +import werkzeug.routing +import werkzeug.urls from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount -from werkzeug.wrappers import Request, Response # type: ignore +from werkzeug.wrappers import Request, Response from basyx.aas import model from ._generic import XML_NS_MAP from .xml import XMLConstructables, read_aas_xml_element, xml_serialization from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from typing import Callable, Dict, Iterator, List, Optional, Type, TypeVar, Union +from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union # TODO: support the path/reference/etc. parameter @@ -49,12 +53,12 @@ def __str__(self): class Message: - def __init__(self, code: str, text: str, type_: MessageType = MessageType.UNDEFINED, + def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNDEFINED, timestamp: Optional[datetime.datetime] = None): - self.code = code - self.text = text - self.messageType = type_ - self.timestamp = timestamp if timestamp is not None else datetime.datetime.utcnow() + self.code: str = code + self.text: str = text + self.message_type: MessageType = message_type + self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.utcnow() class Result: @@ -76,7 +80,7 @@ def _result_to_json(cls, result: Result) -> Dict[str, object]: @classmethod def _message_to_json(cls, message: Message) -> Dict[str, object]: return { - "messageType": message.messageType, + "messageType": message.message_type, "text": message.text, "code": message.code, "timestamp": message.timestamp.isoformat() @@ -167,7 +171,7 @@ def result_to_xml(result: Result, **kwargs) -> etree.Element: def message_to_xml(message: Message) -> etree.Element: message_elem = etree.Element("message") message_type_elem = etree.Element("messageType") - message_type_elem.text = str(message.messageType) + message_type_elem.text = str(message.message_type) text_elem = etree.Element("text") text_elem.text = message.text code_elem = etree.Element("code") @@ -494,8 +498,9 @@ def __init__(self, object_store: model.AbstractObjectStore): "base64url_json": Base64UrlJsonConverter }) - def __call__(self, environ, start_response): - response = self.handle_request(Request(environ)) + # TODO: the parameters can be typed via builtin wsgiref with Python 3.11+ + def __call__(self, environ, start_response) -> Iterable[bytes]: + response: Response = self.handle_request(Request(environ)) return response(environ, start_response) def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: @@ -558,7 +563,7 @@ def handle_request(self, request: Request): endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") - return endpoint(request, values, map_adapter=map_adapter) + return endpoint(request, values, map_adapter=map_adapter) # type: ignore[operator] # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them except werkzeug.exceptions.NotAcceptable as e: @@ -862,6 +867,6 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg if __name__ == "__main__": - from werkzeug.serving import run_simple # type: ignore + from werkzeug.serving import run_simple from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 08f188261d681b395055a0fcb4e994dfb6ff29b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 20:02:56 +0100 Subject: [PATCH 199/474] adapter.http: improve codestyle --- basyx/aas/adapter/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 65ce5d5..5fa2e2f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -196,7 +196,6 @@ def aas_object_to_xml(obj: object) -> etree.Element: return xml_serialization.reference_to_xml(obj) if isinstance(obj, model.Submodel): return xml_serialization.submodel_to_xml(obj) - # TODO: xml serialization needs a constraint_to_xml() function if isinstance(obj, model.Qualifier): return xml_serialization.qualifier_to_xml(obj) if isinstance(obj, model.SubmodelElement): From 7512db358ff0014794552d27ed4392d362e73742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 21:11:59 +0100 Subject: [PATCH 200/474] adapter.http: update license header --- basyx/aas/adapter/http.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 5fa2e2f..d422f55 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -1,13 +1,9 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2024 the Eclipse BaSyx Authors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: MIT # TODO: remove this once the werkzeug type annotations have been fixed # https://github.com/pallets/werkzeug/issues/2836 From faec908f75e42ae7b088c9403ba230cdc5a18e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 15 Feb 2024 14:49:31 +0100 Subject: [PATCH 201/474] adapter.http: document another 'type: ignore' comment --- basyx/aas/adapter/http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d422f55..cb401df 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: MIT # TODO: remove this once the werkzeug type annotations have been fixed -# https://github.com/pallets/werkzeug/issues/2836 +# https://github.com/pallets/werkzeug/issues/2836 # mypy: disable-error-code="arg-type" import abc @@ -558,6 +558,8 @@ def handle_request(self, request: Request): endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") + # TODO: remove this 'type: ignore' comment once the werkzeug type annotations have been fixed + # https://github.com/pallets/werkzeug/issues/2836 return endpoint(request, values, map_adapter=map_adapter) # type: ignore[operator] # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them From a1a52e783824945a24d1a1ee65dea70f64862840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 21 Feb 2024 18:09:21 +0100 Subject: [PATCH 202/474] adapter.http: remove `/aas` submount from AAS repository The submount has been removed from the spec, yay! --- basyx/aas/adapter/http.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index cb401df..52c488c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -421,17 +421,13 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/", methods=["DELETE"], endpoint=self.delete_aas), - Submount("/aas", [ - Rule("/", methods=["GET"], endpoint=self.get_aas), - Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/submodels", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific) - ]) + Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), + Submount("/submodels", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("//", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific) ]) ]) ]), From da898b6cc1d6d6ddd1f074b535ded9639b3483f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 21 Feb 2024 18:15:09 +0100 Subject: [PATCH 203/474] adapter.http: update base URL from `/api/v1` to `/api/v3.0` --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 52c488c..7b5f685 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -413,7 +413,7 @@ class WSGIApp: def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ - Submount("/api/v1", [ + Submount("/api/v3.0", [ Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), Rule("/", methods=["POST"], endpoint=self.post_aas), From 8017a73051884409cb4ff3d0eb7249d5ec9647c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 21 Feb 2024 19:00:04 +0100 Subject: [PATCH 204/474] adapter.http: remove hardcoded encoding from error messages --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7b5f685..e55fddf 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -380,7 +380,7 @@ def to_python(self, value: str) -> model.Identifier: except binascii.Error: raise BadRequest(f"Encoded identifier {value} is invalid base64url!") except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + raise BadRequest(f"Encoded base64url value is not a valid {self.encoding} string!") return decoded From 9de6af16b1d9bc57ebf283a6b091394fc5857e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 17 Feb 2024 17:44:16 +0100 Subject: [PATCH 205/474] adapter.aasx: improve `AASXWriter` docstring Replace a block of text by an `attention` admonition to highlight it properly. Furthermore, add a missing comma. --- basyx/aas/adapter/aasx.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 7bb78e3..10efd91 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -283,9 +283,11 @@ class AASXWriter: file_store) writer.write_core_properties(cp) - **Attention:** The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context - manager functionality (as shown above). Otherwise the resulting AASX file will lack important data structures - and will not be readable. + .. attention:: + + The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context manager + functionality (as shown above). Otherwise, the resulting AASX file will lack important data structures + and will not be readable. """ AASX_ORIGIN_PART_NAME = "/aasx/aasx-origin" From f1e81247648dd7d2db9580cc8fc1d31bf7007ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 Mar 2024 23:57:05 +0100 Subject: [PATCH 206/474] adapter.xml: change type of an exception to `AssertionError` The respective exception marks an error in the program, which should correctly be an `AssertionError`. --- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 6cabeff..5680015 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -316,8 +316,8 @@ def _failsafe_construct_mandatory(element: etree.Element, constructor: Callable[ """ constructed = _failsafe_construct(element, constructor, False, **kwargs) if constructed is None: - raise TypeError("The result of a non-failsafe _failsafe_construct() call was None! " - "This is a bug in the Eclipse BaSyx Python SDK XML deserialization, please report it!") + raise AssertionError("The result of a non-failsafe _failsafe_construct() call was None! " + "This is a bug in the Eclipse BaSyx Python SDK XML deserialization, please report it!") return constructed From b89b7d4efebb4764bd00b0a06fec3f56bab22930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 14 Mar 2024 00:09:56 +0100 Subject: [PATCH 207/474] adapter.xml: rename `XMLConstructables.GLOBAL_REFERENCE` `GlobalReference` has been renamed to `ExternalReference` in V3, but this enum member has been missed in the rename. --- basyx/aas/adapter/xml/xml_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 5680015..d017b3a 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1250,7 +1250,7 @@ class XMLConstructables(enum.Enum): KEY = enum.auto() REFERENCE = enum.auto() MODEL_REFERENCE = enum.auto() - GLOBAL_REFERENCE = enum.auto() + EXTERNAL_REFERENCE = enum.auto() ADMINISTRATIVE_INFORMATION = enum.auto() QUALIFIER = enum.auto() SECURITY = enum.auto() @@ -1317,7 +1317,7 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool constructor = decoder_.construct_reference elif construct == XMLConstructables.MODEL_REFERENCE: constructor = decoder_.construct_model_reference - elif construct == XMLConstructables.GLOBAL_REFERENCE: + elif construct == XMLConstructables.EXTERNAL_REFERENCE: constructor = decoder_.construct_external_reference elif construct == XMLConstructables.ADMINISTRATIVE_INFORMATION: constructor = decoder_.construct_administrative_information From dbcd922926dbfd8a348fa7f19ae1c2db217f08e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 Mar 2024 21:33:58 +0100 Subject: [PATCH 208/474] adapter.{json,xml}: make (de-)serialization interfaces coherent lxml supports paths already, no modification is necessary there. However, the `lxml.etree.ElementTree.write()` function requires `BinaryIO`, i.e. files opened with the 'b' mode. While it would be possible to access the underlying binary buffer of files opened in text mode via `open()`, this isn't possible for `io.StringIO()`, as it doesn't have the `buffer` property. Thus, even if we could support files opened via `open()` in text mode, we couldn't annotate the XML serialization functions with `TextIO`, as `io.StringIO()` remains unsupported. Because of that, I decided to not support `TextIO` for the XML serialization. The builtin JSON module only supports file handles, with the `json.dump()` method only supporting `TextIO` and `json.load()` supporting `TextIO` and `BinaryIO`. Thus, the JSON adapter is modified to `open()` given paths, while the JSON serialization is additionally modified to wrap `BinaryIO` with `io.TextIOWrapper`. Fix #42 --- basyx/aas/adapter/_generic.py | 9 ++++- .../aas/adapter/json/json_deserialization.py | 24 +++++++++---- basyx/aas/adapter/json/json_serialization.py | 34 ++++++++++++++++--- basyx/aas/adapter/xml/xml_deserialization.py | 12 +++---- basyx/aas/adapter/xml/xml_serialization.py | 17 ++++++++-- 5 files changed, 76 insertions(+), 20 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 34c3412..3ca90cc 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -8,10 +8,17 @@ The dicts defined in this module are used in the json and xml modules to translate enum members of our implementation to the respective string and vice versa. """ -from typing import Dict, Type +import os +from typing import BinaryIO, Dict, IO, Type, Union from basyx.aas import model +# type aliases for path-like objects and IO +# used by write_aas_xml_file, read_aas_xml_file, write_aas_json_file, read_aas_json_file +Path = Union[str, bytes, os.PathLike] +PathOrBinaryIO = Union[Path, BinaryIO] +PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO + # XML Namespace definition XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 1e3aecb..df8a7f2 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -30,15 +30,16 @@ Other embedded objects are converted using a number of helper constructor methods. """ import base64 +import contextlib import json import logging import pprint -from typing import Dict, Callable, TypeVar, Type, List, IO, Optional, Set +from typing import Dict, Callable, ContextManager, TypeVar, Type, List, IO, Optional, Set, get_args 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 + DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE, PathOrIO, Path logger = logging.getLogger(__name__) @@ -794,7 +795,7 @@ def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFr return StrictAASFromJsonDecoder -def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, replace_existing: bool = False, +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]: """ @@ -803,7 +804,7 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r :param object_store: The :class:`ObjectStore ` in which the identifiable objects should be stored - :param file: A file-like object to read the JSON-serialized data from + :param file: A filename or file-like object to read the JSON-serialized data from :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. This parameter is ignored if replace_existing is ``True``. @@ -819,8 +820,19 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r ret: Set[model.Identifier] = set() decoder_ = _select_decoder(failsafe, stripped, decoder) + # json.load() accepts TextIO and BinaryIO + cm: ContextManager[IO] + if isinstance(file, get_args(Path)): + # 'file' is a path, needs to be opened first + cm = open(file, "r", encoding="utf-8-sig") + else: + # 'file' is not a path, thus it must already be IO + # mypy seems to have issues narrowing the type due to get_args() + cm = contextlib.nullcontext(file) # type: ignore[arg-type] + # read, parse and convert JSON file - data = json.load(file, cls=decoder_) + with cm as fp: + data = json.load(fp, cls=decoder_) for name, expected_type in (('assetAdministrationShells', model.AssetAdministrationShell), ('submodels', model.Submodel), @@ -864,7 +876,7 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r return ret -def read_aas_json_file(file: IO, **kwargs) -> model.DictObjectStore[model.Identifiable]: +def read_aas_json_file(file: PathOrIO, **kwargs) -> model.DictObjectStore[model.Identifiable]: """ A wrapper of :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects in an empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 08ba971..25a22c4 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -28,8 +28,10 @@ conversion functions to handle all the attributes of abstract base classes. """ import base64 +import contextlib import inspect -from typing import List, Dict, IO, Optional, Type, Callable +import io +from typing import ContextManager, List, Dict, Optional, TextIO, Type, Callable, get_args import json from basyx.aas import model @@ -733,13 +735,21 @@ def object_store_to_json(data: model.AbstractObjectStore, stripped: bool = False return json.dumps(_create_dict(data), cls=encoder_, **kwargs) -def write_aas_json_file(file: IO, data: model.AbstractObjectStore, stripped: bool = False, +class _DetachingTextIOWrapper(io.TextIOWrapper): + """ + Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer. + """ + def __exit__(self, exc_type, exc_val, exc_tb): + self.detach() + + +def write_aas_json_file(file: _generic.PathOrIO, data: model.AbstractObjectStore, stripped: bool = False, encoder: Optional[Type[AASToJsonEncoder]] = None, **kwargs) -> None: """ Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 - :param file: A file-like object to write the JSON-serialized data to + :param file: A filename or file-like object to write the JSON-serialized data to :param data: :class:`ObjectStore ` which contains different objects of the AAS meta model which should be serialized to a JSON file :param stripped: If `True`, objects are serialized to stripped json objects. @@ -749,5 +759,21 @@ def write_aas_json_file(file: IO, data: model.AbstractObjectStore, stripped: boo :param kwargs: Additional keyword arguments to be passed to `json.dump()` """ encoder_ = _select_encoder(stripped, encoder) + + # json.dump() only accepts TextIO + cm: ContextManager[TextIO] + if isinstance(file, get_args(_generic.Path)): + # 'file' is a path, needs to be opened first + cm = open(file, "w", encoding="utf-8") + elif not hasattr(file, "encoding"): + # only TextIO has this attribute, so this must be BinaryIO, which needs to be wrapped + # mypy seems to have issues narrowing the type due to get_args() + cm = _DetachingTextIOWrapper(file, "utf-8", write_through=True) # type: ignore[arg-type] + else: + # we already got TextIO, nothing needs to be done + # mypy seems to have issues narrowing the type due to get_args() + cm = contextlib.nullcontext(file) # type: ignore[arg-type] + # serialize object to json - json.dump(_create_dict(data), file, cls=encoder_, **kwargs) + with cm as fp: + json.dump(_create_dict(data), fp, cls=encoder_, **kwargs) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index d017b3a..3af5ca9 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -48,10 +48,10 @@ import base64 import enum -from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar +from typing import Any, Callable, Dict, Iterable, Optional, Set, Tuple, Type, TypeVar from .._generic import XML_NS_MAP, XML_NS_AAS, 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 + REFERENCE_TYPES_INVERSE, DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE, PathOrIO NS_AAS = XML_NS_AAS REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]} @@ -1187,7 +1187,7 @@ class StrictStrippedAASFromXmlDecoder(StrictAASFromXmlDecoder, StrippedAASFromXm pass -def _parse_xml_document(file: IO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree.Element]: +def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree.Element]: """ Parse an XML document into an element tree @@ -1290,7 +1290,7 @@ class XMLConstructables(enum.Enum): DATA_SPECIFICATION_IEC61360 = enum.auto() -def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool = True, stripped: bool = False, +def read_aas_xml_element(file: PathOrIO, construct: XMLConstructables, failsafe: bool = True, stripped: bool = False, decoder: Optional[Type[AASFromXmlDecoder]] = None, **constructor_kwargs) -> Optional[object]: """ Construct a single object from an XML string. The namespaces have to be declared on the object itself, since there @@ -1398,7 +1398,7 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool return _failsafe_construct(element, constructor, decoder_.failsafe, **constructor_kwargs) -def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identifiable], file: IO, +def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identifiable], file: PathOrIO, replace_existing: bool = False, ignore_existing: bool = False, failsafe: bool = True, stripped: bool = False, decoder: Optional[Type[AASFromXmlDecoder]] = None, **parser_kwargs: Any) -> Set[model.Identifier]: @@ -1471,7 +1471,7 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif return ret -def read_aas_xml_file(file: IO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]: +def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]: """ A wrapper of :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index c6eb2be..bb882a9 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -16,10 +16,21 @@ - For serializing any object to an XML fragment, that fits the XML specification from 'Details of the Asset Administration Shell', chapter 5.4, check out ``_to_xml()``. These functions return an :class:`~lxml.etree.Element` object to be serialized into XML. + +.. attention:: + Unlike the XML deserialization and the JSON (de-)serialization, the XML serialization only supports + :class:`~typing.BinaryIO` and not :class:`~typing.TextIO`. Thus, if you open files by yourself, you have to open + them in binary mode, see the mode table of :func:`open`. + + .. code:: python + + # wb = open for writing + binary mode + with open("example.xml", "wb") as fp: + write_aas_xml_file(fp, object_store) """ from lxml import etree # type: ignore -from typing import Dict, IO, Optional, Type +from typing import Dict, Optional, Type import base64 from basyx.aas import model @@ -840,14 +851,14 @@ def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+" # ############################################################## -def write_aas_xml_file(file: IO, +def write_aas_xml_file(file: _generic.PathOrBinaryIO, data: model.AbstractObjectStore, **kwargs) -> None: """ Write a set of AAS objects to an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 - :param file: A file-like object to write the XML-serialized data to + :param file: A filename or file-like object to write the XML-serialized data to :param data: :class:`ObjectStore ` which contains different objects of the AAS meta model which should be serialized to an XML file :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree.ElementTree.write` From d1f5ad264c72a8c47ca57405b26bfa40adfba85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 Mar 2024 23:12:32 +0100 Subject: [PATCH 209/474] adapter.xml: add function for serializing single objects --- basyx/aas/adapter/xml/__init__.py | 3 +- basyx/aas/adapter/xml/xml_serialization.py | 153 +++++++++++++++++++-- 2 files changed, 140 insertions(+), 16 deletions(-) diff --git a/basyx/aas/adapter/xml/__init__.py b/basyx/aas/adapter/xml/__init__.py index 714c806..af58ad0 100644 --- a/basyx/aas/adapter/xml/__init__.py +++ b/basyx/aas/adapter/xml/__init__.py @@ -11,7 +11,8 @@ """ import os.path -from .xml_serialization import write_aas_xml_file +from .xml_serialization import object_store_to_xml_element, write_aas_xml_file, object_to_xml_element, \ + write_aas_xml_element from .xml_deserialization import AASFromXmlDecoder, StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder, \ StrictStrippedAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index bb882a9..cd02ead 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -14,8 +14,10 @@ - For generating an XML-File from a :class:`~basyx.aas.model.provider.AbstractObjectStore`, check out the function :func:`write_aas_xml_file`. - For serializing any object to an XML fragment, that fits the XML specification from 'Details of the - Asset Administration Shell', chapter 5.4, check out ``_to_xml()``. These functions return - an :class:`~lxml.etree.Element` object to be serialized into XML. + Asset Administration Shell', chapter 5.4, you can either use :func:`object_to_xml_element`, which serializes a given + object and returns it as :class:`~lxml.etree.Element`, **or** :func:`write_aas_xml_element`, which does the same + thing, but writes the :class:`~lxml.etree.Element` to a file instead of returning it. + As a third alternative, you can also use the functions ``_to_xml()`` directly. .. attention:: Unlike the XML deserialization and the JSON (de-)serialization, the XML serialization only supports @@ -30,7 +32,7 @@ """ from lxml import etree # type: ignore -from typing import Dict, Optional, Type +from typing import Callable, Dict, Optional, Type import base64 from basyx.aas import model @@ -231,6 +233,20 @@ def data_element_to_xml(obj: model.DataElement) -> etree.Element: return reference_element_to_xml(obj) +def key_to_xml(obj: model.Key, tag: str = NS_AAS+"key") -> etree.Element: + """ + Serialization of objects of class :class:`~basyx.aas.model.base.Key` to XML + + :param obj: Object of class :class:`~basyx.aas.model.base.Key` + :param tag: Namespace+Tag of the returned element. Default is ``aas:key`` + :return: Serialized :class:`~lxml.etree.Element` object + """ + et_key = _generate_element(tag) + et_key.append(_generate_element(name=NS_AAS + "type", text=_generic.KEY_TYPES[obj.type])) + et_key.append(_generate_element(name=NS_AAS + "value", text=obj.value)) + return et_key + + def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etree.Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.Reference` to XML @@ -245,10 +261,7 @@ def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etr et_reference.append(reference_to_xml(obj.referred_semantic_id, NS_AAS + "referredSemanticId")) et_keys = _generate_element(name=NS_AAS + "keys") for aas_key in obj.key: - et_key = _generate_element(name=NS_AAS + "key") - et_key.append(_generate_element(name=NS_AAS + "type", text=_generic.KEY_TYPES[aas_key.type])) - et_key.append(_generate_element(name=NS_AAS + "value", text=aas_key.value)) - et_keys.append(et_key) + et_keys.append(key_to_xml(aas_key)) et_reference.append(et_keys) return et_reference @@ -850,18 +863,114 @@ def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+" # general functions # ############################################################## +def _write_element(file: _generic.PathOrBinaryIO, element: etree.Element, **kwargs) -> None: + etree.ElementTree(element).write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) + + +def object_to_xml_element(obj: object) -> etree.Element: + """ + Serialize a single object to an :class:`~lxml.etree.Element`. + + :param obj: The object to serialize + """ + serialization_func: Callable[..., etree.Element] + + if isinstance(obj, model.Key): + serialization_func = key_to_xml + elif isinstance(obj, model.Reference): + serialization_func = reference_to_xml + elif isinstance(obj, model.Reference): + serialization_func = reference_to_xml + elif isinstance(obj, model.AdministrativeInformation): + serialization_func = administrative_information_to_xml + elif isinstance(obj, model.Qualifier): + serialization_func = qualifier_to_xml + elif isinstance(obj, model.AnnotatedRelationshipElement): + serialization_func = annotated_relationship_element_to_xml + elif isinstance(obj, model.BasicEventElement): + serialization_func = basic_event_element_to_xml + elif isinstance(obj, model.Blob): + serialization_func = blob_to_xml + elif isinstance(obj, model.Capability): + serialization_func = capability_to_xml + elif isinstance(obj, model.Entity): + serialization_func = entity_to_xml + elif isinstance(obj, model.Extension): + serialization_func = extension_to_xml + elif isinstance(obj, model.File): + serialization_func = file_to_xml + elif isinstance(obj, model.Resource): + serialization_func = resource_to_xml + elif isinstance(obj, model.MultiLanguageProperty): + serialization_func = multi_language_property_to_xml + elif isinstance(obj, model.Operation): + serialization_func = operation_to_xml + elif isinstance(obj, model.Property): + serialization_func = property_to_xml + elif isinstance(obj, model.Range): + serialization_func = range_to_xml + elif isinstance(obj, model.ReferenceElement): + serialization_func = reference_element_to_xml + elif isinstance(obj, model.RelationshipElement): + serialization_func = relationship_element_to_xml + elif isinstance(obj, model.SubmodelElementCollection): + serialization_func = submodel_element_collection_to_xml + elif isinstance(obj, model.SubmodelElementList): + serialization_func = submodel_element_list_to_xml + elif isinstance(obj, model.AssetAdministrationShell): + serialization_func = asset_administration_shell_to_xml + elif isinstance(obj, model.AssetInformation): + serialization_func = asset_information_to_xml + elif isinstance(obj, model.SpecificAssetId): + serialization_func = specific_asset_id_to_xml + elif isinstance(obj, model.Submodel): + serialization_func = submodel_to_xml + elif isinstance(obj, model.ValueReferencePair): + serialization_func = value_reference_pair_to_xml + elif isinstance(obj, model.ConceptDescription): + serialization_func = concept_description_to_xml + elif isinstance(obj, model.LangStringSet): + serialization_func = lang_string_set_to_xml + elif isinstance(obj, model.EmbeddedDataSpecification): + serialization_func = embedded_data_specification_to_xml + elif isinstance(obj, model.DataSpecificationIEC61360): + serialization_func = data_specification_iec61360_to_xml + # generic serialization using the functions for abstract classes + elif isinstance(obj, model.DataElement): + serialization_func = data_element_to_xml + elif isinstance(obj, model.SubmodelElement): + serialization_func = submodel_to_xml + elif isinstance(obj, model.DataSpecificationContent): + serialization_func = data_specification_content_to_xml + # type aliases + elif isinstance(obj, model.ValueList): + serialization_func = value_list_to_xml + else: + raise ValueError(f"{obj!r} cannot be serialized!") -def write_aas_xml_file(file: _generic.PathOrBinaryIO, - data: model.AbstractObjectStore, - **kwargs) -> None: + return serialization_func(obj) + + +def write_aas_xml_element(file: _generic.PathOrBinaryIO, obj: object, **kwargs) -> None: """ - Write a set of AAS objects to an Asset Administration Shell XML file according to 'Details of the Asset - Administration Shell', chapter 5.4 + Serialize a single object to XML. Namespace declarations are added to the object itself, as there is no surrounding + environment element. :param file: A filename or file-like object to write the XML-serialized data to + :param obj: The object to serialize + :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree.ElementTree.write` + """ + return _write_element(file, object_to_xml_element(obj), **kwargs) + + +def object_store_to_xml_element(data: model.AbstractObjectStore) -> etree.Element: + """ + Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree.Element`. + This function is used internally by :meth:`write_aas_xml_file` and shouldn't be + called directly for most use-cases. + :param data: :class:`ObjectStore ` which contains different objects of the AAS meta model which should be serialized to an XML file - :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree.ElementTree.write` """ # separate different kind of objects asset_administration_shells = [] @@ -893,5 +1002,19 @@ def write_aas_xml_file(file: _generic.PathOrBinaryIO, et_concept_descriptions.append(concept_description_to_xml(con_obj)) root.append(et_concept_descriptions) - tree = etree.ElementTree(root) - tree.write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) + return root + + +def write_aas_xml_file(file: _generic.PathOrBinaryIO, + data: model.AbstractObjectStore, + **kwargs) -> None: + """ + Write a set of AAS objects to an Asset Administration Shell XML file according to 'Details of the Asset + Administration Shell', chapter 5.4 + + :param file: A filename or file-like object to write the XML-serialized data to + :param data: :class:`ObjectStore ` which contains different objects of + the AAS meta model which should be serialized to an XML file + :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree.ElementTree.write` + """ + return _write_element(file, object_store_to_xml_element(data), **kwargs) From 8c8c157befc95f4647e9493318fde941a4bfa3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 Mar 2024 23:41:39 +0100 Subject: [PATCH 210/474] adapter.{json,xml}: improve docstrings --- .../aas/adapter/json/json_deserialization.py | 12 ++++++++++ basyx/aas/adapter/xml/xml_deserialization.py | 22 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index df8a7f2..f6c7c41 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -815,6 +815,13 @@ 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 + :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`` + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError, TypeError): **Non-failsafe**: + Errors during construction of the objects + :raises TypeError: **Non-failsafe**: Encountered an element in the wrong list + (e.g. an AssetAdministrationShell in ``submodels``) :return: A set of :class:`Identifiers ` that were added to object_store """ ret: Set[model.Identifier] = set() @@ -884,6 +891,11 @@ def read_aas_json_file(file: PathOrIO, **kwargs) -> model.DictObjectStore[model. :param file: A filename or file-like object to read the JSON-serialized data from :param kwargs: Keyword arguments passed to :meth:`read_aas_json_file_into` + :raises KeyError: **Non-failsafe**: Encountered a duplicate identifier + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError, TypeError): **Non-failsafe**: + Errors during construction of the objects + :raises TypeError: **Non-failsafe**: Encountered an element in the wrong list + (e.g. an AssetAdministrationShell in ``submodels``) :return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the JSON file """ object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 3af5ca9..0a59518 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1195,6 +1195,8 @@ def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: :param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception if the document is malformed, parsing is aborted, an error is logged and None is returned :param parser_kwargs: Keyword arguments passed to the XMLParser constructor + :raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML + :raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document :return: The root element of the element tree """ @@ -1294,7 +1296,7 @@ def read_aas_xml_element(file: PathOrIO, construct: XMLConstructables, failsafe: decoder: Optional[Type[AASFromXmlDecoder]] = None, **constructor_kwargs) -> Optional[object]: """ Construct a single object from an XML string. The namespaces have to be declared on the object itself, since there - is no surrounding aasenv element. + is no surrounding environment element. :param file: A filename or file-like object to read the XML-serialized data from :param construct: A member of the enum :class:`~.XMLConstructables`, specifying which type to construct. @@ -1306,6 +1308,10 @@ def read_aas_xml_element(file: PathOrIO, construct: XMLConstructables, failsafe: This parameter is ignored if a decoder class is specified. :param decoder: The decoder class used to decode the XML elements :param constructor_kwargs: Keyword arguments passed to the constructor function + :raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML + :raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during + construction of the objects :return: The constructed object or None, if an error occurred in failsafe mode. """ decoder_ = _select_decoder(failsafe, stripped, decoder) @@ -1420,6 +1426,14 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif This parameter is ignored if a decoder class is specified. :param decoder: The decoder class used to decode the XML elements :param parser_kwargs: Keyword arguments passed to the XMLParser constructor + :raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML + :raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document + :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`` + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during + construction of the objects + :raises TypeError: **Non-failsafe**: Encountered an undefined top-level list (e.g. ````) :return: A set of :class:`Identifiers ` that were added to object_store """ ret: Set[model.Identifier] = set() @@ -1479,6 +1493,12 @@ def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> model.DictObjectStore[mo :param file: A filename or file-like object to read the XML-serialized data from :param kwargs: Keyword arguments passed to :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` + :raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML + :raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document + :raises KeyError: **Non-failsafe**: Encountered a duplicate identifier + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during + construction of the objects + :raises TypeError: **Non-failsafe**: Encountered an undefined top-level list (e.g. ````) :return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the XML file """ object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() From 99fe00410316930a265d652cc83186b8d2c2161f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 15:43:24 +0100 Subject: [PATCH 211/474] adapter.http: fix base64 decoding without padding Many online tools omit the padding when encoding base64url. Thus, requesters may omit the padding as well, which would cause the decoding to fail. Thus, we simply always append two padding characters, because python doesn't complain about too much padding. --- basyx/aas/adapter/http.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e55fddf..73c0307 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -376,7 +376,11 @@ def to_url(self, value: model.Identifier) -> str: def to_python(self, value: str) -> model.Identifier: value = super().to_python(value) try: - decoded = base64.urlsafe_b64decode(super().to_python(value)).decode(self.encoding) + # If the requester omits the base64 padding, an exception will be raised. + # However, Python doesn't complain about too much padding, + # thus we simply always append two padding characters (==). + # See also: https://stackoverflow.com/a/49459036/4780052 + decoded = base64.urlsafe_b64decode(super().to_python(value) + "==").decode(self.encoding) except binascii.Error: raise BadRequest(f"Encoded identifier {value} is invalid base64url!") except UnicodeDecodeError: From b1809ee9ab68f662388c5231687ef4a65c5d2410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 18:18:28 +0100 Subject: [PATCH 212/474] adapter.http: remove `/submodel` submount from Submodel repository --- basyx/aas/adapter/http.py | 68 ++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 73c0307..15e38d3 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -442,47 +442,43 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Submount("/submodel", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Submount("/submodel-elements", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Submount("/submodel-elements", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), + Submount("/", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_elements_id_short_path), - Submount("/", [ + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + Submount("/constraints", [ Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path), + endpoint=self.get_submodel_submodel_element_constraints), Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_id_short_path), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_id_short_path), - Submount("/constraints", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), - ]) - ]), + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), - ]) + ]), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), ]) ]) ]) From 4704efc23dc159857f11805ff95a89b19877b2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 18:38:40 +0100 Subject: [PATCH 213/474] adapter.http: use builtin id_short path resolution --- basyx/aas/adapter/http.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 15e38d3..ea1eb63 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -514,17 +514,19 @@ def _resolve_reference(self, reference: model.ModelReference[model.base._RT]) -> @classmethod def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, id_shorts: List[str]) \ -> model.SubmodelElement: - current_namespace: Union[model.UniqueIdShortNamespace, model.SubmodelElement] = namespace - for id_short in id_shorts: - current_namespace = cls._expect_namespace(current_namespace, id_short) - next_obj = cls._namespace_submodel_element_op(current_namespace, current_namespace.get_referable, id_short) - if not isinstance(next_obj, model.SubmodelElement): - raise werkzeug.exceptions.InternalServerError(f"{next_obj}, child of {current_namespace!r}, " - f"is not a submodel element!") - current_namespace = next_obj - if not isinstance(current_namespace, model.SubmodelElement): + if not id_shorts: raise ValueError("No id_shorts specified!") - return current_namespace + + try: + ret = namespace.get_referable(id_shorts) + except KeyError as e: + raise NotFound(e.args[0]) + except (TypeError, ValueError) as e: + raise BadRequest(e.args[0]) + + if not isinstance(ret, model.SubmodelElement): + raise BadRequest(f"{ret!r} is not a submodel element!") + return ret @classmethod def _get_submodel_or_nested_submodel_element(cls, submodel: model.Submodel, id_shorts: List[str]) \ From 9896db6584cbbe95e1d449cde2fddaff195a47b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 18:51:34 +0100 Subject: [PATCH 214/474] adapter.http: fix `ModelReference` json deserialization The http adapter still used the old `construct_aas_reference` function, which doesn't exist anymore. The error was masked due to a broad `type: ignore`. These are changed to only ignore assignment type errors. --- basyx/aas/adapter/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index ea1eb63..f45a0b3 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -279,12 +279,12 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool constructor: Optional[Callable[..., T]] = None args = [] if expect_type is model.ModelReference: - constructor = decoder._construct_aas_reference # type: ignore + constructor = decoder._construct_model_reference # type: ignore[assignment] args.append(model.Submodel) elif expect_type is model.AssetInformation: - constructor = decoder._construct_asset_information # type: ignore + constructor = decoder._construct_asset_information # type: ignore[assignment] elif expect_type is model.SpecificAssetId: - constructor = decoder._construct_specific_asset_id # type: ignore + constructor = decoder._construct_specific_asset_id # type: ignore[assignment] if constructor is not None: # construct elements that aren't self-identified From 7f336ec2903f644b1b88e6f437bcf2917d04af47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 18:49:55 +0100 Subject: [PATCH 215/474] adapter.http: simplify XML serialization ... by using (now) builtin functionality. --- basyx/aas/adapter/http.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index f45a0b3..0a309b7 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -27,7 +27,7 @@ from basyx.aas import model from ._generic import XML_NS_MAP -from .xml import XMLConstructables, read_aas_xml_element, xml_serialization +from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union @@ -135,12 +135,12 @@ def serialize(self, obj: ResponseData, stripped: bool) -> str: if isinstance(obj, list): response_elem = etree.Element("list", nsmap=XML_NS_MAP) for obj in obj: - response_elem.append(aas_object_to_xml(obj)) + response_elem.append(object_to_xml_element(obj)) etree.cleanup_namespaces(response_elem) else: # dirty hack to be able to use the namespace prefixes defined in xml_serialization.NS_MAP parent = etree.Element("parent", nsmap=XML_NS_MAP) - response_elem = aas_object_to_xml(obj) + response_elem = object_to_xml_element(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") @@ -182,23 +182,6 @@ def message_to_xml(message: Message) -> etree.Element: return message_elem -def aas_object_to_xml(obj: object) -> etree.Element: - # TODO: a similar function should be implemented in the xml serialization - if isinstance(obj, model.AssetAdministrationShell): - return xml_serialization.asset_administration_shell_to_xml(obj) - if isinstance(obj, model.AssetInformation): - return xml_serialization.asset_information_to_xml(obj) - if isinstance(obj, model.Reference): - return xml_serialization.reference_to_xml(obj) - if isinstance(obj, model.Submodel): - return xml_serialization.submodel_to_xml(obj) - if isinstance(obj, model.Qualifier): - return xml_serialization.qualifier_to_xml(obj) - if isinstance(obj, model.SubmodelElement): - return xml_serialization.submodel_element_to_xml(obj) - raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") - - def get_response_type(request: Request) -> Type[APIResponse]: response_types: Dict[str, Type[APIResponse]] = { "application/json": JsonResponse, From 7b76e6bea0ffeabb3a6390aeedb0d7fcd73510b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 19:01:33 +0100 Subject: [PATCH 216/474] adapter.http: simplify `SubmodelElement` deletion We can always call `_get_submodel_or_nested_submodel_element`, as it returns the submodel itself if we don't pass it any id_shorts. --- basyx/aas/adapter/http.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 0a309b7..eb2c05e 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -752,12 +752,10 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() id_short_path: List[str] = url_args["id_shorts"] - parent: model.UniqueIdShortNamespace = submodel - if len(id_short_path) > 1: - parent = self._expect_namespace( - self._get_nested_submodel_element(submodel, id_short_path[:-1]), - id_short_path[-1] - ) + parent: model.UniqueIdShortNamespace = self._expect_namespace( + self._get_submodel_or_nested_submodel_element(submodel, id_short_path[:-1]), + id_short_path[-1] + ) self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) return response_t() From f6c8ce35dc71960bdc439b0dc18ce6731516c26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 19:14:46 +0100 Subject: [PATCH 217/474] adapter.http: skip validation of id_shorts in URLs created by us --- basyx/aas/adapter/http.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index eb2c05e..15f93c5 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -383,9 +383,6 @@ def validate_id_short(cls, id_short: str) -> bool: return True def to_url(self, value: List[str]) -> str: - for id_short in value: - if not self.validate_id_short(id_short): - raise ValueError(f"{id_short} is not a valid id_short!") return super().to_url(self.id_short_sep.join(id_short for id_short in value)) def to_python(self, value: str) -> List[str]: From 2e9dcc23841d245b1da9a122074622191701d2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 19:15:28 +0100 Subject: [PATCH 218/474] adapter.http: use `Referable.validate_id_short()` to validate id_shorts ... instead of the previous way of creating a `MultiLanguageProperty` to validate id_shorts. --- basyx/aas/adapter/http.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 15f93c5..7bda980 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -374,21 +374,15 @@ def to_python(self, value: str) -> model.Identifier: class IdShortPathConverter(werkzeug.routing.UnicodeConverter): id_short_sep = "." - @classmethod - def validate_id_short(cls, id_short: str) -> bool: - try: - model.MultiLanguageProperty(id_short) - except model.AASConstraintViolation: - return False - return True - def to_url(self, value: List[str]) -> str: return super().to_url(self.id_short_sep.join(id_short for id_short in value)) def to_python(self, value: str) -> List[str]: id_shorts = super().to_python(value).split(self.id_short_sep) for id_short in id_shorts: - if not self.validate_id_short(id_short): + try: + model.Referable.validate_id_short(id_short) + except (ValueError, model.AASConstraintViolation): raise BadRequest(f"{id_short} is not a valid id_short!") return id_shorts From 87985191ff294d1e59a520a4fe5dd486b2c68ecb Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 11:46:48 +0100 Subject: [PATCH 219/474] fixing delete_submodels() --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7bda980..6966d98 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -662,7 +662,7 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.Submodel)) + self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() # --------- SUBMODEL ROUTES --------- From 24725a20ca1d3d60c336e24e68936aeb7e6ae544 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 12:39:55 +0100 Subject: [PATCH 220/474] adapter.http: implement semanticID filtering --- basyx/aas/adapter/http.py | 43 +++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 6966d98..67ef0ee 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -228,9 +228,12 @@ class HTTPApiDecoder: model.SpecificAssetId: XMLConstructables.SPECIFIC_ASSET_ID, model.Qualifier: XMLConstructables.QUALIFIER, model.Submodel: XMLConstructables.SUBMODEL, - model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT + model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT, + model.Reference: XMLConstructables.REFERENCE } + encoding = "utf-8" + @classmethod def check_type_supportance(cls, type_: type): if type_ not in cls.type_constructables_map: @@ -242,6 +245,18 @@ def assert_type(cls, obj: object, type_: Type[T]) -> T: raise UnprocessableEntity(f"Object {obj!r} is not of type {type_.__name__}!") return obj + @classmethod + def base64_decode(cls, data: Union[str, bytes]) -> str: + try: + # If the requester omits the base64 padding, an exception will be raised. + # However, Python doesn't complain about too much padding, + # thus we simply always append two padding characters (==). + # See also: https://stackoverflow.com/a/49459036/4780052 + decoded = base64.urlsafe_b64decode(data + "==").decode(cls.encoding) + except binascii.Error: + raise BadRequest(f"Encoded data {str(data)} is invalid base64url!") + return decoded + @classmethod def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: cls.check_type_supportance(expect_type) @@ -268,6 +283,9 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool constructor = decoder._construct_asset_information # type: ignore[assignment] elif expect_type is model.SpecificAssetId: constructor = decoder._construct_specific_asset_id # type: ignore[assignment] + elif expect_type is model.Reference: + constructor = decoder._construct_model_reference # type: ignore[assignment] + args.append(model.Submodel) if constructor is not None: # construct elements that aren't self-identified @@ -278,10 +296,21 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool return [cls.assert_type(obj, expect_type) for obj in parsed] + @classmethod + def base64json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool)\ + -> List[T]: + data = cls.base64_decode(data) + return cls.json_list(data, expect_type, stripped, expect_single) + @classmethod def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: return cls.json_list(data, expect_type, stripped, True)[0] + @classmethod + def base64json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: + data = cls.base64_decode(data) + return cls.json_list(data, expect_type, stripped, True)[0] + @classmethod def xml(cls, data: bytes, expect_type: Type[T], stripped: bool) -> T: cls.check_type_supportance(expect_type) @@ -553,7 +582,7 @@ def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: aas = filter(lambda shell: shell.id_short == id_short, aas) asset_ids = request.args.get("assetIds") if asset_ids is not None: - spec_asset_ids = HTTPApiDecoder.json_list(asset_ids, model.SpecificAssetId, False, False) + spec_asset_ids = HTTPApiDecoder.base64json_list(asset_ids, model.SpecificAssetId, False, False) # TODO: it's currently unclear how to filter with these SpecificAssetIds return response_t(list(aas)) @@ -643,9 +672,12 @@ def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Respo id_short = request.args.get("idShort") if id_short is not None: submodels = filter(lambda sm: sm.id_short == id_short, submodels) - # TODO: filter by semantic id - # semantic_id = request.args.get("semanticId") - return response_t(list(submodels)) + semantic_id = request.args.get("semanticId") + if semantic_id is not None: + if semantic_id is not None: + spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + return response_t(list(submodels), stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -665,7 +697,6 @@ def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Respon self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() - # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) From 9da3319896851f1e7e81b0407514eb9d7bcfcfb2 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 13:30:21 +0100 Subject: [PATCH 221/474] adapter.http: implement metadata Routes --- basyx/aas/adapter/http.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 67ef0ee..2d862ab 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -441,14 +441,18 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_all), Rule("/", methods=["POST"], endpoint=self.post_submodel), + Rule("/$metadata", methods=["GET"], endpoint=self.get_allsubmodels_metadata), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_elements_id_short_path), + Rule("/$metadata", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_metadata), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path), @@ -458,6 +462,8 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.put_submodel_submodel_elements_id_short_path), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_elements_id_short_path), + Rule("/$metadata", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), Submount("/constraints", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), @@ -692,6 +698,20 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte }, force_external=True) return response_t(submodel, status=201, headers={"Location": created_resource_url}) + def get_allsubmodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + id_short = request.args.get("idShort") + if id_short is not None: + submodels = filter(lambda sm: sm.id_short == id_short, submodels) + semantic_id = request.args.get("semanticId") + if semantic_id is not None: + spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + return response_t(list(submodels), stripped=True) + + # --------- SUBMODEL ROUTES --------- + def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) @@ -704,6 +724,12 @@ def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: submodel.update() return response_t(submodel, stripped=is_stripped_request(request)) + def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(submodel, stripped=True) + def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) @@ -720,6 +746,14 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kw submodel.update() return response_t(list(submodel.submodel_element)) + def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec + # TODO: support content, extent, semanticId parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(list(submodel.submodel_element), stripped=True) + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) @@ -728,6 +762,15 @@ def get_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(submodel_element, stripped=is_stripped_request(request)) + def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + # TODO: support content, extent parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + return response_t(submodel_element, stripped=True) + def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) From 6395025738a97456554948caa01c73007b373522 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 20:31:17 +0100 Subject: [PATCH 222/474] adapter.http: implement reference routes --- basyx/aas/adapter/http.py | 49 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 2d862ab..c4c41ba 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -442,17 +442,21 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_submodel_all), Rule("/", methods=["POST"], endpoint=self.post_submodel), Rule("/$metadata", methods=["GET"], endpoint=self.get_allsubmodels_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_allsubmodels_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_elements_id_short_path), Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_metadata), + Rule("/$reference", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path), @@ -464,6 +468,8 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.delete_submodel_submodel_elements_id_short_path), Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), + Rule("/$reference", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path_reference), Submount("/constraints", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), @@ -710,6 +716,20 @@ def get_allsubmodels_metadata(self, request: Request, url_args: Dict, **_kwargs) submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) return response_t(list(submodels), stripped=True) + def get_allsubmodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + id_short = request.args.get("idShort") + if id_short is not None: + submodels = filter(lambda sm: sm.id_short == id_short, submodels) + semantic_id = request.args.get("semanticId") + if semantic_id is not None: + if semantic_id is not None: + spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + references: Iterator[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] + return response_t(list(references), stripped=is_stripped_request(request)) + # --------- SUBMODEL ROUTES --------- def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -730,6 +750,14 @@ def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> submodel.update() return response_t(submodel, stripped=True) + def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + reference = model.ModelReference.from_referable(submodel) + return response_t(reference, stripped=is_stripped_request(request)) + def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) @@ -744,7 +772,7 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kw response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - return response_t(list(submodel.submodel_element)) + return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec @@ -754,6 +782,16 @@ def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Di submodel.update() return response_t(list(submodel.submodel_element), stripped=True) + def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec + # TODO: support content, extent, semanticId parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + references: Iterator[model.ModelReference] = [model.ModelReference.from_referable(element) for element in + submodel.submodel_element] + return response_t(list(references), stripped=is_stripped_request(request)) + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) @@ -771,6 +809,15 @@ def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(submodel_element, stripped=True) + def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + reference = model.ModelReference.from_referable(submodel_element) + return response_t(reference, stripped=is_stripped_request(request)) + def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) From 692c786fff647364cd4055879ca403692b370c53 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 20:43:35 +0100 Subject: [PATCH 223/474] adapter.http: fix line length --- basyx/aas/adapter/http.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index c4c41ba..d1eeac0 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -727,7 +727,8 @@ def get_allsubmodels_reference(self, request: Request, url_args: Dict, **_kwargs if semantic_id is not None: spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) - references: Iterator[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] + references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) + for submodel in submodels] return response_t(list(references), stripped=is_stripped_request(request)) # --------- SUBMODEL ROUTES --------- @@ -788,8 +789,8 @@ def get_submodel_submodel_elements_reference(self, request: Request, url_args: D response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - references: Iterator[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - submodel.submodel_element] + references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in + submodel.submodel_element] return response_t(list(references), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -809,7 +810,8 @@ def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(submodel_element, stripped=True) - def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ + -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) From 79e254b3e8e2fd53d776760134252b0a65623ba1 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Mon, 18 Mar 2024 11:57:23 +0100 Subject: [PATCH 224/474] adapter.http: refactoring submodel repo routes --- basyx/aas/adapter/http.py | 98 +++++++++++++++------------------------ 1 file changed, 38 insertions(+), 60 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d1eeac0..47432cc 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -678,8 +678,7 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** raise NotFound(f"The AAS {aas!r} doesn't have the reference {url_args['submodel_ref']!r}!") # ------ SUBMODEL REPO ROUTES ------- - def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def _get_submodels_python(self, request: Request, url_args: Dict) -> Iterator[model.Submodel]: submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) id_short = request.args.get("idShort") if id_short is not None: @@ -689,6 +688,11 @@ def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Respo if semantic_id is not None: spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + return submodels + + def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodels = self._get_submodels_python(request, url_args) return response_t(list(submodels), stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: @@ -706,27 +710,12 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte def get_allsubmodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) - id_short = request.args.get("idShort") - if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) - semantic_id = request.args.get("semanticId") - if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + submodels = self._get_submodels_python(request, url_args) return response_t(list(submodels), stripped=True) def get_allsubmodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) - id_short = request.args.get("idShort") - if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) - semantic_id = request.args.get("semanticId") - if semantic_id is not None: - if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + submodels = self._get_submodels_python(request, url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] return response_t(list(references), stripped=is_stripped_request(request)) @@ -738,31 +727,33 @@ def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Respon self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() - def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def _get_submodel_python(self, url_args: Dict) -> model.Submodel: # TODO: support content, extent parameters - response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() + return submodel + + def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters + response_t = get_response_type(request) + submodel = self._get_submodel_python(url_args) return response_t(submodel, stripped=is_stripped_request(request)) def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) return response_t(submodel, stripped=True) def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) reference = model.ModelReference.from_referable(submodel) return response_t(reference, stripped=is_stripped_request(request)) def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request))) submodel.commit() return response_t() @@ -771,61 +762,57 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kw # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) return response_t(list(submodel.submodel_element), stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in submodel.submodel_element] return response_t(list(references), stripped=is_stripped_request(request)) + def _get_submodel_submodel_elements_id_short_path_python(self, url_args: Dict) \ + -> model.SubmodelElement: + # TODO: support content, extent parameters + submodel = self._get_submodel_python(url_args) + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + return submodel_element + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) return response_t(submodel_element, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) return response_t(submodel_element, stripped=True) def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) reference = model.ModelReference.from_referable(submodel_element) return response_t(reference, stripped=is_stripped_request(request)) def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_short_path = url_args.get("id_shorts", []) parent = self._get_submodel_or_nested_submodel_element(submodel, id_short_path) if not isinstance(parent, model.UniqueIdShortNamespace): @@ -848,10 +835,7 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameter response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) # 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, # type: ignore @@ -863,8 +847,7 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_short_path: List[str] = url_args["id_shorts"] parent: model.UniqueIdShortNamespace = self._expect_namespace( self._get_submodel_or_nested_submodel_element(submodel, id_short_path[:-1]), @@ -876,8 +859,7 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: @@ -891,8 +873,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) @@ -911,8 +892,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) @@ -942,9 +922,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier_type = url_args["qualifier_type"] From 8866d8ef766a8ac47a151c43c2028569e804b2ef Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Mon, 18 Mar 2024 12:51:51 +0100 Subject: [PATCH 225/474] adapter.http: fixing post submodelelement route --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 47432cc..f4a27d9 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -823,7 +823,7 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar is_stripped_request(request)) try: parent.add_referable(new_submodel_element) - except KeyError: + except model.AASConstraintViolation: raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!") created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { From 0d41fa48018f1cbf24261d9d29166750d5b5543e Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Mon, 18 Mar 2024 22:02:17 +0100 Subject: [PATCH 226/474] adapter.http: implement the recommended changes --- basyx/aas/adapter/http.py | 174 ++++++++++++++++++-------------------- 1 file changed, 80 insertions(+), 94 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index f4a27d9..9a1beb7 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -143,7 +143,7 @@ def serialize(self, obj: ResponseData, stripped: bool) -> str: response_elem = object_to_xml_element(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) - return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") + return etree.tostring(response_elem, xml_declaration=True, encoding=ENCODING) class XmlResponseAlt(XmlResponse): @@ -218,6 +218,22 @@ def is_stripped_request(request: Request) -> bool: T = TypeVar("T") +ENCODING = "utf-8" + + +def base64url_decode(data: str) -> str: + try: + # If the requester omits the base64 padding, an exception will be raised. + # However, Python doesn't complain about too much padding, + # thus we simply always append two padding characters (==). + # See also: https://stackoverflow.com/a/49459036/4780052 + decoded = base64.urlsafe_b64decode(data + "==").decode(ENCODING) + except binascii.Error: + raise BadRequest(f"Encoded data {str(data)} is invalid base64url!") + except UnicodeDecodeError: + raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + return decoded + class HTTPApiDecoder: # these are the types we can construct (well, only the ones we need) @@ -232,8 +248,6 @@ class HTTPApiDecoder: model.Reference: XMLConstructables.REFERENCE } - encoding = "utf-8" - @classmethod def check_type_supportance(cls, type_: type): if type_ not in cls.type_constructables_map: @@ -245,18 +259,6 @@ def assert_type(cls, obj: object, type_: Type[T]) -> T: raise UnprocessableEntity(f"Object {obj!r} is not of type {type_.__name__}!") return obj - @classmethod - def base64_decode(cls, data: Union[str, bytes]) -> str: - try: - # If the requester omits the base64 padding, an exception will be raised. - # However, Python doesn't complain about too much padding, - # thus we simply always append two padding characters (==). - # See also: https://stackoverflow.com/a/49459036/4780052 - decoded = base64.urlsafe_b64decode(data + "==").decode(cls.encoding) - except binascii.Error: - raise BadRequest(f"Encoded data {str(data)} is invalid base64url!") - return decoded - @classmethod def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: cls.check_type_supportance(expect_type) @@ -284,7 +286,7 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool elif expect_type is model.SpecificAssetId: constructor = decoder._construct_specific_asset_id # type: ignore[assignment] elif expect_type is model.Reference: - constructor = decoder._construct_model_reference # type: ignore[assignment] + constructor = decoder._construct_reference # type: ignore[assignment] args.append(model.Submodel) if constructor is not None: @@ -297,9 +299,9 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool return [cls.assert_type(obj, expect_type) for obj in parsed] @classmethod - def base64json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool)\ + def base64urljson_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool)\ -> List[T]: - data = cls.base64_decode(data) + data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, expect_single) @classmethod @@ -307,8 +309,8 @@ def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> return cls.json_list(data, expect_type, stripped, True)[0] @classmethod - def base64json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: - data = cls.base64_decode(data) + def base64urljson(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: + data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, True)[0] @classmethod @@ -351,7 +353,6 @@ def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> class Base64UrlJsonConverter(werkzeug.routing.UnicodeConverter): - encoding = "utf-8" def __init__(self, url_map, t: str): super().__init__(url_map) @@ -362,17 +363,11 @@ def __init__(self, url_map, t: str): raise ValueError(f"invalid value t={t}") def to_url(self, value: object) -> str: - return super().to_url(base64.urlsafe_b64encode(json.dumps(value, cls=AASToJsonEncoder).encode(self.encoding))) + return super().to_url(base64.urlsafe_b64encode(json.dumps(value, cls=AASToJsonEncoder).encode(ENCODING))) def to_python(self, value: str) -> object: value = super().to_python(value) - try: - decoded = base64.urlsafe_b64decode(super().to_python(value)).decode(self.encoding) - except binascii.Error: - raise BadRequest(f"Encoded json object {value} is invalid base64url!") - except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") - + decoded = base64url_decode(super().to_python(value)) try: return HTTPApiDecoder.json(decoded, self.type, False) except json.JSONDecodeError: @@ -380,23 +375,13 @@ def to_python(self, value: str) -> object: class IdentifierConverter(werkzeug.routing.UnicodeConverter): - encoding = "utf-8" def to_url(self, value: model.Identifier) -> str: - return super().to_url(base64.urlsafe_b64encode(value.encode(self.encoding))) + return super().to_url(base64.urlsafe_b64encode(value.encode(ENCODING))) def to_python(self, value: str) -> model.Identifier: value = super().to_python(value) - try: - # If the requester omits the base64 padding, an exception will be raised. - # However, Python doesn't complain about too much padding, - # thus we simply always append two padding characters (==). - # See also: https://stackoverflow.com/a/49459036/4780052 - decoded = base64.urlsafe_b64decode(super().to_python(value) + "==").decode(self.encoding) - except binascii.Error: - raise BadRequest(f"Encoded identifier {value} is invalid base64url!") - except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid {self.encoding} string!") + decoded = base64url_decode(super().to_python(value)) return decoded @@ -441,8 +426,8 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_all), Rule("/", methods=["POST"], endpoint=self.post_submodel), - Rule("/$metadata", methods=["GET"], endpoint=self.get_allsubmodels_metadata), - Rule("/$reference", methods=["GET"], endpoint=self.get_allsubmodels_reference), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_all_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_all_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), @@ -565,6 +550,30 @@ def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, except KeyError as e: raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e + def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + id_short = request.args.get("idShort") + if id_short is not None: + submodels = filter(lambda sm: sm.id_short == id_short, submodels) + semantic_id = request.args.get("semanticId") + if semantic_id is not None: + spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + return submodels + + def _get_submodel(self, url_args: Dict) -> model.Submodel: + # TODO: support content, extent parameters + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return submodel + + def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ + -> model.SubmodelElement: + # TODO: support content, extent parameters + submodel = self._get_submodel(url_args) + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + return submodel_element + def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: @@ -594,7 +603,7 @@ def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: aas = filter(lambda shell: shell.id_short == id_short, aas) asset_ids = request.args.get("assetIds") if asset_ids is not None: - spec_asset_ids = HTTPApiDecoder.base64json_list(asset_ids, model.SpecificAssetId, False, False) + spec_asset_ids = HTTPApiDecoder.base64urljson_list(asset_ids, model.SpecificAssetId, False, False) # TODO: it's currently unclear how to filter with these SpecificAssetIds return response_t(list(aas)) @@ -678,21 +687,9 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** raise NotFound(f"The AAS {aas!r} doesn't have the reference {url_args['submodel_ref']!r}!") # ------ SUBMODEL REPO ROUTES ------- - def _get_submodels_python(self, request: Request, url_args: Dict) -> Iterator[model.Submodel]: - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) - id_short = request.args.get("idShort") - if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) - semantic_id = request.args.get("semanticId") - if semantic_id is not None: - if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) - return submodels - def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels_python(request, url_args) + submodels = self._get_submodels(request) return response_t(list(submodels), stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: @@ -708,17 +705,17 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte }, force_external=True) return response_t(submodel, status=201, headers={"Location": created_resource_url}) - def get_allsubmodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_submodel_all_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels_python(request, url_args) + submodels = self._get_submodels(request) return response_t(list(submodels), stripped=True) - def get_allsubmodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels_python(request, url_args) + submodels = self._get_submodels(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] - return response_t(list(references), stripped=is_stripped_request(request)) + return response_t(references, stripped=is_stripped_request(request)) # --------- SUBMODEL ROUTES --------- @@ -727,33 +724,27 @@ def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Respon self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() - def _get_submodel_python(self, url_args: Dict) -> model.Submodel: - # TODO: support content, extent parameters - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - return submodel - def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) return response_t(submodel, stripped=is_stripped_request(request)) def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) return response_t(submodel, stripped=True) def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) reference = model.ModelReference.from_referable(submodel) return response_t(reference, stripped=is_stripped_request(request)) def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request))) submodel.commit() return response_t() @@ -762,57 +753,50 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kw # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) return response_t(list(submodel.submodel_element), stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in submodel.submodel_element] - return response_t(list(references), stripped=is_stripped_request(request)) - - def _get_submodel_submodel_elements_id_short_path_python(self, url_args: Dict) \ - -> model.SubmodelElement: - # TODO: support content, extent parameters - submodel = self._get_submodel_python(url_args) - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) - return submodel_element + return response_t(references, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=True) def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) reference = model.ModelReference.from_referable(submodel_element) return response_t(reference, stripped=is_stripped_request(request)) def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_short_path = url_args.get("id_shorts", []) parent = self._get_submodel_or_nested_submodel_element(submodel, id_short_path) if not isinstance(parent, model.UniqueIdShortNamespace): @@ -823,7 +807,9 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar is_stripped_request(request)) try: parent.add_referable(new_submodel_element) - except model.AASConstraintViolation: + except model.AASConstraintViolation as e: + if e.constraint_id != 22: + raise raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!") created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { @@ -835,7 +821,7 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameter response_t = get_response_type(request) - submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) # 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, # type: ignore @@ -847,7 +833,7 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_short_path: List[str] = url_args["id_shorts"] parent: model.UniqueIdShortNamespace = self._expect_namespace( self._get_submodel_or_nested_submodel_element(submodel, id_short_path[:-1]), @@ -859,7 +845,7 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: @@ -873,7 +859,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) @@ -892,7 +878,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) @@ -922,7 +908,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier_type = url_args["qualifier_type"] From 33725b0466115c3535a32d2acf6236c5dc1ddd5c Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Wed, 20 Mar 2024 11:20:44 +0100 Subject: [PATCH 227/474] adapter.http: implement the new recommended changes --- basyx/aas/adapter/http.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 9a1beb7..7a960c8 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -143,7 +143,7 @@ def serialize(self, obj: ResponseData, stripped: bool) -> str: response_elem = object_to_xml_element(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) - return etree.tostring(response_elem, xml_declaration=True, encoding=ENCODING) + return etree.tostring(response_elem, xml_declaration=True, encoding=BASE64URL_ENCODING) class XmlResponseAlt(XmlResponse): @@ -218,7 +218,7 @@ def is_stripped_request(request: Request) -> bool: T = TypeVar("T") -ENCODING = "utf-8" +BASE64URL_ENCODING = "utf-8" def base64url_decode(data: str) -> str: @@ -227,14 +227,19 @@ def base64url_decode(data: str) -> str: # However, Python doesn't complain about too much padding, # thus we simply always append two padding characters (==). # See also: https://stackoverflow.com/a/49459036/4780052 - decoded = base64.urlsafe_b64decode(data + "==").decode(ENCODING) + decoded = base64.urlsafe_b64decode(data + "==").decode(BASE64URL_ENCODING) except binascii.Error: - raise BadRequest(f"Encoded data {str(data)} is invalid base64url!") + raise BadRequest(f"Encoded data {data} is invalid base64url!") except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + raise BadRequest(f"Encoded base64url value is not a valid {BASE64URL_ENCODING} string!") return decoded +def base64url_encode(data: str) -> bytes: + encoded = base64.urlsafe_b64encode(data.encode(BASE64URL_ENCODING)) + return encoded + + class HTTPApiDecoder: # these are the types we can construct (well, only the ones we need) type_constructables_map = { @@ -287,7 +292,6 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool constructor = decoder._construct_specific_asset_id # type: ignore[assignment] elif expect_type is model.Reference: constructor = decoder._construct_reference # type: ignore[assignment] - args.append(model.Submodel) if constructor is not None: # construct elements that aren't self-identified @@ -299,7 +303,7 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool return [cls.assert_type(obj, expect_type) for obj in parsed] @classmethod - def base64urljson_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool)\ + def base64urljson_list(cls, data: str, expect_type: Type[T], stripped: bool, expect_single: bool)\ -> List[T]: data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, expect_single) @@ -309,7 +313,7 @@ def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> return cls.json_list(data, expect_type, stripped, True)[0] @classmethod - def base64urljson(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: + def base64urljson(cls, data: str, expect_type: Type[T], stripped: bool) -> T: data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, True)[0] @@ -363,7 +367,7 @@ def __init__(self, url_map, t: str): raise ValueError(f"invalid value t={t}") def to_url(self, value: object) -> str: - return super().to_url(base64.urlsafe_b64encode(json.dumps(value, cls=AASToJsonEncoder).encode(ENCODING))) + return super().to_url(base64url_encode(json.dumps(value, cls=AASToJsonEncoder))) def to_python(self, value: str) -> object: value = super().to_python(value) @@ -377,7 +381,7 @@ def to_python(self, value: str) -> object: class IdentifierConverter(werkzeug.routing.UnicodeConverter): def to_url(self, value: model.Identifier) -> str: - return super().to_url(base64.urlsafe_b64encode(value.encode(ENCODING))) + return super().to_url(base64url_encode(value)) def to_python(self, value: str) -> model.Identifier: value = super().to_python(value) @@ -557,19 +561,18 @@ def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: submodels = filter(lambda sm: sm.id_short == id_short, submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) + spec_semantic_id = (HTTPApiDecoder.base64urljson + (semantic_id, model.Reference, False)) # type: ignore[type-abstract] submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) return submodels def _get_submodel(self, url_args: Dict) -> model.Submodel: - # TODO: support content, extent parameters submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() return submodel def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: - # TODO: support content, extent parameters submodel = self._get_submodel(url_args) submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return submodel_element @@ -725,7 +728,6 @@ def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Respon return response_t() def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) return response_t(submodel, stripped=is_stripped_request(request)) @@ -736,7 +738,6 @@ def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> return response_t(submodel, stripped=True) def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) reference = model.ModelReference.from_referable(submodel) @@ -773,28 +774,24 @@ def get_submodel_submodel_elements_reference(self, request: Request, url_args: D return response_t(references, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=True) def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) reference = model.ModelReference.from_referable(submodel_element) return response_t(reference, stripped=is_stripped_request(request)) def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): - # TODO: support content, extent parameter response_t = get_response_type(request) submodel = self._get_submodel(url_args) id_short_path = url_args.get("id_shorts", []) @@ -819,7 +816,6 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar return response_t(new_submodel_element, status=201, headers={"Location": created_resource_url}) def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent parameter response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] From 864c7ca74df18feb80bda7fa1542c7a21e05c17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 16:01:18 +0100 Subject: [PATCH 228/474] adapter.http: hardcode `utf-8` for XML serialization --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7a960c8..7095d13 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -143,7 +143,7 @@ def serialize(self, obj: ResponseData, stripped: bool) -> str: response_elem = object_to_xml_element(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) - return etree.tostring(response_elem, xml_declaration=True, encoding=BASE64URL_ENCODING) + return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") class XmlResponseAlt(XmlResponse): From cfa775f62e6b0f71094ae0f9259025ba3624bc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 22:29:22 +0100 Subject: [PATCH 229/474] adapter.http: change `base64url_encode()` function to return `str` --- basyx/aas/adapter/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7095d13..5ca7497 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -235,8 +235,8 @@ def base64url_decode(data: str) -> str: return decoded -def base64url_encode(data: str) -> bytes: - encoded = base64.urlsafe_b64encode(data.encode(BASE64URL_ENCODING)) +def base64url_encode(data: str) -> str: + encoded = base64.urlsafe_b64encode(data.encode(BASE64URL_ENCODING)).decode("ascii") return encoded From 7e618951d75ebde8873975b3c06066953003317d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 17:31:55 +0100 Subject: [PATCH 230/474] adapter.http: update AAS submodel refs path --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 5ca7497..083a9f8 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -419,7 +419,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["DELETE"], endpoint=self.delete_aas), Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/submodels", [ + Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), Rule("//", methods=["DELETE"], From 5e461c7d2fc7ea899224b0feedb91c1e66664336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 17:33:39 +0100 Subject: [PATCH 231/474] adapter.http: update AAS submodel refs `DELETE` route The route now uses the submodel identifier instead of the submodel reference. --- basyx/aas/adapter/http.py | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 083a9f8..9a9b9b0 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -356,28 +356,6 @@ def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> return cls.xml(request.get_data(), expect_type, stripped) -class Base64UrlJsonConverter(werkzeug.routing.UnicodeConverter): - - def __init__(self, url_map, t: str): - super().__init__(url_map) - self.type: type - if t == "ModelReference": - self.type = model.ModelReference - else: - raise ValueError(f"invalid value t={t}") - - def to_url(self, value: object) -> str: - return super().to_url(base64url_encode(json.dumps(value, cls=AASToJsonEncoder))) - - def to_python(self, value: str) -> object: - value = super().to_python(value) - decoded = base64url_decode(super().to_python(value)) - try: - return HTTPApiDecoder.json(decoded, self.type, False) - except json.JSONDecodeError: - raise BadRequest(f"{decoded} is not a valid json string!") - - class IdentifierConverter(werkzeug.routing.UnicodeConverter): def to_url(self, value: model.Identifier) -> str: @@ -422,7 +400,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]) ]) @@ -489,8 +467,7 @@ def __init__(self, object_store: model.AbstractObjectStore): ]) ], converters={ "identifier": IdentifierConverter, - "id_short_path": IdShortPathConverter, - "base64url_json": Base64UrlJsonConverter + "id_short_path": IdShortPathConverter }) # TODO: the parameters can be typed via builtin wsgiref with Python 3.11+ @@ -683,11 +660,11 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() for sm_ref in aas.submodel: - if sm_ref == url_args["submodel_ref"]: + if sm_ref.get_identifier() == url_args["submodel_id"]: aas.submodel.remove(sm_ref) aas.commit() return response_t() - raise NotFound(f"The AAS {aas!r} doesn't have the reference {url_args['submodel_ref']!r}!") + raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {url_args['submodel_id']!r}!") # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: From a6e9a53a1e21ddd991e3bbe735d941d98ded28c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 22:00:55 +0100 Subject: [PATCH 232/474] adapter.http: suffix submodel refs deletion route with a slash --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 9a9b9b0..6ef01ad 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -400,7 +400,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("/", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]) ]) From db85bc46982aad1ba13cd0ee88126e6b03964eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 22:02:32 +0100 Subject: [PATCH 233/474] adapter.http: refactor submodel ref access as separate function --- basyx/aas/adapter/http.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 6ef01ad..25fefe4 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -531,6 +531,16 @@ def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, except KeyError as e: raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e + @classmethod + def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_id: model.NameType) \ + -> model.ModelReference[model.Submodel]: + # TODO: this is currently O(n), could be O(1) as aas.submodel, but keys would have to precisely match, as they + # are hashed including their KeyType + for ref in aas.submodel: + if ref.get_identifier() == submodel_id: + return ref + raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") + def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) id_short = request.args.get("idShort") @@ -659,12 +669,9 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - for sm_ref in aas.submodel: - if sm_ref.get_identifier() == url_args["submodel_id"]: - aas.submodel.remove(sm_ref) - aas.commit() - return response_t() - raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {url_args['submodel_id']!r}!") + aas.submodel.remove(self._get_submodel_reference(aas, url_args["submodel_id"])) + aas.commit() + return response_t() # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: From a13fea5f6bf13b353a9f8813373da9c13e2695c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 22:23:40 +0100 Subject: [PATCH 234/474] adapter.http: suffix slashes to all routes --- basyx/aas/adapter/http.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 25fefe4..556aaca 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -408,21 +408,21 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_all), Rule("/", methods=["POST"], endpoint=self.post_submodel), - Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_all_metadata), - Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_all_reference), + Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), + Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), - Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), + Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$reference/", methods=["GET"], endpoint=self.get_submodels_reference), Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/$metadata", methods=["GET"], + Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_metadata), - Rule("/$reference", methods=["GET"], + Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_reference), Submount("/", [ Rule("/", methods=["GET"], @@ -433,9 +433,9 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.put_submodel_submodel_elements_id_short_path), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_elements_id_short_path), - Rule("/$metadata", methods=["GET"], + Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), - Rule("/$reference", methods=["GET"], + Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), Submount("/constraints", [ Rule("/", methods=["GET"], From 13e4d500f3ff40e53c02ccdd17848f26adf4c06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 28 Mar 2024 17:12:36 +0100 Subject: [PATCH 235/474] adapter.http: implement AAS API submodel routes via redirects The submodel routes of the AAS API `/shells//submodels` are the same already implemented as the submodel API, so we return a redirect here, after checking that the AAS indeed has a reference to the requested submodel. `PUT` and `DELETE` are different, as we also have to update the AAS in this case, either by adding a new updated reference in case of a `PUT` request changes the submodel id, or by removing the submodel reference for `DELETE` requests. --- basyx/aas/adapter/http.py | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 556aaca..ed61721 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -21,6 +21,7 @@ import werkzeug.exceptions import werkzeug.routing import werkzeug.urls +import werkzeug.utils from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response @@ -402,6 +403,12 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) + ]), + Submount("/submodels/", [ + Rule("/", methods=["PUT"], endpoint=self.put_aas_submodel_refs_submodel), + Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_submodel), + Rule("/", endpoint=self.aas_submodel_refs_redirect), + Rule("//", endpoint=self.aas_submodel_refs_redirect) ]) ]) ]), @@ -673,6 +680,49 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** aas.commit() return response_t() + def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) + submodel = self._resolve_reference(sm_ref) + new_submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) + # determine whether the id changed in advance, in case something goes wrong while updating the submodel + id_changed: bool = submodel.id != new_submodel.id + # TODO: https://github.com/eclipse-basyx/basyx-python-sdk/issues/216 + submodel.update_from(new_submodel) + submodel.commit() + if id_changed: + aas.submodel.remove(sm_ref) + aas.submodel.add(model.ModelReference.from_referable(submodel)) + aas.commit() + return response_t() + + def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) + submodel = self._resolve_reference(sm_ref) + self.object_store.remove(submodel) + aas.submodel.remove(sm_ref) + aas.commit() + return response_t() + + def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + # the following makes sure the reference exists + self._get_submodel_reference(aas, url_args["submodel_id"]) + redirect_url = map_adapter.build(self.get_submodel, { + "submodel_id": url_args["submodel_id"] + }, force_external=True) + if "path" in url_args: + redirect_url += url_args["path"] + "/" + if request.query_string: + redirect_url += "?" + request.query_string.decode("ascii") + return werkzeug.utils.redirect(redirect_url, 307) + # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) From cebe4bbc3b2a8c227ad0b9413f3a1d8ac686e641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 28 Mar 2024 17:33:10 +0100 Subject: [PATCH 236/474] adapter.http: move `asset-information` routes to a submount --- basyx/aas/adapter/http.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index ed61721..d79e0db 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -396,8 +396,10 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/", methods=["DELETE"], endpoint=self.delete_aas), - Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), + Submount("/asset-information", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), + ]), Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), From 4dd4055e4d2769b64a0df008b9d49275d63270ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Mar 2024 17:16:40 +0100 Subject: [PATCH 237/474] adapter.http: rename `/constraints` routes to `/qualifiers` --- basyx/aas/adapter/http.py | 48 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d79e0db..f0c0859 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -446,30 +446,30 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), - Submount("/constraints", [ + Submount("/qualifiers", [ Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), + endpoint=self.get_submodel_submodel_element_qualifiers), Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), + endpoint=self.post_submodel_submodel_element_qualifiers), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_qualifiers), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_qualifiers), ]) ]), ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Submount("/qualifiers", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), + endpoint=self.post_submodel_submodel_element_qualifiers), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_qualifiers), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_qualifiers), ]) ]) ]) @@ -874,7 +874,7 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) return response_t() - def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ + def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) submodel = self._get_submodel(url_args) @@ -887,7 +887,7 @@ def get_submodel_submodel_element_constraints(self, request: Request, url_args: except KeyError: raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") - def post_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ + def post_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] @@ -899,14 +899,14 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: raise Conflict(f"Qualifier with type {qualifier.type} already exists!") sm_or_se.qualifier.add(qualifier) sm_or_se.commit() - created_resource_url = map_adapter.build(self.get_submodel_submodel_element_constraints, { + created_resource_url = map_adapter.build(self.get_submodel_submodel_element_qualifiers, { "submodel_id": submodel_identifier, "id_shorts": id_shorts if len(id_shorts) != 0 else None, "qualifier_type": qualifier.type }, force_external=True) return response_t(qualifier, status=201, headers={"Location": created_resource_url}) - def put_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ + def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] @@ -929,7 +929,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: sm_or_se.qualifier.add(new_qualifier) sm_or_se.commit() if qualifier_type_changed: - created_resource_url = map_adapter.build(self.get_submodel_submodel_element_constraints, { + created_resource_url = map_adapter.build(self.get_submodel_submodel_element_qualifiers, { "submodel_id": submodel_identifier, "id_shorts": id_shorts if len(id_shorts) != 0 else None, "qualifier_type": new_qualifier.type @@ -937,7 +937,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: return response_t(new_qualifier, status=201, headers={"Location": created_resource_url}) return response_t(new_qualifier) - def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ + def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) submodel = self._get_submodel(url_args) From c3f711f76513411a260fffd6d978c47b10ab38df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Mar 2024 17:21:03 +0100 Subject: [PATCH 238/474] adapter.http: fix `Qualifier` JSON deserialization --- basyx/aas/adapter/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index f0c0859..74168d1 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -293,6 +293,8 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool constructor = decoder._construct_specific_asset_id # type: ignore[assignment] elif expect_type is model.Reference: constructor = decoder._construct_reference # type: ignore[assignment] + elif expect_type is model.Qualifier: + constructor = decoder._construct_qualifier # type: ignore[assignment] if constructor is not None: # construct elements that aren't self-identified From 4e8b3f5a4397fbe3b82728b8e2a9652681fbf96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Mar 2024 17:21:58 +0100 Subject: [PATCH 239/474] adapter.http: refactor qualifier retrieval/removal --- basyx/aas/adapter/http.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 74168d1..7fb79b9 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -542,6 +542,13 @@ def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, except KeyError as e: raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e + @classmethod + def _qualifiable_qualifier_op(cls, qualifiable: model.Qualifiable, op: Callable[[str], T], arg: str) -> T: + try: + return op(arg) + except KeyError as e: + raise NotFound(f"Qualifier with type {arg!r} not found in {qualifiable!r}") from e + @classmethod def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_id: model.NameType) \ -> model.ModelReference[model.Submodel]: @@ -884,10 +891,7 @@ def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: D qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: return response_t(list(sm_or_se.qualifier)) - try: - return response_t(sm_or_se.get_qualifier_by_type(qualifier_type)) - except KeyError: - raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + return response_t(self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type)) def post_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ -> Response: @@ -917,13 +921,7 @@ def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: D sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) qualifier_type = url_args["qualifier_type"] - try: - qualifier = sm_or_se.get_qualifier_by_type(qualifier_type) - except KeyError: - raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") - if type(qualifier) is not type(new_qualifier): - raise UnprocessableEntity(f"Type of new qualifier {new_qualifier} doesn't not match " - f"the current submodel element {qualifier}") + qualifier = self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type) qualifier_type_changed = qualifier_type != new_qualifier.type if qualifier_type_changed and sm_or_se.qualifier.contains_id("type", new_qualifier.type): raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") @@ -946,10 +944,7 @@ def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier_type = url_args["qualifier_type"] - try: - sm_or_se.remove_qualifier_by_type(qualifier_type) - except KeyError: - raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + self._qualifiable_qualifier_op(sm_or_se, sm_or_se.remove_qualifier_by_type, qualifier_type) sm_or_se.commit() return response_t() From 7d78a512d401d740cc21cfdc3ed8bbebb51e6434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Mar 2024 17:23:44 +0100 Subject: [PATCH 240/474] adapter.http: improve an error mesage --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7fb79b9..2289214 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -924,7 +924,7 @@ def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: D qualifier = self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type) qualifier_type_changed = qualifier_type != new_qualifier.type if qualifier_type_changed and sm_or_se.qualifier.contains_id("type", new_qualifier.type): - raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") + raise Conflict(f"A qualifier of type {new_qualifier.type!r} already exists for {sm_or_se!r}") sm_or_se.remove_qualifier_by_type(qualifier.type) sm_or_se.qualifier.add(new_qualifier) sm_or_se.commit() From dbaa7527df0065a89fe4bd92a87ae7bbc9e2a7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 31 Mar 2024 13:53:24 +0200 Subject: [PATCH 241/474] adapter.http: rename `IdentifierConverter` to `Base64URLConverter` The `IdentifierConverter` is also used to decode values from URLs, that aren't necessarily Identifiers, e.g. Qualifier types. Thus, a name like `Base64URLConverter` suits its use better and is also more expressive. For the same reasons, the key `identifier`, which was used for the `IdentifierConverter`, is renamed to `base64url`. --- basyx/aas/adapter/http.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 2289214..ea36b06 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -359,7 +359,7 @@ def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> return cls.xml(request.get_data(), expect_type, stripped) -class IdentifierConverter(werkzeug.routing.UnicodeConverter): +class Base64URLConverter(werkzeug.routing.UnicodeConverter): def to_url(self, value: model.Identifier) -> str: return super().to_url(base64url_encode(value)) @@ -394,7 +394,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), Rule("/", methods=["POST"], endpoint=self.post_aas), - Submount("/", [ + Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/", methods=["DELETE"], endpoint=self.delete_aas), @@ -405,10 +405,10 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]), - Submount("/submodels/", [ + Submount("/submodels/", [ Rule("/", methods=["PUT"], endpoint=self.put_aas_submodel_refs_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_submodel), Rule("/", endpoint=self.aas_submodel_refs_redirect), @@ -421,7 +421,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["POST"], endpoint=self.post_submodel), Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), - Submount("/", [ + Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), @@ -453,11 +453,11 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_submodel_submodel_element_qualifiers), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("//", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers), ]) ]), @@ -466,18 +466,18 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("//", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers), ]) ]) ]) ]) ], converters={ - "identifier": IdentifierConverter, + "base64url": Base64URLConverter, "id_short_path": IdShortPathConverter }) From ea77752191a504fb15d028a26d8ed0b9d6f710e8 Mon Sep 17 00:00:00 2001 From: Hadi Jannat Date: Wed, 20 Mar 2024 10:40:35 +0100 Subject: [PATCH 242/474] adapter.http: add `SpecificAssetId` filtering to `get_aas_all()` --- basyx/aas/adapter/http.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index ea36b06..ee4dd4e 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -604,16 +604,34 @@ def handle_request(self, request: Request): # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def asset_id_matches(spec_asset_id, specific_asset_ids): + """Checks if a specific asset ID matches any within a list.""" + return any( + spec_asset_id == asset_id + for asset_id in specific_asset_ids + ) + response_t = get_response_type(request) - aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) + aas_iterable: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type( + model.AssetAdministrationShell) + + # Filter by 'idShort' if provided in the request id_short = request.args.get("idShort") if id_short is not None: - aas = filter(lambda shell: shell.id_short == id_short, aas) - asset_ids = request.args.get("assetIds") + aas_iterable = filter(lambda shell: shell.id_short == id_short, aas_iterable) + + # Filtering by base64url encoded SpecificAssetIds if provided + asset_ids = request.args.getlist("assetIds") if asset_ids is not None: - spec_asset_ids = HTTPApiDecoder.base64urljson_list(asset_ids, model.SpecificAssetId, False, False) - # TODO: it's currently unclear how to filter with these SpecificAssetIds - return response_t(list(aas)) + # Decode and instantiate SpecificAssetIds + spec_asset_ids = map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, + False), asset_ids) + # Filter AAS based on these SpecificAssetIds + aas_iterable = filter(lambda shell: all( + asset_id_matches(spec_asset_id, shell.asset_information.specific_asset_id) + for spec_asset_id in spec_asset_ids), aas_iterable) + + return response_t(list(aas_iterable)) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) From 47e67ed1677a5a275deb51b0e711ed3521aac5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 2 Apr 2024 14:20:20 +0200 Subject: [PATCH 243/474] adapter.http: improve `SpecificAssetId` filtering --- basyx/aas/adapter/http.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index ee4dd4e..8790bd6 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -604,34 +604,24 @@ def handle_request(self, request: Request): # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: - def asset_id_matches(spec_asset_id, specific_asset_ids): - """Checks if a specific asset ID matches any within a list.""" - return any( - spec_asset_id == asset_id - for asset_id in specific_asset_ids - ) - response_t = get_response_type(request) - aas_iterable: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type( - model.AssetAdministrationShell) + aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) - # Filter by 'idShort' if provided in the request id_short = request.args.get("idShort") if id_short is not None: - aas_iterable = filter(lambda shell: shell.id_short == id_short, aas_iterable) + aas = filter(lambda shell: shell.id_short == id_short, aas) - # Filtering by base64url encoded SpecificAssetIds if provided asset_ids = request.args.getlist("assetIds") if asset_ids is not None: # Decode and instantiate SpecificAssetIds - spec_asset_ids = map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, - False), asset_ids) + # This needs to be a list, otherwise we can only iterate it once. + specific_asset_ids: List[model.SpecificAssetId] = list( + map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, False), asset_ids)) # Filter AAS based on these SpecificAssetIds - aas_iterable = filter(lambda shell: all( - asset_id_matches(spec_asset_id, shell.asset_information.specific_asset_id) - for spec_asset_id in spec_asset_ids), aas_iterable) + aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id + for specific_asset_id in specific_asset_ids), aas) - return response_t(list(aas_iterable)) + return response_t(list(aas)) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) From 9b3e8360aed2e59dd26ab89b4c575c4e737800d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 3 Apr 2024 15:20:53 +0200 Subject: [PATCH 244/474] adapter.http: refactor AAS retrieval Methods `_get_shell()` and `_get_shells()` are added similarly to `_get_submodel()` and `_get_submodels()`, which were previously added in a5c7d69d28a73286deb2284493ff95740b6c301f. Furthermore, when requesting multiple AAS/Submodels, we're now also updating these in `_get_all_obj_of_type()` before returning them. Finally, updating AAS/Submodel objects is also moved to `_get_obj_ts()`. --- basyx/aas/adapter/http.py | 79 ++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8790bd6..64fb1e1 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -490,11 +490,13 @@ def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._ identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): raise NotFound(f"No {type_.__name__} with {identifier} found!") + identifiable.update() 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_): + obj.update() yield obj def _resolve_reference(self, reference: model.ModelReference[model.base._RT]) -> model.base._RT: @@ -559,6 +561,28 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i return ref raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") + def _get_shells(self, request: Request) -> Iterator[model.AssetAdministrationShell]: + aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) + + id_short = request.args.get("idShort") + if id_short is not None: + aas = filter(lambda shell: shell.id_short == id_short, aas) + + asset_ids = request.args.getlist("assetIds") + if asset_ids is not None: + # Decode and instantiate SpecificAssetIds + # This needs to be a list, otherwise we can only iterate it once. + specific_asset_ids: List[model.SpecificAssetId] = list( + map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, False), asset_ids)) + # Filter AAS based on these SpecificAssetIds + aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id + for specific_asset_id in specific_asset_ids), aas) + + return aas + + def _get_shell(self, url_args: Dict) -> model.AssetAdministrationShell: + return self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) id_short = request.args.get("idShort") @@ -572,9 +596,7 @@ def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: return submodels def _get_submodel(self, url_args: Dict) -> model.Submodel: - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - return submodel + return self._get_obj_ts(url_args["submodel_id"], model.Submodel) def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: @@ -605,23 +627,7 @@ def handle_request(self, request: Request): # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) - - id_short = request.args.get("idShort") - if id_short is not None: - aas = filter(lambda shell: shell.id_short == id_short, aas) - - asset_ids = request.args.getlist("assetIds") - if asset_ids is not None: - # Decode and instantiate SpecificAssetIds - # This needs to be a list, otherwise we can only iterate it once. - specific_asset_ids: List[model.SpecificAssetId] = list( - map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, False), asset_ids)) - # Filter AAS based on these SpecificAssetIds - aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id - for specific_asset_id in specific_asset_ids), aas) - - return response_t(list(aas)) + return response_t(list(self._get_shells(request))) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -638,22 +644,20 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + self.object_store.remove(self._get_shell(url_args)) return response_t() # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content parameter response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) return response_t(aas, stripped=is_stripped_request(request)) def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content parameter response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) aas.update_from(HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, is_stripped_request(request))) aas.commit() @@ -661,29 +665,24 @@ def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: def get_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) return response_t(aas.asset_information) def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) aas.asset_information = HTTPApiDecoder.request_body(request, model.AssetInformation, False) aas.commit() return response_t() def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) return response_t(list(aas.submodel)) def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas_identifier = url_args["aas_id"] - aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) - aas.update() + 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!") @@ -693,16 +692,14 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) aas.submodel.remove(self._get_submodel_reference(aas, url_args["submodel_id"])) aas.commit() return response_t() def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) submodel = self._resolve_reference(sm_ref) new_submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) @@ -719,8 +716,7 @@ def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kw def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) submodel = self._resolve_reference(sm_ref) self.object_store.remove(submodel) @@ -729,8 +725,7 @@ def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, ** return response_t() def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) # the following makes sure the reference exists self._get_submodel_reference(aas, url_args["submodel_id"]) redirect_url = map_adapter.build(self.get_submodel, { From 9b7133db12c560a39f023016544df9b2bfbde503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 3 Apr 2024 15:41:23 +0200 Subject: [PATCH 245/474] adapter.http: remove outdated TODOs --- basyx/aas/adapter/http.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 64fb1e1..dfd5546 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -34,9 +34,6 @@ from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union -# TODO: support the path/reference/etc. parameter - - @enum.unique class MessageType(enum.Enum): UNDEFINED = enum.auto() @@ -649,13 +646,11 @@ def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content parameter response_t = get_response_type(request) aas = self._get_shell(url_args) return response_t(aas, stripped=is_stripped_request(request)) def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content parameter response_t = get_response_type(request) aas = self._get_shell(url_args) aas.update_from(HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, @@ -799,22 +794,16 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: return response_t() def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec - # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec - # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) return response_t(list(submodel.submodel_element), stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec - # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in From 9bad7df330ffbfe7ef99e01b013576815f390357 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Fri, 5 Apr 2024 15:56:36 +0200 Subject: [PATCH 246/474] adapter.http: implement the AAS reference routes --- basyx/aas/adapter/http.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index dfd5546..2009e2d 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -391,10 +391,12 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), Rule("/", methods=["POST"], endpoint=self.post_aas), + Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_all_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/", methods=["DELETE"], endpoint=self.delete_aas), + Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_reference), Submount("/asset-information", [ Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), @@ -639,16 +641,24 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> }, force_external=True) return response_t(aas, status=201, headers={"Location": created_resource_url}) - def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_aas_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - self.object_store.remove(self._get_shell(url_args)) - return response_t() + aashells = self._get_shells(request) + references: list[model.ModelReference] = [model.ModelReference.from_referable(aas) + for aas in aashells] + return response_t(references) # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_shell(url_args) - return response_t(aas, stripped=is_stripped_request(request)) + return response_t(aas) + + def get_aas_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + aas = self._get_shell(url_args) + reference = model.ModelReference.from_referable(aas) + return response_t(reference) def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) @@ -658,6 +668,11 @@ def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: aas.commit() return response_t() + def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + self.object_store.remove(self._get_shell(url_args)) + return response_t() + def get_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_shell(url_args) From b47ab9db6b23ac45114e5e212c81b3188d71991a Mon Sep 17 00:00:00 2001 From: Frosty2500 <125310380+Frosty2500@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:59:21 +0200 Subject: [PATCH 247/474] adapter.http: implement the attachment routes (#33) * adapter.http: implement the attachment routes * adapter.http: fix codestyle errors * adapter.http: implement recommended changes * adapter.http: implement new recommended changes --- basyx/aas/adapter/http.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 2009e2d..bc013c7 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -25,6 +25,7 @@ from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response +from werkzeug.datastructures import FileStorage from basyx.aas import model from ._generic import XML_NS_MAP @@ -447,6 +448,14 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), + Submount("/attachment", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_attachment), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_attachment), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_attachment), + ]), Submount("/qualifiers", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), @@ -890,6 +899,35 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) return response_t() + def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + if not isinstance(submodel_element, model.Blob): + raise BadRequest(f"{submodel_element!r} is not a blob, no file content to download!") + return Response(submodel_element.value, content_type=submodel_element.content_type) + + def put_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + file_storage: Optional[FileStorage] = request.files.get('file') + if file_storage is None: + raise BadRequest(f"Missing file to upload") + if not isinstance(submodel_element, model.Blob): + raise BadRequest(f"{submodel_element!r} is not a blob, no file content to update!") + submodel_element.value = file_storage.read() + submodel_element.commit() + return response_t() + + def delete_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + response_t = get_response_type(request) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + if not isinstance(submodel_element, model.Blob): + raise BadRequest(f"{submodel_element!r} is not a blob, no file content to delete!") + submodel_element.value = None + submodel_element.commit() + return response_t() + def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) From 04455a426248f50205512469baa1504ba16122c4 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Fri, 19 Apr 2024 16:06:46 +0200 Subject: [PATCH 248/474] adapter.http: implement the pagination --- basyx/aas/adapter/http.py | 117 +++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index bc013c7..0a9af89 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -32,7 +32,7 @@ from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union +from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple @enum.unique @@ -100,15 +100,16 @@ class StrippedResultToJsonEncoder(ResultToJsonEncoder): class APIResponse(abc.ABC, Response): @abc.abstractmethod - def __init__(self, obj: Optional[ResponseData] = None, stripped: bool = False, *args, **kwargs): + def __init__(self, obj: Optional[ResponseData] = None, cursor: Optional[int] = None, + stripped: bool = False, *args, **kwargs): super().__init__(*args, **kwargs) if obj is None: self.status_code = 204 else: - self.data = self.serialize(obj, stripped) + self.data = self.serialize(obj, cursor, stripped) @abc.abstractmethod - def serialize(self, obj: ResponseData, stripped: bool) -> str: + def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: pass @@ -116,8 +117,16 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, obj: ResponseData, stripped: bool) -> str: - return json.dumps(obj, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, + def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: + data: Dict[str, Any] = {} + + # Add paging metadata if cursor is not None + if cursor is not None: + data["paging_metadata"] = {"cursor": cursor} + + # Add the result + data["result"] = obj + return json.dumps(data, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, separators=(",", ":")) @@ -125,25 +134,29 @@ class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, obj: ResponseData, stripped: bool) -> str: - # TODO: xml serialization doesn't support stripped objects + def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: + # Create the root XML element + root_elem = etree.Element("response", nsmap=XML_NS_MAP) + # Add the cursor as an attribute of the root element + root_elem.set("cursor", str(cursor)) + # Serialize the obj to XML if isinstance(obj, Result): - response_elem = result_to_xml(obj, nsmap=XML_NS_MAP) - etree.cleanup_namespaces(response_elem) + obj_elem = result_to_xml(obj, **XML_NS_MAP) # Assuming XML_NS_MAP is a namespace mapping else: if isinstance(obj, list): - response_elem = etree.Element("list", nsmap=XML_NS_MAP) - for obj in obj: - response_elem.append(object_to_xml_element(obj)) - etree.cleanup_namespaces(response_elem) + obj_elem = etree.Element("list", nsmap=XML_NS_MAP) + for item in obj: + item_elem = object_to_xml_element(item) + obj_elem.append(item_elem) else: - # dirty hack to be able to use the namespace prefixes defined in xml_serialization.NS_MAP - parent = etree.Element("parent", nsmap=XML_NS_MAP) - response_elem = object_to_xml_element(obj) - parent.append(response_elem) - etree.cleanup_namespaces(parent) - return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") - + obj_elem = object_to_xml_element(obj) + # Add the obj XML element to the root + root_elem.append(obj_elem) + # Clean up namespaces + etree.cleanup_namespaces(root_elem) + # Serialize the XML tree to a string + xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") + return xml_str class XmlResponseAlt(XmlResponse): def __init__(self, *args, content_type="text/xml", **kwargs): @@ -591,21 +604,38 @@ def _get_shells(self, request: Request) -> Iterator[model.AssetAdministrationShe def _get_shell(self, url_args: Dict) -> model.AssetAdministrationShell: return self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], int]: + limit = request.args.get('limit', type=int, default=10) + cursor = request.args.get('cursor', type=int, default=0) + submodels: List[model.Submodel] = list(self._get_all_obj_of_type(model.Submodel)) + # Apply pagination + start_index = cursor + end_index = cursor + limit + paginated_submodels = submodels[start_index:end_index] id_short = request.args.get("idShort") if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) + paginated_submodels = filter(lambda sm: sm.id_short == id_short, paginated_submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: - spec_semantic_id = (HTTPApiDecoder.base64urljson - (semantic_id, model.Reference, False)) # type: ignore[type-abstract] - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) - return submodels + spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) + paginated_submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, paginated_submodels) + return [paginated_submodels, end_index] def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) + def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ + Tuple[List[model.SubmodelElement], int]: + limit = request.args.get('limit', type=int, default=10) + cursor = request.args.get('cursor', type=int, default=0) + submodel = self._get_submodel(url_args) + submodelelements = list(submodel.submodel_element) + # Apply pagination + start_index = cursor + end_index = cursor + limit + paginated_submodelelements = submodelelements[start_index:end_index] + return [paginated_submodelelements, end_index] + def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: submodel = self._get_submodel(url_args) @@ -759,8 +789,9 @@ def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapt # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request) - return response_t(list(submodels), stripped=is_stripped_request(request)) + submodels = self._get_submodels(request)[0] + cursor = self._get_submodels(request)[1] + return response_t(list(submodels), cursor=cursor, stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -777,15 +808,17 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte def get_submodel_all_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request) - return response_t(list(submodels), stripped=True) + submodels = self._get_submodels(request)[0] + cursor = self._get_submodels(request)[1] + return response_t(list(submodels), cursor=cursor, stripped=True) def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request) + submodels = self._get_submodels(request)[0] references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] - return response_t(references, stripped=is_stripped_request(request)) + cursor = self._get_submodels(request)[1] + return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) # --------- SUBMODEL ROUTES --------- @@ -819,20 +852,22 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel(url_args) - return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) + submodelelements = self._get_submodel_submodel_elements(request, url_args)[0] + cursor = self._get_submodel_submodel_elements(request, url_args)[1] + return response_t(submodelelements, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel(url_args) - return response_t(list(submodel.submodel_element), stripped=True) + submodelelements = self._get_submodel_submodel_elements(request, url_args)[0] + cursor = self._get_submodel_submodel_elements(request, url_args)[1] + return response_t(submodelelements, cursor=cursor, stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel(url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - submodel.submodel_element] - return response_t(references, stripped=is_stripped_request(request)) + self._get_submodel_submodel_elements(request, url_args)[0]] + cursor = self._get_submodel_submodel_elements(request, url_args)[1] + return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) From f4b7e5d4224696b4f1af99fdeff9ffbe229a4360 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Fri, 19 Apr 2024 16:24:12 +0200 Subject: [PATCH 249/474] adapter.http: fix codestyle errors --- basyx/aas/adapter/http.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 0a9af89..d0c8e53 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -32,7 +32,7 @@ from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple +from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple, Any @enum.unique @@ -158,6 +158,7 @@ def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") return xml_str + class XmlResponseAlt(XmlResponse): def __init__(self, *args, content_type="text/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) @@ -611,15 +612,15 @@ def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], in # Apply pagination start_index = cursor end_index = cursor + limit - paginated_submodels = submodels[start_index:end_index] + paginated_submodels: Iterator[model.Submodel] = iter(submodels[start_index:end_index]) id_short = request.args.get("idShort") if id_short is not None: paginated_submodels = filter(lambda sm: sm.id_short == id_short, paginated_submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) + spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) # type: ignore paginated_submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, paginated_submodels) - return [paginated_submodels, end_index] + return (paginated_submodels, end_index) def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) @@ -634,7 +635,7 @@ def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ start_index = cursor end_index = cursor + limit paginated_submodelelements = submodelelements[start_index:end_index] - return [paginated_submodelelements, end_index] + return (paginated_submodelelements, end_index) def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: From 9daee381dee5f1cf2f5c4690cf44d87ab4c9d471 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Tue, 23 Apr 2024 13:33:26 +0200 Subject: [PATCH 250/474] adapter.http: implement recommended changes --- basyx/aas/adapter/http.py | 120 +++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 59 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d0c8e53..22b38bd 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -16,6 +16,7 @@ import enum import io import json +import itertools from lxml import etree # type: ignore import werkzeug.exceptions @@ -109,7 +110,7 @@ def __init__(self, obj: Optional[ResponseData] = None, cursor: Optional[int] = N self.data = self.serialize(obj, cursor, stripped) @abc.abstractmethod - def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: + def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: pass @@ -117,39 +118,44 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: - data: Dict[str, Any] = {} - - # Add paging metadata if cursor is not None - if cursor is not None: - data["paging_metadata"] = {"cursor": cursor} - - # Add the result - data["result"] = obj - return json.dumps(data, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, - separators=(",", ":")) + def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: + if cursor is None: + return json.dumps( + obj, + cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, + separators=(",", ":") + ) + data: Dict[str, Any] = { + "paging_metadata": {"cursor": cursor}, + "result": obj + } + return json.dumps( + data, + cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, + separators=(",", ":") + ) class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: + def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: # Create the root XML element root_elem = etree.Element("response", nsmap=XML_NS_MAP) - # Add the cursor as an attribute of the root element - root_elem.set("cursor", str(cursor)) + if cursor is not None: + # Add the cursor as an attribute of the root element + root_elem.set("cursor", str(cursor)) # Serialize the obj to XML if isinstance(obj, Result): obj_elem = result_to_xml(obj, **XML_NS_MAP) # Assuming XML_NS_MAP is a namespace mapping + elif isinstance(obj, list): + obj_elem = etree.Element("list", nsmap=XML_NS_MAP) + for item in obj: + item_elem = object_to_xml_element(item) + obj_elem.append(item_elem) else: - if isinstance(obj, list): - obj_elem = etree.Element("list", nsmap=XML_NS_MAP) - for item in obj: - item_elem = object_to_xml_element(item) - obj_elem.append(item_elem) - else: - obj_elem = object_to_xml_element(obj) + obj_elem = object_to_xml_element(obj) # Add the obj XML element to the root root_elem.append(obj_elem) # Clean up namespaces @@ -583,7 +589,16 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i return ref raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") - def _get_shells(self, request: Request) -> Iterator[model.AssetAdministrationShell]: + @classmethod + def _get_slice(cls, request: Request, iterator: Iterator) -> Tuple[Iterator, int]: + limit = request.args.get('limit', type=int, default=10) + cursor = request.args.get('cursor', type=int, default=0) + start_index = cursor + end_index = cursor + limit + paginated_slice = itertools.islice(iterator, start_index, end_index) + return paginated_slice, end_index + + def _get_shells(self, request: Request) -> Tuple[Iterator[model.AssetAdministrationShell], int]: aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) id_short = request.args.get("idShort") @@ -600,42 +615,32 @@ def _get_shells(self, request: Request) -> Iterator[model.AssetAdministrationShe aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id for specific_asset_id in specific_asset_ids), aas) - return aas + paginated_aas, end_index = self._get_slice(request, aas) + return paginated_aas, end_index def _get_shell(self, url_args: Dict) -> model.AssetAdministrationShell: return self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], int]: - limit = request.args.get('limit', type=int, default=10) - cursor = request.args.get('cursor', type=int, default=0) - submodels: List[model.Submodel] = list(self._get_all_obj_of_type(model.Submodel)) - # Apply pagination - start_index = cursor - end_index = cursor + limit - paginated_submodels: Iterator[model.Submodel] = iter(submodels[start_index:end_index]) + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) id_short = request.args.get("idShort") if id_short is not None: - paginated_submodels = filter(lambda sm: sm.id_short == id_short, paginated_submodels) + submodels = filter(lambda sm: sm.id_short == id_short, submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) # type: ignore - paginated_submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, paginated_submodels) - return (paginated_submodels, end_index) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + paginated_submodels, end_index = self._get_slice(request, submodels) + return paginated_submodels, end_index def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) - def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ + def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ Tuple[List[model.SubmodelElement], int]: - limit = request.args.get('limit', type=int, default=10) - cursor = request.args.get('cursor', type=int, default=0) submodel = self._get_submodel(url_args) - submodelelements = list(submodel.submodel_element) - # Apply pagination - start_index = cursor - end_index = cursor + limit - paginated_submodelelements = submodelelements[start_index:end_index] - return (paginated_submodelelements, end_index) + paginated_submodelelements, end_index = self._get_slice(request, submodel.submodel_element) + return list(paginated_submodelelements), end_index def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: @@ -666,7 +671,8 @@ def handle_request(self, request: Request): # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - return response_t(list(self._get_shells(request))) + aashels, cursor = self._get_shells(request) + return response_t(list(aashels), cursor=cursor) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -683,10 +689,10 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> def get_aas_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aashells = self._get_shells(request) + aashells, cursor = self._get_shells(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(aas) for aas in aashells] - return response_t(references) + return response_t(references, cursor=cursor) # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -728,7 +734,8 @@ def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_shell(url_args) - return response_t(list(aas.submodel)) + submodel_refs, cursor = self._get_slice(request, aas.submodel) + return response_t(list(submodel_refs), cursor=cursor) def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) @@ -790,8 +797,7 @@ def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapt # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request)[0] - cursor = self._get_submodels(request)[1] + submodels, cursor = self._get_submodels(request) return response_t(list(submodels), cursor=cursor, stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: @@ -809,16 +815,14 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte def get_submodel_all_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request)[0] - cursor = self._get_submodels(request)[1] + submodels, cursor = self._get_submodels(request) return response_t(list(submodels), cursor=cursor, stripped=True) def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request)[0] + submodels, cursor = self._get_submodels(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] - cursor = self._get_submodels(request)[1] return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) # --------- SUBMODEL ROUTES --------- @@ -853,21 +857,19 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements = self._get_submodel_submodel_elements(request, url_args)[0] - cursor = self._get_submodel_submodel_elements(request, url_args)[1] + submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) return response_t(submodelelements, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements = self._get_submodel_submodel_elements(request, url_args)[0] - cursor = self._get_submodel_submodel_elements(request, url_args)[1] + submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) return response_t(submodelelements, cursor=cursor, stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) + submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - self._get_submodel_submodel_elements(request, url_args)[0]] - cursor = self._get_submodel_submodel_elements(request, url_args)[1] + submodelelements] return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: From c90cac23c963ef1d8c76cb6c0301eafbbd5b6433 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 28 Apr 2024 16:18:51 +0200 Subject: [PATCH 251/474] adapter.http: implement new recommended changes --- basyx/aas/adapter/http.py | 55 ++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 22b38bd..159bd01 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -120,15 +120,12 @@ def __init__(self, *args, content_type="application/json", **kwargs): def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: if cursor is None: - return json.dumps( - obj, - cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, - separators=(",", ":") - ) - data: Dict[str, Any] = { - "paging_metadata": {"cursor": cursor}, - "result": obj - } + data = obj + else: + data = { + "paging_metadata": {"cursor": cursor}, + "result": obj + } return json.dumps( data, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, @@ -141,26 +138,21 @@ def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: - # Create the root XML element root_elem = etree.Element("response", nsmap=XML_NS_MAP) if cursor is not None: - # Add the cursor as an attribute of the root element root_elem.set("cursor", str(cursor)) - # Serialize the obj to XML if isinstance(obj, Result): - obj_elem = result_to_xml(obj, **XML_NS_MAP) # Assuming XML_NS_MAP is a namespace mapping + result_elem = result_to_xml(obj, **XML_NS_MAP) + root_elem.append(result_elem) elif isinstance(obj, list): - obj_elem = etree.Element("list", nsmap=XML_NS_MAP) for item in obj: item_elem = object_to_xml_element(item) - obj_elem.append(item_elem) + root_elem.append(item_elem) else: obj_elem = object_to_xml_element(obj) - # Add the obj XML element to the root - root_elem.append(obj_elem) - # Clean up namespaces + for child in obj_elem: + root_elem.append(child) etree.cleanup_namespaces(root_elem) - # Serialize the XML tree to a string xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") return xml_str @@ -590,7 +582,7 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") @classmethod - def _get_slice(cls, request: Request, iterator: Iterator) -> Tuple[Iterator, int]: + def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: limit = request.args.get('limit', type=int, default=10) cursor = request.args.get('cursor', type=int, default=0) start_index = cursor @@ -628,7 +620,8 @@ def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], in submodels = filter(lambda sm: sm.id_short == id_short, submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) # type: ignore + spec_semantic_id = HTTPApiDecoder.base64urljson( + semantic_id, model.Reference, False) # type: ignore[type-abstract] submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) paginated_submodels, end_index = self._get_slice(request, submodels) return paginated_submodels, end_index @@ -637,10 +630,11 @@ def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ - Tuple[List[model.SubmodelElement], int]: + Tuple[Iterator[model.SubmodelElement], int]: submodel = self._get_submodel(url_args) - paginated_submodelelements, end_index = self._get_slice(request, submodel.submodel_element) - return list(paginated_submodelelements), end_index + paginated_submodel_elements: Iterator[model.SubmodelElement] + paginated_submodel_elements, end_index = self._get_slice(request, submodel.submodel_element) + return paginated_submodel_elements, end_index def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: @@ -734,6 +728,7 @@ def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_shell(url_args) + submodel_refs: Iterator[model.ModelReference[model.Submodel]] submodel_refs, cursor = self._get_slice(request, aas.submodel) return response_t(list(submodel_refs), cursor=cursor) @@ -857,19 +852,19 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) - return response_t(submodelelements, cursor=cursor, stripped=is_stripped_request(request)) + submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) + return response_t(list(submodel_elements), cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) - return response_t(submodelelements, cursor=cursor, stripped=True) + submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) + return response_t(list(submodel_elements), cursor=cursor, stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) + submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - submodelelements] + list(submodel_elements)] return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: From c15e2b35819a1d83f7f61aa5afc83de83146e9b8 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Tue, 30 Apr 2024 23:52:39 +0200 Subject: [PATCH 252/474] adapter.http: implement new recommended changes --- basyx/aas/adapter/http.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 159bd01..cde58b4 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -143,7 +143,8 @@ def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> root_elem.set("cursor", str(cursor)) if isinstance(obj, Result): result_elem = result_to_xml(obj, **XML_NS_MAP) - root_elem.append(result_elem) + for child in result_elem: + root_elem.append(child) elif isinstance(obj, list): for item in obj: item_elem = object_to_xml_element(item) @@ -585,6 +586,10 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: limit = request.args.get('limit', type=int, default=10) cursor = request.args.get('cursor', type=int, default=0) + limit_str= request.args.get('limit', type=str, default="10") + cursor_str= request.args.get('cursor', type=str, default="0") + if not limit_str.isdigit() or not cursor_str.isdigit(): + raise BadRequest("Cursor and limit must be positive integers!") start_index = cursor end_index = cursor + limit paginated_slice = itertools.islice(iterator, start_index, end_index) From 299bf32bf4c2fd958e7b55640aeefb8bf967f130 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Thu, 9 May 2024 18:39:11 +0200 Subject: [PATCH 253/474] adapter.http: change the limit and cursor check --- basyx/aas/adapter/http.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index cde58b4..179efc5 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -584,11 +584,13 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i @classmethod def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: - limit = request.args.get('limit', type=int, default=10) - cursor = request.args.get('cursor', type=int, default=0) - limit_str= request.args.get('limit', type=str, default="10") - cursor_str= request.args.get('cursor', type=str, default="0") - if not limit_str.isdigit() or not cursor_str.isdigit(): + limit = request.args.get('limit', default="10") + cursor = request.args.get('cursor', default="0") + try: + limit, cursor = int(limit), int(cursor) + if limit < 0 or cursor < 0: + raise ValueError + except ValueError: raise BadRequest("Cursor and limit must be positive integers!") start_index = cursor end_index = cursor + limit From 1d4159cb5905f6f4766123db069887041224eb46 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Thu, 23 May 2024 20:45:31 +0200 Subject: [PATCH 254/474] adapter.http: implement warning for not implemented routes --- basyx/aas/adapter/http.py | 65 ++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 179efc5..8bba1a5 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -23,7 +23,7 @@ import werkzeug.routing import werkzeug.urls import werkzeug.utils -from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity +from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity, NotImplemented from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response from werkzeug.datastructures import FileStorage @@ -402,6 +402,12 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v3.0", [ + Submount("/serialization", [ + Rule("/", methods=["GET"], endpoint=self.not_implemented) + ]), + Submount("/description", [ + Rule("/", methods=["GET"], endpoint=self.not_implemented) + ]), Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), Rule("/", methods=["POST"], endpoint=self.post_aas), @@ -414,6 +420,11 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/asset-information", [ Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), + Submount("/thumbnail", [ + Rule("/", methods=["GET"], endpoint=self.not_implemented), + Rule("/", methods=["PUT"], endpoint=self.not_implemented), + Rule("/", methods=["DELETE"], endpoint=self.not_implemented) + ]) ]), Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), @@ -434,12 +445,19 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["POST"], endpoint=self.post_submodel), Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), + Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodels_reference), + Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/", methods=["POST"], @@ -448,6 +466,8 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_submodel_submodel_elements_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_reference), + Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path), @@ -457,17 +477,40 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.put_submodel_submodel_elements_id_short_path), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), + Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), + Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), Submount("/attachment", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_attachment), Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_element_attachment), Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_attachment), + endpoint=self.delete_submodel_submodel_element_attachment) + ]), + Submount("/invoke", [ + Rule("/", methods=["POST"], endpoint=self.not_implemented), + Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) + ]), + Submount("/invoke-async", [ + Rule("/", methods=["POST"], endpoint=self.not_implemented), + Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) + ]), + Submount("/operation-status", [ + Rule("//", methods=["GET"], + endpoint=self.not_implemented) + ]), + Submount("/operation-results", [ + Rule("//", methods=["GET"], + endpoint=self.not_implemented), + Rule("//$value/", methods=["GET"], + endpoint=self.not_implemented) ]), Submount("/qualifiers", [ Rule("/", methods=["GET"], @@ -479,9 +522,9 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_qualifiers), + endpoint=self.delete_submodel_submodel_element_qualifiers) ]) - ]), + ]) ]), Submount("/qualifiers", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), @@ -492,7 +535,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_qualifiers), + endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) ]) @@ -584,10 +627,10 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i @classmethod def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: - limit = request.args.get('limit', default="10") - cursor = request.args.get('cursor', default="0") + limit_str = request.args.get('limit', default="10") + cursor_str = request.args.get('cursor', default="0") try: - limit, cursor = int(limit), int(cursor) + limit, cursor = int(limit_str), int(cursor_str) if limit < 0 or cursor < 0: raise ValueError except ValueError: @@ -669,6 +712,12 @@ def handle_request(self, request: Request): except werkzeug.exceptions.NotAcceptable as e: return e + # ------ all not implemented ROUTES ------- + def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + raise NotImplemented(f"This route is not implemented!") + return response_t() + # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) From ba16760c2dc1e6a22d3f08cc9171f10e30cf3968 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Fri, 24 May 2024 13:54:40 +0200 Subject: [PATCH 255/474] adapter.http: remove unnecessary lines --- basyx/aas/adapter/http.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8bba1a5..1acd55e 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -23,7 +23,7 @@ import werkzeug.routing import werkzeug.urls import werkzeug.utils -from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity, NotImplemented +from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response from werkzeug.datastructures import FileStorage @@ -714,9 +714,8 @@ def handle_request(self, request: Request): # ------ all not implemented ROUTES ------- def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) - raise NotImplemented(f"This route is not implemented!") - return response_t() + raise werkzeug.exceptions.NotImplemented(f"This route is not implemented!") + # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: From 304ea86f9e115aff2a90dda6fa9159a312b89ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 24 May 2024 20:43:36 +0200 Subject: [PATCH 256/474] adapter.http: remove excess blank line --- basyx/aas/adapter/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 1acd55e..40068a8 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -716,7 +716,6 @@ def handle_request(self, request: Request): def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: raise werkzeug.exceptions.NotImplemented(f"This route is not implemented!") - # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) From afe4e5422838b23132c64d7b1462606e50daca30 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Mon, 4 Mar 2024 13:14:36 +0100 Subject: [PATCH 257/474] Extract compliance tool to be its own package We extract the compliance_tool into its own package, since the functionality is not needed in the general SDK anymore. The new aas-compliance-tool can be found [here](https://github.com/rwth-iat/aas-compliance-tool). --- basyx/aas/adapter/json/__init__.py | 3 - basyx/aas/adapter/json/aasJSONSchema.json | 1528 --------------------- basyx/aas/adapter/xml/AAS.xsd | 1344 ------------------ basyx/aas/adapter/xml/__init__.py | 3 - 4 files changed, 2878 deletions(-) delete mode 100644 basyx/aas/adapter/json/aasJSONSchema.json delete mode 100644 basyx/aas/adapter/xml/AAS.xsd diff --git a/basyx/aas/adapter/json/__init__.py b/basyx/aas/adapter/json/__init__.py index 740b0fb..04b7805 100644 --- a/basyx/aas/adapter/json/__init__.py +++ b/basyx/aas/adapter/json/__init__.py @@ -16,10 +16,7 @@ AAS objects within a JSON file and return them as BaSyx Python SDK :class:`ObjectStore `. """ -import os.path from .json_serialization import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, object_store_to_json from .json_deserialization import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrippedAASFromJsonDecoder, \ StrictStrippedAASFromJsonDecoder, read_aas_json_file, read_aas_json_file_into - -JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json deleted file mode 100644 index f48db4d..0000000 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ /dev/null @@ -1,1528 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "title": "AssetAdministrationShellEnvironment", - "type": "object", - "allOf": [ - { - "$ref": "#/definitions/Environment" - } - ], - "$id": "https://admin-shell.io/aas/3/0", - "definitions": { - "AasSubmodelElements": { - "type": "string", - "enum": [ - "AnnotatedRelationshipElement", - "BasicEventElement", - "Blob", - "Capability", - "DataElement", - "Entity", - "EventElement", - "File", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "SubmodelElement", - "SubmodelElementCollection", - "SubmodelElementList" - ] - }, - "AbstractLangString": { - "type": "object", - "properties": { - "language": { - "type": "string", - "pattern": "^(([a-zA-Z]{2,3}(-[a-zA-Z]{3}(-[a-zA-Z]{3}){2})?|[a-zA-Z]{4}|[a-zA-Z]{5,8})(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-(([a-zA-Z0-9]){5,8}|[0-9]([a-zA-Z0-9]){3}))*(-[0-9A-WY-Za-wy-z](-([a-zA-Z0-9]){2,8})+)*(-[xX](-([a-zA-Z0-9]){1,8})+)?|[xX](-([a-zA-Z0-9]){1,8})+|((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)))$" - }, - "text": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - } - }, - "required": [ - "language", - "text" - ] - }, - "AdministrativeInformation": { - "allOf": [ - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "version": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 4 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^(0|[1-9][0-9]*)$" - } - ] - }, - "revision": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 4 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^(0|[1-9][0-9]*)$" - } - ] - }, - "creator": { - "$ref": "#/definitions/Reference" - }, - "templateId": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - } - } - } - ] - }, - "AnnotatedRelationshipElement": { - "allOf": [ - { - "$ref": "#/definitions/RelationshipElement_abstract" - }, - { - "properties": { - "annotations": { - "type": "array", - "items": { - "$ref": "#/definitions/DataElement_choice" - }, - "minItems": 1 - }, - "modelType": { - "const": "AnnotatedRelationshipElement" - } - } - } - ] - }, - "AssetAdministrationShell": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "derivedFrom": { - "$ref": "#/definitions/Reference" - }, - "assetInformation": { - "$ref": "#/definitions/AssetInformation" - }, - "submodels": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - }, - "modelType": { - "const": "AssetAdministrationShell" - } - }, - "required": [ - "assetInformation" - ] - } - ] - }, - "AssetInformation": { - "type": "object", - "properties": { - "assetKind": { - "$ref": "#/definitions/AssetKind" - }, - "globalAssetId": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "specificAssetIds": { - "type": "array", - "items": { - "$ref": "#/definitions/SpecificAssetId" - }, - "minItems": 1 - }, - "assetType": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "defaultThumbnail": { - "$ref": "#/definitions/Resource" - } - }, - "required": [ - "assetKind" - ] - }, - "AssetKind": { - "type": "string", - "enum": [ - "Instance", - "NotApplicable", - "Type" - ] - }, - "BasicEventElement": { - "allOf": [ - { - "$ref": "#/definitions/EventElement" - }, - { - "properties": { - "observed": { - "$ref": "#/definitions/Reference" - }, - "direction": { - "$ref": "#/definitions/Direction" - }, - "state": { - "$ref": "#/definitions/StateOfEvent" - }, - "messageTopic": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "messageBroker": { - "$ref": "#/definitions/Reference" - }, - "lastUpdate": { - "type": "string", - "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" - }, - "minInterval": { - "type": "string", - "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" - }, - "maxInterval": { - "type": "string", - "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" - }, - "modelType": { - "const": "BasicEventElement" - } - }, - "required": [ - "observed", - "direction", - "state" - ] - } - ] - }, - "Blob": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "value": { - "type": "string", - "contentEncoding": "base64" - }, - "contentType": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 100 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" - } - ] - }, - "modelType": { - "const": "Blob" - } - }, - "required": [ - "contentType" - ] - } - ] - }, - "Capability": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "modelType": { - "const": "Capability" - } - } - } - ] - }, - "ConceptDescription": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "isCaseOf": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - }, - "modelType": { - "const": "ConceptDescription" - } - } - } - ] - }, - "DataElement": { - "$ref": "#/definitions/SubmodelElement" - }, - "DataElement_choice": { - "oneOf": [ - { - "$ref": "#/definitions/Blob" - }, - { - "$ref": "#/definitions/File" - }, - { - "$ref": "#/definitions/MultiLanguageProperty" - }, - { - "$ref": "#/definitions/Property" - }, - { - "$ref": "#/definitions/Range" - }, - { - "$ref": "#/definitions/ReferenceElement" - } - ] - }, - "DataSpecificationContent": { - "type": "object", - "properties": { - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ - "modelType" - ] - }, - "DataSpecificationContent_choice": { - "oneOf": [ - { - "$ref": "#/definitions/DataSpecificationIec61360" - } - ] - }, - "DataSpecificationIec61360": { - "allOf": [ - { - "$ref": "#/definitions/DataSpecificationContent" - }, - { - "properties": { - "preferredName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringPreferredNameTypeIec61360" - }, - "minItems": 1 - }, - "shortName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringShortNameTypeIec61360" - }, - "minItems": 1 - }, - "unit": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "unitId": { - "$ref": "#/definitions/Reference" - }, - "sourceOfDefinition": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "symbol": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "dataType": { - "$ref": "#/definitions/DataTypeIec61360" - }, - "definition": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringDefinitionTypeIec61360" - }, - "minItems": 1 - }, - "valueFormat": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "valueList": { - "$ref": "#/definitions/ValueList" - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "levelType": { - "$ref": "#/definitions/LevelType" - }, - "modelType": { - "const": "DataSpecificationIec61360" - } - }, - "required": [ - "preferredName" - ] - } - ] - }, - "DataTypeDefXsd": { - "type": "string", - "enum": [ - "xs:anyURI", - "xs:base64Binary", - "xs:boolean", - "xs:byte", - "xs:date", - "xs:dateTime", - "xs:decimal", - "xs:double", - "xs:duration", - "xs:float", - "xs:gDay", - "xs:gMonth", - "xs:gMonthDay", - "xs:gYear", - "xs:gYearMonth", - "xs:hexBinary", - "xs:int", - "xs:integer", - "xs:long", - "xs:negativeInteger", - "xs:nonNegativeInteger", - "xs:nonPositiveInteger", - "xs:positiveInteger", - "xs:short", - "xs:string", - "xs:time", - "xs:unsignedByte", - "xs:unsignedInt", - "xs:unsignedLong", - "xs:unsignedShort" - ] - }, - "DataTypeIec61360": { - "type": "string", - "enum": [ - "BLOB", - "BOOLEAN", - "DATE", - "FILE", - "HTML", - "INTEGER_COUNT", - "INTEGER_CURRENCY", - "INTEGER_MEASURE", - "IRDI", - "IRI", - "RATIONAL", - "RATIONAL_MEASURE", - "REAL_COUNT", - "REAL_CURRENCY", - "REAL_MEASURE", - "STRING", - "STRING_TRANSLATABLE", - "TIME", - "TIMESTAMP" - ] - }, - "Direction": { - "type": "string", - "enum": [ - "input", - "output" - ] - }, - "EmbeddedDataSpecification": { - "type": "object", - "properties": { - "dataSpecification": { - "$ref": "#/definitions/Reference" - }, - "dataSpecificationContent": { - "$ref": "#/definitions/DataSpecificationContent_choice" - } - }, - "required": [ - "dataSpecification", - "dataSpecificationContent" - ] - }, - "Entity": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "statements": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement_choice" - }, - "minItems": 1 - }, - "entityType": { - "$ref": "#/definitions/EntityType" - }, - "globalAssetId": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "specificAssetIds": { - "type": "array", - "items": { - "$ref": "#/definitions/SpecificAssetId" - }, - "minItems": 1 - }, - "modelType": { - "const": "Entity" - } - }, - "required": [ - "entityType" - ] - } - ] - }, - "EntityType": { - "type": "string", - "enum": [ - "CoManagedEntity", - "SelfManagedEntity" - ] - }, - "Environment": { - "type": "object", - "properties": { - "assetAdministrationShells": { - "type": "array", - "items": { - "$ref": "#/definitions/AssetAdministrationShell" - }, - "minItems": 1 - }, - "submodels": { - "type": "array", - "items": { - "$ref": "#/definitions/Submodel" - }, - "minItems": 1 - }, - "conceptDescriptions": { - "type": "array", - "items": { - "$ref": "#/definitions/ConceptDescription" - }, - "minItems": 1 - } - } - }, - "EventElement": { - "$ref": "#/definitions/SubmodelElement" - }, - "EventPayload": { - "type": "object", - "properties": { - "source": { - "$ref": "#/definitions/Reference" - }, - "sourceSemanticId": { - "$ref": "#/definitions/Reference" - }, - "observableReference": { - "$ref": "#/definitions/Reference" - }, - "observableSemanticId": { - "$ref": "#/definitions/Reference" - }, - "topic": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "subjectId": { - "$ref": "#/definitions/Reference" - }, - "timeStamp": { - "type": "string", - "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" - }, - "payload": { - "type": "string", - "contentEncoding": "base64" - } - }, - "required": [ - "source", - "observableReference", - "timeStamp" - ] - }, - "Extension": { - "allOf": [ - { - "$ref": "#/definitions/HasSemantics" - }, - { - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "value": { - "type": "string" - }, - "refersTo": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - } - }, - "required": [ - "name" - ] - } - ] - }, - "File": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "value": { - "type": "string" - }, - "contentType": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 100 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" - } - ] - }, - "modelType": { - "const": "File" - } - }, - "required": [ - "contentType" - ] - } - ] - }, - "HasDataSpecification": { - "type": "object", - "properties": { - "embeddedDataSpecifications": { - "type": "array", - "items": { - "$ref": "#/definitions/EmbeddedDataSpecification" - }, - "minItems": 1 - } - } - }, - "HasExtensions": { - "type": "object", - "properties": { - "extensions": { - "type": "array", - "items": { - "$ref": "#/definitions/Extension" - }, - "minItems": 1 - } - } - }, - "HasKind": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/definitions/ModellingKind" - } - } - }, - "HasSemantics": { - "type": "object", - "properties": { - "semanticId": { - "$ref": "#/definitions/Reference" - }, - "supplementalSemanticIds": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - } - } - }, - "Identifiable": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "properties": { - "administration": { - "$ref": "#/definitions/AdministrativeInformation" - }, - "id": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - } - }, - "required": [ - "id" - ] - } - ] - }, - "Key": { - "type": "object", - "properties": { - "type": { - "$ref": "#/definitions/KeyTypes" - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - } - }, - "required": [ - "type", - "value" - ] - }, - "KeyTypes": { - "type": "string", - "enum": [ - "AnnotatedRelationshipElement", - "AssetAdministrationShell", - "BasicEventElement", - "Blob", - "Capability", - "ConceptDescription", - "DataElement", - "Entity", - "EventElement", - "File", - "FragmentReference", - "GlobalReference", - "Identifiable", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "Referable", - "ReferenceElement", - "RelationshipElement", - "Submodel", - "SubmodelElement", - "SubmodelElementCollection", - "SubmodelElementList" - ] - }, - "LangStringDefinitionTypeIec61360": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 1023 - } - } - } - ] - }, - "LangStringNameType": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 128 - } - } - } - ] - }, - "LangStringPreferredNameTypeIec61360": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 255 - } - } - } - ] - }, - "LangStringShortNameTypeIec61360": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 18 - } - } - } - ] - }, - "LangStringTextType": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 1023 - } - } - } - ] - }, - "LevelType": { - "type": "object", - "properties": { - "min": { - "type": "boolean" - }, - "nom": { - "type": "boolean" - }, - "typ": { - "type": "boolean" - }, - "max": { - "type": "boolean" - } - }, - "required": [ - "min", - "nom", - "typ", - "max" - ] - }, - "ModelType": { - "type": "string", - "enum": [ - "AnnotatedRelationshipElement", - "AssetAdministrationShell", - "BasicEventElement", - "Blob", - "Capability", - "ConceptDescription", - "DataSpecificationIec61360", - "Entity", - "File", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "Submodel", - "SubmodelElementCollection", - "SubmodelElementList" - ] - }, - "ModellingKind": { - "type": "string", - "enum": [ - "Instance", - "Template" - ] - }, - "MultiLanguageProperty": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringTextType" - }, - "minItems": 1 - }, - "valueId": { - "$ref": "#/definitions/Reference" - }, - "modelType": { - "const": "MultiLanguageProperty" - } - } - } - ] - }, - "Operation": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "inputVariables": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - }, - "minItems": 1 - }, - "outputVariables": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - }, - "minItems": 1 - }, - "inoutputVariables": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - }, - "minItems": 1 - }, - "modelType": { - "const": "Operation" - } - } - } - ] - }, - "OperationVariable": { - "type": "object", - "properties": { - "value": { - "$ref": "#/definitions/SubmodelElement_choice" - } - }, - "required": [ - "value" - ] - }, - "Property": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "value": { - "type": "string" - }, - "valueId": { - "$ref": "#/definitions/Reference" - }, - "modelType": { - "const": "Property" - } - }, - "required": [ - "valueType" - ] - } - ] - }, - "Qualifiable": { - "type": "object", - "properties": { - "qualifiers": { - "type": "array", - "items": { - "$ref": "#/definitions/Qualifier" - }, - "minItems": 1 - }, - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ - "modelType" - ] - }, - "Qualifier": { - "allOf": [ - { - "$ref": "#/definitions/HasSemantics" - }, - { - "properties": { - "kind": { - "$ref": "#/definitions/QualifierKind" - }, - "type": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "value": { - "type": "string" - }, - "valueId": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "type", - "valueType" - ] - } - ] - }, - "QualifierKind": { - "type": "string", - "enum": [ - "ConceptQualifier", - "TemplateQualifier", - "ValueQualifier" - ] - }, - "Range": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "min": { - "type": "string" - }, - "max": { - "type": "string" - }, - "modelType": { - "const": "Range" - } - }, - "required": [ - "valueType" - ] - } - ] - }, - "Referable": { - "allOf": [ - { - "$ref": "#/definitions/HasExtensions" - }, - { - "properties": { - "category": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "idShort": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 128 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$" - } - ] - }, - "displayName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringNameType" - }, - "minItems": 1 - }, - "description": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringTextType" - }, - "minItems": 1 - }, - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ - "modelType" - ] - } - ] - }, - "Reference": { - "type": "object", - "properties": { - "type": { - "$ref": "#/definitions/ReferenceTypes" - }, - "referredSemanticId": { - "$ref": "#/definitions/Reference" - }, - "keys": { - "type": "array", - "items": { - "$ref": "#/definitions/Key" - }, - "minItems": 1 - } - }, - "required": [ - "type", - "keys" - ] - }, - "ReferenceElement": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "value": { - "$ref": "#/definitions/Reference" - }, - "modelType": { - "const": "ReferenceElement" - } - } - } - ] - }, - "ReferenceTypes": { - "type": "string", - "enum": [ - "ExternalReference", - "ModelReference" - ] - }, - "RelationshipElement": { - "allOf": [ - { - "$ref": "#/definitions/RelationshipElement_abstract" - }, - { - "properties": { - "modelType": { - "const": "RelationshipElement" - } - } - } - ] - }, - "RelationshipElement_abstract": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "first": { - "$ref": "#/definitions/Reference" - }, - "second": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "first", - "second" - ] - } - ] - }, - "RelationshipElement_choice": { - "oneOf": [ - { - "$ref": "#/definitions/RelationshipElement" - }, - { - "$ref": "#/definitions/AnnotatedRelationshipElement" - } - ] - }, - "Resource": { - "type": "object", - "properties": { - "path": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 2000 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$" - } - ] - }, - "contentType": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 100 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" - } - ] - } - }, - "required": [ - "path" - ] - }, - "SpecificAssetId": { - "allOf": [ - { - "$ref": "#/definitions/HasSemantics" - }, - { - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "externalSubjectId": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "name", - "value" - ] - } - ] - }, - "StateOfEvent": { - "type": "string", - "enum": [ - "off", - "on" - ] - }, - "Submodel": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasKind" - }, - { - "$ref": "#/definitions/HasSemantics" - }, - { - "$ref": "#/definitions/Qualifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "submodelElements": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement_choice" - }, - "minItems": 1 - }, - "modelType": { - "const": "Submodel" - } - } - } - ] - }, - "SubmodelElement": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "$ref": "#/definitions/HasSemantics" - }, - { - "$ref": "#/definitions/Qualifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - } - ] - }, - "SubmodelElementCollection": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement_choice" - }, - "minItems": 1 - }, - "modelType": { - "const": "SubmodelElementCollection" - } - } - } - ] - }, - "SubmodelElementList": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "orderRelevant": { - "type": "boolean" - }, - "semanticIdListElement": { - "$ref": "#/definitions/Reference" - }, - "typeValueListElement": { - "$ref": "#/definitions/AasSubmodelElements" - }, - "valueTypeListElement": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement_choice" - }, - "minItems": 1 - }, - "modelType": { - "const": "SubmodelElementList" - } - }, - "required": [ - "typeValueListElement" - ] - } - ] - }, - "SubmodelElement_choice": { - "oneOf": [ - { - "$ref": "#/definitions/RelationshipElement" - }, - { - "$ref": "#/definitions/AnnotatedRelationshipElement" - }, - { - "$ref": "#/definitions/BasicEventElement" - }, - { - "$ref": "#/definitions/Blob" - }, - { - "$ref": "#/definitions/Capability" - }, - { - "$ref": "#/definitions/Entity" - }, - { - "$ref": "#/definitions/File" - }, - { - "$ref": "#/definitions/MultiLanguageProperty" - }, - { - "$ref": "#/definitions/Operation" - }, - { - "$ref": "#/definitions/Property" - }, - { - "$ref": "#/definitions/Range" - }, - { - "$ref": "#/definitions/ReferenceElement" - }, - { - "$ref": "#/definitions/SubmodelElementCollection" - }, - { - "$ref": "#/definitions/SubmodelElementList" - } - ] - }, - "ValueList": { - "type": "object", - "properties": { - "valueReferencePairs": { - "type": "array", - "items": { - "$ref": "#/definitions/ValueReferencePair" - }, - "minItems": 1 - } - }, - "required": [ - "valueReferencePairs" - ] - }, - "ValueReferencePair": { - "type": "object", - "properties": { - "value": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "valueId": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "value", - "valueId" - ] - } - } -} \ No newline at end of file diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd deleted file mode 100644 index 25d7a52..0000000 --- a/basyx/aas/adapter/xml/AAS.xsd +++ /dev/null @@ -1,1344 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/basyx/aas/adapter/xml/__init__.py b/basyx/aas/adapter/xml/__init__.py index af58ad0..aa08288 100644 --- a/basyx/aas/adapter/xml/__init__.py +++ b/basyx/aas/adapter/xml/__init__.py @@ -9,11 +9,8 @@ :ref:`xml_deserialization `: The module offers a function to create an :class:`ObjectStore ` from a given xml document. """ -import os.path from .xml_serialization import object_store_to_xml_element, write_aas_xml_file, object_to_xml_element, \ write_aas_xml_element from .xml_deserialization import AASFromXmlDecoder, StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder, \ StrictStrippedAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element - -XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'AAS.xsd') From 166bc42490d2e8277083ea12cb9f48bceeb9dd6b Mon Sep 17 00:00:00 2001 From: s-heppner Date: Wed, 10 Apr 2024 16:50:36 +0200 Subject: [PATCH 258/474] Update Copyright Notices (#224) This PR fixes the outdated `NOTICE`. While doing that, I `notice`d, that the years in the copyright strings were outdated as well, so I updated them (using the `/etc/scripts/set_copyright_year.sh`) In the future, we should create a recurring task that makes us update the years at least once a year. Maybe it should also become a task before publishing a new release? Fixes #196 Depends on #235 --- basyx/aas/adapter/_generic.py | 2 +- basyx/aas/adapter/aasx.py | 2 +- basyx/aas/adapter/json/json_deserialization.py | 2 +- basyx/aas/adapter/json/json_serialization.py | 2 +- basyx/aas/adapter/xml/xml_deserialization.py | 2 +- basyx/aas/adapter/xml/xml_serialization.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 3ca90cc..6a37c74 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 10efd91..e0bb191 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2024 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. diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index f6c7c41..7e0c39c 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index 25a22c4..c80f890 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 0a59518..0f379aa 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index cd02ead..9d7abfb 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. From df6e7aeae500e0084235635628e015f725bd77c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 14:56:16 +0200 Subject: [PATCH 259/474] adapter.aasx: allow deleting files from `SupplementaryFileContainer` `AbstractSupplementaryFileContainer` and `DictSupplementaryFileContainer` are extended by a `delete_file()` method, that allows deleting files from them. Since different files may have the same content, references to the files contents in `DictSupplementaryFileContainer._store` are tracked via `_store_refcount`. A files contents are only deleted from `_store`, if all filenames referring to these these contents are deleted, i.e. if the refcount reaches 0. --- basyx/aas/adapter/aasx.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index e0bb191..0e9e733 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -776,6 +776,13 @@ def write_file(self, name: str, file: IO[bytes]) -> None: """ pass # pragma: no cover + @abc.abstractmethod + def delete_file(self, name: str) -> None: + """ + Deletes a file from this SupplementaryFileContainer given its name. + """ + pass # pragma: no cover + @abc.abstractmethod def __contains__(self, item: str) -> bool: """ @@ -800,18 +807,23 @@ def __init__(self): self._store: Dict[bytes, bytes] = {} # Maps file names to (sha256, content_type) self._name_map: Dict[str, Tuple[bytes, str]] = {} + # Tracks the number of references to _store keys, + # i.e. the number of different filenames referring to the same file + self._store_refcount: Dict[bytes, int] = {} def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: data = file.read() hash = hashlib.sha256(data).digest() if hash not in self._store: self._store[hash] = data + self._store_refcount[hash] = 0 name_map_data = (hash, content_type) new_name = name i = 1 while True: if new_name not in self._name_map: self._name_map[new_name] = name_map_data + self._store_refcount[hash] += 1 return new_name elif self._name_map[new_name] == name_map_data: return new_name @@ -837,6 +849,16 @@ def get_sha256(self, name: str) -> bytes: def write_file(self, name: str, file: IO[bytes]) -> None: file.write(self._store[self._name_map[name][0]]) + def delete_file(self, name: str) -> None: + # The number of different files with the same content are kept track of via _store_refcount. + # The contents are only deleted, once the refcount reaches zero. + hash: bytes = self._name_map[name][0] + self._store_refcount[hash] -= 1 + if self._store_refcount[hash] == 0: + del self._store[hash] + del self._store_refcount[hash] + del self._name_map[name] + def __contains__(self, item: object) -> bool: return item in self._name_map From 2e4e5e328c97880261b02b280764fbd3bb568a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 23:59:00 +0200 Subject: [PATCH 260/474] adapter.http: allow changing the API base path --- basyx/aas/adapter/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 40068a8..bec1a1a 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -398,10 +398,10 @@ def to_python(self, value: str) -> List[str]: class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore): + def __init__(self, object_store: model.AbstractObjectStore, base_path: str = "/api/v3.0"): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ - Submount("/api/v3.0", [ + Submount(base_path, [ Submount("/serialization", [ Rule("/", methods=["GET"], endpoint=self.not_implemented) ]), From 02a9dd5320a40a463e915a4538307a23dc6da787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 22:48:19 +0200 Subject: [PATCH 261/474] adapter.http: fix a `DeprecationWarning` DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC) --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index bec1a1a..9b00395 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -54,7 +54,7 @@ def __init__(self, code: str, text: str, message_type: MessageType = MessageType self.code: str = code self.text: str = text self.message_type: MessageType = message_type - self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.utcnow() + self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.now(datetime.UTC) class Result: From aa0d8392c41470984bd703b2967509e2be1e24f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 23:43:02 +0200 Subject: [PATCH 262/474] adapter.http: allow retrieving and modifying `File` attachments via API This change makes use of the `SupplementaryFileContainer` interface of the AASX adapter. It allows the API to operate seamlessly on AASX files, including the contained supplementary files, without having to access the filesystem. Furthermore, the support for the modification of `Blob` values is removed (the spec prohibits it). --- basyx/aas/adapter/http.py | 79 ++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 9b00395..fec2ed3 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -32,6 +32,7 @@ from ._generic import XML_NS_MAP from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder +from . import aasx from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple, Any @@ -54,7 +55,7 @@ def __init__(self, code: str, text: str, message_type: MessageType = MessageType self.code: str = code self.text: str = text self.message_type: MessageType = message_type - self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.now(datetime.UTC) + self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.utcnow() class Result: @@ -398,10 +399,11 @@ def to_python(self, value: str) -> List[str]: class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore, base_path: str = "/api/v3.0"): + def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.AbstractSupplementaryFileContainer): self.object_store: model.AbstractObjectStore = object_store + self.file_store: aasx.AbstractSupplementaryFileContainer = file_store self.url_map = werkzeug.routing.Map([ - Submount(base_path, [ + Submount("/api/v3.0", [ Submount("/serialization", [ Rule("/", methods=["GET"], endpoint=self.not_implemented) ]), @@ -989,19 +991,55 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - if not isinstance(submodel_element, model.Blob): - raise BadRequest(f"{submodel_element!r} is not a blob, no file content to download!") - return Response(submodel_element.value, content_type=submodel_element.content_type) + if not isinstance(submodel_element, (model.Blob, model.File)): + raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to download!") + if submodel_element.value is None: + raise NotFound(f"{submodel_element!r} has no attachment!") + + value: bytes + if isinstance(submodel_element, model.Blob): + value = submodel_element.value + else: + if not submodel_element.value.startswith("/"): + raise BadRequest(f"{submodel_element!r} references an external file: {submodel_element.value}") + bytes_io = io.BytesIO() + try: + self.file_store.write_file(submodel_element.value, bytes_io) + except KeyError: + raise NotFound(f"No such file: {submodel_element.value}") + value = bytes_io.getvalue() + + # Blob and File both have the content_type attribute + return Response(value, content_type=submodel_element.content_type) # type: ignore[attr-defined] def put_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + + # spec allows PUT only for File, not for Blob + if not isinstance(submodel_element, model.File): + raise BadRequest(f"{submodel_element!r} is not a File, no file content to update!") + + if submodel_element.value is not None: + raise Conflict(f"{submodel_element!r} already references a file!") + + filename = request.form.get('fileName') + if filename is None: + raise BadRequest(f"No 'fileName' specified!") + + if not filename.startswith("/"): + raise BadRequest(f"Given 'fileName' doesn't start with a slash (/): {filename}") + file_storage: Optional[FileStorage] = request.files.get('file') if file_storage is None: raise BadRequest(f"Missing file to upload") - if not isinstance(submodel_element, model.Blob): - raise BadRequest(f"{submodel_element!r} is not a blob, no file content to update!") - submodel_element.value = file_storage.read() + + if file_storage.mimetype != submodel_element.content_type: + raise werkzeug.exceptions.UnsupportedMediaType( + f"Request body is of type {file_storage.mimetype!r}, " + f"while {submodel_element!r} has content_type {submodel_element.content_type!r}!") + + submodel_element.value = self.file_store.add_file(filename, file_storage.stream, submodel_element.content_type) submodel_element.commit() return response_t() @@ -1009,9 +1047,23 @@ def delete_submodel_submodel_element_attachment(self, request: Request, url_args -> Response: response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - if not isinstance(submodel_element, model.Blob): - raise BadRequest(f"{submodel_element!r} is not a blob, no file content to delete!") - submodel_element.value = None + if not isinstance(submodel_element, (model.Blob, model.File)): + raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to delete!") + + if submodel_element.value is None: + raise NotFound(f"{submodel_element!r} has no attachment!") + + if isinstance(submodel_element, model.Blob): + submodel_element.value = None + else: + if not submodel_element.value.startswith("/"): + raise BadRequest(f"{submodel_element!r} references an external file: {submodel_element.value}") + try: + self.file_store.delete_file(submodel_element.value) + except KeyError: + pass + submodel_element.value = None + submodel_element.commit() return response_t() @@ -1084,4 +1136,5 @@ def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args if __name__ == "__main__": from werkzeug.serving import run_simple from basyx.aas.examples.data.example_aas import create_full_example - run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) + run_simple("localhost", 8080, WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()), + use_debugger=True, use_reloader=True) From 5e5f0e9fd06be84d2535caeebee8d41eb62136ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 19 Jun 2024 17:27:03 +0200 Subject: [PATCH 263/474] adapter.http: remove nonfunctional 'Not Implemented' check This check was intended to return 501 instead of 404 for routes that haven't been implemented. However, we explicitly implement these routes to return 501 now anyway, returning 501 for all other paths would be semantically incorrect anyway and the check never worked. --- basyx/aas/adapter/http.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index fec2ed3..e3526bb 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -698,8 +698,6 @@ def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = map_adapter.match() - if endpoint is None: - raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") # TODO: remove this 'type: ignore' comment once the werkzeug type annotations have been fixed # https://github.com/pallets/werkzeug/issues/2836 return endpoint(request, values, map_adapter=map_adapter) # type: ignore[operator] From 959c6e547240947c75f3e74e6edd139b0e873547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 20 Jun 2024 00:15:57 +0200 Subject: [PATCH 264/474] adapter.xml, test.adapter.xml: add lxml typechecking Now that we require `lxml-stubs`, we can remove the `type: ignore` coments from the `lxml` imports. To make mypy happy, many typehints are adjusted, mostly `etree.Element` -> `etree._Element`. --- basyx/aas/adapter/xml/xml_deserialization.py | 143 +++++++-------- basyx/aas/adapter/xml/xml_serialization.py | 172 ++++++++++--------- 2 files changed, 160 insertions(+), 155 deletions(-) diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index 0f379aa..b053c24 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -43,7 +43,7 @@ """ from ... import model -from lxml import etree # type: ignore +from lxml import etree import logging import base64 import enum @@ -78,7 +78,7 @@ def _str_to_bool(string: str) -> bool: return string == "true" -def _tag_replace_namespace(tag: str, nsmap: Dict[str, str]) -> str: +def _tag_replace_namespace(tag: str, nsmap: Dict[Optional[str], str]) -> str: """ Attempts to replace the namespace in front of a tag with the prefix used in the xml document. @@ -93,7 +93,7 @@ def _tag_replace_namespace(tag: str, nsmap: Dict[str, str]) -> str: return tag -def _element_pretty_identifier(element: etree.Element) -> str: +def _element_pretty_identifier(element: etree._Element) -> str: """ Returns a pretty element identifier for a given XML element. @@ -130,7 +130,7 @@ def _exception_to_str(exception: BaseException) -> str: return string[1:-1] if isinstance(exception, KeyError) else string -def _get_child_mandatory(parent: etree.Element, child_tag: str) -> etree.Element: +def _get_child_mandatory(parent: etree._Element, child_tag: str) -> etree._Element: """ A helper function for getting a mandatory child element. @@ -146,7 +146,7 @@ def _get_child_mandatory(parent: etree.Element, child_tag: str) -> etree.Element return child -def _get_all_children_expect_tag(parent: etree.Element, expected_tag: str, failsafe: bool) -> Iterable[etree.Element]: +def _get_all_children_expect_tag(parent: etree._Element, expected_tag: str, failsafe: bool) -> Iterable[etree._Element]: """ Iterates over all children, matching the tag. @@ -169,7 +169,7 @@ def _get_all_children_expect_tag(parent: etree.Element, expected_tag: str, fails yield child -def _get_attrib_mandatory(element: etree.Element, attrib: str) -> str: +def _get_attrib_mandatory(element: etree._Element, attrib: str) -> str: """ A helper function for getting a mandatory attribute of an element. @@ -180,10 +180,10 @@ def _get_attrib_mandatory(element: etree.Element, attrib: str) -> str: """ if attrib not in element.attrib: raise KeyError(f"{_element_pretty_identifier(element)} has no attribute with name {attrib}!") - return element.attrib[attrib] + return element.attrib[attrib] # type: ignore[return-value] -def _get_attrib_mandatory_mapped(element: etree.Element, attrib: str, dct: Dict[str, T]) -> T: +def _get_attrib_mandatory_mapped(element: etree._Element, attrib: str, dct: Dict[str, T]) -> T: """ A helper function for getting a mapped mandatory attribute of an xml element. @@ -204,7 +204,7 @@ def _get_attrib_mandatory_mapped(element: etree.Element, attrib: str, dct: Dict[ return dct[attrib_value] -def _get_text_or_none(element: Optional[etree.Element]) -> Optional[str]: +def _get_text_or_none(element: Optional[etree._Element]) -> Optional[str]: """ A helper function for getting the text of an element, when it's not clear whether the element exists or not. @@ -220,7 +220,7 @@ def _get_text_or_none(element: Optional[etree.Element]) -> Optional[str]: return element.text if element is not None else None -def _get_text_mapped_or_none(element: Optional[etree.Element], dct: Dict[str, T]) -> Optional[T]: +def _get_text_mapped_or_none(element: Optional[etree._Element], dct: Dict[str, T]) -> Optional[T]: """ Returns dct[element.text] or None, if the element is None, has no text or the text is not in dct. @@ -234,7 +234,7 @@ def _get_text_mapped_or_none(element: Optional[etree.Element], dct: Dict[str, T] return dct[text] -def _get_text_mandatory(element: etree.Element) -> str: +def _get_text_mandatory(element: etree._Element) -> str: """ A helper function for getting the mandatory text of an element. @@ -248,7 +248,7 @@ def _get_text_mandatory(element: etree.Element) -> str: return text -def _get_text_mandatory_mapped(element: etree.Element, dct: Dict[str, T]) -> T: +def _get_text_mandatory_mapped(element: etree._Element, dct: Dict[str, T]) -> T: """ A helper function for getting the mapped mandatory text of an element. @@ -267,7 +267,7 @@ def _get_text_mandatory_mapped(element: etree.Element, dct: Dict[str, T]) -> T: return dct[text] -def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[..., T], failsafe: bool, +def _failsafe_construct(element: Optional[etree._Element], constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Optional[T]: """ A wrapper function that is used to handle exceptions raised in constructor functions. @@ -303,7 +303,7 @@ def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[ return None -def _failsafe_construct_mandatory(element: etree.Element, constructor: Callable[..., T], **kwargs: Any) -> T: +def _failsafe_construct_mandatory(element: etree._Element, constructor: Callable[..., T], **kwargs: Any) -> T: """ _failsafe_construct() but not failsafe and it returns T instead of Optional[T] @@ -321,7 +321,7 @@ def _failsafe_construct_mandatory(element: etree.Element, constructor: Callable[ return constructed -def _failsafe_construct_multiple(elements: Iterable[etree.Element], constructor: Callable[..., T], failsafe: bool, +def _failsafe_construct_multiple(elements: Iterable[etree._Element], constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Iterable[T]: """ A generator function that applies _failsafe_construct() to multiple elements. @@ -340,7 +340,7 @@ def _failsafe_construct_multiple(elements: Iterable[etree.Element], constructor: yield parsed -def _child_construct_mandatory(parent: etree.Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any) \ +def _child_construct_mandatory(parent: etree._Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any) \ -> T: """ Shorthand for _failsafe_construct_mandatory() in combination with _get_child_mandatory(). @@ -354,7 +354,7 @@ def _child_construct_mandatory(parent: etree.Element, child_tag: str, constructo return _failsafe_construct_mandatory(_get_child_mandatory(parent, child_tag), constructor, **kwargs) -def _child_construct_multiple(parent: etree.Element, expected_tag: str, constructor: Callable[..., T], +def _child_construct_multiple(parent: etree._Element, expected_tag: str, constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Iterable[T]: """ Shorthand for _failsafe_construct_multiple() in combination with _get_child_multiple(). @@ -371,7 +371,7 @@ def _child_construct_multiple(parent: etree.Element, expected_tag: str, construc failsafe, **kwargs) -def _child_text_mandatory(parent: etree.Element, child_tag: str) -> str: +def _child_text_mandatory(parent: etree._Element, child_tag: str) -> str: """ Shorthand for _get_text_mandatory() in combination with _get_child_mandatory(). @@ -382,7 +382,7 @@ def _child_text_mandatory(parent: etree.Element, child_tag: str) -> str: return _get_text_mandatory(_get_child_mandatory(parent, child_tag)) -def _child_text_mandatory_mapped(parent: etree.Element, child_tag: str, dct: Dict[str, T]) -> T: +def _child_text_mandatory_mapped(parent: etree._Element, child_tag: str, dct: Dict[str, T]) -> T: """ Shorthand for _get_text_mandatory_mapped() in combination with _get_child_mandatory(). @@ -394,7 +394,7 @@ def _child_text_mandatory_mapped(parent: etree.Element, child_tag: str, dct: Dic return _get_text_mandatory_mapped(_get_child_mandatory(parent, child_tag), dct) -def _get_kind(element: etree.Element) -> model.ModellingKind: +def _get_kind(element: etree._Element) -> model.ModellingKind: """ Returns the modelling kind of an element with the default value INSTANCE, if none specified. @@ -405,7 +405,7 @@ def _get_kind(element: etree.Element) -> model.ModellingKind: return modelling_kind if modelling_kind is not None else model.ModellingKind.INSTANCE -def _expect_reference_type(element: etree.Element, expected_type: Type[model.Reference]) -> None: +def _expect_reference_type(element: etree._Element, expected_type: Type[model.Reference]) -> None: """ Validates the type attribute of a Reference. @@ -431,7 +431,7 @@ class AASFromXmlDecoder: stripped = False @classmethod - def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None: + def _amend_abstract_attributes(cls, obj: object, element: etree._Element) -> None: """ A helper function that amends optional attributes to already constructed class instances, if they inherit from an abstract class like Referable, Identifiable, HasSemantics or Qualifiable. @@ -490,7 +490,7 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None obj.extension.add(extension) @classmethod - def _construct_relationship_element_internal(cls, element: etree.Element, object_class: Type[RE], **_kwargs: Any) \ + def _construct_relationship_element_internal(cls, element: etree._Element, object_class: Type[RE], **_kwargs: Any) \ -> RE: """ Helper function used by construct_relationship_element() and construct_annotated_relationship_element() @@ -505,7 +505,7 @@ def _construct_relationship_element_internal(cls, element: etree.Element, object return relationship_element @classmethod - def _construct_key_tuple(cls, element: etree.Element, namespace: str = NS_AAS, **_kwargs: Any) \ + def _construct_key_tuple(cls, element: etree._Element, namespace: str = NS_AAS, **_kwargs: Any) \ -> Tuple[model.Key, ...]: """ Helper function used by construct_reference() and construct_aas_reference() to reduce duplicate code @@ -514,7 +514,7 @@ def _construct_key_tuple(cls, element: etree.Element, namespace: str = NS_AAS, * return tuple(_child_construct_multiple(keys, namespace + "key", cls.construct_key, cls.failsafe)) @classmethod - def _construct_submodel_reference(cls, element: etree.Element, **kwargs: Any) \ + def _construct_submodel_reference(cls, element: etree._Element, **kwargs: Any) \ -> model.ModelReference[model.Submodel]: """ Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. @@ -522,7 +522,7 @@ def _construct_submodel_reference(cls, element: etree.Element, **kwargs: Any) \ return cls.construct_model_reference_expect_type(element, model.Submodel, **kwargs) @classmethod - def _construct_asset_administration_shell_reference(cls, element: etree.Element, **kwargs: Any) \ + def _construct_asset_administration_shell_reference(cls, element: etree._Element, **kwargs: Any) \ -> model.ModelReference[model.AssetAdministrationShell]: """ Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. @@ -530,7 +530,7 @@ def _construct_asset_administration_shell_reference(cls, element: etree.Element, return cls.construct_model_reference_expect_type(element, model.AssetAdministrationShell, **kwargs) @classmethod - def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ + def _construct_referable_reference(cls, element: etree._Element, **kwargs: Any) \ -> model.ModelReference[model.Referable]: """ Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. @@ -540,7 +540,7 @@ def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \ return cls.construct_model_reference_expect_type(element, model.Referable, **kwargs) # type: ignore @classmethod - def _construct_operation_variable(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement: + def _construct_operation_variable(cls, element: etree._Element, **kwargs: Any) -> model.SubmodelElement: """ Since we don't implement ``OperationVariable``, this constructor discards the wrapping `aas:operationVariable` and `aas:value` and just returns the contained :class:`~basyx.aas.model.submodel.SubmodelElement`. @@ -554,7 +554,7 @@ def _construct_operation_variable(cls, element: etree.Element, **kwargs: Any) -> return cls.construct_submodel_element(value[0], **kwargs) @classmethod - def construct_key(cls, element: etree.Element, object_class=model.Key, **_kwargs: Any) \ + def construct_key(cls, element: etree._Element, object_class=model.Key, **_kwargs: Any) \ -> model.Key: return object_class( _child_text_mandatory_mapped(element, NS_AAS + "type", KEY_TYPES_INVERSE), @@ -562,7 +562,7 @@ def construct_key(cls, element: etree.Element, object_class=model.Key, **_kwargs ) @classmethod - def construct_reference(cls, element: etree.Element, namespace: str = NS_AAS, **kwargs: Any) -> model.Reference: + def construct_reference(cls, element: etree._Element, namespace: str = NS_AAS, **kwargs: Any) -> model.Reference: reference_type: Type[model.Reference] = _child_text_mandatory_mapped(element, NS_AAS + "type", REFERENCE_TYPES_INVERSE) references: Dict[Type[model.Reference], Callable[..., model.Reference]] = { @@ -574,7 +574,7 @@ def construct_reference(cls, element: etree.Element, namespace: str = NS_AAS, ** return references[reference_type](element, namespace=namespace, **kwargs) @classmethod - def construct_external_reference(cls, element: etree.Element, namespace: str = NS_AAS, + def construct_external_reference(cls, element: etree._Element, namespace: str = NS_AAS, object_class=model.ExternalReference, **_kwargs: Any) \ -> model.ExternalReference: _expect_reference_type(element, model.ExternalReference) @@ -583,7 +583,7 @@ def construct_external_reference(cls, element: etree.Element, namespace: str = N cls.failsafe, namespace=namespace)) @classmethod - def construct_model_reference(cls, element: etree.Element, object_class=model.ModelReference, **_kwargs: Any) \ + def construct_model_reference(cls, element: etree._Element, object_class=model.ModelReference, **_kwargs: Any) \ -> model.ModelReference: """ This constructor for ModelReference determines the type of the ModelReference by its keys. If no keys are @@ -600,7 +600,7 @@ def construct_model_reference(cls, element: etree.Element, object_class=model.Mo cls.construct_reference, cls.failsafe)) @classmethod - def construct_model_reference_expect_type(cls, element: etree.Element, type_: Type[model.base._RT], + def construct_model_reference_expect_type(cls, element: etree._Element, type_: Type[model.base._RT], object_class=model.ModelReference, **_kwargs: Any) \ -> model.ModelReference[model.base._RT]: """ @@ -617,7 +617,7 @@ def construct_model_reference_expect_type(cls, element: etree.Element, type_: Ty cls.construct_reference, cls.failsafe)) @classmethod - def construct_administrative_information(cls, element: etree.Element, object_class=model.AdministrativeInformation, + def construct_administrative_information(cls, element: etree._Element, object_class=model.AdministrativeInformation, **_kwargs: Any) -> model.AdministrativeInformation: administrative_information = object_class( revision=_get_text_or_none(element.find(NS_AAS + "revision")), @@ -631,7 +631,7 @@ def construct_administrative_information(cls, element: etree.Element, object_cla return administrative_information @classmethod - def construct_lang_string_set(cls, element: etree.Element, expected_tag: str, object_class: Type[LSS], + def construct_lang_string_set(cls, element: etree._Element, expected_tag: str, object_class: Type[LSS], **_kwargs: Any) -> LSS: collected_lang_strings: Dict[str, str] = {} for lang_string_elem in _get_all_children_expect_tag(element, expected_tag, cls.failsafe): @@ -640,36 +640,36 @@ def construct_lang_string_set(cls, element: etree.Element, expected_tag: str, ob return object_class(collected_lang_strings) @classmethod - def construct_multi_language_name_type(cls, element: etree.Element, object_class=model.MultiLanguageNameType, + def construct_multi_language_name_type(cls, element: etree._Element, object_class=model.MultiLanguageNameType, **kwargs: Any) -> model.MultiLanguageNameType: return cls.construct_lang_string_set(element, NS_AAS + "langStringNameType", object_class, **kwargs) @classmethod - def construct_multi_language_text_type(cls, element: etree.Element, object_class=model.MultiLanguageTextType, + def construct_multi_language_text_type(cls, element: etree._Element, object_class=model.MultiLanguageTextType, **kwargs: Any) -> model.MultiLanguageTextType: return cls.construct_lang_string_set(element, NS_AAS + "langStringTextType", object_class, **kwargs) @classmethod - def construct_definition_type_iec61360(cls, element: etree.Element, object_class=model.DefinitionTypeIEC61360, + def construct_definition_type_iec61360(cls, element: etree._Element, object_class=model.DefinitionTypeIEC61360, **kwargs: Any) -> model.DefinitionTypeIEC61360: return cls.construct_lang_string_set(element, NS_AAS + "langStringDefinitionTypeIec61360", object_class, **kwargs) @classmethod - def construct_preferred_name_type_iec61360(cls, element: etree.Element, + def construct_preferred_name_type_iec61360(cls, element: etree._Element, object_class=model.PreferredNameTypeIEC61360, **kwargs: Any) -> model.PreferredNameTypeIEC61360: return cls.construct_lang_string_set(element, NS_AAS + "langStringPreferredNameTypeIec61360", object_class, **kwargs) @classmethod - def construct_short_name_type_iec61360(cls, element: etree.Element, object_class=model.ShortNameTypeIEC61360, + def construct_short_name_type_iec61360(cls, element: etree._Element, object_class=model.ShortNameTypeIEC61360, **kwargs: Any) -> model.ShortNameTypeIEC61360: return cls.construct_lang_string_set(element, NS_AAS + "langStringShortNameTypeIec61360", object_class, **kwargs) @classmethod - def construct_qualifier(cls, element: etree.Element, object_class=model.Qualifier, **_kwargs: Any) \ + def construct_qualifier(cls, element: etree._Element, object_class=model.Qualifier, **_kwargs: Any) \ -> model.Qualifier: qualifier = object_class( _child_text_mandatory(element, NS_AAS + "type"), @@ -688,7 +688,7 @@ def construct_qualifier(cls, element: etree.Element, object_class=model.Qualifie return qualifier @classmethod - def construct_extension(cls, element: etree.Element, object_class=model.Extension, **_kwargs: Any) \ + def construct_extension(cls, element: etree._Element, object_class=model.Extension, **_kwargs: Any) \ -> model.Extension: extension = object_class( _child_text_mandatory(element, NS_AAS + "name")) @@ -707,7 +707,7 @@ def construct_extension(cls, element: etree.Element, object_class=model.Extensio return extension @classmethod - def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement: + def construct_submodel_element(cls, element: etree._Element, **kwargs: Any) -> model.SubmodelElement: """ This function doesn't support the object_class parameter. Overwrite each individual SubmodelElement/DataElement constructor function instead. @@ -727,7 +727,7 @@ def construct_submodel_element(cls, element: etree.Element, **kwargs: Any) -> mo return submodel_elements[element.tag](element, **kwargs) @classmethod - def construct_data_element(cls, element: etree.Element, abstract_class_name: str = "DataElement", **kwargs: Any) \ + def construct_data_element(cls, element: etree._Element, abstract_class_name: str = "DataElement", **kwargs: Any) \ -> model.DataElement: """ This function does not support the object_class parameter. @@ -746,7 +746,7 @@ def construct_data_element(cls, element: etree.Element, abstract_class_name: str return data_elements[element.tag](element, **kwargs) @classmethod - def construct_annotated_relationship_element(cls, element: etree.Element, + def construct_annotated_relationship_element(cls, element: etree._Element, object_class=model.AnnotatedRelationshipElement, **_kwargs: Any) \ -> model.AnnotatedRelationshipElement: annotated_relationship_element = cls._construct_relationship_element_internal(element, object_class) @@ -759,7 +759,7 @@ def construct_annotated_relationship_element(cls, element: etree.Element, return annotated_relationship_element @classmethod - def construct_basic_event_element(cls, element: etree.Element, object_class=model.BasicEventElement, + def construct_basic_event_element(cls, element: etree._Element, object_class=model.BasicEventElement, **_kwargs: Any) -> model.BasicEventElement: basic_event_element = object_class( None, @@ -787,7 +787,7 @@ def construct_basic_event_element(cls, element: etree.Element, object_class=mode return basic_event_element @classmethod - def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: + def construct_blob(cls, element: etree._Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: blob = object_class( None, _child_text_mandatory(element, NS_AAS + "contentType") @@ -799,14 +799,14 @@ def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwar return blob @classmethod - def construct_capability(cls, element: etree.Element, object_class=model.Capability, **_kwargs: Any) \ + def construct_capability(cls, element: etree._Element, object_class=model.Capability, **_kwargs: Any) \ -> model.Capability: capability = object_class(None) cls._amend_abstract_attributes(capability, element) return capability @classmethod - def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_kwargs: Any) -> model.Entity: + def construct_entity(cls, element: etree._Element, object_class=model.Entity, **_kwargs: Any) -> model.Entity: specific_asset_id = set() specific_assset_ids = element.find(NS_AAS + "specificAssetIds") if specific_assset_ids is not None: @@ -830,7 +830,7 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ return entity @classmethod - def construct_file(cls, element: etree.Element, object_class=model.File, **_kwargs: Any) -> model.File: + def construct_file(cls, element: etree._Element, object_class=model.File, **_kwargs: Any) -> model.File: file = object_class( None, _child_text_mandatory(element, NS_AAS + "contentType") @@ -842,7 +842,7 @@ def construct_file(cls, element: etree.Element, object_class=model.File, **_kwar return file @classmethod - def construct_resource(cls, element: etree.Element, object_class=model.Resource, **_kwargs: Any) -> model.Resource: + def construct_resource(cls, element: etree._Element, object_class=model.Resource, **_kwargs: Any) -> model.Resource: resource = object_class( _child_text_mandatory(element, NS_AAS + "path") ) @@ -853,7 +853,7 @@ def construct_resource(cls, element: etree.Element, object_class=model.Resource, return resource @classmethod - def construct_multi_language_property(cls, element: etree.Element, object_class=model.MultiLanguageProperty, + def construct_multi_language_property(cls, element: etree._Element, object_class=model.MultiLanguageProperty, **_kwargs: Any) -> model.MultiLanguageProperty: multi_language_property = object_class(None) value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_multi_language_text_type, @@ -867,7 +867,7 @@ def construct_multi_language_property(cls, element: etree.Element, object_class= return multi_language_property @classmethod - def construct_operation(cls, element: etree.Element, object_class=model.Operation, **_kwargs: Any) \ + def construct_operation(cls, element: etree._Element, object_class=model.Operation, **_kwargs: Any) \ -> model.Operation: operation = object_class(None) for tag, target in ((NS_AAS + "inputVariables", operation.input_variable), @@ -882,7 +882,7 @@ def construct_operation(cls, element: etree.Element, object_class=model.Operatio return operation @classmethod - def construct_property(cls, element: etree.Element, object_class=model.Property, **_kwargs: Any) -> model.Property: + def construct_property(cls, element: etree._Element, object_class=model.Property, **_kwargs: Any) -> model.Property: property_ = object_class( None, value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) @@ -897,7 +897,7 @@ def construct_property(cls, element: etree.Element, object_class=model.Property, return property_ @classmethod - def construct_range(cls, element: etree.Element, object_class=model.Range, **_kwargs: Any) -> model.Range: + def construct_range(cls, element: etree._Element, object_class=model.Range, **_kwargs: Any) -> model.Range: range_ = object_class( None, value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) @@ -912,7 +912,7 @@ def construct_range(cls, element: etree.Element, object_class=model.Range, **_kw return range_ @classmethod - def construct_reference_element(cls, element: etree.Element, object_class=model.ReferenceElement, **_kwargs: Any) \ + def construct_reference_element(cls, element: etree._Element, object_class=model.ReferenceElement, **_kwargs: Any) \ -> model.ReferenceElement: reference_element = object_class(None) value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_reference, cls.failsafe) @@ -922,12 +922,13 @@ def construct_reference_element(cls, element: etree.Element, object_class=model. return reference_element @classmethod - def construct_relationship_element(cls, element: etree.Element, object_class=model.RelationshipElement, + def construct_relationship_element(cls, element: etree._Element, object_class=model.RelationshipElement, **_kwargs: Any) -> model.RelationshipElement: return cls._construct_relationship_element_internal(element, object_class=object_class, **_kwargs) @classmethod - def construct_submodel_element_collection(cls, element: etree.Element, object_class=model.SubmodelElementCollection, + def construct_submodel_element_collection(cls, element: etree._Element, + object_class=model.SubmodelElementCollection, **_kwargs: Any) -> model.SubmodelElementCollection: collection = object_class(None) if not cls.stripped: @@ -940,7 +941,7 @@ def construct_submodel_element_collection(cls, element: etree.Element, object_cl return collection @classmethod - def construct_submodel_element_list(cls, element: etree.Element, object_class=model.SubmodelElementList, + def construct_submodel_element_list(cls, element: etree._Element, object_class=model.SubmodelElementList, **_kwargs: Any) -> model.SubmodelElementList: type_value_list_element = KEY_TYPES_CLASSES_INVERSE[ _child_text_mandatory_mapped(element, NS_AAS + "typeValueListElement", KEY_TYPES_INVERSE)] @@ -966,7 +967,7 @@ def construct_submodel_element_list(cls, element: etree.Element, object_class=mo return list_ @classmethod - def construct_asset_administration_shell(cls, element: etree.Element, object_class=model.AssetAdministrationShell, + def construct_asset_administration_shell(cls, element: etree._Element, object_class=model.AssetAdministrationShell, **_kwargs: Any) -> model.AssetAdministrationShell: aas = object_class( id_=_child_text_mandatory(element, NS_AAS + "id"), @@ -987,7 +988,7 @@ def construct_asset_administration_shell(cls, element: etree.Element, object_cla return aas @classmethod - def construct_specific_asset_id(cls, element: etree.Element, object_class=model.SpecificAssetId, + def construct_specific_asset_id(cls, element: etree._Element, object_class=model.SpecificAssetId, **_kwargs: Any) -> model.SpecificAssetId: # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable return object_class( @@ -999,7 +1000,7 @@ def construct_specific_asset_id(cls, element: etree.Element, object_class=model. ) @classmethod - def construct_asset_information(cls, element: etree.Element, object_class=model.AssetInformation, **_kwargs: Any) \ + def construct_asset_information(cls, element: etree._Element, object_class=model.AssetInformation, **_kwargs: Any) \ -> model.AssetInformation: specific_asset_id = set() specific_assset_ids = element.find(NS_AAS + "specificAssetIds") @@ -1026,7 +1027,7 @@ def construct_asset_information(cls, element: etree.Element, object_class=model. return asset_information @classmethod - def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **_kwargs: Any) \ + def construct_submodel(cls, element: etree._Element, object_class=model.Submodel, **_kwargs: Any) \ -> model.Submodel: submodel = object_class( _child_text_mandatory(element, NS_AAS + "id"), @@ -1042,13 +1043,13 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, return submodel @classmethod - def construct_value_reference_pair(cls, element: etree.Element, object_class=model.ValueReferencePair, + def construct_value_reference_pair(cls, element: etree._Element, object_class=model.ValueReferencePair, **_kwargs: Any) -> model.ValueReferencePair: return object_class(_child_text_mandatory(element, NS_AAS + "value"), _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference)) @classmethod - def construct_value_list(cls, element: etree.Element, **_kwargs: Any) -> model.ValueList: + def construct_value_list(cls, element: etree._Element, **_kwargs: Any) -> model.ValueList: """ This function doesn't support the object_class parameter, because ValueList is just a generic type alias. """ @@ -1060,7 +1061,7 @@ def construct_value_list(cls, element: etree.Element, **_kwargs: Any) -> model.V ) @classmethod - def construct_concept_description(cls, element: etree.Element, object_class=model.ConceptDescription, + def construct_concept_description(cls, element: etree._Element, object_class=model.ConceptDescription, **_kwargs: Any) -> model.ConceptDescription: cd = object_class(_child_text_mandatory(element, NS_AAS + "id")) is_case_of = element.find(NS_AAS + "isCaseOf") @@ -1072,7 +1073,8 @@ def construct_concept_description(cls, element: etree.Element, object_class=mode return cd @classmethod - def construct_embedded_data_specification(cls, element: etree.Element, object_class=model.EmbeddedDataSpecification, + def construct_embedded_data_specification(cls, element: etree._Element, + object_class=model.EmbeddedDataSpecification, **_kwargs: Any) -> model.EmbeddedDataSpecification: data_specification_content = _get_child_mandatory(element, NS_AAS + "dataSpecificationContent") if len(data_specification_content) == 0: @@ -1088,7 +1090,7 @@ def construct_embedded_data_specification(cls, element: etree.Element, object_cl return embedded_data_specification @classmethod - def construct_data_specification_content(cls, element: etree.Element, **kwargs: Any) \ + def construct_data_specification_content(cls, element: etree._Element, **kwargs: Any) \ -> model.DataSpecificationContent: """ This function doesn't support the object_class parameter. @@ -1103,7 +1105,8 @@ def construct_data_specification_content(cls, element: etree.Element, **kwargs: return data_specification_contents[element.tag](element, **kwargs) @classmethod - def construct_data_specification_iec61360(cls, element: etree.Element, object_class=model.DataSpecificationIEC61360, + def construct_data_specification_iec61360(cls, element: etree._Element, + object_class=model.DataSpecificationIEC61360, **_kwargs: Any) -> model.DataSpecificationIEC61360: ds_iec = object_class(_child_construct_mandatory(element, NS_AAS + "preferredName", cls.construct_preferred_name_type_iec61360)) @@ -1187,7 +1190,7 @@ class StrictStrippedAASFromXmlDecoder(StrictAASFromXmlDecoder, StrippedAASFromXm pass -def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree.Element]: +def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree._Element]: """ Parse an XML document into an element tree diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 9d7abfb..6f962c8 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -15,8 +15,8 @@ :func:`write_aas_xml_file`. - For serializing any object to an XML fragment, that fits the XML specification from 'Details of the Asset Administration Shell', chapter 5.4, you can either use :func:`object_to_xml_element`, which serializes a given - object and returns it as :class:`~lxml.etree.Element`, **or** :func:`write_aas_xml_element`, which does the same - thing, but writes the :class:`~lxml.etree.Element` to a file instead of returning it. + object and returns it as :class:`~lxml.etree._Element`, **or** :func:`write_aas_xml_element`, which does the same + thing, but writes the :class:`~lxml.etree._Element` to a file instead of returning it. As a third alternative, you can also use the functions ``_to_xml()`` directly. .. attention:: @@ -31,7 +31,7 @@ write_aas_xml_file(fp, object_store) """ -from lxml import etree # type: ignore +from lxml import etree from typing import Callable, Dict, Optional, Type import base64 @@ -47,14 +47,14 @@ def _generate_element(name: str, text: Optional[str] = None, - attributes: Optional[Dict] = None) -> etree.Element: + attributes: Optional[Dict] = None) -> etree._Element: """ - generate an :class:`~lxml.etree.Element` object + generate an :class:`~lxml.etree._Element` object :param name: namespace+tag_name of the element :param text: Text of the element. Default is None :param attributes: Attributes of the elements in form of a dict ``{"attribute_name": "attribute_content"}`` - :return: :class:`~lxml.etree.Element` object + :return: :class:`~lxml.etree._Element` object """ et_element = etree.Element(name) if text: @@ -83,7 +83,7 @@ def boolean_to_xml(obj: bool) -> str: # ############################################################## -def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: +def abstract_classes_to_xml(tag: str, obj: object) -> etree._Element: """ Generates an XML element and adds attributes of abstract base classes of ``obj``. @@ -151,14 +151,14 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: def _value_to_xml(value: model.ValueDataType, value_type: model.DataTypeDefXsd, - tag: str = NS_AAS+"value") -> etree.Element: + tag: str = NS_AAS+"value") -> etree._Element: """ Serialization of objects of :class:`~basyx.aas.model.base.ValueDataType` to XML :param value: :class:`~basyx.aas.model.base.ValueDataType` object :param value_type: Corresponding :class:`~basyx.aas.model.base.DataTypeDefXsd` :param tag: tag of the serialized :class:`~basyx.aas.model.base.ValueDataType` object - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ # todo: add "{NS_XSI+"type": "xs:"+model.datatypes.XSD_TYPE_NAMES[value_type]}" as attribute, if the schema allows # it @@ -167,13 +167,13 @@ def _value_to_xml(value: model.ValueDataType, text=model.datatypes.xsd_repr(value)) -def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: +def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.LangStringSet` to XML :param obj: Object of class :class:`~basyx.aas.model.base.LangStringSet` :param tag: Namespace+Tag name of the returned XML element. - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ LANG_STRING_SET_TAGS: Dict[Type[model.LangStringSet], str] = {k: NS_AAS + v for k, v in { model.MultiLanguageNameType: "langStringNameType", @@ -192,13 +192,13 @@ def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree.Element: def administrative_information_to_xml(obj: model.AdministrativeInformation, - tag: str = NS_AAS+"administration") -> etree.Element: + tag: str = NS_AAS+"administration") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.AdministrativeInformation` to XML :param obj: Object of class :class:`~basyx.aas.model.base.AdministrativeInformation` :param tag: Namespace+Tag of the serialized element. Default is ``aas:administration`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_administration = abstract_classes_to_xml(tag, obj) if obj.version: @@ -212,12 +212,12 @@ def administrative_information_to_xml(obj: model.AdministrativeInformation, return et_administration -def data_element_to_xml(obj: model.DataElement) -> etree.Element: +def data_element_to_xml(obj: model.DataElement) -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.DataElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.DataElement` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ if isinstance(obj, model.MultiLanguageProperty): return multi_language_property_to_xml(obj) @@ -231,15 +231,16 @@ def data_element_to_xml(obj: model.DataElement) -> etree.Element: return file_to_xml(obj) if isinstance(obj, model.ReferenceElement): return reference_element_to_xml(obj) + raise AssertionError(f"Type {obj.__class__.__name__} is not yet supported by the XML serialization!") -def key_to_xml(obj: model.Key, tag: str = NS_AAS+"key") -> etree.Element: +def key_to_xml(obj: model.Key, tag: str = NS_AAS+"key") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.Key` to XML :param obj: Object of class :class:`~basyx.aas.model.base.Key` :param tag: Namespace+Tag of the returned element. Default is ``aas:key`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_key = _generate_element(tag) et_key.append(_generate_element(name=NS_AAS + "type", text=_generic.KEY_TYPES[obj.type])) @@ -247,13 +248,13 @@ def key_to_xml(obj: model.Key, tag: str = NS_AAS+"key") -> etree.Element: return et_key -def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etree.Element: +def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.Reference` to XML :param obj: Object of class :class:`~basyx.aas.model.base.Reference` :param tag: Namespace+Tag of the returned element. Default is ``aas:reference`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_reference = _generate_element(tag) et_reference.append(_generate_element(NS_AAS + "type", text=_generic.REFERENCE_TYPES[obj.__class__])) @@ -267,13 +268,13 @@ def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etr return et_reference -def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etree.Element: +def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.Qualifier` to XML :param obj: Object of class :class:`~basyx.aas.model.base.Qualifier` :param tag: Namespace+Tag of the serialized ElementTree object. Default is ``aas:qualifier`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_qualifier = abstract_classes_to_xml(tag, obj) et_qualifier.append(_generate_element(NS_AAS + "kind", text=_generic.QUALIFIER_KIND[obj.kind])) @@ -286,13 +287,13 @@ def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etr return et_qualifier -def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etree.Element: +def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.Extension` to XML :param obj: Object of class :class:`~basyx.aas.model.base.Extension` :param tag: Namespace+Tag of the serialized ElementTree object. Default is ``aas:extension`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_extension = abstract_classes_to_xml(tag, obj) et_extension.append(_generate_element(NS_AAS + "name", text=obj.name)) @@ -310,13 +311,13 @@ def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etr def value_reference_pair_to_xml(obj: model.ValueReferencePair, - tag: str = NS_AAS+"valueReferencePair") -> etree.Element: + tag: str = NS_AAS+"valueReferencePair") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.ValueReferencePair` to XML :param obj: Object of class :class:`~basyx.aas.model.base.ValueReferencePair` :param tag: Namespace+Tag of the serialized element. Default is ``aas:valueReferencePair`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_vrp = _generate_element(tag) # TODO: value_type isn't used at all by _value_to_xml(), thus we can ignore the type here for now @@ -326,7 +327,7 @@ def value_reference_pair_to_xml(obj: model.ValueReferencePair, def value_list_to_xml(obj: model.ValueList, - tag: str = NS_AAS+"valueList") -> etree.Element: + tag: str = NS_AAS+"valueList") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.ValueList` to XML @@ -334,7 +335,7 @@ def value_list_to_xml(obj: model.ValueList, :param obj: Object of class :class:`~basyx.aas.model.base.ValueList` :param tag: Namespace+Tag of the serialized element. Default is ``aas:valueList`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_value_list = _generate_element(tag) et_value_reference_pairs = _generate_element(NS_AAS+"valueReferencePairs") @@ -350,13 +351,13 @@ def value_list_to_xml(obj: model.ValueList, def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "specifidAssetId") \ - -> etree.Element: + -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.SpecificAssetId` to XML :param obj: Object of class :class:`~basyx.aas.model.base.SpecificAssetId` :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:identifierKeyValuePair`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_asset_information = abstract_classes_to_xml(tag, obj) et_asset_information.append(_generate_element(name=NS_AAS + "name", text=obj.name)) @@ -367,13 +368,13 @@ def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "sp return et_asset_information -def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"assetInformation") -> etree.Element: +def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"assetInformation") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.aas.AssetInformation` to XML :param obj: Object of class :class:`~basyx.aas.model.aas.AssetInformation` :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:assetInformation`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_asset_information = abstract_classes_to_xml(tag, obj) et_asset_information.append(_generate_element(name=NS_AAS + "assetKind", text=_generic.ASSET_KIND[obj.asset_kind])) @@ -393,13 +394,13 @@ def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"ass def concept_description_to_xml(obj: model.ConceptDescription, - tag: str = NS_AAS+"conceptDescription") -> etree.Element: + tag: str = NS_AAS+"conceptDescription") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.concept.ConceptDescription` to XML :param obj: Object of class :class:`~basyx.aas.model.concept.ConceptDescription` :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:conceptDescription`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_concept_description = abstract_classes_to_xml(tag, obj) if obj.is_case_of: @@ -411,13 +412,13 @@ def concept_description_to_xml(obj: model.ConceptDescription, def embedded_data_specification_to_xml(obj: model.EmbeddedDataSpecification, - tag: str = NS_AAS+"embeddedDataSpecification") -> etree.Element: + tag: str = NS_AAS+"embeddedDataSpecification") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.EmbeddedDataSpecification` to XML :param obj: Object of class :class:`~basyx.aas.model.base.EmbeddedDataSpecification` :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:embeddedDataSpecification`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_embedded_data_specification = abstract_classes_to_xml(tag, obj) et_embedded_data_specification.append(reference_to_xml(obj.data_specification, tag=NS_AAS + "dataSpecification")) @@ -426,13 +427,13 @@ def embedded_data_specification_to_xml(obj: model.EmbeddedDataSpecification, def data_specification_content_to_xml(obj: model.DataSpecificationContent, - tag: str = NS_AAS+"dataSpecificationContent") -> etree.Element: + tag: str = NS_AAS+"dataSpecificationContent") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.DataSpecificationContent` to XML :param obj: Object of class :class:`~basyx.aas.model.base.DataSpecificationContent` :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:dataSpecificationContent`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_data_specification_content = abstract_classes_to_xml(tag, obj) if isinstance(obj, model.DataSpecificationIEC61360): @@ -443,13 +444,13 @@ def data_specification_content_to_xml(obj: model.DataSpecificationContent, def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, - tag: str = NS_AAS+"dataSpecificationIec61360") -> etree.Element: + tag: str = NS_AAS+"dataSpecificationIec61360") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.DataSpecificationIEC61360` to XML :param obj: Object of class :class:`~basyx.aas.model.base.DataSpecificationIEC61360` :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:dataSpecificationIec61360`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_data_specification_iec61360 = abstract_classes_to_xml(tag, obj) et_data_specification_iec61360.append(lang_string_set_to_xml(obj.preferred_name, NS_AAS + "preferredName")) @@ -487,13 +488,13 @@ def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, - tag: str = NS_AAS+"assetAdministrationShell") -> etree.Element: + tag: str = NS_AAS+"assetAdministrationShell") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.aas.AssetAdministrationShell` to XML :param obj: Object of class :class:`~basyx.aas.model.aas.AssetAdministrationShell` :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:assetAdministrationShell`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_aas = abstract_classes_to_xml(tag, obj) if obj.derived_from: @@ -512,12 +513,12 @@ def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, # ############################################################## -def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: +def submodel_element_to_xml(obj: model.SubmodelElement) -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ if isinstance(obj, model.DataElement): return data_element_to_xml(obj) @@ -537,16 +538,17 @@ def submodel_element_to_xml(obj: model.SubmodelElement) -> etree.Element: return submodel_element_collection_to_xml(obj) if isinstance(obj, model.SubmodelElementList): return submodel_element_list_to_xml(obj) + raise AssertionError(f"Type {obj.__class__.__name__} is not yet supported by the XML serialization!") def submodel_to_xml(obj: model.Submodel, - tag: str = NS_AAS+"submodel") -> etree.Element: + tag: str = NS_AAS+"submodel") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.Submodel` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Submodel` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodel`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_submodel = abstract_classes_to_xml(tag, obj) if obj.submodel_element: @@ -558,13 +560,13 @@ def submodel_to_xml(obj: model.Submodel, def property_to_xml(obj: model.Property, - tag: str = NS_AAS+"property") -> etree.Element: + tag: str = NS_AAS+"property") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.Property` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Property` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:property`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_property = abstract_classes_to_xml(tag, obj) et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) @@ -576,13 +578,13 @@ def property_to_xml(obj: model.Property, def multi_language_property_to_xml(obj: model.MultiLanguageProperty, - tag: str = NS_AAS+"multiLanguageProperty") -> etree.Element: + tag: str = NS_AAS+"multiLanguageProperty") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.MultiLanguageProperty` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.MultiLanguageProperty` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:multiLanguageProperty`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_multi_language_property = abstract_classes_to_xml(tag, obj) if obj.value: @@ -593,13 +595,13 @@ def multi_language_property_to_xml(obj: model.MultiLanguageProperty, def range_to_xml(obj: model.Range, - tag: str = NS_AAS+"range") -> etree.Element: + tag: str = NS_AAS+"range") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.Range` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Range` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:range`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_range = abstract_classes_to_xml(tag, obj) et_range.append(_generate_element(name=NS_AAS + "valueType", @@ -612,13 +614,13 @@ def range_to_xml(obj: model.Range, def blob_to_xml(obj: model.Blob, - tag: str = NS_AAS+"blob") -> etree.Element: + tag: str = NS_AAS+"blob") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.Blob` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Blob` :param tag: Namespace+Tag of the serialized element. Default is ``aas:blob`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_blob = abstract_classes_to_xml(tag, obj) et_value = etree.Element(NS_AAS + "value") @@ -630,13 +632,13 @@ def blob_to_xml(obj: model.Blob, def file_to_xml(obj: model.File, - tag: str = NS_AAS+"file") -> etree.Element: + tag: str = NS_AAS+"file") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.File` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.File` :param tag: Namespace+Tag of the serialized element. Default is ``aas:file`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_file = abstract_classes_to_xml(tag, obj) if obj.value: @@ -646,13 +648,13 @@ def file_to_xml(obj: model.File, def resource_to_xml(obj: model.Resource, - tag: str = NS_AAS+"resource") -> etree.Element: + tag: str = NS_AAS+"resource") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.base.Resource` to XML :param obj: Object of class :class:`~basyx.aas.model.base.Resource` :param tag: Namespace+Tag of the serialized element. Default is ``aas:resource`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_resource = abstract_classes_to_xml(tag, obj) et_resource.append(_generate_element(NS_AAS + "path", text=obj.path)) @@ -662,13 +664,13 @@ def resource_to_xml(obj: model.Resource, def reference_element_to_xml(obj: model.ReferenceElement, - tag: str = NS_AAS+"referenceElement") -> etree.Element: + tag: str = NS_AAS+"referenceElement") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.ReferenceElement` to XMl :param obj: Object of class :class:`~basyx.aas.model.submodel.ReferenceElement` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:referenceElement`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_reference_element = abstract_classes_to_xml(tag, obj) if obj.value: @@ -677,13 +679,13 @@ def reference_element_to_xml(obj: model.ReferenceElement, def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, - tag: str = NS_AAS+"submodelElementCollection") -> etree.Element: + tag: str = NS_AAS+"submodelElementCollection") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElementCollection` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElementCollection` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodelElementCollection`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_submodel_element_collection = abstract_classes_to_xml(tag, obj) if obj.value: @@ -695,13 +697,13 @@ def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, def submodel_element_list_to_xml(obj: model.SubmodelElementList, - tag: str = NS_AAS+"submodelElementList") -> etree.Element: + tag: str = NS_AAS+"submodelElementList") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElementList` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElementList` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodelElementList`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_submodel_element_list = abstract_classes_to_xml(tag, obj) et_submodel_element_list.append(_generate_element(NS_AAS + "orderRelevant", boolean_to_xml(obj.order_relevant))) @@ -722,13 +724,13 @@ def submodel_element_list_to_xml(obj: model.SubmodelElementList, def relationship_element_to_xml(obj: model.RelationshipElement, - tag: str = NS_AAS+"relationshipElement") -> etree.Element: + tag: str = NS_AAS+"relationshipElement") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.RelationshipElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.RelationshipElement` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:relationshipElement`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_relationship_element = abstract_classes_to_xml(tag, obj) et_relationship_element.append(reference_to_xml(obj.first, NS_AAS+"first")) @@ -737,13 +739,13 @@ def relationship_element_to_xml(obj: model.RelationshipElement, def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElement, - tag: str = NS_AAS+"annotatedRelationshipElement") -> etree.Element: + tag: str = NS_AAS+"annotatedRelationshipElement") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement` :param tag: Namespace+Tag of the serialized element (optional): Default is ``aas:annotatedRelationshipElement`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_annotated_relationship_element = relationship_element_to_xml(obj, tag) if obj.annotation: @@ -754,7 +756,7 @@ def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElemen return et_annotated_relationship_element -def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"operationVariable") -> etree.Element: +def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"operationVariable") -> etree._Element: """ Serialization of :class:`~basyx.aas.model.submodel.SubmodelElement` to the XML OperationVariable representation Since we don't implement the ``OperationVariable`` class, which is just a wrapper for a single @@ -763,7 +765,7 @@ def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"ope :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:operationVariable`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_operation_variable = _generate_element(tag) et_value = _generate_element(NS_AAS+"value") @@ -773,13 +775,13 @@ def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"ope def operation_to_xml(obj: model.Operation, - tag: str = NS_AAS+"operation") -> etree.Element: + tag: str = NS_AAS+"operation") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.Operation` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Operation` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:operation`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_operation = abstract_classes_to_xml(tag, obj) for tag, nss in ((NS_AAS+"inputVariables", obj.input_variable), @@ -794,25 +796,25 @@ def operation_to_xml(obj: model.Operation, def capability_to_xml(obj: model.Capability, - tag: str = NS_AAS+"capability") -> etree.Element: + tag: str = NS_AAS+"capability") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.Capability` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Capability` :param tag: Namespace+Tag of the serialized element, default is ``aas:capability`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ return abstract_classes_to_xml(tag, obj) def entity_to_xml(obj: model.Entity, - tag: str = NS_AAS+"entity") -> etree.Element: + tag: str = NS_AAS+"entity") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.Entity` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.Entity` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:entity`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_entity = abstract_classes_to_xml(tag, obj) if obj.statement: @@ -831,13 +833,13 @@ def entity_to_xml(obj: model.Entity, return et_entity -def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+"basicEventElement") -> etree.Element: +def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+"basicEventElement") -> etree._Element: """ Serialization of objects of class :class:`~basyx.aas.model.submodel.BasicEventElement` to XML :param obj: Object of class :class:`~basyx.aas.model.submodel.BasicEventElement` :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:basicEventElement`` - :return: Serialized :class:`~lxml.etree.Element` object + :return: Serialized :class:`~lxml.etree._Element` object """ et_basic_event_element = abstract_classes_to_xml(tag, obj) et_basic_event_element.append(reference_to_xml(obj.observed, NS_AAS+"observed")) @@ -863,17 +865,17 @@ def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+" # general functions # ############################################################## -def _write_element(file: _generic.PathOrBinaryIO, element: etree.Element, **kwargs) -> None: +def _write_element(file: _generic.PathOrBinaryIO, element: etree._Element, **kwargs) -> None: etree.ElementTree(element).write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) -def object_to_xml_element(obj: object) -> etree.Element: +def object_to_xml_element(obj: object) -> etree._Element: """ - Serialize a single object to an :class:`~lxml.etree.Element`. + Serialize a single object to an :class:`~lxml.etree._Element`. :param obj: The object to serialize """ - serialization_func: Callable[..., etree.Element] + serialization_func: Callable[..., etree._Element] if isinstance(obj, model.Key): serialization_func = key_to_xml @@ -958,14 +960,14 @@ def write_aas_xml_element(file: _generic.PathOrBinaryIO, obj: object, **kwargs) :param file: A filename or file-like object to write the XML-serialized data to :param obj: The object to serialize - :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree.ElementTree.write` + :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree._ElementTree.write` """ return _write_element(file, object_to_xml_element(obj), **kwargs) -def object_store_to_xml_element(data: model.AbstractObjectStore) -> etree.Element: +def object_store_to_xml_element(data: model.AbstractObjectStore) -> etree._Element: """ - Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree.Element`. + Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree._Element`. This function is used internally by :meth:`write_aas_xml_file` and shouldn't be called directly for most use-cases. @@ -1015,6 +1017,6 @@ def write_aas_xml_file(file: _generic.PathOrBinaryIO, :param file: A filename or file-like object to write the XML-serialized data to :param data: :class:`ObjectStore ` which contains different objects of the AAS meta model which should be serialized to an XML file - :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree.ElementTree.write` + :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree._ElementTree.write` """ return _write_element(file, object_store_to_xml_element(data), **kwargs) From 501b3efb5c35dd5ecd198bd91ddc74087305c2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 20 Jun 2024 14:59:07 +0200 Subject: [PATCH 265/474] adapter.http: fix `lxml` typing --- basyx/aas/adapter/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e3526bb..d033f09 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -156,7 +156,7 @@ def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> root_elem.append(child) etree.cleanup_namespaces(root_elem) xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") - return xml_str + return xml_str # type: ignore[return-value] class XmlResponseAlt(XmlResponse): @@ -164,7 +164,7 @@ def __init__(self, *args, content_type="text/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) -def result_to_xml(result: Result, **kwargs) -> etree.Element: +def result_to_xml(result: Result, **kwargs) -> etree._Element: result_elem = etree.Element("result", **kwargs) success_elem = etree.Element("success") success_elem.text = xml_serialization.boolean_to_xml(result.success) @@ -177,7 +177,7 @@ def result_to_xml(result: Result, **kwargs) -> etree.Element: return result_elem -def message_to_xml(message: Message) -> etree.Element: +def message_to_xml(message: Message) -> etree._Element: message_elem = etree.Element("message") message_type_elem = etree.Element("messageType") message_type_elem.text = str(message.message_type) From 4ca0142c680b767cff2859e27c86fa7150b22568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 19 Jun 2024 17:03:01 +0200 Subject: [PATCH 266/474] adapter.http: improve type hints Remove 'type: ignore' comments now that we require werkzeug >=3.0.3 [1]. Furthermore, fix the type hint of `WSGIApp._get_slice()` and make two other 'type: ignore' comments more explicit. [1]: https://github.com/pallets/werkzeug/issues/2836 --- basyx/aas/adapter/http.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d033f09..3cc2dfe 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -5,10 +5,6 @@ # # SPDX-License-Identifier: MIT -# TODO: remove this once the werkzeug type annotations have been fixed -# https://github.com/pallets/werkzeug/issues/2836 -# mypy: disable-error-code="arg-type" - import abc import base64 import binascii @@ -18,7 +14,7 @@ import json import itertools -from lxml import etree # type: ignore +from lxml import etree import werkzeug.exceptions import werkzeug.routing import werkzeug.urls @@ -55,7 +51,7 @@ def __init__(self, code: str, text: str, message_type: MessageType = MessageType self.code: str = code self.text: str = text self.message_type: MessageType = message_type - self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.utcnow() + self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.now(datetime.UTC) class Result: @@ -399,11 +395,12 @@ def to_python(self, value: str) -> List[str]: class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.AbstractSupplementaryFileContainer): + def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.AbstractSupplementaryFileContainer, + base_path: str = "/api/v3.0"): self.object_store: model.AbstractObjectStore = object_store self.file_store: aasx.AbstractSupplementaryFileContainer = file_store self.url_map = werkzeug.routing.Map([ - Submount("/api/v3.0", [ + Submount(base_path, [ Submount("/serialization", [ Rule("/", methods=["GET"], endpoint=self.not_implemented) ]), @@ -628,7 +625,7 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") @classmethod - def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: + def _get_slice(cls, request: Request, iterator: Iterable[T]) -> Tuple[Iterator[T], int]: limit_str = request.args.get('limit', default="10") cursor_str = request.args.get('cursor', default="0") try: @@ -698,9 +695,9 @@ def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = map_adapter.match() - # TODO: remove this 'type: ignore' comment once the werkzeug type annotations have been fixed - # https://github.com/pallets/werkzeug/issues/2836 - return endpoint(request, values, map_adapter=map_adapter) # type: ignore[operator] + if endpoint is None: + raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") + return endpoint(request, values, map_adapter=map_adapter) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them except werkzeug.exceptions.NotAcceptable as e: @@ -948,7 +945,8 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar raise BadRequest(f"{parent!r} is not a namespace, can't add child submodel element!") # 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, # type: ignore + new_submodel_element = HTTPApiDecoder.request_body(request, + model.SubmodelElement, # type: ignore[type-abstract] is_stripped_request(request)) try: parent.add_referable(new_submodel_element) @@ -968,7 +966,8 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) # 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, # type: ignore + new_submodel_element = HTTPApiDecoder.request_body(request, + model.SubmodelElement, # type: ignore[type-abstract] is_stripped_request(request)) submodel_element.update_from(new_submodel_element) submodel_element.commit() From ff492979a4646ec5ba199513e309ce3718e8971f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 20 Jul 2024 00:06:10 +0200 Subject: [PATCH 267/474] adapter.http: remove unnecessary generator expression --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 3cc2dfe..7bf3c65 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -382,7 +382,7 @@ class IdShortPathConverter(werkzeug.routing.UnicodeConverter): id_short_sep = "." def to_url(self, value: List[str]) -> str: - return super().to_url(self.id_short_sep.join(id_short for id_short in value)) + return super().to_url(self.id_short_sep.join(value)) def to_python(self, value: str) -> List[str]: id_shorts = super().to_python(value).split(self.id_short_sep) From 6a0b3f13cae7d7c8539f90dfe3d8b87e033efaea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 19 Jul 2024 18:13:33 +0200 Subject: [PATCH 268/474] adapter.http: remove trailing slashes from routes This removes trailing slashes (and redirects to paths with trailing slashes) from the API and makes it compatible with the PCF2 showcase and other webapps. Previously, all routes were implemented with a trailing slash, e.g. `/submodels/` instead of `/submodels`. While the API spec only specifies the routes without a trailing slash, this has the advantage of being compatible with requests to the path with a trailing slash and without trailing slash, as werkzeug redirects requests to the slash-terminated path, if available. However, this poses a problem with browsers that make use of [CORS preflight requests][1] (e.g. Chromium-based browsers). Here, before doing an actual API request, the browser sends an `OPTIONS` request to the path it wants to request. This is done to check potential CORS headers (e.g. `Access-Control-Allow-Origin`) for the path, without retrieving the actual data. Our implementation doesn't support `OPTIONS` requests, which is fine. After the browser has received the response to the preflight request (which may or may not have been successful), it attempts to retrieve the actual data by sending the request again with the correct request method (e.g. `GET`). With our server this request now results in a redirect, as we redirect to the path with a trailing slash appended. This is a problem, as the browser didn't send a CORS preflight request to the path it is now redirected to. It also doesn't attempt to send another CORS preflight request, as it already sent one, with the difference being the now slash-terminated path. Thus, following the redirect is prevented by CORS policy and the data fails to load. By making the routes available via non-slash-terminated paths we avoid the need for redirects, which makes the server compatible with webapps viewed in browsers that use preflight requests. Requests to slash-terminated paths will no longer work (they won't redirect to the path without trailing slash). This shouldn't be a problem though, as the API is only specified without trailing slashes anyway. --- basyx/aas/adapter/http.py | 198 ++++++++++++++++++-------------------- 1 file changed, 91 insertions(+), 107 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7bf3c65..23da578 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -401,139 +401,123 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs self.file_store: aasx.AbstractSupplementaryFileContainer = file_store self.url_map = werkzeug.routing.Map([ Submount(base_path, [ - Submount("/serialization", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented) - ]), - Submount("/description", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented) - ]), + Rule("/serialization", methods=["GET"], endpoint=self.not_implemented), + Rule("/description", methods=["GET"], endpoint=self.not_implemented), + Rule("/shells", methods=["GET"], endpoint=self.get_aas_all), + Rule("/shells", methods=["POST"], endpoint=self.post_aas), Submount("/shells", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_all), - Rule("/", methods=["POST"], endpoint=self.post_aas), - Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_all_reference), + Rule("/$reference", methods=["GET"], endpoint=self.get_aas_all_reference), + Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/", methods=["DELETE"], endpoint=self.delete_aas), Submount("/", [ - Rule("/", methods=["GET"], endpoint=self.get_aas), - Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/", methods=["DELETE"], endpoint=self.delete_aas), - Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_reference), - Submount("/asset-information", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/thumbnail", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented), - Rule("/", methods=["PUT"], endpoint=self.not_implemented), - Rule("/", methods=["DELETE"], endpoint=self.not_implemented) - ]) - ]), - Submount("/submodel-refs", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific) - ]), - Submount("/submodels/", [ - Rule("/", methods=["PUT"], endpoint=self.put_aas_submodel_refs_submodel), - Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_submodel), - Rule("/", endpoint=self.aas_submodel_refs_redirect), - Rule("//", endpoint=self.aas_submodel_refs_redirect) + Rule("/$reference", methods=["GET"], endpoint=self.get_aas_reference), + Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), + Rule("/asset-information/thumbnail", methods=["GET", "PUT", "DELETE"], + endpoint=self.not_implemented), + Rule("/submodel-refs", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/submodel-refs", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("/submodel-refs/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific), + Submount("/submodels", [ + Rule("/", methods=["PUT"], + endpoint=self.put_aas_submodel_refs_submodel), + Rule("/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_submodel), + Rule("/", endpoint=self.aas_submodel_refs_redirect), + Rule("//", endpoint=self.aas_submodel_refs_redirect) ]) ]) ]), + Rule("/submodels", methods=["GET"], endpoint=self.get_submodel_all), + Rule("/submodels", methods=["POST"], endpoint=self.post_submodel), Submount("/submodels", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_all), - Rule("/", methods=["POST"], endpoint=self.post_submodel), - Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), - Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_all_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_all_reference), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Submount("/", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodels_metadata), - Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$reference/", methods=["GET"], endpoint=self.get_submodels_reference), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/submodel-elements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodel-elements", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), Submount("/submodel-elements", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/$metadata/", methods=["GET"], + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_metadata), - Rule("/$reference/", methods=["GET"], + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_submodel_elements_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Submount("/", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_id_short_path), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$metadata/", methods=["GET"], + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), - Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$reference/", methods=["GET"], + Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), - Submount("/attachment", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_attachment), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_attachment), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_attachment) - ]), - Submount("/invoke", [ - Rule("/", methods=["POST"], endpoint=self.not_implemented), - Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) - ]), - Submount("/invoke-async", [ - Rule("/", methods=["POST"], endpoint=self.not_implemented), - Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) - ]), - Submount("/operation-status", [ - Rule("//", methods=["GET"], - endpoint=self.not_implemented) - ]), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/attachment", methods=["GET"], + endpoint=self.get_submodel_submodel_element_attachment), + Rule("/attachment", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_attachment), + Rule("/attachment", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_attachment), + Rule("/invoke", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke/$value", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke-async", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke-async/$value", methods=["POST"], endpoint=self.not_implemented), + Rule("/operation-status/", methods=["GET"], + endpoint=self.not_implemented), Submount("/operation-results", [ - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.not_implemented), - Rule("//$value/", methods=["GET"], + Rule("//$value", methods=["GET"], endpoint=self.not_implemented) ]), + Rule("/qualifiers", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("/qualifiers", methods=["POST"], + endpoint=self.post_submodel_submodel_element_qualifiers), Submount("/qualifiers", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) ]), + Rule("/qualifiers", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("/qualifiers", methods=["POST"], + endpoint=self.post_submodel_submodel_element_qualifiers), Submount("/qualifiers", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) @@ -542,7 +526,7 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs ], converters={ "base64url": Base64URLConverter, "id_short_path": IdShortPathConverter - }) + }, strict_slashes=False) # TODO: the parameters can be typed via builtin wsgiref with Python 3.11+ def __call__(self, environ, start_response) -> Iterable[bytes]: From 69753ab71e0271334793bfafbaa47764f1056d1d Mon Sep 17 00:00:00 2001 From: Frosty2500 <125310380+Frosty2500@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:29:15 +0200 Subject: [PATCH 269/474] adapter.http: add documentation of not implemented features (#52) This adds a module docstring to `adapter.http`, that details which features from the Specification of the Asset Administration Shell Part 2 (API) were not implemented. --- basyx/aas/adapter/http.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 23da578..413027f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -4,6 +4,35 @@ # the LICENSE file of this project. # # SPDX-License-Identifier: MIT +""" +This module implements the "Specification of the Asset Administration Shell Part 2 Application Programming Interfaces". +However, several features and routes are currently not supported: + +1. Correlation ID: Not implemented because it was deemed unnecessary for this server. + +2. Extent Parameter (`withBlobValue/withoutBlobValue`): + Not implemented due to the lack of support in JSON/XML serialization. + +3. Route `/shells/{aasIdentifier}/asset-information/thumbnail`: Not implemented because the specification lacks clarity. + +4. Serialization and Description Routes: + - `/serialization` + - `/description` + These routes are not implemented at this time. + +5. Value, Path, and PATCH Routes: + - All `/…/value$`, `/…/path$`, and `PATCH` routes are currently not implemented. + +6. Operation Invocation Routes: The following routes are not implemented because operation invocation + is not yet supported by the `basyx-python-sdk`: + - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke` + - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke/$value` + - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async` + - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async/$value` + - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-status/{handleId}` + - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-results/{handleId}` + - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-results/{handleId}/$value` +""" import abc import base64 From 792f0316cf13034f47a725de76f64fbd17c73ac3 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 27 Aug 2024 18:01:02 +0200 Subject: [PATCH 270/474] adapter.http: add CD-Repo routes --- basyx/aas/adapter/http.py | 57 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 413027f..9911536 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -550,7 +550,14 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) - ]) + ]), + Rule("/concept-descriptions", methods=["GET"], endpoint=self.get_concept_description_all), + Rule("/concept-descriptions", methods=["POST"], endpoint=self.post_concept_description), + Submount("/concept-descriptions", [ + Rule("/", methods=["GET"], endpoint=self.get_concept_description), + Rule("/", methods=["PUT"], endpoint=self.put_concept_description), + Rule("/", methods=["DELETE"], endpoint=self.delete_concept_description), + ]), ]) ], converters={ "base64url": Base64URLConverter, @@ -729,8 +736,8 @@ def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Respon # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aashels, cursor = self._get_shells(request) - return response_t(list(aashels), cursor=cursor) + aashells, cursor = self._get_shells(request) + return response_t(list(aashells), cursor=cursor) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -1142,9 +1149,53 @@ def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args sm_or_se.commit() return response_t() + # --------- CONCEPT DESCRIPTION ROUTES --------- + def get_concept_description_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + 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)) + + def post_concept_description(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request)) + try: + self.object_store.add(concept_description) + except KeyError as e: + raise Conflict(f"ConceptDescription with Identifier {concept_description.id} already exists!") from e + concept_description.commit() + created_resource_url = map_adapter.build(self.get_concept_description, { + "concept_id": concept_description.id + }, force_external=True) + return response_t(concept_description, status=201, headers={"Location": created_resource_url}) + + def get_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + concept_description = self._get_concept_description(url_args) + return response_t(concept_description, stripped=is_stripped_request(request)) + + def _get_concept_description(self, url_args): + return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) + + def put_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + concept_description = self._get_concept_description(url_args) + concept_description.update_from(HTTPApiDecoder.request_body(request, model.ConceptDescription, + is_stripped_request(request))) + concept_description.commit() + return response_t() + + def delete_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + self.object_store.remove(self._get_concept_description(url_args)) + return response_t() + if __name__ == "__main__": from werkzeug.serving import run_simple from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()), use_debugger=True, use_reloader=True) + + +# Commit msg: Add CD-Repo routes to the server \ No newline at end of file From c2e41a0a6d2078a00d8e2eb31e553bc6919bde2c Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 27 Aug 2024 18:10:48 +0200 Subject: [PATCH 271/474] adapter.http: code style fixes --- basyx/aas/adapter/http.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 9911536..b655c75 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -1158,7 +1158,8 @@ def get_concept_description_all(self, request: Request, url_args: Dict, **_kwarg def post_concept_description(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) - concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request)) + concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, + is_stripped_request(request)) try: self.object_store.add(concept_description) except KeyError as e: @@ -1196,6 +1197,3 @@ def delete_concept_description(self, request: Request, url_args: Dict, **_kwargs from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()), use_debugger=True, use_reloader=True) - - -# Commit msg: Add CD-Repo routes to the server \ No newline at end of file From 024852fd2d62ab5284d26db4b80e4a822bd9c01b Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:12:14 +0300 Subject: [PATCH 272/474] adapter.http: Refactor and improve codestyle (#292) This commit refactors the `adapter.http` module. - We remove `get_response_type()` in the beginning of all endpoint methods by integrating it in`handle_request()` and adding an argument for the response type in all endpoint methods. - Before, the `_get_submodel_or_nested_submodel_element()` method had `submodel` and `id_shorts` in its arguments, so in all methods that used it, a `submodel` and `id_short_path` needed to be explored with separate methods. We refactor those two arguments make the method an instance method. Now, it has only `url_args` as an argument and returns a `SubmodelElement` or a `Submodel`. --- basyx/aas/adapter/http.py | 293 +++++++++++++++++--------------------- 1 file changed, 132 insertions(+), 161 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index b655c75..4f07f43 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -59,7 +59,7 @@ from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder from . import aasx -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple, Any +from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple @enum.unique @@ -230,7 +230,7 @@ def get_response_type(request: Request) -> Type[APIResponse]: return JsonResponse mime_type = request.accept_mimetypes.best_match(response_types) if mime_type is None: - raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " + raise werkzeug.exceptions.NotAcceptable("This server supports the following content types: " + ", ".join(response_types.keys())) return response_types[mime_type] @@ -329,7 +329,7 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool elif expect_type is model.SpecificAssetId: constructor = decoder._construct_specific_asset_id # type: ignore[assignment] elif expect_type is model.Reference: - constructor = decoder._construct_reference # type: ignore[assignment] + constructor = decoder._construct_reference # type: ignore[assignment] elif expect_type is model.Qualifier: constructor = decoder._construct_qualifier # type: ignore[assignment] @@ -343,8 +343,7 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool return [cls.assert_type(obj, expect_type) for obj in parsed] @classmethod - def base64urljson_list(cls, data: str, expect_type: Type[T], stripped: bool, expect_single: bool)\ - -> List[T]: + def base64urljson_list(cls, data: str, expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, expect_single) @@ -605,11 +604,11 @@ def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, i raise BadRequest(f"{ret!r} is not a submodel element!") return ret - @classmethod - def _get_submodel_or_nested_submodel_element(cls, submodel: model.Submodel, id_shorts: List[str]) \ - -> Union[model.Submodel, model.SubmodelElement]: + def _get_submodel_or_nested_submodel_element(self, url_args: Dict) -> Union[model.Submodel, model.SubmodelElement]: + submodel = self._get_submodel(url_args) + id_shorts: List[str] = url_args.get("id_shorts", []) try: - return cls._get_nested_submodel_element(submodel, id_shorts) + return self._get_nested_submodel_element(submodel, id_shorts) except ValueError: return submodel @@ -674,7 +673,7 @@ def _get_shells(self, request: Request) -> Tuple[Iterator[model.AssetAdministrat map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, False), asset_ids)) # Filter AAS based on these SpecificAssetIds aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id - for specific_asset_id in specific_asset_ids), aas) + for specific_asset_id in specific_asset_ids), aas) paginated_aas, end_index = self._get_slice(request, aas) return paginated_aas, end_index @@ -698,49 +697,50 @@ def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], in def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) - def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ + def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> \ Tuple[Iterator[model.SubmodelElement], int]: submodel = self._get_submodel(url_args) paginated_submodel_elements: Iterator[model.SubmodelElement] paginated_submodel_elements, end_index = self._get_slice(request, submodel.submodel_element) return paginated_submodel_elements, end_index - def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ - -> model.SubmodelElement: + def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) -> model.SubmodelElement: submodel = self._get_submodel(url_args) submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return submodel_element + def _get_concept_description(self, url_args): + return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) + def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) + try: + response_t = get_response_type(request) + except werkzeug.exceptions.NotAcceptable as e: + return e + try: endpoint, values = map_adapter.match() - if endpoint is None: - raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") - return endpoint(request, values, map_adapter=map_adapter) + # TODO: remove this 'type: ignore' comment once the werkzeug type annotations have been fixed + # https://github.com/pallets/werkzeug/issues/2836 + return endpoint(request, values, response_t=response_t, map_adapter=map_adapter) # type: ignore[operator] + # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them - except werkzeug.exceptions.NotAcceptable as e: - return e except werkzeug.exceptions.HTTPException as e: - try: - # get_response_type() may raise a NotAcceptable error, so we have to handle that - return http_exception_to_response(e, get_response_type(request)) - except werkzeug.exceptions.NotAcceptable as e: - return e + return http_exception_to_response(e, response_t) # ------ all not implemented ROUTES ------- def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: - raise werkzeug.exceptions.NotImplemented(f"This route is not implemented!") + raise werkzeug.exceptions.NotImplemented("This route is not implemented!") # ------ AAS REPO ROUTES ------- - def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: aashells, cursor = self._get_shells(request) return response_t(list(aashells), cursor=cursor) - def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) + def post_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: aas = HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, False) try: self.object_store.add(aas) @@ -752,59 +752,56 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> }, force_external=True) return response_t(aas, status=201, headers={"Location": created_resource_url}) - def get_aas_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas_all_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aashells, cursor = self._get_shells(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(aas) for aas in aashells] return response_t(references, cursor=cursor) # --------- AAS ROUTES --------- - def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: aas = self._get_shell(url_args) return response_t(aas) - def get_aas_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: aas = self._get_shell(url_args) reference = model.ModelReference.from_referable(aas) return response_t(reference) - def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def put_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: aas = self._get_shell(url_args) aas.update_from(HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, is_stripped_request(request))) aas.commit() return response_t() - def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) - self.object_store.remove(self._get_shell(url_args)) + def delete_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: + aas = self._get_shell(url_args) + self.object_store.remove(aas) return response_t() - def get_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas_asset_information(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) return response_t(aas.asset_information) - def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def put_aas_asset_information(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) aas.asset_information = HTTPApiDecoder.request_body(request, model.AssetInformation, False) aas.commit() return response_t() - def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) submodel_refs: Iterator[model.ModelReference[model.Submodel]] submodel_refs, cursor = self._get_slice(request, aas.submodel) return response_t(list(submodel_refs), cursor=cursor) - def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def post_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) sm_ref = HTTPApiDecoder.request_body(request, model.ModelReference, False) if sm_ref in aas.submodel: @@ -813,15 +810,15 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas.commit() return response_t(sm_ref, status=201) - def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) aas.submodel.remove(self._get_submodel_reference(aas, url_args["submodel_id"])) aas.commit() return response_t() - def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) submodel = self._resolve_reference(sm_ref) @@ -837,8 +834,8 @@ def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kw aas.commit() return response_t() - def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) submodel = self._resolve_reference(sm_ref) @@ -861,13 +858,12 @@ def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapt return werkzeug.utils.redirect(redirect_url, 307) # ------ SUBMODEL REPO ROUTES ------- - def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodel_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: submodels, cursor = self._get_submodels(request) return response_t(list(submodels), cursor=cursor, stripped=is_stripped_request(request)) - def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) + def post_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) try: self.object_store.add(submodel) @@ -879,13 +875,13 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte }, force_external=True) return response_t(submodel, status=201, headers={"Location": created_resource_url}) - def get_submodel_all_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodel_all_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodels, cursor = self._get_submodels(request) return response_t(list(submodels), cursor=cursor, stripped=True) - def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodel_all_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodels, cursor = self._get_submodels(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] @@ -893,74 +889,69 @@ def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs # --------- SUBMODEL ROUTES --------- - def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def delete_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() - def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: submodel = self._get_submodel(url_args) return response_t(submodel, stripped=is_stripped_request(request)) - def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodels_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodel = self._get_submodel(url_args) return response_t(submodel, stripped=True) - def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodels_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodel = self._get_submodel(url_args) reference = model.ModelReference.from_referable(submodel) return response_t(reference, stripped=is_stripped_request(request)) - def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def put_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: submodel = self._get_submodel(url_args) submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request))) submodel.commit() return response_t() - def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodel_submodel_elements(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) return response_t(list(submodel_elements), cursor=cursor, stripped=is_stripped_request(request)) - def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) return response_t(list(submodel_elements), cursor=cursor, stripped=True) - def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in list(submodel_elements)] return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) - def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_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) return response_t(submodel_element, stripped=is_stripped_request(request)) - def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ - -> Response: - response_t = get_response_type(request) + def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], **_kwargs) -> Response: submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=True) - def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ - -> Response: - response_t = get_response_type(request) + def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], **_kwargs) -> Response: submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) reference = model.ModelReference.from_referable(submodel_element) return response_t(reference, stripped=is_stripped_request(request)) - def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): - response_t = get_response_type(request) - submodel = self._get_submodel(url_args) - id_short_path = url_args.get("id_shorts", []) - parent = self._get_submodel_or_nested_submodel_element(submodel, id_short_path) + def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + map_adapter: MapAdapter): + parent = self._get_submodel_or_nested_submodel_element(url_args) if not isinstance(parent, model.UniqueIdShortNamespace): raise BadRequest(f"{parent!r} is not a namespace, can't add child submodel element!") # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] @@ -975,14 +966,17 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar raise raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!") + submodel = self._get_submodel(url_args) + id_short_path = url_args.get("id_shorts", []) created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { "submodel_id": submodel.id, "id_shorts": id_short_path + [new_submodel_element.id_short] }, force_external=True) return response_t(new_submodel_element, status=201, headers={"Location": created_resource_url}) - def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + 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) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 @@ -993,20 +987,15 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element.commit() return response_t() - def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) \ - -> Response: - response_t = get_response_type(request) - submodel = self._get_submodel(url_args) - id_short_path: List[str] = url_args["id_shorts"] - parent: model.UniqueIdShortNamespace = self._expect_namespace( - self._get_submodel_or_nested_submodel_element(submodel, id_short_path[:-1]), - id_short_path[-1] - ) - self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) + 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) + 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) return response_t() - def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ - -> Response: + def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response: submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) if not isinstance(submodel_element, (model.Blob, model.File)): raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to download!") @@ -1029,29 +1018,26 @@ def get_submodel_submodel_element_attachment(self, request: Request, url_args: D # Blob and File both have the content_type attribute return Response(value, content_type=submodel_element.content_type) # type: ignore[attr-defined] - def put_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def put_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) # spec allows PUT only for File, not for Blob if not isinstance(submodel_element, model.File): raise BadRequest(f"{submodel_element!r} is not a File, no file content to update!") - - if submodel_element.value is not None: + elif submodel_element.value is not None: raise Conflict(f"{submodel_element!r} already references a file!") filename = request.form.get('fileName') if filename is None: - raise BadRequest(f"No 'fileName' specified!") - - if not filename.startswith("/"): + raise BadRequest("No 'fileName' specified!") + elif not filename.startswith("/"): raise BadRequest(f"Given 'fileName' doesn't start with a slash (/): {filename}") file_storage: Optional[FileStorage] = request.files.get('file') if file_storage is None: - raise BadRequest(f"Missing file to upload") - - if file_storage.mimetype != submodel_element.content_type: + raise BadRequest("Missing file to upload") + elif file_storage.mimetype != submodel_element.content_type: raise werkzeug.exceptions.UnsupportedMediaType( f"Request body is of type {file_storage.mimetype!r}, " f"while {submodel_element!r} has content_type {submodel_element.content_type!r}!") @@ -1060,14 +1046,13 @@ def put_submodel_submodel_element_attachment(self, request: Request, url_args: D submodel_element.commit() return response_t() - def delete_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ - -> Response: - response_t = get_response_type(request) + def delete_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + **_kwargs) -> Response: submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) if not isinstance(submodel_element, (model.Blob, model.File)): raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to delete!") - - if submodel_element.value is None: + elif submodel_element.value is None: raise NotFound(f"{submodel_element!r} has no attachment!") if isinstance(submodel_element, model.Blob): @@ -1084,42 +1069,32 @@ def delete_submodel_submodel_element_attachment(self, request: Request, url_args submodel_element.commit() return response_t() - def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, **_kwargs) \ - -> Response: - response_t = get_response_type(request) - submodel = self._get_submodel(url_args) - sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) + def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: return response_t(list(sm_or_se.qualifier)) return response_t(self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type)) - def post_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ - -> Response: - response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_submodel(url_args) - id_shorts: List[str] = url_args.get("id_shorts", []) - sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + def post_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) if sm_or_se.qualifier.contains_id("type", qualifier.type): raise Conflict(f"Qualifier with type {qualifier.type} already exists!") sm_or_se.qualifier.add(qualifier) sm_or_se.commit() created_resource_url = map_adapter.build(self.get_submodel_submodel_element_qualifiers, { - "submodel_id": submodel_identifier, - "id_shorts": id_shorts if len(id_shorts) != 0 else None, + "submodel_id": url_args["submodel_id"], + "id_shorts": url_args.get("id_shorts") or None, "qualifier_type": qualifier.type }, force_external=True) return response_t(qualifier, status=201, headers={"Location": created_resource_url}) - def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ - -> Response: - response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_submodel(url_args) - id_shorts: List[str] = url_args.get("id_shorts", []) - sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) qualifier_type = url_args["qualifier_type"] qualifier = self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type) @@ -1131,33 +1106,31 @@ def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: D sm_or_se.commit() if qualifier_type_changed: created_resource_url = map_adapter.build(self.get_submodel_submodel_element_qualifiers, { - "submodel_id": submodel_identifier, - "id_shorts": id_shorts if len(id_shorts) != 0 else None, + "submodel_id": url_args["submodel_id"], + "id_shorts": url_args.get("id_shorts") or None, "qualifier_type": new_qualifier.type }, force_external=True) return response_t(new_qualifier, status=201, headers={"Location": created_resource_url}) return response_t(new_qualifier) - def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, **_kwargs) \ - -> Response: - response_t = get_response_type(request) - submodel = self._get_submodel(url_args) - id_shorts: List[str] = url_args.get("id_shorts", []) - sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + **_kwargs) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) qualifier_type = url_args["qualifier_type"] self._qualifiable_qualifier_op(sm_or_se, sm_or_se.remove_qualifier_by_type, qualifier_type) sm_or_se.commit() return response_t() # --------- CONCEPT DESCRIPTION ROUTES --------- - def get_concept_description_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_concept_description_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> 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)) - def post_concept_description(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) + def post_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request)) try: @@ -1170,24 +1143,21 @@ def post_concept_description(self, request: Request, url_args: Dict, map_adapter }, force_external=True) return response_t(concept_description, status=201, headers={"Location": created_resource_url}) - def get_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: concept_description = self._get_concept_description(url_args) return response_t(concept_description, stripped=is_stripped_request(request)) - def _get_concept_description(self, url_args): - return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) - - def put_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def put_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: concept_description = self._get_concept_description(url_args) concept_description.update_from(HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request))) concept_description.commit() return response_t() - def delete_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def delete_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: self.object_store.remove(self._get_concept_description(url_args)) return response_t() @@ -1195,5 +1165,6 @@ def delete_concept_description(self, request: Request, url_args: Dict, **_kwargs if __name__ == "__main__": from werkzeug.serving import run_simple from basyx.aas.examples.data.example_aas import create_full_example + run_simple("localhost", 8080, WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()), use_debugger=True, use_reloader=True) From 33336cf23990886cfcdf7e78c4709f523bede1f2 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 26 Sep 2024 12:46:19 +0200 Subject: [PATCH 273/474] sdk/basyx/adapter/: move copied files to desired location --- sdk/basyx/aasx.py | 868 ------------------ {basyx/aas => sdk/basyx}/adapter/__init__.py | 0 {basyx/aas => sdk/basyx}/adapter/_generic.py | 0 {basyx/aas => sdk/basyx}/adapter/aasx.py | 2 +- {basyx/aas => sdk/basyx}/adapter/http.py | 0 .../basyx}/adapter/json/__init__.py | 0 .../adapter/json/json_deserialization.py | 0 .../basyx}/adapter/json/json_serialization.py | 0 .../aas => sdk/basyx}/adapter/xml/__init__.py | 0 .../basyx}/adapter/xml/xml_deserialization.py | 2 +- .../basyx}/adapter/xml/xml_serialization.py | 4 +- 11 files changed, 4 insertions(+), 872 deletions(-) delete mode 100644 sdk/basyx/aasx.py rename {basyx/aas => sdk/basyx}/adapter/__init__.py (100%) rename {basyx/aas => sdk/basyx}/adapter/_generic.py (100%) rename {basyx/aas => sdk/basyx}/adapter/aasx.py (99%) rename {basyx/aas => sdk/basyx}/adapter/http.py (100%) rename {basyx/aas => sdk/basyx}/adapter/json/__init__.py (100%) rename {basyx/aas => sdk/basyx}/adapter/json/json_deserialization.py (100%) rename {basyx/aas => sdk/basyx}/adapter/json/json_serialization.py (100%) rename {basyx/aas => sdk/basyx}/adapter/xml/__init__.py (100%) rename {basyx/aas => sdk/basyx}/adapter/xml/xml_deserialization.py (99%) rename {basyx/aas => sdk/basyx}/adapter/xml/xml_serialization.py (99%) diff --git a/sdk/basyx/aasx.py b/sdk/basyx/aasx.py deleted file mode 100644 index 30bb394..0000000 --- a/sdk/basyx/aasx.py +++ /dev/null @@ -1,868 +0,0 @@ -# Copyright (c) 2024 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 -""" -.. _adapter.aasx: - -Functionality for reading and writing AASX files according to "Details of the Asset Administration Shell Part 1 V2.0", -section 7. - -The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the ``pyecma376_2`` library -for low level OPC reading and writing. It currently supports all required features except for embedded digital -signatures. - -Writing and reading of AASX packages is performed through the :class:`~.AASXReader` and :class:`~.AASXWriter` classes. -Each instance of these classes wraps an existing AASX file resp. a file to be created and allows to read/write the -included AAS objects into/form :class:`ObjectStores `. -For handling of embedded supplementary files, this module provides the -:class:`~.AbstractSupplementaryFileContainer` class -interface and the :class:`~.DictSupplementaryFileContainer` implementation. -""" - -import abc -import hashlib -import io -import itertools -import logging -import os -import re -from typing import Dict, Tuple, IO, Union, List, Set, Optional, Iterable, Iterator - -from .xml import read_aas_xml_file, write_aas_xml_file -from .. import model -from .json import read_aas_json_file, write_aas_json_file -import pyecma376_2 -from ..util import traversal - -logger = logging.getLogger(__name__) - -RELATIONSHIP_TYPE_AASX_ORIGIN = "http://admin-shell.io/aasx/relationships/aasx-origin" -RELATIONSHIP_TYPE_AAS_SPEC = "http://admin-shell.io/aasx/relationships/aas-spec" -RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://admin-shell.io/aasx/relationships/aas-spec-split" -RELATIONSHIP_TYPE_AAS_SUPL = "http://admin-shell.io/aasx/relationships/aas-suppl" - - -class AASXReader: - """ - An AASXReader wraps an existing AASX package file to allow reading its contents and metadata. - - Basic usage: - - .. code-block:: python - - objects = DictObjectStore() - files = DictSupplementaryFileContainer() - with AASXReader("filename.aasx") as reader: - meta_data = reader.get_core_properties() - reader.read_into(objects, files) - - """ - def __init__(self, file: Union[os.PathLike, str, IO]): - """ - Open an AASX reader for the given filename or file handle - - The given file is opened as OPC ZIP package. Make sure to call ``AASXReader.close()`` after reading the file - contents to close the underlying ZIP file reader. You may also use the AASXReader as a context manager to ensure - closing under any circumstances. - - :param file: A filename, file path or an open file-like object in binary mode - :raises FileNotFoundError: If the file does not exist - :raises ValueError: If the file is not a valid OPC zip package - """ - try: - logger.debug("Opening {} as AASX pacakge for reading ...".format(file)) - self.reader = pyecma376_2.ZipPackageReader(file) - except FileNotFoundError: - raise - except Exception as e: - raise ValueError("{} is not a valid ECMA376-2 (OPC) file: {}".format(file, e)) from e - - def get_core_properties(self) -> pyecma376_2.OPCCoreProperties: - """ - Retrieve the OPC Core Properties (meta data) of the AASX package file. - - If no meta data is provided in the package file, an emtpy OPCCoreProperties object is returned. - - :return: The AASX package's meta data - """ - return self.reader.get_core_properties() - - def get_thumbnail(self) -> Optional[bytes]: - """ - Retrieve the packages thumbnail image - - The thumbnail image file is read into memory and returned as bytes object. You may use some python image library - for further processing or conversion, e.g. ``pillow``: - - .. code-block:: python - - import io - from PIL import Image - thumbnail = Image.open(io.BytesIO(reader.get_thumbnail())) - - :return: The AASX package thumbnail's file contents or None if no thumbnail is provided - """ - try: - thumbnail_part = self.reader.get_related_parts_by_type()[pyecma376_2.RELATIONSHIP_TYPE_THUMBNAIL][0] - except IndexError: - return None - - with self.reader.open_part(thumbnail_part) as p: - return p.read() - - def read_into(self, object_store: model.AbstractObjectStore, - file_store: "AbstractSupplementaryFileContainer", - override_existing: bool = False, **kwargs) -> Set[model.Identifier]: - """ - Read the contents of the AASX package and add them into a given - :class:`ObjectStore ` - - This function does the main job of reading the AASX file's contents. It traverses the relationships within the - package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - ``object_store``. While doing so, it searches all parsed :class:`Submodels ` - for :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced - supplementary files are added to the given ``file_store`` and the :class:`~basyx.aas.model.submodel.File` - objects' values are updated with the absolute name of the supplementary file to allow for robust resolution the - file within the ``file_store`` later. - - :param object_store: An :class:`ObjectStore ` to add the AAS - objects from the AASX file to - :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the - embedded supplementary files to - :param override_existing: If ``True``, existing objects in the object store are overridden with objects from the - AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects - from the AASX. - :return: A set of the :class:`Identifiers ` of all - :class:`~basyx.aas.model.base.Identifiable` objects parsed from the AASX file - """ - # Find AASX-Origin part - core_rels = self.reader.get_related_parts_by_type() - try: - aasx_origin_part = core_rels[RELATIONSHIP_TYPE_AASX_ORIGIN][0] - except IndexError as e: - raise ValueError("Not a valid AASX file: aasx-origin Relationship is missing.") from e - - read_identifiables: Set[model.Identifier] = set() - - # Iterate AAS files - for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ - RELATIONSHIP_TYPE_AAS_SPEC]: - self._read_aas_part_into(aas_part, object_store, file_store, - read_identifiables, override_existing, **kwargs) - - # Iterate split parts of AAS file - for split_part in self.reader.get_related_parts_by_type(aas_part)[ - RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: - self._read_aas_part_into(split_part, object_store, file_store, - read_identifiables, override_existing, **kwargs) - - return read_identifiables - - def close(self) -> None: - """ - Close the AASXReader and the underlying OPC / ZIP file readers. Must be called after reading the file. - """ - self.reader.close() - - def __enter__(self) -> "AASXReader": - return self - - def __exit__(self, exc_type, exc_val, exc_tb) -> None: - self.close() - - def _read_aas_part_into(self, part_name: str, - object_store: model.AbstractObjectStore, - file_store: "AbstractSupplementaryFileContainer", - read_identifiables: Set[model.Identifier], - override_existing: bool, **kwargs) -> None: - """ - Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. - - This method primarily checks for duplicate objects. It uses ``_parse_aas_parse()`` to do the actual parsing and - ``_collect_supplementary_files()`` for supplementary file processing of non-duplicate objects. - - :param part_name: The OPC part name to read - :param object_store: An ObjectStore to add the AAS objects from the AASX file to - :param file_store: A SupplementaryFileContainer to add the embedded supplementary files to, which are reference - from a File object of this part - :param read_identifiables: A set of Identifiers of objects which have already been read. New objects' - Identifiers are added to this set. Objects with already known Identifiers are skipped silently. - :param override_existing: If True, existing objects in the object store are overridden with objects from the - AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. - """ - for obj in self._parse_aas_part(part_name, **kwargs): - if obj.id in read_identifiables: - continue - if obj.id in object_store: - if override_existing: - logger.info("Overriding existing object in ObjectStore with {} ...".format(obj)) - object_store.discard(obj) - else: - logger.warning("Skipping {}, since an object with the same id is already contained in the " - "ObjectStore".format(obj)) - continue - object_store.add(obj) - read_identifiables.add(obj.id) - if isinstance(obj, model.Submodel): - self._collect_supplementary_files(part_name, obj, file_store) - - def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore: - """ - Helper function to parse the AAS objects from a single JSON or XML part of the AASX package. - - This method chooses and calls the correct parser. - - :param part_name: The OPC part name of the part to be parsed - :return: A DictObjectStore containing the parsed AAS objects - """ - content_type = self.reader.get_content_type(part_name) - extension = part_name.split("/")[-1].split(".")[-1] - if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": - logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) - with self.reader.open_part(part_name) as p: - return read_aas_xml_file(p, **kwargs) - elif content_type.split(";")[0] in ("text/json", "application/json") \ - or content_type == "" and extension == "json": - logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) - with self.reader.open_part(part_name) as p: - return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs) - else: - logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}" - .format(part_name, content_type, extension)) - return model.DictObjectStore() - - def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel, - file_store: "AbstractSupplementaryFileContainer") -> None: - """ - Helper function to search File objects within a single parsed Submodel, extract the referenced supplementary - files and update the File object's values with the absolute path. - - :param part_name: The OPC part name of the part the submodel has been parsed from. This is used to resolve - relative file paths. - :param submodel: The Submodel to process - :param file_store: The SupplementaryFileContainer to add the extracted supplementary files to - """ - for element in traversal.walk_submodel(submodel): - if isinstance(element, model.File): - if element.value is None: - continue - # Only absolute-path references and relative-path URI references (see RFC 3986, sec. 4.2) are considered - # to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute - # URIs and network-path references) - if element.value.startswith('//') or ':' in element.value.split('/')[0]: - logger.info("Skipping supplementary file %s, since it seems to be an absolute URI or network-path " - "URI reference", element.value) - continue - absolute_name = pyecma376_2.package_model.part_realpath(element.value, part_name) - logger.debug("Reading supplementary file {} from AASX package ...".format(absolute_name)) - with self.reader.open_part(absolute_name) as p: - final_name = file_store.add_file(absolute_name, p, self.reader.get_content_type(absolute_name)) - element.value = final_name - - -class AASXWriter: - """ - An AASXWriter wraps a new AASX package file to write its contents to it piece by piece. - - Basic usage: - - .. code-block:: python - - # object_store and file_store are expected to be given (e.g. some storage backend or previously created data) - cp = OPCCoreProperties() - cp.creator = "ACPLT" - cp.created = datetime.datetime.now() - - with AASXWriter("filename.aasx") as writer: - writer.write_aas("https://acplt.org/AssetAdministrationShell", - object_store, - file_store) - writer.write_aas("https://acplt.org/AssetAdministrationShell2", - object_store, - file_store) - writer.write_core_properties(cp) - - .. attention:: - - The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context manager - functionality (as shown above). Otherwise, the resulting AASX file will lack important data structures - and will not be readable. - """ - AASX_ORIGIN_PART_NAME = "/aasx/aasx-origin" - - def __init__(self, file: Union[os.PathLike, str, IO]): - """ - Create a new AASX package in the given file and open the AASXWriter to add contents to the package. - - Make sure to call ``AASXWriter.close()`` after writing all contents to write the aas-spec relationships for all - AAS parts to the file and close the underlying ZIP file writer. You may also use the AASXWriter as a context - manager to ensure closing under any circumstances. - - :param file: filename, path, or binary file handle opened for writing - """ - # names of aas-spec parts, used by `_write_aasx_origin_relationships()` - self._aas_part_names: List[str] = [] - # name of the thumbnail part (if any) - self._thumbnail_part: Optional[str] = None - # name of the core properties part (if any) - self._properties_part: Optional[str] = None - # names and hashes of all supplementary file parts that have already been written - self._supplementary_part_names: Dict[str, Optional[bytes]] = {} - - # Open OPC package writer - self.writer = pyecma376_2.ZipPackageWriter(file) - - # Create AASX origin part - logger.debug("Creating AASX origin part in AASX package ...") - p = self.writer.open_part(self.AASX_ORIGIN_PART_NAME, "text/plain") - p.close() - - def write_aas(self, - aas_ids: Union[model.Identifier, Iterable[model.Identifier]], - object_store: model.AbstractObjectStore, - file_store: "AbstractSupplementaryFileContainer", - write_json: bool = False) -> None: - """ - Convenience method to write one or more - :class:`AssetAdministrationShells ` with all included - and referenced objects to the AASX package according to the part name conventions from DotAAS. - - This method takes the AASs' :class:`Identifiers ` (as ``aas_ids``) to retrieve - the AASs from the given ``object_store``. - :class:`References ` to :class:`Submodels ` - and :class:`ConceptDescriptions ` (via semanticId attributes) are - also resolved using the ``object_store``. All of these objects are written to an aas-spec part - ``/aasx/data.xml`` or ``/aasx/data.json`` in the AASX package, compliant to the convention presented in - "Details of the Asset Administration Shell". Supplementary files which are referenced by a - :class:`~basyx.aas.model.submodel.File` object in any of the - :class:`Submodels ` are also added to the AASX package. - - This method uses :meth:`write_all_aas_objects` to write the AASX part. - - .. attention:: - - This method **must only be used once** on a single AASX package. Otherwise, the ``/aasx/data.json`` - (or ``...xml``) part would be written twice to the package, hiding the first part and possibly causing - problems when reading the package. - - To write multiple Asset Administration Shells to a single AASX package file, call this method once, passing - a list of AAS Identifiers to the ``aas_ids`` parameter. - - :param aas_ids: :class:`~basyx.aas.model.base.Identifier` or Iterable of - :class:`Identifiers ` of the AAS(s) to be written to the AASX file - :param object_store: :class:`ObjectStore ` to retrieve the - :class:`~basyx.aas.model.base.Identifiable` AAS objects - (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, - :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.Submodel`) from - :param file_store: :class:`SupplementaryFileContainer ` to retrieve - supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects - :param write_json: If ``True``, JSON parts are created for the AAS and each - :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts. - Defaults to ``False``. - :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable - :class:`Submodels ` and - :class:`ConceptDescriptions ` are skipped, logging a - warning/info message) - :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another - :class:`~basyx.aas.model.base.Identifiable` object) - """ - if isinstance(aas_ids, model.Identifier): - aas_ids = (aas_ids,) - - objects_to_be_written: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - for aas_id in aas_ids: - try: - aas = object_store.get_identifiable(aas_id) - # TODO add failsafe mode - except KeyError: - raise - if not isinstance(aas, model.AssetAdministrationShell): - raise TypeError(f"Identifier {aas_id} does not belong to an AssetAdminstrationShell object but to " - f"{aas!r}") - - # Add the AssetAdministrationShell object to the data part - objects_to_be_written.add(aas) - - # Add referenced Submodels to the data part - for submodel_ref in aas.submodel: - try: - submodel = submodel_ref.resolve(object_store) - except KeyError: - logger.warning("Could not find submodel %s. Skipping it.", str(submodel_ref)) - continue - objects_to_be_written.add(submodel) - - # Traverse object tree and check if semanticIds are referencing to existing ConceptDescriptions in the - # ObjectStore - concept_descriptions: List[model.ConceptDescription] = [] - for identifiable in objects_to_be_written: - for semantic_id in traversal.walk_semantic_ids_recursive(identifiable): - if not isinstance(semantic_id, model.ModelReference) \ - or semantic_id.type is not model.ConceptDescription: - logger.info("semanticId %s does not reference a ConceptDescription.", str(semantic_id)) - continue - try: - cd = semantic_id.resolve(object_store) - except KeyError: - logger.info("ConceptDescription for semantidId %s not found in object store.", str(semantic_id)) - continue - except model.UnexpectedTypeError as e: - logger.error("semantidId %s resolves to %s, which is not a ConceptDescription", - str(semantic_id), e.value) - continue - concept_descriptions.append(cd) - objects_to_be_written.update(concept_descriptions) - - # Write AAS data part - self.write_all_aas_objects("/aasx/data.{}".format("json" if write_json else "xml"), - objects_to_be_written, file_store, write_json) - - # TODO remove `method` parameter in future version. - # Not actually required since you can always create a local dict - def write_aas_objects(self, - part_name: str, - object_ids: Iterable[model.Identifier], - object_store: model.AbstractObjectStore, - file_store: "AbstractSupplementaryFileContainer", - write_json: bool = False, - split_part: bool = False, - additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: - """ - A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - - This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as ``aas_id``) to retrieve it - from the given object_store. If the list of written objects includes :class:`~basyx.aas.model.submodel.Submodel` - objects, Supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within - those submodels, are also added to the AASX package. - - .. attention:: - - You must make sure to call this method or :meth:`write_all_aas_objects` only once per unique ``part_name`` - on a single package instance. - - :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 - part name and unique within the package. The extension of the part should match the data format (i.e. - '.json' if ``write_json`` else '.xml'). - :param object_ids: A list of :class:`Identifiers ` of the objects to be written - to the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included - :class:`~basyx.aas.model.base.Referable` objects) are written to the package. - :param object_store: The objects store to retrieve the :class:`~basyx.aas.model.base.Identifiable` objects from - :param file_store: The - :class:`SupplementaryFileContainer ` - to retrieve supplementary files from (if there are any :class:`~basyx.aas.model.submodel.File` - objects within the written objects. - :param write_json: If ``True``, the part is written as a JSON file instead of an XML file. Defaults to - ``False``. - :param split_part: If ``True``, no aas-spec relationship is added from the aasx-origin to this part. You must - make sure to reference it via a aas-spec-split relationship from another aas-spec part - :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object - part to be written, in addition to the aas-suppl relationships which are created automatically. - """ - logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) - - objects: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - - # Retrieve objects and scan for referenced supplementary files - for identifier in object_ids: - try: - the_object = object_store.get_identifiable(identifier) - except KeyError: - logger.error("Could not find object {} in ObjectStore".format(identifier)) - continue - objects.add(the_object) - - self.write_all_aas_objects(part_name, objects, file_store, write_json, split_part, additional_relationships) - - # TODO remove `split_part` parameter in future version. - # Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 - def write_all_aas_objects(self, - part_name: str, - objects: model.AbstractObjectStore[model.Identifiable], - file_store: "AbstractSupplementaryFileContainer", - write_json: bool = False, - split_part: bool = False, - additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: - """ - Write all AAS objects in a given :class:`ObjectStore ` to an XML - or JSON part in the AASX package and add the referenced supplementary files to the package. - - This method takes an :class:`ObjectStore ` and writes all - contained objects into an ``aas_env`` part in the AASX package. If the ObjectStore includes - :class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by - :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store`` - and added to the AASX package. - - .. attention:: - - You must make sure to call this method only once per unique ``part_name`` on a single package instance. - - :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 - part name and unique within the package. The extension of the part should match the data format (i.e. - '.json' if ``write_json`` else '.xml'). - :param objects: The objects to be written to the AASX package. Only these Identifiable objects (and included - Referable objects) are written to the package. - :param file_store: The SupplementaryFileContainer to retrieve supplementary files from (if there are any - ``File`` objects within the written objects. - :param write_json: If True, the part is written as a JSON file instead of an XML file. Defaults to False. - :param split_part: If True, no aas-spec relationship is added from the aasx-origin to this part. You must make - sure to reference it via a aas-spec-split relationship from another aas-spec part - :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object - part to be written, in addition to the aas-suppl relationships which are created automatically. - """ - logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) - supplementary_files: List[str] = [] - - # Retrieve objects and scan for referenced supplementary files - for the_object in objects: - if isinstance(the_object, model.Submodel): - for element in traversal.walk_submodel(the_object): - if isinstance(element, model.File): - file_name = element.value - # Skip File objects with empty value URI references that are considered to be no local file - # (absolute URIs or network-path URI references) - if file_name is None or file_name.startswith('//') or ':' in file_name.split('/')[0]: - continue - supplementary_files.append(file_name) - - # Add aas-spec relationship - if not split_part: - self._aas_part_names.append(part_name) - - # Write part - # TODO allow writing xml *and* JSON part - with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p: - if write_json: - write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects) - else: - write_aas_xml_file(p, objects) - - # Write submodel's supplementary files to AASX file - supplementary_file_names = [] - for file_name in supplementary_files: - try: - content_type = file_store.get_content_type(file_name) - hash = file_store.get_sha256(file_name) - except KeyError: - logger.warning("Could not find file {} in file store.".format(file_name)) - continue - # Check if this supplementary file has already been written to the AASX package or has a name conflict - if self._supplementary_part_names.get(file_name) == hash: - continue - elif file_name in self._supplementary_part_names: - logger.error("Trying to write supplementary file {} to AASX twice with different contents" - .format(file_name)) - logger.debug("Writing supplementary file {} to AASX package ...".format(file_name)) - with self.writer.open_part(file_name, content_type) as p: - file_store.write_file(file_name, p) - supplementary_file_names.append(pyecma376_2.package_model.normalize_part_name(file_name)) - self._supplementary_part_names[file_name] = hash - - # Add relationships from submodel to supplementary parts - logger.debug("Writing aas-suppl relationships for AAS object part {} to AASX package ...".format(part_name)) - self.writer.write_relationships( - itertools.chain( - (pyecma376_2.OPCRelationship("r{}".format(i), - RELATIONSHIP_TYPE_AAS_SUPL, - submodel_file_name, - pyecma376_2.OPCTargetMode.INTERNAL) - for i, submodel_file_name in enumerate(supplementary_file_names)), - additional_relationships), - part_name) - - def write_core_properties(self, core_properties: pyecma376_2.OPCCoreProperties): - """ - Write OPC Core Properties (meta data) to the AASX package file. - - .. Attention:: - This method may only be called once for each AASXWriter! - - :param core_properties: The OPCCoreProperties object with the meta data to be written to the package file - """ - if self._properties_part is not None: - raise RuntimeError("Core Properties have already been written.") - logger.debug("Writing core properties to AASX package ...") - with self.writer.open_part(pyecma376_2.DEFAULT_CORE_PROPERTIES_NAME, "application/xml") as p: - core_properties.write_xml(p) - self._properties_part = pyecma376_2.DEFAULT_CORE_PROPERTIES_NAME - - def write_thumbnail(self, name: str, data: bytearray, content_type: str): - """ - Write an image file as thumbnail image to the AASX package. - - .. Attention:: - This method may only be called once for each AASXWriter! - - :param name: The OPC part name of the thumbnail part. Should not contain '/' or URI-encoded '/' or '\'. - :param data: The image file's binary contents to be written - :param content_type: OPC content type (MIME type) of the image file - """ - if self._thumbnail_part is not None: - raise RuntimeError("package thumbnail has already been written to {}.".format(self._thumbnail_part)) - with self.writer.open_part(name, content_type) as p: - p.write(data) - self._thumbnail_part = name - - def close(self): - """ - Write relationships for all data files to package and close underlying OPC package and ZIP file. - """ - self._write_aasx_origin_relationships() - self._write_package_relationships() - self.writer.close() - - def __enter__(self) -> "AASXWriter": - return self - - def __exit__(self, exc_type, exc_val, exc_tb) -> None: - self.close() - - def _write_aasx_origin_relationships(self): - """ - Helper function to write aas-spec relationships of the aasx-origin part. - - This method uses the list of aas-spec parts in ``_aas_part_names``. It should be called just before closing the - file to make sure all aas-spec parts of the package have already been written. - """ - # Add relationships from AASX-origin part to AAS parts - logger.debug("Writing aas-spec relationships to AASX package ...") - self.writer.write_relationships( - (pyecma376_2.OPCRelationship("r{}".format(i), RELATIONSHIP_TYPE_AAS_SPEC, - aas_part_name, - pyecma376_2.OPCTargetMode.INTERNAL) - for i, aas_part_name in enumerate(self._aas_part_names)), - self.AASX_ORIGIN_PART_NAME) - - def _write_package_relationships(self): - """ - Helper function to write package (root) relationships to the OPC package. - - This method must be called just before closing the package file to make sure we write exactly the correct - relationships: - * aasx-origin (always) - * core-properties (if core properties have been added) - * thumbnail (if thumbnail part has been added) - """ - logger.debug("Writing package relationships to AASX package ...") - package_relationships: List[pyecma376_2.OPCRelationship] = [ - pyecma376_2.OPCRelationship("r1", RELATIONSHIP_TYPE_AASX_ORIGIN, - self.AASX_ORIGIN_PART_NAME, - pyecma376_2.OPCTargetMode.INTERNAL), - ] - if self._properties_part is not None: - package_relationships.append(pyecma376_2.OPCRelationship( - "r2", pyecma376_2.RELATIONSHIP_TYPE_CORE_PROPERTIES, self._properties_part, - pyecma376_2.OPCTargetMode.INTERNAL)) - if self._thumbnail_part is not None: - package_relationships.append(pyecma376_2.OPCRelationship( - "r3", pyecma376_2.RELATIONSHIP_TYPE_THUMBNAIL, self._thumbnail_part, - pyecma376_2.OPCTargetMode.INTERNAL)) - self.writer.write_relationships(package_relationships) - - -# TODO remove in future version. -# Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 -class NameFriendlyfier: - """ - A simple helper class to create unique "AAS friendly names" according to DotAAS, section 7.6. - - Objects of this class store the already created friendly names to avoid name collisions within one set of names. - """ - RE_NON_ALPHANUMERICAL = re.compile(r"[^a-zA-Z0-9]") - - def __init__(self) -> None: - self.issued_names: Set[str] = set() - - def get_friendly_name(self, identifier: model.Identifier): - """ - Generate a friendly name from an AAS identifier. - - TODO: This information is outdated. The whole class is no longer needed. - - According to section 7.6 of "Details of the Asset Administration Shell", all non-alphanumerical characters are - replaced with underscores. We also replace all non-ASCII characters to generate valid URIs as the result. - If this replacement results in a collision with a previously generated friendly name of this NameFriendlifier, - a number is appended with underscore to the friendly name. - - Example: - - .. code-block:: python - - friendlyfier = NameFriendlyfier() - friendlyfier.get_friendly_name("http://example.com/AAS-a") - > "http___example_com_AAS_a" - - friendlyfier.get_friendly_name("http://example.com/AAS+a") - > "http___example_com_AAS_a_1" - - """ - # friendlify name - raw_name = self.RE_NON_ALPHANUMERICAL.sub('_', identifier) - - # Unify name (avoid collisions) - amended_name = raw_name - i = 1 - while amended_name in self.issued_names: - amended_name = "{}_{}".format(raw_name, i) - i += 1 - - self.issued_names.add(amended_name) - return amended_name - - -class AbstractSupplementaryFileContainer(metaclass=abc.ABCMeta): - """ - Abstract interface for containers of supplementary files for AASs. - - Supplementary files may be PDF files or other binary or textual files, referenced in a File object of an AAS by - their name. They are used to provide associated documents without embedding their contents (as - :class:`~basyx.aas.model.submodel.Blob` object) in the AAS. - - A SupplementaryFileContainer keeps track of the name and content_type (MIME type) for each file. Additionally it - allows to resolve name conflicts by comparing the files' contents and providing an alternative name for a dissimilar - new file. It also provides each files sha256 hash sum to allow name conflict checking in other classes (e.g. when - writing AASX files). - """ - @abc.abstractmethod - def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: - """ - Add a new file to the SupplementaryFileContainer and resolve name conflicts. - - The file contents must be provided as a binary file-like object to be read by the SupplementaryFileContainer. - If the container already contains an equally named file, the content_type and file contents are compared (using - a hash sum). In case of dissimilar files, a new unique name for the new file is computed and returned. It should - be used to update in the File object of the AAS. - - :param name: The file's proposed name. Should start with a '/'. Should not contain URI-encoded '/' or '\' - :param file: A binary file-like opened for reading the file contents - :param content_type: The file's content_type - :return: The file name as stored in the SupplementaryFileContainer. Typically ``name`` or a modified version of - ``name`` to resolve conflicts. - """ - pass # pragma: no cover - - @abc.abstractmethod - def get_content_type(self, name: str) -> str: - """ - Get a stored file's content_type. - - :param name: file name of questioned file - :return: The file's content_type - :raises KeyError: If no file with this name is stored - """ - pass # pragma: no cover - - @abc.abstractmethod - def get_sha256(self, name: str) -> bytes: - """ - Get a stored file content's sha256 hash sum. - - This may be used by other classes (e.g. the AASXWriter) to check for name conflicts. - - :param name: file name of questioned file - :return: The file content's sha256 hash sum - :raises KeyError: If no file with this name is stored - """ - pass # pragma: no cover - - @abc.abstractmethod - def write_file(self, name: str, file: IO[bytes]) -> None: - """ - Retrieve a stored file's contents by writing them into a binary writable file-like object. - - :param name: file name of questioned file - :param file: A binary file-like object with write() method to write the file contents into - :raises KeyError: If no file with this name is stored - """ - pass # pragma: no cover - - @abc.abstractmethod - def delete_file(self, name: str) -> None: - """ - Deletes a file from this SupplementaryFileContainer given its name. - """ - pass # pragma: no cover - - @abc.abstractmethod - def __contains__(self, item: str) -> bool: - """ - Check if a file with the given name is stored in this SupplementaryFileContainer. - """ - pass # pragma: no cover - - @abc.abstractmethod - def __iter__(self) -> Iterator[str]: - """ - Return an iterator over all file names stored in this SupplementaryFileContainer. - """ - pass # pragma: no cover - - -class DictSupplementaryFileContainer(AbstractSupplementaryFileContainer): - """ - SupplementaryFileContainer implementation using a dict to store the file contents in-memory. - """ - def __init__(self): - # Stores the files' contents, identified by their sha256 hash - self._store: Dict[bytes, bytes] = {} - # Maps file names to (sha256, content_type) - self._name_map: Dict[str, Tuple[bytes, str]] = {} - # Tracks the number of references to _store keys, - # i.e. the number of different filenames referring to the same file - self._store_refcount: Dict[bytes, int] = {} - - def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: - data = file.read() - hash = hashlib.sha256(data).digest() - if hash not in self._store: - self._store[hash] = data - self._store_refcount[hash] = 0 - name_map_data = (hash, content_type) - new_name = name - i = 1 - while True: - if new_name not in self._name_map: - self._name_map[new_name] = name_map_data - self._store_refcount[hash] += 1 - return new_name - elif self._name_map[new_name] == name_map_data: - return new_name - new_name = self._append_counter(name, i) - i += 1 - - @staticmethod - def _append_counter(name: str, i: int) -> str: - split1 = name.split('/') - split2 = split1[-1].split('.') - index = -2 if len(split2) > 1 else -1 - new_basename = "{}_{:04d}".format(split2[index], i) - split2[index] = new_basename - split1[-1] = ".".join(split2) - return "/".join(split1) - - def get_content_type(self, name: str) -> str: - return self._name_map[name][1] - - def get_sha256(self, name: str) -> bytes: - return self._name_map[name][0] - - def write_file(self, name: str, file: IO[bytes]) -> None: - file.write(self._store[self._name_map[name][0]]) - - def delete_file(self, name: str) -> None: - # The number of different files with the same content are kept track of via _store_refcount. - # The contents are only deleted, once the refcount reaches zero. - hash: bytes = self._name_map[name][0] - self._store_refcount[hash] -= 1 - if self._store_refcount[hash] == 0: - del self._store[hash] - del self._store_refcount[hash] - del self._name_map[name] - - def __contains__(self, item: object) -> bool: - return item in self._name_map - - def __iter__(self) -> Iterator[str]: - return iter(self._name_map) diff --git a/basyx/aas/adapter/__init__.py b/sdk/basyx/adapter/__init__.py similarity index 100% rename from basyx/aas/adapter/__init__.py rename to sdk/basyx/adapter/__init__.py diff --git a/basyx/aas/adapter/_generic.py b/sdk/basyx/adapter/_generic.py similarity index 100% rename from basyx/aas/adapter/_generic.py rename to sdk/basyx/adapter/_generic.py diff --git a/basyx/aas/adapter/aasx.py b/sdk/basyx/adapter/aasx.py similarity index 99% rename from basyx/aas/adapter/aasx.py rename to sdk/basyx/adapter/aasx.py index 0e9e733..cab639f 100644 --- a/basyx/aas/adapter/aasx.py +++ b/sdk/basyx/adapter/aasx.py @@ -32,7 +32,7 @@ from typing import Dict, Tuple, IO, Union, List, Set, Optional, Iterable, Iterator from .xml import read_aas_xml_file, write_aas_xml_file -from .. import model +from basyx.aas import model from .json import read_aas_json_file, write_aas_json_file import pyecma376_2 from ..util import traversal diff --git a/basyx/aas/adapter/http.py b/sdk/basyx/adapter/http.py similarity index 100% rename from basyx/aas/adapter/http.py rename to sdk/basyx/adapter/http.py diff --git a/basyx/aas/adapter/json/__init__.py b/sdk/basyx/adapter/json/__init__.py similarity index 100% rename from basyx/aas/adapter/json/__init__.py rename to sdk/basyx/adapter/json/__init__.py diff --git a/basyx/aas/adapter/json/json_deserialization.py b/sdk/basyx/adapter/json/json_deserialization.py similarity index 100% rename from basyx/aas/adapter/json/json_deserialization.py rename to sdk/basyx/adapter/json/json_deserialization.py diff --git a/basyx/aas/adapter/json/json_serialization.py b/sdk/basyx/adapter/json/json_serialization.py similarity index 100% rename from basyx/aas/adapter/json/json_serialization.py rename to sdk/basyx/adapter/json/json_serialization.py diff --git a/basyx/aas/adapter/xml/__init__.py b/sdk/basyx/adapter/xml/__init__.py similarity index 100% rename from basyx/aas/adapter/xml/__init__.py rename to sdk/basyx/adapter/xml/__init__.py diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/sdk/basyx/adapter/xml/xml_deserialization.py similarity index 99% rename from basyx/aas/adapter/xml/xml_deserialization.py rename to sdk/basyx/adapter/xml/xml_deserialization.py index b053c24..c6e555b 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/sdk/basyx/adapter/xml/xml_deserialization.py @@ -42,7 +42,7 @@ and construct them if available, and so on. """ -from ... import model +from basyx.aas import model from lxml import etree import logging import base64 diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/sdk/basyx/adapter/xml/xml_serialization.py similarity index 99% rename from basyx/aas/adapter/xml/xml_serialization.py rename to sdk/basyx/adapter/xml/xml_serialization.py index 6f962c8..07d75d3 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/sdk/basyx/adapter/xml/xml_serialization.py @@ -843,8 +843,8 @@ def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+" """ et_basic_event_element = abstract_classes_to_xml(tag, obj) et_basic_event_element.append(reference_to_xml(obj.observed, NS_AAS+"observed")) - et_basic_event_element.append(_generate_element(NS_AAS+"direction", text=_generic.DIRECTION[obj.direction])) - et_basic_event_element.append(_generate_element(NS_AAS+"state", text=_generic.STATE_OF_EVENT[obj.state])) + et_basic_event_element.append(_generate_element(NS_AAS +"direction", text=_generic.DIRECTION[obj.direction])) + et_basic_event_element.append(_generate_element(NS_AAS +"state", text=_generic.STATE_OF_EVENT[obj.state])) if obj.message_topic is not None: et_basic_event_element.append(_generate_element(NS_AAS+"messageTopic", text=obj.message_topic)) if obj.message_broker is not None: From be31e3ecdd812e641c7f0775786eff51a60f7b57 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Wed, 23 Oct 2024 10:08:31 +0200 Subject: [PATCH 274/474] sdk/basyx/adapter/: copied and modified the adapter from the basyx-python-sdk. The serialisation is based on the serialisation of aas-core3.0 --- sdk/basyx/adapter/_generic.py | 101 +- sdk/basyx/adapter/aasx.py | 127 +- sdk/basyx/adapter/json/__init__.py | 6 +- .../adapter/json/json_deserialization.py | 869 +--------- sdk/basyx/adapter/json/json_serialization.py | 728 +-------- sdk/basyx/adapter/xml/__init__.py | 8 +- sdk/basyx/adapter/xml/xml_deserialization.py | 1425 +---------------- sdk/basyx/adapter/xml/xml_serialization.py | 973 +---------- 8 files changed, 213 insertions(+), 4024 deletions(-) diff --git a/sdk/basyx/adapter/_generic.py b/sdk/basyx/adapter/_generic.py index 6a37c74..02a6bed 100644 --- a/sdk/basyx/adapter/_generic.py +++ b/sdk/basyx/adapter/_generic.py @@ -11,7 +11,6 @@ import os from typing import BinaryIO, Dict, IO, Type, Union -from basyx.aas import model # type aliases for path-like objects and IO # used by write_aas_xml_file, read_aas_xml_file, write_aas_json_file, read_aas_json_file @@ -21,102 +20,4 @@ # XML Namespace definition XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} -XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" - -MODELLING_KIND: Dict[model.ModellingKind, str] = { - model.ModellingKind.TEMPLATE: 'Template', - model.ModellingKind.INSTANCE: 'Instance'} - -ASSET_KIND: Dict[model.AssetKind, str] = { - model.AssetKind.TYPE: 'Type', - model.AssetKind.INSTANCE: 'Instance', - model.AssetKind.NOT_APPLICABLE: 'NotApplicable'} - -QUALIFIER_KIND: Dict[model.QualifierKind, str] = { - model.QualifierKind.CONCEPT_QUALIFIER: 'ConceptQualifier', - model.QualifierKind.TEMPLATE_QUALIFIER: 'TemplateQualifier', - model.QualifierKind.VALUE_QUALIFIER: 'ValueQualifier'} - -DIRECTION: Dict[model.Direction, str] = { - model.Direction.INPUT: 'input', - model.Direction.OUTPUT: 'output'} - -STATE_OF_EVENT: Dict[model.StateOfEvent, str] = { - model.StateOfEvent.ON: 'on', - model.StateOfEvent.OFF: 'off'} - -REFERENCE_TYPES: Dict[Type[model.Reference], str] = { - model.ExternalReference: 'ExternalReference', - model.ModelReference: 'ModelReference'} - -KEY_TYPES: Dict[model.KeyTypes, str] = { - model.KeyTypes.ASSET_ADMINISTRATION_SHELL: 'AssetAdministrationShell', - model.KeyTypes.CONCEPT_DESCRIPTION: 'ConceptDescription', - model.KeyTypes.SUBMODEL: 'Submodel', - model.KeyTypes.ANNOTATED_RELATIONSHIP_ELEMENT: 'AnnotatedRelationshipElement', - model.KeyTypes.BASIC_EVENT_ELEMENT: 'BasicEventElement', - model.KeyTypes.BLOB: 'Blob', - model.KeyTypes.CAPABILITY: 'Capability', - model.KeyTypes.DATA_ELEMENT: 'DataElement', - model.KeyTypes.ENTITY: 'Entity', - model.KeyTypes.EVENT_ELEMENT: 'EventElement', - model.KeyTypes.FILE: 'File', - model.KeyTypes.MULTI_LANGUAGE_PROPERTY: 'MultiLanguageProperty', - model.KeyTypes.OPERATION: 'Operation', - model.KeyTypes.PROPERTY: 'Property', - model.KeyTypes.RANGE: 'Range', - model.KeyTypes.REFERENCE_ELEMENT: 'ReferenceElement', - model.KeyTypes.RELATIONSHIP_ELEMENT: 'RelationshipElement', - model.KeyTypes.SUBMODEL_ELEMENT: 'SubmodelElement', - model.KeyTypes.SUBMODEL_ELEMENT_COLLECTION: 'SubmodelElementCollection', - model.KeyTypes.SUBMODEL_ELEMENT_LIST: 'SubmodelElementList', - model.KeyTypes.GLOBAL_REFERENCE: 'GlobalReference', - model.KeyTypes.FRAGMENT_REFERENCE: 'FragmentReference'} - -ENTITY_TYPES: Dict[model.EntityType, str] = { - model.EntityType.CO_MANAGED_ENTITY: 'CoManagedEntity', - model.EntityType.SELF_MANAGED_ENTITY: 'SelfManagedEntity'} - -IEC61360_DATA_TYPES: Dict[model.base.DataTypeIEC61360, str] = { - model.base.DataTypeIEC61360.DATE: 'DATE', - model.base.DataTypeIEC61360.STRING: 'STRING', - model.base.DataTypeIEC61360.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE', - model.base.DataTypeIEC61360.INTEGER_MEASURE: 'INTEGER_MEASURE', - model.base.DataTypeIEC61360.INTEGER_COUNT: 'INTEGER_COUNT', - model.base.DataTypeIEC61360.INTEGER_CURRENCY: 'INTEGER_CURRENCY', - model.base.DataTypeIEC61360.REAL_MEASURE: 'REAL_MEASURE', - model.base.DataTypeIEC61360.REAL_COUNT: 'REAL_COUNT', - model.base.DataTypeIEC61360.REAL_CURRENCY: 'REAL_CURRENCY', - model.base.DataTypeIEC61360.BOOLEAN: 'BOOLEAN', - model.base.DataTypeIEC61360.IRI: 'IRI', - model.base.DataTypeIEC61360.IRDI: 'IRDI', - model.base.DataTypeIEC61360.RATIONAL: 'RATIONAL', - model.base.DataTypeIEC61360.RATIONAL_MEASURE: 'RATIONAL_MEASURE', - model.base.DataTypeIEC61360.TIME: 'TIME', - model.base.DataTypeIEC61360.TIMESTAMP: 'TIMESTAMP', - model.base.DataTypeIEC61360.HTML: 'HTML', - model.base.DataTypeIEC61360.BLOB: 'BLOB', - model.base.DataTypeIEC61360.FILE: 'FILE', -} - -IEC61360_LEVEL_TYPES: Dict[model.base.IEC61360LevelType, str] = { - model.base.IEC61360LevelType.MIN: 'min', - model.base.IEC61360LevelType.NOM: 'nom', - model.base.IEC61360LevelType.TYP: 'typ', - model.base.IEC61360LevelType.MAX: 'max', -} - -MODELLING_KIND_INVERSE: Dict[str, model.ModellingKind] = {v: k for k, v in MODELLING_KIND.items()} -ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} -QUALIFIER_KIND_INVERSE: Dict[str, model.QualifierKind] = {v: k for k, v in QUALIFIER_KIND.items()} -DIRECTION_INVERSE: Dict[str, model.Direction] = {v: k for k, v in DIRECTION.items()} -STATE_OF_EVENT_INVERSE: Dict[str, model.StateOfEvent] = {v: k for k, v in STATE_OF_EVENT.items()} -REFERENCE_TYPES_INVERSE: Dict[str, Type[model.Reference]] = {v: k for k, v in REFERENCE_TYPES.items()} -KEY_TYPES_INVERSE: Dict[str, model.KeyTypes] = {v: k for k, v in KEY_TYPES.items()} -ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()} -IEC61360_DATA_TYPES_INVERSE: Dict[str, model.base.DataTypeIEC61360] = {v: k for k, v in IEC61360_DATA_TYPES.items()} -IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.base.IEC61360LevelType] = \ - {v: k for k, v in IEC61360_LEVEL_TYPES.items()} - -KEY_TYPES_CLASSES_INVERSE: Dict[model.KeyTypes, Type[model.Referable]] = \ - {v: k for k, v in model.KEY_TYPES_CLASSES.items()} +XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" \ No newline at end of file diff --git a/sdk/basyx/adapter/aasx.py b/sdk/basyx/adapter/aasx.py index cab639f..9f76bca 100644 --- a/sdk/basyx/adapter/aasx.py +++ b/sdk/basyx/adapter/aasx.py @@ -16,7 +16,7 @@ Writing and reading of AASX packages is performed through the :class:`~.AASXReader` and :class:`~.AASXWriter` classes. Each instance of these classes wraps an existing AASX file resp. a file to be created and allows to read/write the -included AAS objects into/form :class:`ObjectStores `. +included AAS objects into/form :class:`ObjectStores `. For handling of embedded supplementary files, this module provides the :class:`~.AbstractSupplementaryFileContainer` class interface and the :class:`~.DictSupplementaryFileContainer` implementation. @@ -31,12 +31,14 @@ import re from typing import Dict, Tuple, IO, Union, List, Set, Optional, Iterable, Iterator -from .xml import read_aas_xml_file, write_aas_xml_file -from basyx.aas import model -from .json import read_aas_json_file, write_aas_json_file +from basyx.object_store import ObjectStore +from aas_core3 import types as model +from .json.json_serialization import write_aas_json_file +from .json.json_deserialization import read_aas_json_file +from .xml.xml_serialization import write_aas_xml_file +from .xml.xml_deserialization import read_aas_xml_file import pyecma376_2 -from ..util import traversal - +from .xml.xml_serialization import write_aas_xml_file logger = logging.getLogger(__name__) RELATIONSHIP_TYPE_AASX_ORIGIN = "http://admin-shell.io/aasx/relationships/aasx-origin" @@ -113,9 +115,9 @@ def get_thumbnail(self) -> Optional[bytes]: with self.reader.open_part(thumbnail_part) as p: return p.read() - def read_into(self, object_store: model.AbstractObjectStore, + def read_into(self, object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", - override_existing: bool = False) -> Set[model.Identifier]: + override_existing: bool = False, **kwargs) -> Set[str]: """ Read the contents of the AASX package and add them into a given :class:`ObjectStore ` @@ -145,17 +147,19 @@ def read_into(self, object_store: model.AbstractObjectStore, except IndexError as e: raise ValueError("Not a valid AASX file: aasx-origin Relationship is missing.") from e - read_identifiables: Set[model.Identifier] = set() + read_identifiables: Set[str] = set() # Iterate AAS files for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ RELATIONSHIP_TYPE_AAS_SPEC]: - self._read_aas_part_into(aas_part, object_store, file_store, read_identifiables, override_existing) + self._read_aas_part_into(aas_part, object_store, file_store, + read_identifiables, override_existing, **kwargs) # Iterate split parts of AAS file for split_part in self.reader.get_related_parts_by_type(aas_part)[ RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: - self._read_aas_part_into(split_part, object_store, file_store, read_identifiables, override_existing) + self._read_aas_part_into(split_part, object_store, file_store, + read_identifiables, override_existing, **kwargs) return read_identifiables @@ -172,10 +176,10 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> None: self.close() def _read_aas_part_into(self, part_name: str, - object_store: model.AbstractObjectStore, + object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", - read_identifiables: Set[model.Identifier], - override_existing: bool) -> None: + read_identifiables: Set[str], + override_existing: bool, **kwargs) -> None: """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. @@ -191,7 +195,9 @@ def _read_aas_part_into(self, part_name: str, :param override_existing: If True, existing objects in the object store are overridden with objects from the AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. """ - for obj in self._parse_aas_part(part_name): + #print("asd123123123") + + for obj in self._parse_aas_part(part_name, **kwargs): if obj.id in read_identifiables: continue if obj.id in object_store: @@ -207,7 +213,7 @@ def _read_aas_part_into(self, part_name: str, if isinstance(obj, model.Submodel): self._collect_supplementary_files(part_name, obj, file_store) - def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: + def _parse_aas_part(self, part_name: str, **kwargs) -> ObjectStore: """ Helper function to parse the AAS objects from a single JSON or XML part of the AASX package. @@ -216,21 +222,26 @@ def _parse_aas_part(self, part_name: str) -> model.DictObjectStore: :param part_name: The OPC part name of the part to be parsed :return: A DictObjectStore containing the parsed AAS objects """ + #print("asd123123123") + content_type = self.reader.get_content_type(part_name) extension = part_name.split("/")[-1].split(".")[-1] if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_xml_file(p) + #print(part_name) + return read_aas_xml_file(p, **kwargs) elif content_type.split(";")[0] in ("text/json", "application/json") \ or content_type == "" and extension == "json": logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) + #print("asd123123123") + with self.reader.open_part(part_name) as p: - return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig')) + return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs) else: logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}" .format(part_name, content_type, extension)) - return model.DictObjectStore() + return ObjectStore() def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel, file_store: "AbstractSupplementaryFileContainer") -> None: @@ -243,7 +254,7 @@ def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel, :param submodel: The Submodel to process :param file_store: The SupplementaryFileContainer to add the extracted supplementary files to """ - for element in traversal.walk_submodel(submodel): + for element in submodel.descend(): if isinstance(element, model.File): if element.value is None: continue @@ -261,6 +272,7 @@ def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel, element.value = final_name + class AASXWriter: """ An AASXWriter wraps a new AASX package file to write its contents to it piece by piece. @@ -319,8 +331,8 @@ def __init__(self, file: Union[os.PathLike, str, IO]): p.close() def write_aas(self, - aas_ids: Union[model.Identifier, Iterable[model.Identifier]], - object_store: model.AbstractObjectStore, + aas_ids: Union[str], + object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", write_json: bool = False) -> None: """ @@ -367,10 +379,10 @@ def write_aas(self, :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another :class:`~basyx.aas.model.base.Identifiable` object) """ - if isinstance(aas_ids, model.Identifier): - aas_ids = (aas_ids,) + #if isinstance(aas_ids, model.Identifiable.id): + # aas_ids = (aas_ids,) - objects_to_be_written: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + objects_to_be_written: ObjectStore[model.Identifiable] = ObjectStore() for aas_id in aas_ids: try: aas = object_store.get_identifiable(aas_id) @@ -380,39 +392,40 @@ def write_aas(self, if not isinstance(aas, model.AssetAdministrationShell): raise TypeError(f"Identifier {aas_id} does not belong to an AssetAdminstrationShell object but to " f"{aas!r}") - + assert isinstance(aas,model.AssetAdministrationShell) # Add the AssetAdministrationShell object to the data part objects_to_be_written.add(aas) # Add referenced Submodels to the data part - for submodel_ref in aas.submodel: + for submodel_ref in aas.submodels: try: - submodel = submodel_ref.resolve(object_store) + submodel_keys = submodel_ref.keys + for key in submodel_keys: + submodel_id = key.value + try: + submodel = object_store.get_identifiable(submodel_id) + except Exception: + continue + objects_to_be_written.add(submodel) + except KeyError: logger.warning("Could not find submodel %s. Skipping it.", str(submodel_ref)) continue - objects_to_be_written.add(submodel) # Traverse object tree and check if semanticIds are referencing to existing ConceptDescriptions in the # ObjectStore concept_descriptions: List[model.ConceptDescription] = [] for identifiable in objects_to_be_written: - for semantic_id in traversal.walk_semantic_ids_recursive(identifiable): - if not isinstance(semantic_id, model.ModelReference) \ - or semantic_id.type is not model.ConceptDescription: - logger.info("semanticId %s does not reference a ConceptDescription.", str(semantic_id)) - continue + for element in identifiable.descend(): try: - cd = semantic_id.resolve(object_store) - except KeyError: - logger.info("ConceptDescription for semantidId %s not found in object store.", str(semantic_id)) - continue - except model.UnexpectedTypeError as e: - logger.error("semantidId %s resolves to %s, which is not a ConceptDescription", - str(semantic_id), e.value) - continue - concept_descriptions.append(cd) - objects_to_be_written.update(concept_descriptions) + semantic_id = element.semantic_id + cd = object_store.get_identifiable(semantic_id) + concept_descriptions.append(cd) + except Exception: + continue + + for element in concept_descriptions: + objects_to_be_written.add(element) # Write AAS data part self.write_all_aas_objects("/aasx/data.{}".format("json" if write_json else "xml"), @@ -422,8 +435,8 @@ def write_aas(self, # Not actually required since you can always create a local dict def write_aas_objects(self, part_name: str, - object_ids: Iterable[model.Identifier], - object_store: model.AbstractObjectStore, + object_ids: Iterable[str], + object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", write_json: bool = False, split_part: bool = False, @@ -461,7 +474,7 @@ def write_aas_objects(self, """ logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) - objects: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + objects: ObjectStore[model.Identifiable] = ObjectStore() # Retrieve objects and scan for referenced supplementary files for identifier in object_ids: @@ -478,7 +491,7 @@ def write_aas_objects(self, # Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 def write_all_aas_objects(self, part_name: str, - objects: model.AbstractObjectStore[model.Identifiable], + objects: ObjectStore, file_store: "AbstractSupplementaryFileContainer", write_json: bool = False, split_part: bool = False, @@ -513,10 +526,11 @@ def write_all_aas_objects(self, logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) supplementary_files: List[str] = [] + # Retrieve objects and scan for referenced supplementary files for the_object in objects: if isinstance(the_object, model.Submodel): - for element in traversal.walk_submodel(the_object): + for element in the_object.descend(): if isinstance(element, model.File): file_name = element.value # Skip File objects with empty value URI references that are considered to be no local file @@ -525,18 +539,28 @@ def write_all_aas_objects(self, continue supplementary_files.append(file_name) + + + # Add aas-spec relationship if not split_part: self._aas_part_names.append(part_name) + + # Write part # TODO allow writing xml *and* JSON part + #print(part_name) with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p: if write_json: write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects) else: write_aas_xml_file(p, objects) + + + + # Write submodel's supplementary files to AASX file supplementary_file_names = [] for file_name in supplementary_files: @@ -570,6 +594,9 @@ def write_all_aas_objects(self, additional_relationships), part_name) + + + def write_core_properties(self, core_properties: pyecma376_2.OPCCoreProperties): """ Write OPC Core Properties (meta data) to the AASX package file. @@ -673,7 +700,7 @@ class NameFriendlyfier: def __init__(self) -> None: self.issued_names: Set[str] = set() - def get_friendly_name(self, identifier: model.Identifier): + def get_friendly_name(self, identifier: str): """ Generate a friendly name from an AAS identifier. diff --git a/sdk/basyx/adapter/json/__init__.py b/sdk/basyx/adapter/json/__init__.py index 04b7805..427833c 100644 --- a/sdk/basyx/adapter/json/__init__.py +++ b/sdk/basyx/adapter/json/__init__.py @@ -17,6 +17,6 @@ :class:`ObjectStore `. """ -from .json_serialization import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, object_store_to_json -from .json_deserialization import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrippedAASFromJsonDecoder, \ - StrictStrippedAASFromJsonDecoder, read_aas_json_file, read_aas_json_file_into +#from .json_serialization import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, object_store_to_json +#from .json_deserialization import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrippedAASFromJsonDecoder, \ + # StrictStrippedAASFromJsonDecoder, read_aas_json_file, read_aas_json_file_into diff --git a/sdk/basyx/adapter/json/json_deserialization.py b/sdk/basyx/adapter/json/json_deserialization.py index 7e0c39c..8df4e01 100644 --- a/sdk/basyx/adapter/json/json_deserialization.py +++ b/sdk/basyx/adapter/json/json_deserialization.py @@ -1,55 +1,13 @@ -# Copyright (c) 2023 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 -""" -.. _adapter.json.json_deserialization: - -Module for deserializing Asset Administration Shell data from the official JSON format - -The module provides custom JSONDecoder classes :class:`~.AASFromJsonDecoder` and :class:`~.StrictAASFromJsonDecoder` to -be used with the Python standard :mod:`json` module. - -Furthermore it provides two classes :class:`~basyx.aas.adapter.json.json_deserialization.StrippedAASFromJsonDecoder` and -:class:`~basyx.aas.adapter.json.json_deserialization.StrictStrippedAASFromJsonDecoder` for parsing stripped -JSON objects, which are used in the http adapter (see https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91). -The classes contain a custom :meth:`~basyx.aas.adapter.json.json_deserialization.AASFromJsonDecoder.object_hook` -function to detect encoded AAS objects within the JSON data and convert them to BaSyx Python SDK objects while parsing. -Additionally, there's the :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into` function, that -takes a complete AAS JSON file, reads its contents and stores the objects in the provided -:class:`~basyx.aas.model.provider.AbstractObjectStore`. :meth:`read_aas_json_file` is a wrapper for this function. -Instead of storing the objects in a given :class:`~basyx.aas.model.provider.AbstractObjectStore`, -it returns a :class:`~basyx.aas.model.provider.DictObjectStore` containing parsed objects. - -The deserialization is performed in a bottom-up approach: The ``object_hook()`` method gets called for every parsed JSON -object (as dict) and checks for existence of the ``modelType`` attribute. If it is present, the ``AAS_CLASS_PARSERS`` -dict defines, which of the constructor methods of the class is to be used for converting the dict into an object. -Embedded objects that should have a ``modelType`` themselves are expected to be converted already. -Other embedded objects are converted using a number of helper constructor methods. -""" -import base64 import contextlib import json -import logging -import pprint from typing import Dict, Callable, ContextManager, TypeVar, Type, List, IO, Optional, Set, get_args +from aas_core3.types import AssetAdministrationShell, Submodel, ConceptDescription +import aas_core3.jsonization as aas_jsonization -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 - -logger = logging.getLogger(__name__) - - -# ############################################################################# -# Helper functions (for simplifying implementation of constructor functions) -# ############################################################################# +from basyx.object_store import ObjectStore, Identifiable +from .._generic import PathOrIO, Path T = TypeVar('T') -LSS = TypeVar('LSS', bound=model.LangStringSet) def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: @@ -71,733 +29,8 @@ def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: return val -def _expect_type(object_: object, type_: Type, context: str, failsafe: bool) -> bool: - """ - Helper function to check type of an embedded object. - - This function may be used in any constructor function for an AAS object that expects to find already constructed - AAS objects of a certain type within its data dict. In this case, we want to ensure that the object has this kind - and raise a TypeError if not. In failsafe mode, we want to log the error and prevent the object from being added - to the parent object. A typical use of this function would look like this: - - if _expect_type(element, model.SubmodelElement, str(submodel), failsafe): - submodel.submodel_element.add(element) - - :param object_: The object to by type-checked - :param type_: The expected type - :param context: A string to add to the exception message / log message, that describes the context in that the - object has been found - :param failsafe: Log error and return false instead of raising a TypeError - :return: True if the - :raises TypeError: If the object is not of the expected type and the failsafe mode is not active - """ - if isinstance(object_, type_): - return True - if failsafe: - logger.error("Expected a %s in %s, but found %s", type_.__name__, context, repr(object_)) - else: - raise TypeError("Expected a %s in %s, but found %s" % (type_.__name__, context, repr(object_))) - return False - - -class AASFromJsonDecoder(json.JSONDecoder): - """ - Custom JSONDecoder class to use the :mod:`json` module for deserializing Asset Administration Shell data from the - official JSON format - - The class contains a custom :meth:`~.AASFromJsonDecoder.object_hook` function to detect encoded AAS objects within - the JSON data and convert them to BaSyx Python SDK objects while parsing. - - Typical usage: - - .. code-block:: python - - data = json.loads(json_string, cls=AASFromJsonDecoder) - - The ``object_hook`` function uses a set of ``_construct_*()`` methods, one for each - AAS object type to transform the JSON objects in to BaSyx Python SDK objects. These constructor methods are divided - into two parts: "Helper Constructor Methods", that are used to construct AAS object types without a ``modelType`` - attribute as embedded objects within other AAS objects, and "Direct Constructor Methods" for AAS object types *with* - ``modelType`` attribute. The former are called from other constructor methods or utility methods based on the - expected type of an attribute, the latter are called directly from the ``object_hook()`` function based on the - ``modelType`` attribute. - - This class may be subclassed to override some of the constructor functions, e.g. to construct objects of specialized - subclasses of the BaSyx Python SDK object classes instead of these normal classes from the ``model`` package. To - simplify this tasks, (nearly) all the constructor methods take a parameter ``object_type`` defaulting to the normal - BaSyx Python SDK object class, that can be overridden in a derived function: - - .. code-block:: python - - .. code-block:: python - - class EnhancedSubmodel(model.Submodel): - pass - - class EnhancedAASDecoder(StrictAASFromJsonDecoder): - @classmethod - def _construct_submodel(cls, dct, object_class=EnhancedSubmodel): - return super()._construct_submodel(dct, object_class=object_class) - - - :cvar failsafe: If ``True`` (the default), don't raise Exceptions for missing attributes and wrong types, but - instead skip defective objects and use logger to output warnings. Use StrictAASFromJsonDecoder for a - non-failsafe version. - :cvar stripped: If ``True``, the JSON objects will be parsed in a stripped manner, excluding some attributes. - Defaults to ``False``. - See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 - """ - failsafe = True - stripped = False - - 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 - - # 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]] = { - 'AssetAdministrationShell': cls._construct_asset_administration_shell, - 'AssetInformation': cls._construct_asset_information, - 'SpecificAssetId': cls._construct_specific_asset_id, - 'ConceptDescription': cls._construct_concept_description, - 'Extension': cls._construct_extension, - 'Submodel': cls._construct_submodel, - 'Capability': cls._construct_capability, - 'Entity': cls._construct_entity, - 'BasicEventElement': cls._construct_basic_event_element, - 'Operation': cls._construct_operation, - 'RelationshipElement': cls._construct_relationship_element, - 'AnnotatedRelationshipElement': cls._construct_annotated_relationship_element, - 'SubmodelElementCollection': cls._construct_submodel_element_collection, - 'SubmodelElementList': cls._construct_submodel_element_list, - 'Blob': cls._construct_blob, - 'File': cls._construct_file, - 'MultiLanguageProperty': cls._construct_multi_language_property, - 'Property': cls._construct_property, - 'Range': cls._construct_range, - 'ReferenceElement': cls._construct_reference_element, - 'DataSpecificationIec61360': cls._construct_data_specification_iec61360, - } - - # Get modelType and constructor function - if not isinstance(dct['modelType'], str): - logger.warning("JSON object has unexpected format of modelType: %s", dct['modelType']) - # Even in strict mode, we consider 'modelType' attributes of wrong type as non-AAS objects instead of - # raising an exception. However, the object's type will probably checked later by read_json_aas_file() or - # _expect_type() - return dct - model_type = dct['modelType'] - if model_type not in AAS_CLASS_PARSERS: - if not cls.failsafe: - raise TypeError("Found JSON object with modelType=\"%s\", which is not a known AAS class" % model_type) - logger.error("Found JSON object with modelType=\"%s\", which is not a known AAS class", model_type) - return dct - - # Use constructor function to transform JSON representation into BaSyx Python SDK model object - try: - return AAS_CLASS_PARSERS[model_type](dct) - except (KeyError, TypeError, model.AASConstraintViolation) as e: - error_message = "Error while trying to convert JSON object into {}: {} >>> {}".format( - model_type, e, pprint.pformat(dct, depth=2, width=2**14, compact=True)) - if cls.failsafe: - logger.error(error_message, exc_info=e) - # In failsafe mode, we return the raw JSON object dict, if there were errors while parsing an object, so - # a client application is able to handle this data. The read_json_aas_file() function and all - # constructors for complex objects will skip those items by using _expect_type(). - return dct - else: - raise (type(e) if isinstance(e, (KeyError, TypeError)) else TypeError)(error_message) from e - - # ################################################################################################## - # Utility Methods used in constructor methods to add general attributes (from abstract base classes) - # ################################################################################################## - - @classmethod - def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None: - """ - Utility method to add the optional attributes of the abstract meta classes Referable, Identifiable, - HasSemantics, HasKind and Qualifiable to an object inheriting from any of these classes, if present - - :param obj: The object to amend its attributes - :param dct: The object's dict representation from JSON - """ - if isinstance(obj, model.Referable): - if 'idShort' in dct: - obj.id_short = _get_ts(dct, 'idShort', str) - if 'category' in dct: - obj.category = _get_ts(dct, 'category', str) - if 'displayName' in dct: - obj.display_name = cls._construct_lang_string_set(_get_ts(dct, 'displayName', list), - model.MultiLanguageNameType) - if 'description' in dct: - obj.description = cls._construct_lang_string_set(_get_ts(dct, 'description', list), - model.MultiLanguageTextType) - if isinstance(obj, model.Identifiable): - if 'administration' in dct: - obj.administration = cls._construct_administrative_information(_get_ts(dct, 'administration', dict)) - if isinstance(obj, model.HasSemantics): - if 'semanticId' in dct: - obj.semantic_id = cls._construct_reference(_get_ts(dct, 'semanticId', dict)) - if 'supplementalSemanticIds' in dct: - for ref in _get_ts(dct, 'supplementalSemanticIds', list): - obj.supplemental_semantic_id.append(cls._construct_reference(ref)) - # `HasKind` provides only mandatory, immutable attributes; so we cannot do anything here, after object creation. - # However, the `cls._get_kind()` function may assist by retrieving them from the JSON object - if isinstance(obj, model.Qualifiable) and not cls.stripped: - if 'qualifiers' in dct: - for constraint_dct in _get_ts(dct, 'qualifiers', list): - constraint = cls._construct_qualifier(constraint_dct) - obj.qualifier.add(constraint) - if isinstance(obj, model.HasDataSpecification) and not cls.stripped: - if 'embeddedDataSpecifications' in dct: - for dspec in _get_ts(dct, 'embeddedDataSpecifications', list): - obj.embedded_data_specifications.append( - # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - model.EmbeddedDataSpecification( - data_specification=cls._construct_reference(_get_ts(dspec, 'dataSpecification', dict)), - data_specification_content=_get_ts(dspec, 'dataSpecificationContent', - model.DataSpecificationContent) # type: ignore - ) - ) - if isinstance(obj, model.HasExtension) and not cls.stripped: - if 'extensions' in dct: - for extension in _get_ts(dct, 'extensions', list): - obj.extension.add(cls._construct_extension(extension)) - - @classmethod - def _get_kind(cls, dct: Dict[str, object]) -> model.ModellingKind: - """ - Utility method to get the kind of an HasKind object from its JSON representation. - - :param dct: The object's dict representation from JSON - :return: The object's ``kind`` value - """ - return MODELLING_KIND_INVERSE[_get_ts(dct, "kind", str)] if 'kind' in dct else model.ModellingKind.INSTANCE - - # ############################################################################# - # Helper Constructor Methods starting from here - # ############################################################################# - - # These constructor methods create objects that are not identified by a 'modelType' JSON attribute, so they can not - # be called from the object_hook() method. Instead, they are called by other constructor functions to transform - # embedded JSON data into the expected type at their location in the outer JSON object. - - @classmethod - def _construct_key(cls, dct: Dict[str, object], object_class=model.Key) -> model.Key: - return object_class(type_=KEY_TYPES_INVERSE[_get_ts(dct, 'type', str)], - value=_get_ts(dct, 'value', str)) - - @classmethod - def _construct_specific_asset_id(cls, dct: Dict[str, object], object_class=model.SpecificAssetId) \ - -> model.SpecificAssetId: - # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable - return object_class(name=_get_ts(dct, 'name', str), - value=_get_ts(dct, 'value', str), - external_subject_id=cls._construct_external_reference( - _get_ts(dct, 'externalSubjectId', dict)) if 'externalSubjectId' in dct else None, - semantic_id=cls._construct_reference(_get_ts(dct, 'semanticId', dict)) - if 'semanticId' in dct else None, - supplemental_semantic_id=[ - cls._construct_reference(ref) for ref in - _get_ts(dct, 'supplementalSemanticIds', list)] - if 'supplementalSemanticIds' in dct else ()) - - @classmethod - def _construct_reference(cls, dct: Dict[str, object]) -> model.Reference: - reference_type: Type[model.Reference] = REFERENCE_TYPES_INVERSE[_get_ts(dct, 'type', str)] - if reference_type is model.ModelReference: - return cls._construct_model_reference(dct, model.Referable) # type: ignore - elif reference_type is model.ExternalReference: - return cls._construct_external_reference(dct) - raise ValueError(f"Unsupported reference type {reference_type}!") - - @classmethod - def _construct_external_reference(cls, dct: Dict[str, object], object_class=model.ExternalReference)\ - -> model.ExternalReference: - reference_type: Type[model.Reference] = REFERENCE_TYPES_INVERSE[_get_ts(dct, 'type', str)] - if reference_type is not model.ExternalReference: - raise ValueError(f"Expected a reference of type {model.ExternalReference}, got {reference_type}!") - keys = [cls._construct_key(key_data) for key_data in _get_ts(dct, "keys", list)] - return object_class(tuple(keys), cls._construct_reference(_get_ts(dct, 'referredSemanticId', dict)) - if 'referredSemanticId' in dct else None) - - @classmethod - def _construct_model_reference(cls, dct: Dict[str, object], type_: Type[T], object_class=model.ModelReference)\ - -> model.ModelReference: - reference_type: Type[model.Reference] = REFERENCE_TYPES_INVERSE[_get_ts(dct, 'type', str)] - if reference_type is not model.ModelReference: - raise ValueError(f"Expected a reference of type {model.ModelReference}, got {reference_type}!") - keys = [cls._construct_key(key_data) for key_data in _get_ts(dct, "keys", list)] - if keys and not issubclass(KEY_TYPES_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): - logger.warning("type %s of last key of reference to %s does not match reference type %s", - keys[-1].type.name, " / ".join(str(k) for k in keys), type_.__name__) - return object_class(tuple(keys), type_, cls._construct_reference(_get_ts(dct, 'referredSemanticId', dict)) - if 'referredSemanticId' in dct else None) - - @classmethod - def _construct_administrative_information( - cls, dct: Dict[str, object], object_class=model.AdministrativeInformation)\ - -> model.AdministrativeInformation: - ret = object_class() - cls._amend_abstract_attributes(ret, dct) - if 'version' in dct: - ret.version = _get_ts(dct, 'version', str) - if 'revision' in dct: - ret.revision = _get_ts(dct, 'revision', str) - elif 'revision' in dct: - logger.warning("Ignoring 'revision' attribute of AdministrativeInformation object due to missing 'version'") - if 'creator' in dct: - ret.creator = cls._construct_reference(_get_ts(dct, 'creator', dict)) - if 'templateId' in dct: - ret.template_id = _get_ts(dct, 'templateId', str) - return ret - - @classmethod - def _construct_operation_variable(cls, dct: Dict[str, object]) -> model.SubmodelElement: - """ - Since we don't implement ``OperationVariable``, this constructor discards the wrapping ``OperationVariable`` - object and just returns the contained :class:`~basyx.aas.model.submodel.SubmodelElement`. - """ - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - return _get_ts(dct, 'value', model.SubmodelElement) # type: ignore - - @classmethod - def _construct_lang_string_set(cls, lst: List[Dict[str, object]], object_class: Type[LSS]) -> LSS: - ret = {} - for desc in lst: - try: - ret[_get_ts(desc, 'language', str)] = _get_ts(desc, 'text', str) - except (KeyError, TypeError) as e: - error_message = "Error while trying to convert JSON object into {}: {} >>> {}".format( - object_class.__name__, e, pprint.pformat(desc, depth=2, width=2 ** 14, compact=True)) - if cls.failsafe: - logger.error(error_message, exc_info=e) - else: - raise type(e)(error_message) from e - return object_class(ret) - - @classmethod - def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList: - ret: model.ValueList = set() - for element in _get_ts(dct, 'valueReferencePairs', list): - try: - ret.add(cls._construct_value_reference_pair(element)) - except (KeyError, TypeError) as e: - error_message = "Error while trying to convert JSON object into ValueReferencePair: {} >>> {}".format( - e, pprint.pformat(element, depth=2, width=2 ** 14, compact=True)) - if cls.failsafe: - logger.error(error_message, exc_info=e) - else: - raise type(e)(error_message) from e - return ret - - @classmethod - def _construct_value_reference_pair(cls, dct: Dict[str, object], - object_class=model.ValueReferencePair) -> model.ValueReferencePair: - return object_class(value=_get_ts(dct, 'value', str), - value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict))) - - # ############################################################################# - # Direct Constructor Methods (for classes with `modelType`) starting from here - # ############################################################################# - - # These constructor methods create objects that *are* identified by a 'modelType' JSON attribute, so they can be - # be called from the object_hook() method directly. - - @classmethod - def _construct_asset_information(cls, dct: Dict[str, object], object_class=model.AssetInformation)\ - -> model.AssetInformation: - global_asset_id = None - if 'globalAssetId' in dct: - global_asset_id = _get_ts(dct, 'globalAssetId', str) - specific_asset_id = set() - if 'specificAssetIds' in dct: - for desc_data in _get_ts(dct, "specificAssetIds", list): - specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) - - ret = object_class(asset_kind=ASSET_KIND_INVERSE[_get_ts(dct, 'assetKind', str)], - global_asset_id=global_asset_id, - specific_asset_id=specific_asset_id) - cls._amend_abstract_attributes(ret, dct) - - if 'assetType' in dct: - ret.asset_type = _get_ts(dct, 'assetType', str) - if 'defaultThumbnail' in dct: - ret.default_thumbnail = cls._construct_resource(_get_ts(dct, 'defaultThumbnail', dict)) - return ret - - @classmethod - def _construct_asset_administration_shell( - cls, dct: Dict[str, object], object_class=model.AssetAdministrationShell) -> model.AssetAdministrationShell: - ret = object_class( - asset_information=cls._construct_asset_information(_get_ts(dct, 'assetInformation', dict), - model.AssetInformation), - id_=_get_ts(dct, 'id', str)) - cls._amend_abstract_attributes(ret, dct) - if not cls.stripped and 'submodels' in dct: - for sm_data in _get_ts(dct, 'submodels', list): - ret.submodel.add(cls._construct_model_reference(sm_data, model.Submodel)) - if 'derivedFrom' in dct: - ret.derived_from = cls._construct_model_reference(_get_ts(dct, 'derivedFrom', dict), - model.AssetAdministrationShell) - return ret - - @classmethod - def _construct_concept_description(cls, dct: Dict[str, object], object_class=model.ConceptDescription)\ - -> model.ConceptDescription: - ret = object_class(id_=_get_ts(dct, 'id', str)) - cls._amend_abstract_attributes(ret, dct) - if 'isCaseOf' in dct: - for case_data in _get_ts(dct, "isCaseOf", list): - ret.is_case_of.add(cls._construct_reference(case_data)) - return ret - - @classmethod - def _construct_data_specification_iec61360(cls, dct: Dict[str, object], - object_class=model.base.DataSpecificationIEC61360)\ - -> model.base.DataSpecificationIEC61360: - ret = object_class(preferred_name=cls._construct_lang_string_set(_get_ts(dct, 'preferredName', list), - model.PreferredNameTypeIEC61360)) - if 'dataType' in dct: - ret.data_type = IEC61360_DATA_TYPES_INVERSE[_get_ts(dct, 'dataType', str)] - if 'definition' in dct: - ret.definition = cls._construct_lang_string_set(_get_ts(dct, 'definition', list), - model.DefinitionTypeIEC61360) - if 'shortName' in dct: - ret.short_name = cls._construct_lang_string_set(_get_ts(dct, 'shortName', list), - model.ShortNameTypeIEC61360) - if 'unit' in dct: - ret.unit = _get_ts(dct, 'unit', str) - if 'unitId' in dct: - ret.unit_id = cls._construct_reference(_get_ts(dct, 'unitId', dict)) - if 'sourceOfDefinition' in dct: - ret.source_of_definition = _get_ts(dct, 'sourceOfDefinition', str) - if 'symbol' in dct: - ret.symbol = _get_ts(dct, 'symbol', str) - if 'valueFormat' in dct: - ret.value_format = _get_ts(dct, 'valueFormat', str) - if 'valueList' in dct: - ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict)) - if 'value' in dct: - ret.value = _get_ts(dct, 'value', str) - if 'valueId' in dct: - ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) - if 'levelType' in dct: - for k, v in _get_ts(dct, 'levelType', dict).items(): - if v: - ret.level_types.add(IEC61360_LEVEL_TYPES_INVERSE[k]) - return ret - - @classmethod - def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> model.Entity: - global_asset_id = None - if 'globalAssetId' in dct: - global_asset_id = _get_ts(dct, 'globalAssetId', str) - specific_asset_id = set() - if 'specificAssetIds' in dct: - for desc_data in _get_ts(dct, "specificAssetIds", list): - specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) - - ret = object_class(id_short=None, - entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], - global_asset_id=global_asset_id, - specific_asset_id=specific_asset_id) - cls._amend_abstract_attributes(ret, dct) - if not cls.stripped and 'statements' in dct: - for element in _get_ts(dct, "statements", list): - if _expect_type(element, model.SubmodelElement, str(ret), cls.failsafe): - ret.statement.add(element) - return ret - - @classmethod - def _construct_qualifier(cls, dct: Dict[str, object], object_class=model.Qualifier) -> model.Qualifier: - ret = object_class(type_=_get_ts(dct, 'type', str), - value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)]) - cls._amend_abstract_attributes(ret, dct) - if 'value' in dct: - ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) - if 'valueId' in dct: - ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) - if 'kind' in dct: - ret.kind = QUALIFIER_KIND_INVERSE[_get_ts(dct, 'kind', str)] - return ret - - @classmethod - def _construct_extension(cls, dct: Dict[str, object], object_class=model.Extension) -> model.Extension: - ret = object_class(name=_get_ts(dct, 'name', str)) - cls._amend_abstract_attributes(ret, dct) - if 'valueType' in dct: - ret.value_type = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)] - if 'value' in dct: - ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) - if 'refersTo' in dct: - ret.refers_to = {cls._construct_model_reference(refers_to, model.Referable) # type: ignore - for refers_to in _get_ts(dct, 'refersTo', list)} - return ret - - @classmethod - def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel) -> model.Submodel: - ret = object_class(id_=_get_ts(dct, 'id', str), - kind=cls._get_kind(dct)) - cls._amend_abstract_attributes(ret, dct) - if not cls.stripped and 'submodelElements' in dct: - for element in _get_ts(dct, "submodelElements", list): - if _expect_type(element, model.SubmodelElement, str(ret), cls.failsafe): - ret.submodel_element.add(element) - return ret - - @classmethod - def _construct_capability(cls, dct: Dict[str, object], object_class=model.Capability) -> model.Capability: - ret = object_class(id_short=None) - cls._amend_abstract_attributes(ret, dct) - return ret - - @classmethod - def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=model.BasicEventElement) \ - -> model.BasicEventElement: - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - ret = object_class(id_short=None, - observed=cls._construct_model_reference(_get_ts(dct, 'observed', dict), - model.Referable), # type: ignore - direction=DIRECTION_INVERSE[_get_ts(dct, "direction", str)], - state=STATE_OF_EVENT_INVERSE[_get_ts(dct, "state", str)]) - cls._amend_abstract_attributes(ret, dct) - if 'messageTopic' in dct: - ret.message_topic = _get_ts(dct, 'messageTopic', str) - if 'messageBroker' in dct: - ret.message_broker = cls._construct_reference(_get_ts(dct, 'messageBroker', dict)) - if 'lastUpdate' in dct: - ret.last_update = model.datatypes.from_xsd(_get_ts(dct, 'lastUpdate', str), model.datatypes.DateTime) - if 'minInterval' in dct: - ret.min_interval = model.datatypes.from_xsd(_get_ts(dct, 'minInterval', str), model.datatypes.Duration) - if 'maxInterval' in dct: - ret.max_interval = model.datatypes.from_xsd(_get_ts(dct, 'maxInterval', str), model.datatypes.Duration) - return ret - - @classmethod - def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operation) -> model.Operation: - ret = object_class(None) - cls._amend_abstract_attributes(ret, dct) - - # Deserialize variables (they are not Referable, thus we don't - for json_name, target in (('inputVariables', ret.input_variable), - ('outputVariables', ret.output_variable), - ('inoutputVariables', ret.in_output_variable)): - if json_name in dct: - for variable_data in _get_ts(dct, json_name, list): - try: - target.add(cls._construct_operation_variable(variable_data)) - except (KeyError, TypeError) as e: - error_message = "Error while trying to convert JSON object into {} of {}: {}".format( - json_name, ret, pprint.pformat(variable_data, depth=2, width=2 ** 14, compact=True)) - if cls.failsafe: - logger.error(error_message, exc_info=e) - else: - raise type(e)(error_message) from e - return ret - - @classmethod - def _construct_relationship_element( - cls, dct: Dict[str, object], object_class=model.RelationshipElement) -> model.RelationshipElement: - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - ret = object_class(id_short=None, - first=cls._construct_reference(_get_ts(dct, 'first', dict)), - second=cls._construct_reference(_get_ts(dct, 'second', dict))) - cls._amend_abstract_attributes(ret, dct) - return ret - - @classmethod - def _construct_annotated_relationship_element( - cls, dct: Dict[str, object], object_class=model.AnnotatedRelationshipElement)\ - -> model.AnnotatedRelationshipElement: - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - ret = object_class( - id_short=None, - first=cls._construct_reference(_get_ts(dct, 'first', dict)), - second=cls._construct_reference(_get_ts(dct, 'second', dict))) - cls._amend_abstract_attributes(ret, dct) - if not cls.stripped and 'annotations' in dct: - for element in _get_ts(dct, 'annotations', list): - if _expect_type(element, model.DataElement, str(ret), cls.failsafe): - ret.annotation.add(element) - return ret - - @classmethod - def _construct_submodel_element_collection(cls, dct: Dict[str, object], - object_class=model.SubmodelElementCollection)\ - -> model.SubmodelElementCollection: - ret = object_class(id_short=None) - cls._amend_abstract_attributes(ret, dct) - if not cls.stripped and 'value' in dct: - for element in _get_ts(dct, "value", list): - if _expect_type(element, model.SubmodelElement, str(ret), cls.failsafe): - ret.value.add(element) - return ret - - @classmethod - def _construct_submodel_element_list(cls, dct: Dict[str, object], object_class=model.SubmodelElementList)\ - -> model.SubmodelElementList: - type_value_list_element = KEY_TYPES_CLASSES_INVERSE[ - KEY_TYPES_INVERSE[_get_ts(dct, 'typeValueListElement', str)]] - if not issubclass(type_value_list_element, model.SubmodelElement): - raise ValueError("Expected a SubmodelElementList with a typeValueListElement that is a subclass of" - f"{model.SubmodelElement}, got {type_value_list_element}!") - order_relevant = _get_ts(dct, 'orderRelevant', bool) if 'orderRelevant' in dct else True - semantic_id_list_element = cls._construct_reference(_get_ts(dct, 'semanticIdListElement', dict))\ - if 'semanticIdListElement' in dct else None - value_type_list_element = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueTypeListElement', str)]\ - if 'valueTypeListElement' in dct else None - ret = object_class(id_short=None, - type_value_list_element=type_value_list_element, - order_relevant=order_relevant, - semantic_id_list_element=semantic_id_list_element, - value_type_list_element=value_type_list_element) - cls._amend_abstract_attributes(ret, dct) - if not cls.stripped and 'value' in dct: - for element in _get_ts(dct, 'value', list): - if _expect_type(element, type_value_list_element, str(ret), cls.failsafe): - ret.value.add(element) - return ret - - @classmethod - def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> model.Blob: - ret = object_class(id_short=None, - content_type=_get_ts(dct, "contentType", str)) - cls._amend_abstract_attributes(ret, dct) - if 'value' in dct: - ret.value = base64.b64decode(_get_ts(dct, 'value', str)) - return ret - - @classmethod - def _construct_file(cls, dct: Dict[str, object], object_class=model.File) -> model.File: - ret = object_class(id_short=None, - value=None, - content_type=_get_ts(dct, "contentType", str)) - cls._amend_abstract_attributes(ret, dct) - if 'value' in dct and dct['value'] is not None: - ret.value = _get_ts(dct, 'value', str) - return ret - - @classmethod - def _construct_resource(cls, dct: Dict[str, object], object_class=model.Resource) -> model.Resource: - ret = object_class(path=_get_ts(dct, "path", str)) - cls._amend_abstract_attributes(ret, dct) - if 'contentType' in dct and dct['contentType'] is not None: - ret.content_type = _get_ts(dct, 'contentType', str) - return ret - - @classmethod - def _construct_multi_language_property( - cls, dct: Dict[str, object], object_class=model.MultiLanguageProperty) -> model.MultiLanguageProperty: - ret = object_class(id_short=None) - cls._amend_abstract_attributes(ret, dct) - if 'value' in dct and dct['value'] is not None: - ret.value = cls._construct_lang_string_set(_get_ts(dct, 'value', list), model.MultiLanguageTextType) - if 'valueId' in dct: - ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) - return ret - - @classmethod - def _construct_property(cls, dct: Dict[str, object], object_class=model.Property) -> model.Property: - ret = object_class(id_short=None, - value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)],) - cls._amend_abstract_attributes(ret, dct) - if 'value' in dct and dct['value'] is not None: - ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_type) - if 'valueId' in dct: - ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict)) - return ret - - @classmethod - def _construct_range(cls, dct: Dict[str, object], object_class=model.Range) -> model.Range: - ret = object_class(id_short=None, - value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)],) - cls._amend_abstract_attributes(ret, dct) - if 'min' in dct and dct['min'] is not None: - ret.min = model.datatypes.from_xsd(_get_ts(dct, 'min', str), ret.value_type) - if 'max' in dct and dct['max'] is not None: - ret.max = model.datatypes.from_xsd(_get_ts(dct, 'max', str), ret.value_type) - return ret - - @classmethod - def _construct_reference_element( - cls, dct: Dict[str, object], object_class=model.ReferenceElement) -> model.ReferenceElement: - ret = object_class(id_short=None, - value=None) - cls._amend_abstract_attributes(ret, dct) - if 'value' in dct and dct['value'] is not None: - ret.value = cls._construct_reference(_get_ts(dct, 'value', dict)) - return ret - - -class StrictAASFromJsonDecoder(AASFromJsonDecoder): - """ - A strict version of the AASFromJsonDecoder class for deserializing Asset Administration Shell data from the - official JSON format - - This version has set ``failsafe = False``, which will lead to Exceptions raised for every missing attribute or wrong - object type. - """ - failsafe = False - - -class StrippedAASFromJsonDecoder(AASFromJsonDecoder): - """ - Decoder for stripped JSON objects. Used in the HTTP adapter. - """ - stripped = True - - -class StrictStrippedAASFromJsonDecoder(StrictAASFromJsonDecoder, StrippedAASFromJsonDecoder): - """ - Non-failsafe decoder for stripped JSON objects. - """ - pass - - -def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFromJsonDecoder]]) \ - -> Type[AASFromJsonDecoder]: - """ - Returns the correct decoder based on the parameters failsafe and stripped. If a decoder class is given, failsafe - and stripped are ignored. - - :param failsafe: If ``True``, a failsafe decoder is selected. Ignored if a decoder class is specified. - :param stripped: If ``True``, a decoder for parsing stripped JSON objects is selected. Ignored if a decoder class is - specified. - :param decoder: Is returned, if specified. - :return: An :class:`~.AASFromJsonDecoder` (sub)class. - """ - if decoder is not None: - return decoder - if failsafe: - if stripped: - return StrippedAASFromJsonDecoder - return AASFromJsonDecoder - else: - if stripped: - return StrictStrippedAASFromJsonDecoder - return StrictAASFromJsonDecoder - - -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]: +def read_aas_json_file_into(object_store: ObjectStore, file: PathOrIO, replace_existing: bool = False, + ignore_existing: bool = False) -> Set[str]: """ Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 into a given object store. @@ -808,24 +41,14 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathO :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. This parameter is ignored if replace_existing is ``True``. - :param failsafe: If ``True``, the document is parsed in a failsafe way: Missing attributes and elements are logged - instead of causing exceptions. Defect objects are skipped. - This parameter is ignored if a decoder class is specified. - :param stripped: If ``True``, stripped JSON objects are parsed. - 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 - :raises KeyError: **Non-failsafe**: Encountered a duplicate identifier + :raises KeyError: 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`` - :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError, TypeError): **Non-failsafe**: - Errors during construction of the objects :raises TypeError: **Non-failsafe**: Encountered an element in the wrong list (e.g. an AssetAdministrationShell in ``submodels``) :return: A set of :class:`Identifiers ` that were added to object_store """ - ret: Set[model.Identifier] = set() - decoder_ = _select_decoder(failsafe, stripped, decoder) + ret: Set[str] = set() # json.load() accepts TextIO and BinaryIO cm: ContextManager[IO] @@ -839,65 +62,47 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathO # read, parse and convert JSON file with cm as fp: - data = json.load(fp, cls=decoder_) + data = json.load(fp) - for name, expected_type in (('assetAdministrationShells', model.AssetAdministrationShell), - ('submodels', model.Submodel), - ('conceptDescriptions', model.ConceptDescription)): + for name, expected_type in (('assetAdministrationShells', AssetAdministrationShell), + ('submodels', Submodel), + ('conceptDescriptions', ConceptDescription)): 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)) - 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 item.id in ret: - error_message = 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...") - 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}!" - if not ignore_existing: - raise KeyError(error_message + f" failed to insert {item}!") - logger.info(error_message + f" skipping insertion of {item}...") - continue - object_store.discard(existing_element) - object_store.add(item) - ret.add(item.id) - elif decoder_.failsafe: - logger.error(error_message) - else: - raise TypeError(error_message) + identifiable = aas_jsonization.identifiable_from_jsonable(item) + + if identifiable.id in ret: + error_message = f"{item} has a duplicate identifier already parsed in the document!" + raise KeyError(error_message) + existing_element = object_store.get(identifiable.id) + if existing_element is not None: + if not replace_existing: + error_message = f"object with identifier {identifiable.id} already exists " \ + f"in the object store: {existing_element}!" + if not ignore_existing: + raise KeyError(error_message + f" failed to insert {identifiable}!") + object_store.discard(existing_element) + object_store.add(identifiable) + ret.add(identifiable.id) + return ret -def read_aas_json_file(file: PathOrIO, **kwargs) -> model.DictObjectStore[model.Identifiable]: +def read_aas_json_file(file, **kwargs) -> ObjectStore[Identifiable]: """ - A wrapper of :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects - in an empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as - :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into`. + A wrapper of :meth:`~basyx.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects + in an empty :class:`~basyx.model.provider.DictObjectStore`. This function supports the same keyword arguments as + :meth:`~basyx.adapter.json.json_deserialization.read_aas_json_file_into`. :param file: A filename or file-like object to read the JSON-serialized data from :param kwargs: Keyword arguments passed to :meth:`read_aas_json_file_into` - :raises KeyError: **Non-failsafe**: Encountered a duplicate identifier - :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError, TypeError): **Non-failsafe**: - Errors during construction of the objects - :raises TypeError: **Non-failsafe**: Encountered an element in the wrong list - (e.g. an AssetAdministrationShell in ``submodels``) - :return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the JSON file + :raises KeyError: Encountered a duplicate identifier + :return: A :class:`~basyx.ObjectStore` containing all AAS objects from the JSON file """ - object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - read_aas_json_file_into(object_store, file, **kwargs) - return object_store + obj_store: ObjectStore[Identifiable] = ObjectStore() + read_aas_json_file_into(obj_store, file, **kwargs) + return obj_store diff --git a/sdk/basyx/adapter/json/json_serialization.py b/sdk/basyx/adapter/json/json_serialization.py index c80f890..7d8b924 100644 --- a/sdk/basyx/adapter/json/json_serialization.py +++ b/sdk/basyx/adapter/json/json_serialization.py @@ -17,7 +17,7 @@ Each class contains a custom :meth:`~.AASToJsonEncoder.default` function which converts BaSyx Python SDK objects to simple python types for an automatic JSON serialization. To simplify the usage of this module, the :meth:`write_aas_json_file` and :meth:`object_store_to_json` are provided. -The former is used to serialize a given :class:`~basyx.aas.model.provider.AbstractObjectStore` to a file, while the +The former is used to serialize a given :class:`~basyx.AbstractObjectStore` to a file, while the latter serializes the object store to a string and returns it. The serialization is performed in an iterative approach: The :meth:`~.AASToJsonEncoder.default` function gets called for @@ -31,681 +31,35 @@ import contextlib import inspect import io +import time from typing import ContextManager, List, Dict, Optional, TextIO, Type, Callable, get_args import json - -from basyx.aas import model +from basyx.object_store import ObjectStore +from aas_core3.types import AssetAdministrationShell, Submodel, ConceptDescription +from aas_core3.jsonization import to_jsonable from .. import _generic +import os +from typing import BinaryIO, Dict, IO, Type, Union -class AASToJsonEncoder(json.JSONEncoder): - """ - Custom JSON Encoder class to use the :mod:`json` module for serializing Asset Administration Shell data into the - official JSON format - - The class overrides the ``default()`` method to transform BaSyx Python SDK objects into dicts that may be serialized - by the standard encode method. - - Typical usage: - - .. code-block:: python - - json_string = json.dumps(data, cls=AASToJsonEncoder) - - :cvar stripped: If True, the JSON objects will be serialized in a stripped manner, excluding some attributes. - Defaults to ``False``. - See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 - """ - stripped = False - - def default(self, obj: object) -> object: - """ - The overwritten ``default`` method for :class:`json.JSONEncoder` - - :param obj: The object to serialize to json - :return: The serialized object - """ - mapping: Dict[Type, Callable] = { - model.AdministrativeInformation: self._administrative_information_to_json, - model.AnnotatedRelationshipElement: self._annotated_relationship_element_to_json, - model.AssetAdministrationShell: self._asset_administration_shell_to_json, - model.AssetInformation: self._asset_information_to_json, - model.BasicEventElement: self._basic_event_element_to_json, - model.Blob: self._blob_to_json, - model.Capability: self._capability_to_json, - model.ConceptDescription: self._concept_description_to_json, - model.DataSpecificationIEC61360: self._data_specification_iec61360_to_json, - model.Entity: self._entity_to_json, - model.Extension: self._extension_to_json, - model.File: self._file_to_json, - model.Key: self._key_to_json, - model.LangStringSet: self._lang_string_set_to_json, - model.MultiLanguageProperty: self._multi_language_property_to_json, - model.Operation: self._operation_to_json, - model.Property: self._property_to_json, - model.Qualifier: self._qualifier_to_json, - model.Range: self._range_to_json, - model.Reference: self._reference_to_json, - model.ReferenceElement: self._reference_element_to_json, - model.RelationshipElement: self._relationship_element_to_json, - model.Resource: self._resource_to_json, - model.SpecificAssetId: self._specific_asset_id_to_json, - model.Submodel: self._submodel_to_json, - model.SubmodelElementCollection: self._submodel_element_collection_to_json, - model.SubmodelElementList: self._submodel_element_list_to_json, - model.ValueReferencePair: self._value_reference_pair_to_json, - } - for typ in mapping: - if isinstance(obj, typ): - mapping_method = mapping[typ] - return mapping_method(obj) - return super().default(obj) - - @classmethod - def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]: - """ - transformation function to serialize abstract classes from model.base which are inherited by many classes - - :param obj: object which must be serialized - :return: dict with the serialized attributes of the abstract classes this object inherits from - """ - data: Dict[str, object] = {} - if isinstance(obj, model.HasExtension) and not cls.stripped: - if obj.extension: - data['extensions'] = list(obj.extension) - if isinstance(obj, model.HasDataSpecification) and not cls.stripped: - if obj.embedded_data_specifications: - data['embeddedDataSpecifications'] = [ - {'dataSpecification': spec.data_specification, - 'dataSpecificationContent': spec.data_specification_content} - for spec in obj.embedded_data_specifications - ] - - if isinstance(obj, model.Referable): - if obj.id_short and not isinstance(obj.parent, model.SubmodelElementList): - data['idShort'] = obj.id_short - if obj.display_name: - data['displayName'] = obj.display_name - if obj.category: - data['category'] = obj.category - if obj.description: - data['description'] = obj.description - try: - ref_type = next(iter(t for t in inspect.getmro(type(obj)) if t in model.KEY_TYPES_CLASSES)) - except StopIteration as e: - raise TypeError("Object of type {} is Referable but does not inherit from a known AAS type" - .format(obj.__class__.__name__)) from e - data['modelType'] = ref_type.__name__ - if isinstance(obj, model.Identifiable): - data['id'] = obj.id - if obj.administration: - data['administration'] = obj.administration - if isinstance(obj, model.HasSemantics): - if obj.semantic_id: - data['semanticId'] = obj.semantic_id - if obj.supplemental_semantic_id: - data['supplementalSemanticIds'] = list(obj.supplemental_semantic_id) - if isinstance(obj, model.HasKind): - if obj.kind is model.ModellingKind.TEMPLATE: - data['kind'] = _generic.MODELLING_KIND[obj.kind] - if isinstance(obj, model.Qualifiable) and not cls.stripped: - if obj.qualifier: - data['qualifiers'] = list(obj.qualifier) - return data - - # ############################################################# - # transformation functions to serialize classes from model.base - # ############################################################# - - @classmethod - def _lang_string_set_to_json(cls, obj: model.LangStringSet) -> List[Dict[str, object]]: - return [{'language': k, 'text': v} - for k, v in obj.items()] - - @classmethod - def _key_to_json(cls, obj: model.Key) -> Dict[str, object]: - """ - serialization of an object from class Key to json - - :param obj: object of class Key - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data.update({'type': _generic.KEY_TYPES[obj.type], - 'value': obj.value}) - return data - - @classmethod - def _administrative_information_to_json(cls, obj: model.AdministrativeInformation) -> Dict[str, object]: - """ - serialization of an object from class AdministrativeInformation to json - - :param obj: object of class AdministrativeInformation - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if obj.version: - data['version'] = obj.version - if obj.revision: - data['revision'] = obj.revision - if obj.creator: - data['creator'] = obj.creator - if obj.template_id: - data['templateId'] = obj.template_id - return data - - @classmethod - def _reference_to_json(cls, obj: model.Reference) -> Dict[str, object]: - """ - serialization of an object from class Reference to json - - :param obj: object of class Reference - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['type'] = _generic.REFERENCE_TYPES[obj.__class__] - data['keys'] = list(obj.key) - if obj.referred_semantic_id is not None: - data['referredSemanticId'] = cls._reference_to_json(obj.referred_semantic_id) - return data - - @classmethod - def _namespace_to_json(cls, obj): # not in specification yet - """ - serialization of an object from class Namespace to json - - :param obj: object of class Namespace - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - return data - - @classmethod - def _qualifier_to_json(cls, obj: model.Qualifier) -> Dict[str, object]: - """ - serialization of an object from class Qualifier to json - - :param obj: object of class Qualifier - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if obj.value: - data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None - if obj.value_id: - data['valueId'] = obj.value_id - # Even though kind is optional in the schema, it's better to always serialize it instead of specifying - # the default value in multiple locations. - data['kind'] = _generic.QUALIFIER_KIND[obj.kind] - data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] - data['type'] = obj.type - return data - - @classmethod - def _extension_to_json(cls, obj: model.Extension) -> Dict[str, object]: - """ - serialization of an object from class Extension to json - - :param obj: object of class Extension - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if obj.value: - data['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None - if obj.refers_to: - data['refersTo'] = list(obj.refers_to) - if obj.value_type: - data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] - data['name'] = obj.name - return data - - @classmethod - def _value_reference_pair_to_json(cls, obj: model.ValueReferencePair) -> Dict[str, object]: - """ - serialization of an object from class ValueReferencePair to json - - :param obj: object of class ValueReferencePair - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data.update({'value': model.datatypes.xsd_repr(obj.value), - 'valueId': obj.value_id}) - return data - - @classmethod - def _value_list_to_json(cls, obj: model.ValueList) -> Dict[str, object]: - """ - serialization of an object from class ValueList to json - - :param obj: object of class ValueList - :return: dict with the serialized attributes of this object - """ - return {'valueReferencePairs': list(obj)} - - # ############################################################ - # transformation functions to serialize classes from model.aas - # ############################################################ - - @classmethod - def _specific_asset_id_to_json(cls, obj: model.SpecificAssetId) -> Dict[str, object]: - """ - serialization of an object from class SpecificAssetId to json - - :param obj: object of class SpecificAssetId - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['name'] = obj.name - data['value'] = obj.value - if obj.external_subject_id: - data['externalSubjectId'] = obj.external_subject_id - return data - - @classmethod - def _asset_information_to_json(cls, obj: model.AssetInformation) -> Dict[str, object]: - """ - serialization of an object from class AssetInformation to json - - :param obj: object of class AssetInformation - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['assetKind'] = _generic.ASSET_KIND[obj.asset_kind] - if obj.global_asset_id: - data['globalAssetId'] = obj.global_asset_id - if obj.specific_asset_id: - data['specificAssetIds'] = list(obj.specific_asset_id) - if obj.asset_type: - data['assetType'] = obj.asset_type - if obj.default_thumbnail: - data['defaultThumbnail'] = obj.default_thumbnail - return data - - @classmethod - def _concept_description_to_json(cls, obj: model.ConceptDescription) -> Dict[str, object]: - """ - serialization of an object from class ConceptDescription to json - - :param obj: object of class ConceptDescription - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if obj.is_case_of: - data['isCaseOf'] = list(obj.is_case_of) - return data - - @classmethod - def _data_specification_iec61360_to_json( - cls, obj: model.base.DataSpecificationIEC61360) -> Dict[str, object]: - """ - serialization of an object from class DataSpecificationIEC61360 to json - - :param obj: object of class DataSpecificationIEC61360 - :return: dict with the serialized attributes of this object - """ - data_spec: Dict[str, object] = { - 'modelType': 'DataSpecificationIec61360', - 'preferredName': obj.preferred_name - } - if obj.data_type is not None: - data_spec['dataType'] = _generic.IEC61360_DATA_TYPES[obj.data_type] - if obj.definition is not None: - data_spec['definition'] = obj.definition - if obj.short_name is not None: - data_spec['shortName'] = obj.short_name - if obj.unit is not None: - data_spec['unit'] = obj.unit - if obj.unit_id is not None: - data_spec['unitId'] = obj.unit_id - if obj.source_of_definition is not None: - data_spec['sourceOfDefinition'] = obj.source_of_definition - if obj.symbol is not None: - data_spec['symbol'] = obj.symbol - if obj.value_format is not None: - data_spec['valueFormat'] = obj.value_format - if obj.value_list is not None: - data_spec['valueList'] = cls._value_list_to_json(obj.value_list) - if obj.value is not None: - data_spec['value'] = obj.value - if obj.level_types: - data_spec['levelType'] = {v: k in obj.level_types for k, v in _generic.IEC61360_LEVEL_TYPES.items()} - return data_spec - - @classmethod - def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell) -> Dict[str, object]: - """ - serialization of an object from class AssetAdministrationShell to json - - :param obj: object of class AssetAdministrationShell - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data.update(cls._namespace_to_json(obj)) - if obj.derived_from: - data["derivedFrom"] = obj.derived_from - if obj.asset_information: - data["assetInformation"] = obj.asset_information - if not cls.stripped and obj.submodel: - data["submodels"] = list(obj.submodel) - return data - - # ################################################################# - # transformation functions to serialize classes from model.submodel - # ################################################################# - - @classmethod - def _submodel_to_json(cls, obj: model.Submodel) -> Dict[str, object]: # TODO make kind optional - """ - serialization of an object from class Submodel to json - - :param obj: object of class Submodel - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if not cls.stripped and obj.submodel_element != set(): - data['submodelElements'] = list(obj.submodel_element) - return data - - @classmethod - def _data_element_to_json(cls, obj: model.DataElement) -> Dict[str, object]: # no attributes in specification yet - """ - serialization of an object from class DataElement to json - - :param obj: object of class DataElement - :return: dict with the serialized attributes of this object - """ - return {} - - @classmethod - def _property_to_json(cls, obj: model.Property) -> Dict[str, object]: - """ - serialization of an object from class Property to json - - :param obj: object of class Property - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if obj.value is not None: - data['value'] = model.datatypes.xsd_repr(obj.value) - if obj.value_id: - data['valueId'] = obj.value_id - data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] - return data - - @classmethod - def _multi_language_property_to_json(cls, obj: model.MultiLanguageProperty) -> Dict[str, object]: - """ - serialization of an object from class MultiLanguageProperty to json - - :param obj: object of class MultiLanguageProperty - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if obj.value: - data['value'] = obj.value - if obj.value_id: - data['valueId'] = obj.value_id - return data - - @classmethod - def _range_to_json(cls, obj: model.Range) -> Dict[str, object]: - """ - serialization of an object from class Range to json - :param obj: object of class Range - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['valueType'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type] - if obj.min is not None: - data['min'] = model.datatypes.xsd_repr(obj.min) - if obj.max is not None: - data['max'] = model.datatypes.xsd_repr(obj.max) - return data +Path = Union[str, bytes, os.PathLike] +PathOrBinaryIO = Union[Path, BinaryIO] +PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO - @classmethod - def _blob_to_json(cls, obj: model.Blob) -> Dict[str, object]: - """ - serialization of an object from class Blob to json - :param obj: object of class Blob - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['contentType'] = obj.content_type - if obj.value is not None: - data['value'] = base64.b64encode(obj.value).decode() - return data - - @classmethod - def _file_to_json(cls, obj: model.File) -> Dict[str, object]: - """ - serialization of an object from class File to json - - :param obj: object of class File - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['contentType'] = obj.content_type - if obj.value is not None: - data['value'] = obj.value - return data - - @classmethod - def _resource_to_json(cls, obj: model.Resource) -> Dict[str, object]: - """ - serialization of an object from class Resource to json - - :param obj: object of class Resource - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['path'] = obj.path - if obj.content_type is not None: - data['contentType'] = obj.content_type - return data - - @classmethod - def _reference_element_to_json(cls, obj: model.ReferenceElement) -> Dict[str, object]: - """ - serialization of an object from class Reference to json - - :param obj: object of class Reference - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if obj.value: - data['value'] = obj.value - return data - - @classmethod - def _submodel_element_collection_to_json(cls, obj: model.SubmodelElementCollection) -> Dict[str, object]: - """ - serialization of an object from class SubmodelElementCollection to json - - :param obj: object of class SubmodelElementCollection - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if not cls.stripped and len(obj.value) > 0: - data['value'] = list(obj.value) - return data - - @classmethod - def _submodel_element_list_to_json(cls, obj: model.SubmodelElementList) -> Dict[str, object]: - """ - serialization of an object from class SubmodelElementList to json - - :param obj: object of class SubmodelElementList - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - # Even though orderRelevant is optional in the schema, it's better to always serialize it instead of specifying - # the default value in multiple locations. - data['orderRelevant'] = obj.order_relevant - data['typeValueListElement'] = _generic.KEY_TYPES[model.KEY_TYPES_CLASSES[obj.type_value_list_element]] - if obj.semantic_id_list_element is not None: - data['semanticIdListElement'] = obj.semantic_id_list_element - if obj.value_type_list_element is not None: - data['valueTypeListElement'] = model.datatypes.XSD_TYPE_NAMES[obj.value_type_list_element] - if not cls.stripped and len(obj.value) > 0: - data['value'] = list(obj.value) - return data - - @classmethod - def _relationship_element_to_json(cls, obj: model.RelationshipElement) -> Dict[str, object]: - """ - serialization of an object from class RelationshipElement to json - - :param obj: object of class RelationshipElement - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data.update({'first': obj.first, 'second': obj.second}) - return data - - @classmethod - def _annotated_relationship_element_to_json(cls, obj: model.AnnotatedRelationshipElement) -> Dict[str, object]: - """ - serialization of an object from class AnnotatedRelationshipElement to json - - :param obj: object of class AnnotatedRelationshipElement - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data.update({'first': obj.first, 'second': obj.second}) - if not cls.stripped and obj.annotation: - data['annotations'] = list(obj.annotation) - return data - - @classmethod - def _operation_variable_to_json(cls, obj: model.SubmodelElement) -> Dict[str, object]: - """ - serialization of an object from class SubmodelElement to a json OperationVariable representation - Since we don't implement the ``OperationVariable`` class, which is just a wrapper for a single - :class:`~basyx.aas.model.submodel.SubmodelElement`, elements are serialized as the ``value`` attribute of an - ``operationVariable`` object. - - :param obj: object of class :class:`~basyx.aas.model.submodel.SubmodelElement` - :return: ``OperationVariable`` wrapper containing the serialized - :class:`~basyx.aas.model.submodel.SubmodelElement` - """ - return {'value': obj} - - @classmethod - def _operation_to_json(cls, obj: model.Operation) -> Dict[str, object]: - """ - serialization of an object from class Operation to json - - :param obj: object of class Operation - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - for tag, nss in (('inputVariables', obj.input_variable), - ('outputVariables', obj.output_variable), - ('inoutputVariables', obj.in_output_variable)): - if nss: - data[tag] = [cls._operation_variable_to_json(obj) for obj in nss] - return data - - @classmethod - def _capability_to_json(cls, obj: model.Capability) -> Dict[str, object]: - """ - serialization of an object from class Capability to json - - :param obj: object of class Capability - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - # no attributes in specification yet - return data - - @classmethod - def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]: - """ - serialization of an object from class Entity to json - - :param obj: object of class Entity - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - if not cls.stripped and obj.statement: - data['statements'] = list(obj.statement) - data['entityType'] = _generic.ENTITY_TYPES[obj.entity_type] - if obj.global_asset_id: - data['globalAssetId'] = obj.global_asset_id - if obj.specific_asset_id: - data['specificAssetIds'] = list(obj.specific_asset_id) - return data - - @classmethod - def _event_element_to_json(cls, obj: model.EventElement) -> Dict[str, object]: # no attributes in specification yet - """ - serialization of an object from class EventElement to json - - :param obj: object of class EventElement - :return: dict with the serialized attributes of this object - """ - return {} - - @classmethod - def _basic_event_element_to_json(cls, obj: model.BasicEventElement) -> Dict[str, object]: - """ - serialization of an object from class BasicEventElement to json - - :param obj: object of class BasicEventElement - :return: dict with the serialized attributes of this object - """ - data = cls._abstract_classes_to_json(obj) - data['observed'] = obj.observed - data['direction'] = _generic.DIRECTION[obj.direction] - data['state'] = _generic.STATE_OF_EVENT[obj.state] - if obj.message_topic is not None: - data['messageTopic'] = obj.message_topic - if obj.message_broker is not None: - data['messageBroker'] = cls._reference_to_json(obj.message_broker) - if obj.last_update is not None: - data['lastUpdate'] = model.datatypes.xsd_repr(obj.last_update) - if obj.min_interval is not None: - data['minInterval'] = model.datatypes.xsd_repr(obj.min_interval) - if obj.max_interval is not None: - data['maxInterval'] = model.datatypes.xsd_repr(obj.max_interval) - return data - - -class StrippedAASToJsonEncoder(AASToJsonEncoder): - """ - AASToJsonEncoder for stripped objects. Used in the HTTP API. - See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 - """ - stripped = True - - -def _select_encoder(stripped: bool, encoder: Optional[Type[AASToJsonEncoder]] = None) -> Type[AASToJsonEncoder]: - """ - Returns the correct encoder based on the stripped parameter. If an encoder class is given, stripped is ignored. - - :param stripped: If true, an encoder for parsing stripped JSON objects is selected. Ignored if an encoder class is - specified. - :param encoder: Is returned, if specified. - :return: A AASToJsonEncoder (sub)class. - """ - if encoder is not None: - return encoder - return AASToJsonEncoder if not stripped else StrippedAASToJsonEncoder - - -def _create_dict(data: model.AbstractObjectStore) -> dict: +def _create_dict(data: ObjectStore) -> dict: # separate different kind of objects - asset_administration_shells: List[model.AssetAdministrationShell] = [] - submodels: List[model.Submodel] = [] - concept_descriptions: List[model.ConceptDescription] = [] + asset_administration_shells: List = [] + submodels: List = [] + concept_descriptions: List = [] for obj in data: - if isinstance(obj, model.AssetAdministrationShell): - asset_administration_shells.append(obj) - elif isinstance(obj, model.Submodel): - submodels.append(obj) - elif isinstance(obj, model.ConceptDescription): - concept_descriptions.append(obj) + if isinstance(obj, AssetAdministrationShell): + asset_administration_shells.append(to_jsonable(obj)) + elif isinstance(obj, Submodel): + submodels.append(to_jsonable(obj)) + elif isinstance(obj, ConceptDescription): + concept_descriptions.append(to_jsonable(obj)) dict_: Dict[str, List] = {} if asset_administration_shells: dict_['assetAdministrationShells'] = asset_administration_shells @@ -715,36 +69,7 @@ def _create_dict(data: model.AbstractObjectStore) -> dict: dict_['conceptDescriptions'] = concept_descriptions return dict_ - -def object_store_to_json(data: model.AbstractObjectStore, stripped: bool = False, - encoder: Optional[Type[AASToJsonEncoder]] = None, **kwargs) -> str: - """ - Create a json serialization of a set of AAS objects according to 'Details of the Asset Administration Shell', - chapter 5.5 - - :param data: :class:`ObjectStore ` which contains different objects of - the AAS meta model which should be serialized to a JSON file - :param stripped: If true, objects are serialized to stripped json objects. - See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 - This parameter is ignored if an encoder class is specified. - :param encoder: The encoder class used to encode the JSON objects - :param kwargs: Additional keyword arguments to be passed to :func:`json.dumps` - """ - encoder_ = _select_encoder(stripped, encoder) - # serialize object to json - return json.dumps(_create_dict(data), cls=encoder_, **kwargs) - - -class _DetachingTextIOWrapper(io.TextIOWrapper): - """ - Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer. - """ - def __exit__(self, exc_type, exc_val, exc_tb): - self.detach() - - -def write_aas_json_file(file: _generic.PathOrIO, data: model.AbstractObjectStore, stripped: bool = False, - encoder: Optional[Type[AASToJsonEncoder]] = None, **kwargs) -> None: +def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: """ Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 @@ -752,13 +77,8 @@ def write_aas_json_file(file: _generic.PathOrIO, data: model.AbstractObjectStore :param file: A filename or file-like object to write the JSON-serialized data to :param data: :class:`ObjectStore ` which contains different objects of the AAS meta model which should be serialized to a JSON file - :param stripped: If `True`, objects are serialized to stripped json objects. - See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91 - This parameter is ignored if an encoder class is specified. - :param encoder: The encoder class used to encode the JSON objects :param kwargs: Additional keyword arguments to be passed to `json.dump()` """ - encoder_ = _select_encoder(stripped, encoder) # json.dump() only accepts TextIO cm: ContextManager[TextIO] @@ -773,7 +93,9 @@ def write_aas_json_file(file: _generic.PathOrIO, data: model.AbstractObjectStore # we already got TextIO, nothing needs to be done # mypy seems to have issues narrowing the type due to get_args() cm = contextlib.nullcontext(file) # type: ignore[arg-type] + # serialize object to json# - # serialize object to json with cm as fp: - json.dump(_create_dict(data), fp, cls=encoder_, **kwargs) + json.dump(_create_dict(data), fp, **kwargs) + + diff --git a/sdk/basyx/adapter/xml/__init__.py b/sdk/basyx/adapter/xml/__init__.py index aa08288..e986321 100644 --- a/sdk/basyx/adapter/xml/__init__.py +++ b/sdk/basyx/adapter/xml/__init__.py @@ -10,7 +10,7 @@ :class:`ObjectStore ` from a given xml document. """ -from .xml_serialization import object_store_to_xml_element, write_aas_xml_file, object_to_xml_element, \ - write_aas_xml_element -from .xml_deserialization import AASFromXmlDecoder, StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder, \ - StrictStrippedAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element +#from .xml_serialization import object_store_to_xml_element, write_aas_xml_file, object_to_xml_element, \ +# write_aas_xml_element +#from .xml_deserialization import AASFromXmlDecoder, StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder, \ +# StrictStrippedAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element diff --git a/sdk/basyx/adapter/xml/xml_deserialization.py b/sdk/basyx/adapter/xml/xml_deserialization.py index c6e555b..193a0aa 100644 --- a/sdk/basyx/adapter/xml/xml_deserialization.py +++ b/sdk/basyx/adapter/xml/xml_deserialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2023 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ .. _adapter.xml.xml_deserialization: @@ -12,21 +11,10 @@ This module provides the following functions for parsing XML documents: -- :func:`read_aas_xml_element` constructs a single object from an XML document containing a single element - :func:`read_aas_xml_file_into` constructs all elements of an XML document and stores them in a given - :class:`ObjectStore ` + :class:`ObjectStore ` - :func:`read_aas_xml_file` constructs all elements of an XML document and returns them in a - :class:`~basyx.aas.model.provider.DictObjectStore` - -These functions take a decoder class as keyword argument, which allows parsing in failsafe (default) or non-failsafe -mode. Parsing stripped elements - used in the HTTP adapter - is also possible. It is also possible to subclass the -default decoder class and provide an own decoder. - -In failsafe mode errors regarding missing attributes and elements or invalid values are caught and logged. -In non-failsafe mode any error would abort parsing. -Error handling is done only by ``_failsafe_construct()`` in this module. Nearly all constructor functions are called -by other constructor functions via ``_failsafe_construct()``, so an error chain is constructed in the error case, -which allows printing stacktrace-like error messages like the following in the error case (in failsafe mode of course): + :class:`~basyx.ObjectStore` .. code-block:: @@ -36,22 +24,17 @@ -> Failed to construct aas:conceptDescription on line 247 using construct_concept_description! -Unlike the JSON deserialization, parsing is done top-down. Elements with a specific tag are searched on the level -directly below the level of the current xml element (in terms of parent and child relation) and parsed when -found. Constructor functions of these elements will then again search for mandatory and optional child elements -and construct them if available, and so on. + """ -from basyx.aas import model +from aas_core3 import types as model from lxml import etree import logging -import base64 -import enum +import aas_core3.xmlization as aas_xmlization +from basyx.object_store import ObjectStore, Identifiable -from typing import Any, Callable, Dict, Iterable, Optional, Set, Tuple, Type, TypeVar -from .._generic import XML_NS_MAP, XML_NS_AAS, 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 +from typing import Any, Callable, Dict, Iterable, Optional, Set, Tuple, Type, TypeVar, List +from .._generic import XML_NS_MAP, XML_NS_AAS, PathOrIO NS_AAS = XML_NS_AAS REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]} @@ -60,37 +43,6 @@ T = TypeVar("T") RE = TypeVar("RE", bound=model.RelationshipElement) -LSS = TypeVar("LSS", bound=model.LangStringSet) - - -def _str_to_bool(string: str) -> bool: - """ - XML only allows ``false`` and ``true`` (case-sensitive) as valid values for a boolean. - - This function checks the string and raises a ValueError if the string is neither ``true`` nor ``false``. - - :param string: String representation of a boolean. (``true`` or ``false``) - :return: The respective boolean value. - :raises ValueError: If string is neither ``true`` nor ``false``. - """ - if string not in ("true", "false"): - raise ValueError(f"{string} is not a valid boolean! Only true and false are allowed.") - return string == "true" - - -def _tag_replace_namespace(tag: str, nsmap: Dict[Optional[str], str]) -> str: - """ - Attempts to replace the namespace in front of a tag with the prefix used in the xml document. - - :param tag: The tag of an xml element. - :param nsmap: A dict mapping prefixes to namespaces. - :return: The modified element tag. If the namespace wasn't found in nsmap, the unmodified tag is returned. - """ - split = tag.split("}") - for prefix, namespace in nsmap.items(): - if namespace == split[0][1:]: - return prefix + ":" + split[1] - return tag def _element_pretty_identifier(element: etree._Element) -> str: @@ -117,1079 +69,6 @@ def _element_pretty_identifier(element: etree._Element) -> str: return identifier -def _exception_to_str(exception: BaseException) -> str: - """ - A helper function used to stringify exceptions. - - It removes the quotation marks '' that are put around str(KeyError), otherwise it's just calls str(exception). - - :param exception: The exception to stringify. - :return: The stringified exception. - """ - string = str(exception) - return string[1:-1] if isinstance(exception, KeyError) else string - - -def _get_child_mandatory(parent: etree._Element, child_tag: str) -> etree._Element: - """ - A helper function for getting a mandatory child element. - - :param parent: The parent element. - :param child_tag: The tag of the child element to return. - :return: The child element. - :raises KeyError: If the parent element has no child element with the given tag. - """ - child = parent.find(child_tag) - if child is None: - raise KeyError(_element_pretty_identifier(parent) - + f" has no child {_tag_replace_namespace(child_tag, parent.nsmap)}!") - return child - - -def _get_all_children_expect_tag(parent: etree._Element, expected_tag: str, failsafe: bool) -> Iterable[etree._Element]: - """ - Iterates over all children, matching the tag. - - not failsafe: Throws an error if a child element doesn't match. - failsafe: Logs a warning if a child element doesn't match. - - :param parent: The parent element. - :param expected_tag: The tag of the children. - :return: An iterator over all child elements that match child_tag. - :raises KeyError: If the tag of a child element doesn't match and failsafe is true. - """ - for child in parent: - if child.tag != expected_tag: - error_message = f"{_element_pretty_identifier(child)}, child of {_element_pretty_identifier(parent)}, " \ - f"doesn't match the expected tag {_tag_replace_namespace(expected_tag, child.nsmap)}!" - if not failsafe: - raise KeyError(error_message) - logger.warning(error_message) - continue - yield child - - -def _get_attrib_mandatory(element: etree._Element, attrib: str) -> str: - """ - A helper function for getting a mandatory attribute of an element. - - :param element: The xml element. - :param attrib: The name of the attribute. - :return: The value of the attribute. - :raises KeyError: If the attribute does not exist. - """ - if attrib not in element.attrib: - raise KeyError(f"{_element_pretty_identifier(element)} has no attribute with name {attrib}!") - return element.attrib[attrib] # type: ignore[return-value] - - -def _get_attrib_mandatory_mapped(element: etree._Element, attrib: str, dct: Dict[str, T]) -> T: - """ - A helper function for getting a mapped mandatory attribute of an xml element. - - It first gets the attribute value using _get_attrib_mandatory(), which raises a KeyError if the attribute - does not exist. - Then it returns dct[] and raises a ValueError, if the attribute value does not exist in the dict. - - :param element: The xml element. - :param attrib: The name of the attribute. - :param dct: The dictionary that is used to map the attribute value. - :return: The mapped value of the attribute. - :raises ValueError: If the value of the attribute does not exist in dct. - """ - attrib_value = _get_attrib_mandatory(element, attrib) - if attrib_value not in dct: - raise ValueError(f"Attribute {attrib} of {_element_pretty_identifier(element)} " - f"has invalid value: {attrib_value}") - return dct[attrib_value] - - -def _get_text_or_none(element: Optional[etree._Element]) -> Optional[str]: - """ - A helper function for getting the text of an element, when it's not clear whether the element exists or not. - - This function is useful whenever the text of an optional child element is needed. - Then the text can be get with: text = _get_text_or_none(element.find("childElement") - element.find() returns either the element or None, if it doesn't exist. This is why this function accepts - an optional element, to reduce the amount of code in the constructor functions below. - - :param element: The xml element or None. - :return: The text of the xml element if the xml element is not None and if the xml element has a text. - None otherwise. - """ - return element.text if element is not None else None - - -def _get_text_mapped_or_none(element: Optional[etree._Element], dct: Dict[str, T]) -> Optional[T]: - """ - Returns dct[element.text] or None, if the element is None, has no text or the text is not in dct. - - :param element: The xml element or None. - :param dct: The dictionary that is used to map the text. - :return: The mapped text or None. - """ - text = _get_text_or_none(element) - if text is None or text not in dct: - return None - return dct[text] - - -def _get_text_mandatory(element: etree._Element) -> str: - """ - A helper function for getting the mandatory text of an element. - - :param element: The xml element. - :return: The text of the xml element. - :raises KeyError: If the xml element has no text. - """ - text = element.text - if text is None: - raise KeyError(_element_pretty_identifier(element) + " has no text!") - return text - - -def _get_text_mandatory_mapped(element: etree._Element, dct: Dict[str, T]) -> T: - """ - A helper function for getting the mapped mandatory text of an element. - - It first gets the text of the element using _get_text_mandatory(), - which raises a KeyError if the element has no text. - Then it returns dct[] and raises a ValueError, if the text of the element does not exist in the dict. - - :param element: The xml element. - :param dct: The dictionary that is used to map the text. - :return: The mapped text of the element. - :raises ValueError: If the text of the xml element does not exist in dct. - """ - text = _get_text_mandatory(element) - if text not in dct: - raise ValueError(_element_pretty_identifier(element) + f" has invalid text: {text}") - return dct[text] - - -def _failsafe_construct(element: Optional[etree._Element], constructor: Callable[..., T], failsafe: bool, - **kwargs: Any) -> Optional[T]: - """ - A wrapper function that is used to handle exceptions raised in constructor functions. - - This is the only function of this module where exceptions are caught. - This is why constructor functions should (in almost all cases) call other constructor functions using this function, - so errors can be caught and logged in failsafe mode. - The functions accepts None as a valid value for element for the same reason _get_text_or_none() does, so it can be - called like _failsafe_construct(element.find("childElement"), ...), since element.find() can return None. - This function will also return None in this case. - - :param element: The xml element or None. - :param constructor: The constructor function to apply on the element. - :param failsafe: Indicates whether errors should be caught or re-raised. - :param kwargs: Optional keyword arguments that are passed to the constructor function. - :return: The constructed class instance, if construction was successful. - None if the element was None or if the construction failed. - """ - if element is None: - return None - try: - return constructor(element, **kwargs) - except (KeyError, ValueError, model.AASConstraintViolation) as e: - error_message = f"Failed to construct {_element_pretty_identifier(element)} using {constructor.__name__}!" - if not failsafe: - raise (type(e) if isinstance(e, (KeyError, ValueError)) else ValueError)(error_message) from e - error_type = type(e).__name__ - cause: Optional[BaseException] = e - while cause is not None: - error_message = _exception_to_str(cause) + "\n -> " + error_message - cause = cause.__cause__ - logger.error(error_type + ": " + error_message) - return None - - -def _failsafe_construct_mandatory(element: etree._Element, constructor: Callable[..., T], **kwargs: Any) -> T: - """ - _failsafe_construct() but not failsafe and it returns T instead of Optional[T] - - :param element: The xml element. - :param constructor: The constructor function to apply on the xml element. - :param kwargs: Optional keyword arguments that are passed to the constructor function. - :return: The constructed child element. - :raises TypeError: If the result of _failsafe_construct() in non-failsafe mode was None. - This shouldn't be possible and if it happens, indicates a bug in _failsafe_construct(). - """ - constructed = _failsafe_construct(element, constructor, False, **kwargs) - if constructed is None: - raise AssertionError("The result of a non-failsafe _failsafe_construct() call was None! " - "This is a bug in the Eclipse BaSyx Python SDK XML deserialization, please report it!") - return constructed - - -def _failsafe_construct_multiple(elements: Iterable[etree._Element], constructor: Callable[..., T], failsafe: bool, - **kwargs: Any) -> Iterable[T]: - """ - A generator function that applies _failsafe_construct() to multiple elements. - - :param elements: Any iterable containing any number of xml elements. - :param constructor: The constructor function to apply on the xml elements. - :param failsafe: Indicates whether errors should be caught or re-raised. - :param kwargs: Optional keyword arguments that are passed to the constructor function. - :return: An iterator over the successfully constructed elements. - If an error occurred while constructing an element and while in failsafe mode, - the respective element will be skipped. - """ - for element in elements: - parsed = _failsafe_construct(element, constructor, failsafe, **kwargs) - if parsed is not None: - yield parsed - - -def _child_construct_mandatory(parent: etree._Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any) \ - -> T: - """ - Shorthand for _failsafe_construct_mandatory() in combination with _get_child_mandatory(). - - :param parent: The xml element where the child element is searched. - :param child_tag: The tag of the child element to construct. - :param constructor: The constructor function for the child element. - :param kwargs: Optional keyword arguments that are passed to the constructor function. - :return: The constructed child element. - """ - return _failsafe_construct_mandatory(_get_child_mandatory(parent, child_tag), constructor, **kwargs) - - -def _child_construct_multiple(parent: etree._Element, expected_tag: str, constructor: Callable[..., T], - failsafe: bool, **kwargs: Any) -> Iterable[T]: - """ - Shorthand for _failsafe_construct_multiple() in combination with _get_child_multiple(). - - :param parent: The xml element where child elements are searched. - :param expected_tag: The expected tag of the child elements. - :param constructor: The constructor function for the child element. - :param kwargs: Optional keyword arguments that are passed to the constructor function. - :return: An iterator over successfully constructed child elements. - If an error occurred while constructing an element and while in failsafe mode, - the respective element will be skipped. - """ - return _failsafe_construct_multiple(_get_all_children_expect_tag(parent, expected_tag, failsafe), constructor, - failsafe, **kwargs) - - -def _child_text_mandatory(parent: etree._Element, child_tag: str) -> str: - """ - Shorthand for _get_text_mandatory() in combination with _get_child_mandatory(). - - :param parent: The xml element where the child element is searched. - :param child_tag: The tag of the child element to get the text from. - :return: The text of the child element. - """ - return _get_text_mandatory(_get_child_mandatory(parent, child_tag)) - - -def _child_text_mandatory_mapped(parent: etree._Element, child_tag: str, dct: Dict[str, T]) -> T: - """ - Shorthand for _get_text_mandatory_mapped() in combination with _get_child_mandatory(). - - :param parent: The xml element where the child element is searched. - :param child_tag: The tag of the child element to get the text from. - :param dct: The dictionary that is used to map the text of the child element. - :return: The mapped text of the child element. - """ - return _get_text_mandatory_mapped(_get_child_mandatory(parent, child_tag), dct) - - -def _get_kind(element: etree._Element) -> model.ModellingKind: - """ - Returns the modelling kind of an element with the default value INSTANCE, if none specified. - - :param element: The xml element. - :return: The modelling kind of the element. - """ - modelling_kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), MODELLING_KIND_INVERSE) - return modelling_kind if modelling_kind is not None else model.ModellingKind.INSTANCE - - -def _expect_reference_type(element: etree._Element, expected_type: Type[model.Reference]) -> None: - """ - Validates the type attribute of a Reference. - - :param element: The xml element. - :param expected_type: The expected type of the Reference. - :return: None - """ - actual_type = _child_text_mandatory_mapped(element, NS_AAS + "type", REFERENCE_TYPES_INVERSE) - if actual_type is not expected_type: - raise ValueError(f"{_element_pretty_identifier(element)} is of type {actual_type}, expected {expected_type}!") - - -class AASFromXmlDecoder: - """ - The default XML decoder class. - - It parses XML documents in a failsafe manner, meaning any errors encountered will be logged and invalid XML elements - will be skipped. - Most member functions support the ``object_class`` parameter. It was introduced so they can be overwritten - in subclasses, which allows constructing instances of subtypes. - """ - failsafe = True - stripped = False - - @classmethod - def _amend_abstract_attributes(cls, obj: object, element: etree._Element) -> None: - """ - A helper function that amends optional attributes to already constructed class instances, if they inherit - from an abstract class like Referable, Identifiable, HasSemantics or Qualifiable. - - :param obj: The constructed class instance. - :param element: The respective xml element. - :return: None - """ - if isinstance(obj, model.Referable): - id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) - if id_short is not None: - obj.id_short = id_short - category = _get_text_or_none(element.find(NS_AAS + "category")) - display_name = _failsafe_construct(element.find(NS_AAS + "displayName"), - cls.construct_multi_language_name_type, cls.failsafe) - if display_name is not None: - obj.display_name = display_name - if category is not None: - obj.category = category - description = _failsafe_construct(element.find(NS_AAS + "description"), - cls.construct_multi_language_text_type, cls.failsafe) - if description is not None: - obj.description = description - if isinstance(obj, model.Identifiable): - administration = _failsafe_construct(element.find(NS_AAS + "administration"), - cls.construct_administrative_information, cls.failsafe) - if administration: - obj.administration = administration - if isinstance(obj, model.HasSemantics): - semantic_id = _failsafe_construct(element.find(NS_AAS + "semanticId"), cls.construct_reference, - cls.failsafe) - if semantic_id is not None: - obj.semantic_id = semantic_id - supplemental_semantic_ids = element.find(NS_AAS + "supplementalSemanticIds") - if supplemental_semantic_ids is not None: - for supplemental_semantic_id in _child_construct_multiple(supplemental_semantic_ids, - NS_AAS + "reference", cls.construct_reference, - cls.failsafe): - obj.supplemental_semantic_id.append(supplemental_semantic_id) - if isinstance(obj, model.Qualifiable) and not cls.stripped: - qualifiers_elem = element.find(NS_AAS + "qualifiers") - if qualifiers_elem is not None and len(qualifiers_elem) > 0: - for qualifier in _failsafe_construct_multiple(qualifiers_elem, cls.construct_qualifier, cls.failsafe): - obj.qualifier.add(qualifier) - if isinstance(obj, model.HasDataSpecification) and not cls.stripped: - embedded_data_specifications_elem = element.find(NS_AAS + "embeddedDataSpecifications") - if embedded_data_specifications_elem is not None: - for eds in _failsafe_construct_multiple(embedded_data_specifications_elem, - cls.construct_embedded_data_specification, cls.failsafe): - obj.embedded_data_specifications.append(eds) - if isinstance(obj, model.HasExtension) and not cls.stripped: - extension_elem = element.find(NS_AAS + "extensions") - if extension_elem is not None: - for extension in _child_construct_multiple(extension_elem, NS_AAS + "extension", - cls.construct_extension, cls.failsafe): - obj.extension.add(extension) - - @classmethod - def _construct_relationship_element_internal(cls, element: etree._Element, object_class: Type[RE], **_kwargs: Any) \ - -> RE: - """ - Helper function used by construct_relationship_element() and construct_annotated_relationship_element() - to reduce duplicate code - """ - relationship_element = object_class( - None, - _child_construct_mandatory(element, NS_AAS + "first", cls.construct_reference), - _child_construct_mandatory(element, NS_AAS + "second", cls.construct_reference) - ) - cls._amend_abstract_attributes(relationship_element, element) - return relationship_element - - @classmethod - def _construct_key_tuple(cls, element: etree._Element, namespace: str = NS_AAS, **_kwargs: Any) \ - -> Tuple[model.Key, ...]: - """ - Helper function used by construct_reference() and construct_aas_reference() to reduce duplicate code - """ - keys = _get_child_mandatory(element, namespace + "keys") - return tuple(_child_construct_multiple(keys, namespace + "key", cls.construct_key, cls.failsafe)) - - @classmethod - def _construct_submodel_reference(cls, element: etree._Element, **kwargs: Any) \ - -> model.ModelReference[model.Submodel]: - """ - Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. - """ - return cls.construct_model_reference_expect_type(element, model.Submodel, **kwargs) - - @classmethod - def _construct_asset_administration_shell_reference(cls, element: etree._Element, **kwargs: Any) \ - -> model.ModelReference[model.AssetAdministrationShell]: - """ - Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. - """ - return cls.construct_model_reference_expect_type(element, model.AssetAdministrationShell, **kwargs) - - @classmethod - def _construct_referable_reference(cls, element: etree._Element, **kwargs: Any) \ - -> model.ModelReference[model.Referable]: - """ - Helper function. Doesn't support the object_class parameter. Overwrite construct_aas_reference instead. - """ - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - return cls.construct_model_reference_expect_type(element, model.Referable, **kwargs) # type: ignore - - @classmethod - def _construct_operation_variable(cls, element: etree._Element, **kwargs: Any) -> model.SubmodelElement: - """ - Since we don't implement ``OperationVariable``, this constructor discards the wrapping `aas:operationVariable` - and `aas:value` and just returns the contained :class:`~basyx.aas.model.submodel.SubmodelElement`. - """ - value = _get_child_mandatory(element, NS_AAS + "value") - if len(value) == 0: - raise KeyError(f"{_element_pretty_identifier(value)} has no submodel element!") - if len(value) > 1: - logger.warning(f"{_element_pretty_identifier(value)} has more than one submodel element, " - "using the first one...") - return cls.construct_submodel_element(value[0], **kwargs) - - @classmethod - def construct_key(cls, element: etree._Element, object_class=model.Key, **_kwargs: Any) \ - -> model.Key: - return object_class( - _child_text_mandatory_mapped(element, NS_AAS + "type", KEY_TYPES_INVERSE), - _child_text_mandatory(element, NS_AAS + "value") - ) - - @classmethod - def construct_reference(cls, element: etree._Element, namespace: str = NS_AAS, **kwargs: Any) -> model.Reference: - reference_type: Type[model.Reference] = _child_text_mandatory_mapped(element, NS_AAS + "type", - REFERENCE_TYPES_INVERSE) - references: Dict[Type[model.Reference], Callable[..., model.Reference]] = { - model.ExternalReference: cls.construct_external_reference, - model.ModelReference: cls.construct_model_reference - } - if reference_type not in references: - raise KeyError(_element_pretty_identifier(element) + f" is of unsupported Reference type {reference_type}!") - return references[reference_type](element, namespace=namespace, **kwargs) - - @classmethod - def construct_external_reference(cls, element: etree._Element, namespace: str = NS_AAS, - object_class=model.ExternalReference, **_kwargs: Any) \ - -> model.ExternalReference: - _expect_reference_type(element, model.ExternalReference) - return object_class(cls._construct_key_tuple(element, namespace=namespace), - _failsafe_construct(element.find(NS_AAS + "referredSemanticId"), cls.construct_reference, - cls.failsafe, namespace=namespace)) - - @classmethod - def construct_model_reference(cls, element: etree._Element, object_class=model.ModelReference, **_kwargs: Any) \ - -> model.ModelReference: - """ - This constructor for ModelReference determines the type of the ModelReference by its keys. If no keys are - present, it will default to the type Referable. This behaviour is wanted in read_aas_xml_element(). - """ - _expect_reference_type(element, model.ModelReference) - keys = cls._construct_key_tuple(element) - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - type_: Type[model.Referable] = model.Referable # type: ignore - if len(keys) > 0: - type_ = KEY_TYPES_CLASSES_INVERSE.get(keys[-1].type, model.Referable) # type: ignore - return object_class(keys, type_, _failsafe_construct(element.find(NS_AAS + "referredSemanticId"), - cls.construct_reference, cls.failsafe)) - - @classmethod - def construct_model_reference_expect_type(cls, element: etree._Element, type_: Type[model.base._RT], - object_class=model.ModelReference, **_kwargs: Any) \ - -> model.ModelReference[model.base._RT]: - """ - This constructor for ModelReference allows passing an expected type, which is checked against the type of the - last key of the reference. This constructor function is used by other constructor functions, since all expect a - specific target type. - """ - _expect_reference_type(element, model.ModelReference) - keys = cls._construct_key_tuple(element) - if keys and not issubclass(KEY_TYPES_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): - logger.warning("type %s of last key of reference to %s does not match reference type %s", - keys[-1].type.name, " / ".join(str(k) for k in keys), type_.__name__) - return object_class(keys, type_, _failsafe_construct(element.find(NS_AAS + "referredSemanticId"), - cls.construct_reference, cls.failsafe)) - - @classmethod - def construct_administrative_information(cls, element: etree._Element, object_class=model.AdministrativeInformation, - **_kwargs: Any) -> model.AdministrativeInformation: - administrative_information = object_class( - revision=_get_text_or_none(element.find(NS_AAS + "revision")), - version=_get_text_or_none(element.find(NS_AAS + "version")), - template_id=_get_text_or_none(element.find(NS_AAS + "templateId")) - ) - creator = _failsafe_construct(element.find(NS_AAS + "creator"), cls.construct_reference, cls.failsafe) - if creator is not None: - administrative_information.creator = creator - cls._amend_abstract_attributes(administrative_information, element) - return administrative_information - - @classmethod - def construct_lang_string_set(cls, element: etree._Element, expected_tag: str, object_class: Type[LSS], - **_kwargs: Any) -> LSS: - collected_lang_strings: Dict[str, str] = {} - for lang_string_elem in _get_all_children_expect_tag(element, expected_tag, cls.failsafe): - collected_lang_strings[_child_text_mandatory(lang_string_elem, NS_AAS + "language")] = \ - _child_text_mandatory(lang_string_elem, NS_AAS + "text") - return object_class(collected_lang_strings) - - @classmethod - def construct_multi_language_name_type(cls, element: etree._Element, object_class=model.MultiLanguageNameType, - **kwargs: Any) -> model.MultiLanguageNameType: - return cls.construct_lang_string_set(element, NS_AAS + "langStringNameType", object_class, **kwargs) - - @classmethod - def construct_multi_language_text_type(cls, element: etree._Element, object_class=model.MultiLanguageTextType, - **kwargs: Any) -> model.MultiLanguageTextType: - return cls.construct_lang_string_set(element, NS_AAS + "langStringTextType", object_class, **kwargs) - - @classmethod - def construct_definition_type_iec61360(cls, element: etree._Element, object_class=model.DefinitionTypeIEC61360, - **kwargs: Any) -> model.DefinitionTypeIEC61360: - return cls.construct_lang_string_set(element, NS_AAS + "langStringDefinitionTypeIec61360", object_class, - **kwargs) - - @classmethod - def construct_preferred_name_type_iec61360(cls, element: etree._Element, - object_class=model.PreferredNameTypeIEC61360, - **kwargs: Any) -> model.PreferredNameTypeIEC61360: - return cls.construct_lang_string_set(element, NS_AAS + "langStringPreferredNameTypeIec61360", object_class, - **kwargs) - - @classmethod - def construct_short_name_type_iec61360(cls, element: etree._Element, object_class=model.ShortNameTypeIEC61360, - **kwargs: Any) -> model.ShortNameTypeIEC61360: - return cls.construct_lang_string_set(element, NS_AAS + "langStringShortNameTypeIec61360", object_class, - **kwargs) - - @classmethod - def construct_qualifier(cls, element: etree._Element, object_class=model.Qualifier, **_kwargs: Any) \ - -> model.Qualifier: - qualifier = object_class( - _child_text_mandatory(element, NS_AAS + "type"), - _child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) - ) - kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), QUALIFIER_KIND_INVERSE) - if kind is not None: - qualifier.kind = kind - value = _get_text_or_none(element.find(NS_AAS + "value")) - if value is not None: - qualifier.value = model.datatypes.from_xsd(value, qualifier.value_type) - value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) - if value_id is not None: - qualifier.value_id = value_id - cls._amend_abstract_attributes(qualifier, element) - return qualifier - - @classmethod - def construct_extension(cls, element: etree._Element, object_class=model.Extension, **_kwargs: Any) \ - -> model.Extension: - extension = object_class( - _child_text_mandatory(element, NS_AAS + "name")) - value_type = _get_text_or_none(element.find(NS_AAS + "valueType")) - if value_type is not None: - extension.value_type = model.datatypes.XSD_TYPE_CLASSES[value_type] - value = _get_text_or_none(element.find(NS_AAS + "value")) - if value is not None: - extension.value = model.datatypes.from_xsd(value, extension.value_type) - refers_to = element.find(NS_AAS + "refersTo") - if refers_to is not None: - for ref in _child_construct_multiple(refers_to, NS_AAS + "reference", cls._construct_referable_reference, - cls.failsafe): - extension.refers_to.add(ref) - cls._amend_abstract_attributes(extension, element) - return extension - - @classmethod - def construct_submodel_element(cls, element: etree._Element, **kwargs: Any) -> model.SubmodelElement: - """ - This function doesn't support the object_class parameter. - Overwrite each individual SubmodelElement/DataElement constructor function instead. - """ - submodel_elements: Dict[str, Callable[..., model.SubmodelElement]] = {NS_AAS + k: v for k, v in { - "annotatedRelationshipElement": cls.construct_annotated_relationship_element, - "basicEventElement": cls.construct_basic_event_element, - "capability": cls.construct_capability, - "entity": cls.construct_entity, - "operation": cls.construct_operation, - "relationshipElement": cls.construct_relationship_element, - "submodelElementCollection": cls.construct_submodel_element_collection, - "submodelElementList": cls.construct_submodel_element_list - }.items()} - if element.tag not in submodel_elements: - return cls.construct_data_element(element, abstract_class_name="SubmodelElement", **kwargs) - return submodel_elements[element.tag](element, **kwargs) - - @classmethod - def construct_data_element(cls, element: etree._Element, abstract_class_name: str = "DataElement", **kwargs: Any) \ - -> model.DataElement: - """ - This function does not support the object_class parameter. - Overwrite each individual DataElement constructor function instead. - """ - data_elements: Dict[str, Callable[..., model.DataElement]] = {NS_AAS + k: v for k, v in { - "blob": cls.construct_blob, - "file": cls.construct_file, - "multiLanguageProperty": cls.construct_multi_language_property, - "property": cls.construct_property, - "range": cls.construct_range, - "referenceElement": cls.construct_reference_element, - }.items()} - if element.tag not in data_elements: - raise KeyError(_element_pretty_identifier(element) + f" is not a valid {abstract_class_name}!") - return data_elements[element.tag](element, **kwargs) - - @classmethod - def construct_annotated_relationship_element(cls, element: etree._Element, - object_class=model.AnnotatedRelationshipElement, **_kwargs: Any) \ - -> model.AnnotatedRelationshipElement: - annotated_relationship_element = cls._construct_relationship_element_internal(element, object_class) - if not cls.stripped: - annotations = element.find(NS_AAS + "annotations") - if annotations is not None: - for data_element in _failsafe_construct_multiple(annotations, cls.construct_data_element, - cls.failsafe): - annotated_relationship_element.annotation.add(data_element) - return annotated_relationship_element - - @classmethod - def construct_basic_event_element(cls, element: etree._Element, object_class=model.BasicEventElement, - **_kwargs: Any) -> model.BasicEventElement: - basic_event_element = object_class( - None, - _child_construct_mandatory(element, NS_AAS + "observed", cls._construct_referable_reference), - _child_text_mandatory_mapped(element, NS_AAS + "direction", DIRECTION_INVERSE), - _child_text_mandatory_mapped(element, NS_AAS + "state", STATE_OF_EVENT_INVERSE) - ) - message_topic = _get_text_or_none(element.find(NS_AAS + "messageTopic")) - if message_topic is not None: - basic_event_element.message_topic = message_topic - message_broker = element.find(NS_AAS + "messageBroker") - if message_broker is not None: - basic_event_element.message_broker = _failsafe_construct(message_broker, cls.construct_reference, - cls.failsafe) - last_update = _get_text_or_none(element.find(NS_AAS + "lastUpdate")) - if last_update is not None: - basic_event_element.last_update = model.datatypes.from_xsd(last_update, model.datatypes.DateTime) - min_interval = _get_text_or_none(element.find(NS_AAS + "minInterval")) - if min_interval is not None: - basic_event_element.min_interval = model.datatypes.from_xsd(min_interval, model.datatypes.Duration) - max_interval = _get_text_or_none(element.find(NS_AAS + "maxInterval")) - if max_interval is not None: - basic_event_element.max_interval = model.datatypes.from_xsd(max_interval, model.datatypes.Duration) - cls._amend_abstract_attributes(basic_event_element, element) - return basic_event_element - - @classmethod - def construct_blob(cls, element: etree._Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: - blob = object_class( - None, - _child_text_mandatory(element, NS_AAS + "contentType") - ) - value = _get_text_or_none(element.find(NS_AAS + "value")) - if value is not None: - blob.value = base64.b64decode(value) - cls._amend_abstract_attributes(blob, element) - return blob - - @classmethod - def construct_capability(cls, element: etree._Element, object_class=model.Capability, **_kwargs: Any) \ - -> model.Capability: - capability = object_class(None) - cls._amend_abstract_attributes(capability, element) - return capability - - @classmethod - def construct_entity(cls, element: etree._Element, object_class=model.Entity, **_kwargs: Any) -> model.Entity: - specific_asset_id = set() - specific_assset_ids = element.find(NS_AAS + "specificAssetIds") - if specific_assset_ids is not None: - for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", - cls.construct_specific_asset_id, cls.failsafe): - specific_asset_id.add(id) - - entity = object_class( - id_short=None, - entity_type=_child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), - global_asset_id=_get_text_or_none(element.find(NS_AAS + "globalAssetId")), - specific_asset_id=specific_asset_id) - - if not cls.stripped: - statements = element.find(NS_AAS + "statements") - if statements is not None: - for submodel_element in _failsafe_construct_multiple(statements, cls.construct_submodel_element, - cls.failsafe): - entity.statement.add(submodel_element) - cls._amend_abstract_attributes(entity, element) - return entity - - @classmethod - def construct_file(cls, element: etree._Element, object_class=model.File, **_kwargs: Any) -> model.File: - file = object_class( - None, - _child_text_mandatory(element, NS_AAS + "contentType") - ) - value = _get_text_or_none(element.find(NS_AAS + "value")) - if value is not None: - file.value = value - cls._amend_abstract_attributes(file, element) - return file - - @classmethod - def construct_resource(cls, element: etree._Element, object_class=model.Resource, **_kwargs: Any) -> model.Resource: - resource = object_class( - _child_text_mandatory(element, NS_AAS + "path") - ) - content_type = _get_text_or_none(element.find(NS_AAS + "contentType")) - if content_type is not None: - resource.content_type = content_type - cls._amend_abstract_attributes(resource, element) - return resource - - @classmethod - def construct_multi_language_property(cls, element: etree._Element, object_class=model.MultiLanguageProperty, - **_kwargs: Any) -> model.MultiLanguageProperty: - multi_language_property = object_class(None) - value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_multi_language_text_type, - cls.failsafe) - if value is not None: - multi_language_property.value = value - value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) - if value_id is not None: - multi_language_property.value_id = value_id - cls._amend_abstract_attributes(multi_language_property, element) - return multi_language_property - - @classmethod - def construct_operation(cls, element: etree._Element, object_class=model.Operation, **_kwargs: Any) \ - -> model.Operation: - operation = object_class(None) - for tag, target in ((NS_AAS + "inputVariables", operation.input_variable), - (NS_AAS + "outputVariables", operation.output_variable), - (NS_AAS + "inoutputVariables", operation.in_output_variable)): - variables = element.find(tag) - if variables is not None: - for var in _child_construct_multiple(variables, NS_AAS + "operationVariable", - cls._construct_operation_variable, cls.failsafe): - target.add(var) - cls._amend_abstract_attributes(operation, element) - return operation - - @classmethod - def construct_property(cls, element: etree._Element, object_class=model.Property, **_kwargs: Any) -> model.Property: - property_ = object_class( - None, - value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) - ) - value = _get_text_or_none(element.find(NS_AAS + "value")) - if value is not None: - property_.value = model.datatypes.from_xsd(value, property_.value_type) - value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), cls.construct_reference, cls.failsafe) - if value_id is not None: - property_.value_id = value_id - cls._amend_abstract_attributes(property_, element) - return property_ - - @classmethod - def construct_range(cls, element: etree._Element, object_class=model.Range, **_kwargs: Any) -> model.Range: - range_ = object_class( - None, - value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) - ) - max_ = _get_text_or_none(element.find(NS_AAS + "max")) - if max_ is not None: - range_.max = model.datatypes.from_xsd(max_, range_.value_type) - min_ = _get_text_or_none(element.find(NS_AAS + "min")) - if min_ is not None: - range_.min = model.datatypes.from_xsd(min_, range_.value_type) - cls._amend_abstract_attributes(range_, element) - return range_ - - @classmethod - def construct_reference_element(cls, element: etree._Element, object_class=model.ReferenceElement, **_kwargs: Any) \ - -> model.ReferenceElement: - reference_element = object_class(None) - value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_reference, cls.failsafe) - if value is not None: - reference_element.value = value - cls._amend_abstract_attributes(reference_element, element) - return reference_element - - @classmethod - def construct_relationship_element(cls, element: etree._Element, object_class=model.RelationshipElement, - **_kwargs: Any) -> model.RelationshipElement: - return cls._construct_relationship_element_internal(element, object_class=object_class, **_kwargs) - - @classmethod - def construct_submodel_element_collection(cls, element: etree._Element, - object_class=model.SubmodelElementCollection, - **_kwargs: Any) -> model.SubmodelElementCollection: - collection = object_class(None) - if not cls.stripped: - value = element.find(NS_AAS + "value") - if value is not None: - for submodel_element in _failsafe_construct_multiple(value, cls.construct_submodel_element, - cls.failsafe): - collection.value.add(submodel_element) - cls._amend_abstract_attributes(collection, element) - return collection - - @classmethod - def construct_submodel_element_list(cls, element: etree._Element, object_class=model.SubmodelElementList, - **_kwargs: Any) -> model.SubmodelElementList: - type_value_list_element = KEY_TYPES_CLASSES_INVERSE[ - _child_text_mandatory_mapped(element, NS_AAS + "typeValueListElement", KEY_TYPES_INVERSE)] - if not issubclass(type_value_list_element, model.SubmodelElement): - raise ValueError("Expected a SubmodelElementList with a typeValueListElement that is a subclass of" - f"{model.SubmodelElement}, got {type_value_list_element}!") - order_relevant = element.find(NS_AAS + "orderRelevant") - list_ = object_class( - None, - type_value_list_element, - semantic_id_list_element=_failsafe_construct(element.find(NS_AAS + "semanticIdListElement"), - cls.construct_reference, cls.failsafe), - value_type_list_element=_get_text_mapped_or_none(element.find(NS_AAS + "valueTypeListElement"), - model.datatypes.XSD_TYPE_CLASSES), - order_relevant=_str_to_bool(_get_text_mandatory(order_relevant)) - if order_relevant is not None else True - ) - if not cls.stripped: - value = element.find(NS_AAS + "value") - if value is not None: - list_.value.extend(_failsafe_construct_multiple(value, cls.construct_submodel_element, cls.failsafe)) - cls._amend_abstract_attributes(list_, element) - return list_ - - @classmethod - def construct_asset_administration_shell(cls, element: etree._Element, object_class=model.AssetAdministrationShell, - **_kwargs: Any) -> model.AssetAdministrationShell: - aas = object_class( - id_=_child_text_mandatory(element, NS_AAS + "id"), - asset_information=_child_construct_mandatory(element, NS_AAS + "assetInformation", - cls.construct_asset_information) - ) - if not cls.stripped: - submodels = element.find(NS_AAS + "submodels") - if submodels is not None: - for ref in _child_construct_multiple(submodels, NS_AAS + "reference", - cls._construct_submodel_reference, cls.failsafe): - aas.submodel.add(ref) - derived_from = _failsafe_construct(element.find(NS_AAS + "derivedFrom"), - cls._construct_asset_administration_shell_reference, cls.failsafe) - if derived_from is not None: - aas.derived_from = derived_from - cls._amend_abstract_attributes(aas, element) - return aas - - @classmethod - def construct_specific_asset_id(cls, element: etree._Element, object_class=model.SpecificAssetId, - **_kwargs: Any) -> model.SpecificAssetId: - # semantic_id can't be applied by _amend_abstract_attributes because specificAssetId is immutable - return object_class( - name=_get_text_or_none(element.find(NS_AAS + "name")), - value=_get_text_or_none(element.find(NS_AAS + "value")), - external_subject_id=_failsafe_construct(element.find(NS_AAS + "externalSubjectId"), - cls.construct_external_reference, cls.failsafe), - semantic_id=_failsafe_construct(element.find(NS_AAS + "semanticId"), cls.construct_reference, cls.failsafe) - ) - - @classmethod - def construct_asset_information(cls, element: etree._Element, object_class=model.AssetInformation, **_kwargs: Any) \ - -> model.AssetInformation: - specific_asset_id = set() - specific_assset_ids = element.find(NS_AAS + "specificAssetIds") - if specific_assset_ids is not None: - for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", - cls.construct_specific_asset_id, cls.failsafe): - specific_asset_id.add(id) - - asset_information = object_class( - _child_text_mandatory_mapped(element, NS_AAS + "assetKind", ASSET_KIND_INVERSE), - global_asset_id=_get_text_or_none(element.find(NS_AAS + "globalAssetId")), - specific_asset_id=specific_asset_id, - ) - - asset_type = _get_text_or_none(element.find(NS_AAS + "assetType")) - if asset_type is not None: - asset_information.asset_type = asset_type - thumbnail = _failsafe_construct(element.find(NS_AAS + "defaultThumbnail"), - cls.construct_resource, cls.failsafe) - if thumbnail is not None: - asset_information.default_thumbnail = thumbnail - - cls._amend_abstract_attributes(asset_information, element) - return asset_information - - @classmethod - def construct_submodel(cls, element: etree._Element, object_class=model.Submodel, **_kwargs: Any) \ - -> model.Submodel: - submodel = object_class( - _child_text_mandatory(element, NS_AAS + "id"), - kind=_get_kind(element) - ) - if not cls.stripped: - submodel_elements = element.find(NS_AAS + "submodelElements") - if submodel_elements is not None: - for submodel_element in _failsafe_construct_multiple(submodel_elements, cls.construct_submodel_element, - cls.failsafe): - submodel.submodel_element.add(submodel_element) - cls._amend_abstract_attributes(submodel, element) - return submodel - - @classmethod - def construct_value_reference_pair(cls, element: etree._Element, object_class=model.ValueReferencePair, - **_kwargs: Any) -> model.ValueReferencePair: - return object_class(_child_text_mandatory(element, NS_AAS + "value"), - _child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference)) - - @classmethod - def construct_value_list(cls, element: etree._Element, **_kwargs: Any) -> model.ValueList: - """ - This function doesn't support the object_class parameter, because ValueList is just a generic type alias. - """ - - return set( - _child_construct_multiple(_get_child_mandatory(element, NS_AAS + "valueReferencePairs"), - NS_AAS + "valueReferencePair", cls.construct_value_reference_pair, - cls.failsafe) - ) - - @classmethod - def construct_concept_description(cls, element: etree._Element, object_class=model.ConceptDescription, - **_kwargs: Any) -> model.ConceptDescription: - cd = object_class(_child_text_mandatory(element, NS_AAS + "id")) - is_case_of = element.find(NS_AAS + "isCaseOf") - if is_case_of is not None: - for ref in _child_construct_multiple(is_case_of, NS_AAS + "reference", cls.construct_reference, - cls.failsafe): - cd.is_case_of.add(ref) - cls._amend_abstract_attributes(cd, element) - return cd - - @classmethod - def construct_embedded_data_specification(cls, element: etree._Element, - object_class=model.EmbeddedDataSpecification, - **_kwargs: Any) -> model.EmbeddedDataSpecification: - data_specification_content = _get_child_mandatory(element, NS_AAS + "dataSpecificationContent") - if len(data_specification_content) == 0: - raise KeyError(f"{_element_pretty_identifier(data_specification_content)} has no data specification!") - if len(data_specification_content) > 1: - logger.warning(f"{_element_pretty_identifier(data_specification_content)} has more than one " - "data specification, using the first one...") - embedded_data_specification = object_class( - _child_construct_mandatory(element, NS_AAS + "dataSpecification", cls.construct_external_reference), - _failsafe_construct_mandatory(data_specification_content[0], cls.construct_data_specification_content) - ) - cls._amend_abstract_attributes(embedded_data_specification, element) - return embedded_data_specification - - @classmethod - def construct_data_specification_content(cls, element: etree._Element, **kwargs: Any) \ - -> model.DataSpecificationContent: - """ - This function doesn't support the object_class parameter. - Overwrite each individual DataSpecificationContent constructor function instead. - """ - data_specification_contents: Dict[str, Callable[..., model.DataSpecificationContent]] = \ - {NS_AAS + k: v for k, v in { - "dataSpecificationIec61360": cls.construct_data_specification_iec61360, - }.items()} - if element.tag not in data_specification_contents: - raise KeyError(f"{_element_pretty_identifier(element)} is not a valid DataSpecificationContent!") - return data_specification_contents[element.tag](element, **kwargs) - - @classmethod - def construct_data_specification_iec61360(cls, element: etree._Element, - object_class=model.DataSpecificationIEC61360, - **_kwargs: Any) -> model.DataSpecificationIEC61360: - ds_iec = object_class(_child_construct_mandatory(element, NS_AAS + "preferredName", - cls.construct_preferred_name_type_iec61360)) - short_name = _failsafe_construct(element.find(NS_AAS + "shortName"), cls.construct_short_name_type_iec61360, - cls.failsafe) - if short_name is not None: - ds_iec.short_name = short_name - unit = _get_text_or_none(element.find(NS_AAS + "unit")) - if unit is not None: - ds_iec.unit = unit - unit_id = _failsafe_construct(element.find(NS_AAS + "unitId"), cls.construct_reference, cls.failsafe) - if unit_id is not None: - ds_iec.unit_id = unit_id - source_of_definiion = _get_text_or_none(element.find(NS_AAS + "sourceOfDefinition")) - if source_of_definiion is not None: - ds_iec.source_of_definition = source_of_definiion - symbol = _get_text_or_none(element.find(NS_AAS + "symbol")) - if symbol is not None: - ds_iec.symbol = symbol - data_type = _get_text_mapped_or_none(element.find(NS_AAS + "dataType"), IEC61360_DATA_TYPES_INVERSE) - if data_type is not None: - ds_iec.data_type = data_type - definition = _failsafe_construct(element.find(NS_AAS + "definition"), cls.construct_definition_type_iec61360, - cls.failsafe) - if definition is not None: - ds_iec.definition = definition - value_format = _get_text_or_none(element.find(NS_AAS + "valueFormat")) - if value_format is not None: - ds_iec.value_format = value_format - value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe) - if value_list is not None: - ds_iec.value_list = value_list - value = _get_text_or_none(element.find(NS_AAS + "value")) - if value is not None and value_format is not None: - ds_iec.value = value - level_type = element.find(NS_AAS + "levelType") - if level_type is not None: - for child in level_type: - tag = child.tag.split(NS_AAS, 1)[-1] - if tag not in IEC61360_LEVEL_TYPES_INVERSE: - error_message = f"{_element_pretty_identifier(element)} has invalid levelType: {tag}" - if not cls.failsafe: - raise ValueError(error_message) - logger.warning(error_message) - continue - try: - if child.text is None: - raise ValueError - level_type_value = _str_to_bool(child.text) - except ValueError: - error_message = f"levelType {tag} of {_element_pretty_identifier(element)} has invalid boolean: " \ - + str(child.text) - if not cls.failsafe: - raise ValueError(error_message) - logger.warning(error_message) - continue - if level_type_value: - ds_iec.level_types.add(IEC61360_LEVEL_TYPES_INVERSE[tag]) - cls._amend_abstract_attributes(ds_iec, element) - return ds_iec - - -class StrictAASFromXmlDecoder(AASFromXmlDecoder): - """ - Non-failsafe XML decoder. Encountered errors won't be caught and abort parsing. - """ - failsafe = False - - -class StrippedAASFromXmlDecoder(AASFromXmlDecoder): - """ - Decoder for stripped XML elements. Used in the HTTP adapter. - """ - stripped = True - - -class StrictStrippedAASFromXmlDecoder(StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder): - """ - Non-failsafe decoder for stripped XML elements. - """ - pass - - def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree._Element]: """ Parse an XML document into an element tree @@ -1198,8 +77,8 @@ def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: :param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception if the document is malformed, parsing is aborted, an error is logged and None is returned :param parser_kwargs: Keyword arguments passed to the XMLParser constructor - :raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML - :raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document + :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML + :raises KeyError: If a required namespace has not been declared on the XML document :return: The root element of the element tree """ @@ -1223,194 +102,8 @@ def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: return root -def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFromXmlDecoder]]) \ - -> Type[AASFromXmlDecoder]: - """ - Returns the correct decoder based on the parameters failsafe and stripped. If a decoder class is given, failsafe - and stripped are ignored. - - :param failsafe: If true, a failsafe decoder is selected. Ignored if a decoder class is specified. - :param stripped: If true, a decoder for parsing stripped XML elements is selected. Ignored if a decoder class is - specified. - :param decoder: Is returned, if specified. - :return: A AASFromXmlDecoder (sub)class. - """ - if decoder is not None: - return decoder - if failsafe: - if stripped: - return StrippedAASFromXmlDecoder - return AASFromXmlDecoder - else: - if stripped: - return StrictStrippedAASFromXmlDecoder - return StrictAASFromXmlDecoder - - -@enum.unique -class XMLConstructables(enum.Enum): - """ - This enum is used to specify which type to construct in read_aas_xml_element(). - """ - KEY = enum.auto() - REFERENCE = enum.auto() - MODEL_REFERENCE = enum.auto() - EXTERNAL_REFERENCE = enum.auto() - ADMINISTRATIVE_INFORMATION = enum.auto() - QUALIFIER = enum.auto() - SECURITY = enum.auto() - ANNOTATED_RELATIONSHIP_ELEMENT = enum.auto() - BASIC_EVENT_ELEMENT = enum.auto() - BLOB = enum.auto() - CAPABILITY = enum.auto() - ENTITY = enum.auto() - EXTENSION = enum.auto() - FILE = enum.auto() - RESOURCE = enum.auto() - MULTI_LANGUAGE_PROPERTY = enum.auto() - OPERATION = enum.auto() - PROPERTY = enum.auto() - RANGE = enum.auto() - REFERENCE_ELEMENT = enum.auto() - RELATIONSHIP_ELEMENT = enum.auto() - SUBMODEL_ELEMENT_COLLECTION = enum.auto() - SUBMODEL_ELEMENT_LIST = enum.auto() - ASSET_ADMINISTRATION_SHELL = enum.auto() - ASSET_INFORMATION = enum.auto() - SPECIFIC_ASSET_ID = enum.auto() - SUBMODEL = enum.auto() - VALUE_REFERENCE_PAIR = enum.auto() - IEC61360_CONCEPT_DESCRIPTION = enum.auto() - CONCEPT_DESCRIPTION = enum.auto() - DATA_ELEMENT = enum.auto() - SUBMODEL_ELEMENT = enum.auto() - VALUE_LIST = enum.auto() - MULTI_LANGUAGE_NAME_TYPE = enum.auto() - MULTI_LANGUAGE_TEXT_TYPE = enum.auto() - DEFINITION_TYPE_IEC61360 = enum.auto() - PREFERRED_NAME_TYPE_IEC61360 = enum.auto() - SHORT_NAME_TYPE_IEC61360 = enum.auto() - EMBEDDED_DATA_SPECIFICATION = enum.auto() - DATA_SPECIFICATION_CONTENT = enum.auto() - DATA_SPECIFICATION_IEC61360 = enum.auto() - - -def read_aas_xml_element(file: PathOrIO, construct: XMLConstructables, failsafe: bool = True, stripped: bool = False, - decoder: Optional[Type[AASFromXmlDecoder]] = None, **constructor_kwargs) -> Optional[object]: - """ - Construct a single object from an XML string. The namespaces have to be declared on the object itself, since there - is no surrounding environment element. - - :param file: A filename or file-like object to read the XML-serialized data from - :param construct: A member of the enum :class:`~.XMLConstructables`, specifying which type to construct. - :param failsafe: If true, the document is parsed in a failsafe way: missing attributes and elements are logged - instead of causing exceptions. Defect objects are skipped. - This parameter is ignored if a decoder class is specified. - :param stripped: If true, stripped XML elements are parsed. - 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 XML elements - :param constructor_kwargs: Keyword arguments passed to the constructor function - :raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML - :raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document - :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during - construction of the objects - :return: The constructed object or None, if an error occurred in failsafe mode. - """ - decoder_ = _select_decoder(failsafe, stripped, decoder) - constructor: Callable[..., object] - - if construct == XMLConstructables.KEY: - constructor = decoder_.construct_key - elif construct == XMLConstructables.REFERENCE: - constructor = decoder_.construct_reference - elif construct == XMLConstructables.MODEL_REFERENCE: - constructor = decoder_.construct_model_reference - elif construct == XMLConstructables.EXTERNAL_REFERENCE: - constructor = decoder_.construct_external_reference - elif construct == XMLConstructables.ADMINISTRATIVE_INFORMATION: - constructor = decoder_.construct_administrative_information - elif construct == XMLConstructables.QUALIFIER: - constructor = decoder_.construct_qualifier - elif construct == XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT: - constructor = decoder_.construct_annotated_relationship_element - elif construct == XMLConstructables.BASIC_EVENT_ELEMENT: - constructor = decoder_.construct_basic_event_element - elif construct == XMLConstructables.BLOB: - constructor = decoder_.construct_blob - elif construct == XMLConstructables.CAPABILITY: - constructor = decoder_.construct_capability - elif construct == XMLConstructables.ENTITY: - constructor = decoder_.construct_entity - elif construct == XMLConstructables.EXTENSION: - constructor = decoder_.construct_extension - elif construct == XMLConstructables.FILE: - constructor = decoder_.construct_file - elif construct == XMLConstructables.RESOURCE: - constructor = decoder_.construct_resource - elif construct == XMLConstructables.MULTI_LANGUAGE_PROPERTY: - constructor = decoder_.construct_multi_language_property - elif construct == XMLConstructables.OPERATION: - constructor = decoder_.construct_operation - elif construct == XMLConstructables.PROPERTY: - constructor = decoder_.construct_property - elif construct == XMLConstructables.RANGE: - constructor = decoder_.construct_range - elif construct == XMLConstructables.REFERENCE_ELEMENT: - constructor = decoder_.construct_reference_element - elif construct == XMLConstructables.RELATIONSHIP_ELEMENT: - constructor = decoder_.construct_relationship_element - elif construct == XMLConstructables.SUBMODEL_ELEMENT_COLLECTION: - constructor = decoder_.construct_submodel_element_collection - elif construct == XMLConstructables.SUBMODEL_ELEMENT_LIST: - constructor = decoder_.construct_submodel_element_list - elif construct == XMLConstructables.ASSET_ADMINISTRATION_SHELL: - constructor = decoder_.construct_asset_administration_shell - elif construct == XMLConstructables.ASSET_INFORMATION: - constructor = decoder_.construct_asset_information - elif construct == XMLConstructables.SPECIFIC_ASSET_ID: - constructor = decoder_.construct_specific_asset_id - elif construct == XMLConstructables.SUBMODEL: - constructor = decoder_.construct_submodel - elif construct == XMLConstructables.VALUE_REFERENCE_PAIR: - constructor = decoder_.construct_value_reference_pair - elif construct == XMLConstructables.CONCEPT_DESCRIPTION: - constructor = decoder_.construct_concept_description - elif construct == XMLConstructables.MULTI_LANGUAGE_NAME_TYPE: - constructor = decoder_.construct_multi_language_name_type - elif construct == XMLConstructables.MULTI_LANGUAGE_TEXT_TYPE: - constructor = decoder_.construct_multi_language_text_type - elif construct == XMLConstructables.DEFINITION_TYPE_IEC61360: - constructor = decoder_.construct_definition_type_iec61360 - elif construct == XMLConstructables.PREFERRED_NAME_TYPE_IEC61360: - constructor = decoder_.construct_preferred_name_type_iec61360 - elif construct == XMLConstructables.SHORT_NAME_TYPE_IEC61360: - constructor = decoder_.construct_short_name_type_iec61360 - elif construct == XMLConstructables.EMBEDDED_DATA_SPECIFICATION: - constructor = decoder_.construct_embedded_data_specification - elif construct == XMLConstructables.DATA_SPECIFICATION_IEC61360: - constructor = decoder_.construct_data_specification_iec61360 - # the following constructors decide which constructor to call based on the elements tag - elif construct == XMLConstructables.DATA_ELEMENT: - constructor = decoder_.construct_data_element - elif construct == XMLConstructables.SUBMODEL_ELEMENT: - constructor = decoder_.construct_submodel_element - elif construct == XMLConstructables.DATA_SPECIFICATION_CONTENT: - constructor = decoder_.construct_data_specification_content - # type aliases - elif construct == XMLConstructables.VALUE_LIST: - constructor = decoder_.construct_value_list - else: - raise ValueError(f"{construct.name} cannot be constructed!") - - element = _parse_xml_document(file, failsafe=decoder_.failsafe) - return _failsafe_construct(element, constructor, decoder_.failsafe, **constructor_kwargs) - - -def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identifiable], file: PathOrIO, - replace_existing: bool = False, ignore_existing: bool = False, failsafe: bool = True, - stripped: bool = False, decoder: Optional[Type[AASFromXmlDecoder]] = None, - **parser_kwargs: Any) -> Set[model.Identifier]: +def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, + replace_existing: bool = False, ignore_existing: bool = False) -> Set[str]: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 into a given :class:`ObjectStore `. @@ -1421,89 +114,81 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. This parameter is ignored if replace_existing is True. - :param failsafe: If ``True``, the document is parsed in a failsafe way: missing attributes and elements are logged - instead of causing exceptions. Defect objects are skipped. - This parameter is ignored if a decoder class is specified. - :param stripped: If ``True``, stripped XML elements are parsed. - 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 XML elements :param parser_kwargs: Keyword arguments passed to the XMLParser constructor - :raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML - :raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document - :raises KeyError: **Non-failsafe**: Encountered a duplicate identifier + :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML + :raises KeyError: If a required namespace has not been declared on the XML document + :raises KeyError: 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`` - :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): Errors during construction of the objects - :raises TypeError: **Non-failsafe**: Encountered an undefined top-level list (e.g. ````) + :raises TypeError: Encountered an undefined top-level list (e.g. ````) :return: A set of :class:`Identifiers ` that were added to object_store """ - ret: Set[model.Identifier] = set() - - decoder_ = _select_decoder(failsafe, stripped, decoder) + ret: Set = set() element_constructors: Dict[str, Callable[..., model.Identifiable]] = { - "assetAdministrationShell": decoder_.construct_asset_administration_shell, - "conceptDescription": decoder_.construct_concept_description, - "submodel": decoder_.construct_submodel + "assetAdministrationShell": aas_xmlization.asset_administration_shell_from_str, + "conceptDescription": aas_xmlization.concept_description_from_str, + "submodel": aas_xmlization.submodel_from_str } element_constructors = {NS_AAS + k: v for k, v in element_constructors.items()} - root = _parse_xml_document(file, failsafe=decoder_.failsafe, **parser_kwargs) + root = etree.parse(file).getroot() + if root is None: return ret - # Add AAS objects to ObjectStore for list_ in root: + element_tag = list_.tag[:-1] if list_.tag[-1] != "s" or element_tag not in element_constructors: error_message = f"Unexpected top-level list {_element_pretty_identifier(list_)}!" - if not decoder_.failsafe: - raise TypeError(error_message) + logger.warning(error_message) continue - constructor = element_constructors[element_tag] - for element in _child_construct_multiple(list_, element_tag, constructor, decoder_.failsafe): - if element.id in ret: + + for element in list_: + str = etree.tostring(element).decode("utf-8-sig") + constructor = element_constructors[element_tag](str) + + if constructor.id in ret: error_message = f"{element} has a duplicate identifier already parsed in the document!" - if not decoder_.failsafe: - raise KeyError(error_message) - logger.error(error_message + " skipping it...") - continue - existing_element = object_store.get(element.id) + raise KeyError(error_message) + existing_element = object_store.get(constructor.id) if existing_element is not None: if not replace_existing: - error_message = f"object with identifier {element.id} already exists " \ + error_message = f"object with identifier {constructor.id} already exists " \ f"in the object store: {existing_element}!" if not ignore_existing: - raise KeyError(error_message + f" failed to insert {element}!") - logger.info(error_message + f" skipping insertion of {element}...") + raise KeyError(error_message + f" failed to insert {constructor}!") + logger.info(error_message + f" skipping insertion of {constructor}...") continue object_store.discard(existing_element) - object_store.add(element) - ret.add(element.id) + object_store.add(constructor) + ret.add(constructor.id) + return ret -def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]: +def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> ObjectStore[Identifiable]: """ - A wrapper of :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an - empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports - the same keyword arguments as :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`. + A wrapper of :meth:`~basyx.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an + empty :class:`~basyx.ObjectStore`. This function supports + the same keyword arguments as :meth:`~basyx.adapter.xml.xml_deserialization.read_aas_xml_file_into`. :param file: A filename or file-like object to read the XML-serialized data from :param kwargs: Keyword arguments passed to :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` - :raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML - :raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document - :raises KeyError: **Non-failsafe**: Encountered a duplicate identifier - :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during + :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML + :raises KeyError: If a required namespace has not been declared on the XML document + :raises KeyError: Encountered a duplicate identifier + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): Errors during construction of the objects - :raises TypeError: **Non-failsafe**: Encountered an undefined top-level list (e.g. ````) - :return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the XML file + :raises TypeError: Encountered an undefined top-level list (e.g. ````) + :return: A :class:`~basyx.ObjectStore` containing all AAS objects from the XML file """ - object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - read_aas_xml_file_into(object_store, file, **kwargs) - return object_store + obj_store: ObjectStore[Identifiable] = ObjectStore() + read_aas_xml_file_into(obj_store, file, **kwargs) + return obj_store diff --git a/sdk/basyx/adapter/xml/xml_serialization.py b/sdk/basyx/adapter/xml/xml_serialization.py index 07d75d3..1c2e7c3 100644 --- a/sdk/basyx/adapter/xml/xml_serialization.py +++ b/sdk/basyx/adapter/xml/xml_serialization.py @@ -1,977 +1,26 @@ -# Copyright (c) 2023 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 -""" -.. _adapter.xml.xml_serialization: - -Module for serializing Asset Administration Shell data to the official XML format - -How to use: - -- For generating an XML-File from a :class:`~basyx.aas.model.provider.AbstractObjectStore`, check out the function - :func:`write_aas_xml_file`. -- For serializing any object to an XML fragment, that fits the XML specification from 'Details of the - Asset Administration Shell', chapter 5.4, you can either use :func:`object_to_xml_element`, which serializes a given - object and returns it as :class:`~lxml.etree._Element`, **or** :func:`write_aas_xml_element`, which does the same - thing, but writes the :class:`~lxml.etree._Element` to a file instead of returning it. - As a third alternative, you can also use the functions ``_to_xml()`` directly. - -.. attention:: - Unlike the XML deserialization and the JSON (de-)serialization, the XML serialization only supports - :class:`~typing.BinaryIO` and not :class:`~typing.TextIO`. Thus, if you open files by yourself, you have to open - them in binary mode, see the mode table of :func:`open`. - - .. code:: python - - # wb = open for writing + binary mode - with open("example.xml", "wb") as fp: - write_aas_xml_file(fp, object_store) -""" - from lxml import etree from typing import Callable, Dict, Optional, Type import base64 -from basyx.aas import model +from aas_core3 import types as model from .. import _generic +from basyx.object_store import ObjectStore +import aas_core3.xmlization as aas_xmlization NS_AAS = _generic.XML_NS_AAS -# ############################################################## -# functions to manipulate etree.Elements more effectively -# ############################################################## - -def _generate_element(name: str, - text: Optional[str] = None, - attributes: Optional[Dict] = None) -> etree._Element: - """ - generate an :class:`~lxml.etree._Element` object - - :param name: namespace+tag_name of the element - :param text: Text of the element. Default is None - :param attributes: Attributes of the elements in form of a dict ``{"attribute_name": "attribute_content"}`` - :return: :class:`~lxml.etree._Element` object - """ - et_element = etree.Element(name) - if text: - et_element.text = text - if attributes: - for key, value in attributes.items(): - et_element.set(key, value) - return et_element - - -def boolean_to_xml(obj: bool) -> str: - """ - Serialize a boolean to XML - - :param obj: Boolean (``True``, ``False``) - :return: String in the XML accepted form (``true``, ``false``) - """ - if obj: - return "true" - else: - return "false" - - -# ############################################################## -# transformation functions to serialize abstract classes from model.base -# ############################################################## - - -def abstract_classes_to_xml(tag: str, obj: object) -> etree._Element: - """ - Generates an XML element and adds attributes of abstract base classes of ``obj``. - - If the object obj is inheriting from any abstract AAS class, this function adds all the serialized information of - those abstract classes to the generated element. - - :param tag: Tag of the element - :param obj: An object of the AAS - :return: Parent element with the serialized information from the abstract classes - """ - elm = _generate_element(tag) - if isinstance(obj, model.HasExtension): - if obj.extension: - et_extension = _generate_element(NS_AAS + "extensions") - for extension in obj.extension: - if isinstance(extension, model.Extension): - et_extension.append(extension_to_xml(extension, tag=NS_AAS + "extension")) - elm.append(et_extension) - if isinstance(obj, model.Referable): - if obj.category: - elm.append(_generate_element(name=NS_AAS + "category", text=obj.category)) - if obj.id_short and not isinstance(obj.parent, model.SubmodelElementList): - elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) - if obj.display_name: - elm.append(lang_string_set_to_xml(obj.display_name, tag=NS_AAS + "displayName")) - if obj.description: - elm.append(lang_string_set_to_xml(obj.description, tag=NS_AAS + "description")) - if isinstance(obj, model.Identifiable): - if obj.administration: - elm.append(administrative_information_to_xml(obj.administration)) - elm.append(_generate_element(name=NS_AAS + "id", text=obj.id)) - if isinstance(obj, model.HasKind): - if obj.kind is model.ModellingKind.TEMPLATE: - elm.append(_generate_element(name=NS_AAS + "kind", text="Template")) - else: - # then modelling-kind is Instance - elm.append(_generate_element(name=NS_AAS + "kind", text="Instance")) - if isinstance(obj, model.HasSemantics): - if obj.semantic_id: - elm.append(reference_to_xml(obj.semantic_id, tag=NS_AAS+"semanticId")) - if obj.supplemental_semantic_id: - et_supplemental_semantic_ids = _generate_element(NS_AAS + "supplementalSemanticIds") - for supplemental_semantic_id in obj.supplemental_semantic_id: - et_supplemental_semantic_ids.append(reference_to_xml(supplemental_semantic_id, NS_AAS+"reference")) - elm.append(et_supplemental_semantic_ids) - if isinstance(obj, model.Qualifiable): - if obj.qualifier: - et_qualifier = _generate_element(NS_AAS + "qualifiers") - for qualifier in obj.qualifier: - et_qualifier.append(qualifier_to_xml(qualifier, tag=NS_AAS+"qualifier")) - elm.append(et_qualifier) - if isinstance(obj, model.HasDataSpecification): - if obj.embedded_data_specifications: - et_embedded_data_specifications = _generate_element(NS_AAS + "embeddedDataSpecifications") - for eds in obj.embedded_data_specifications: - et_embedded_data_specifications.append(embedded_data_specification_to_xml(eds)) - elm.append(et_embedded_data_specifications) - return elm - - -# ############################################################## -# transformation functions to serialize classes from model.base -# ############################################################## - - -def _value_to_xml(value: model.ValueDataType, - value_type: model.DataTypeDefXsd, - tag: str = NS_AAS+"value") -> etree._Element: - """ - Serialization of objects of :class:`~basyx.aas.model.base.ValueDataType` to XML - - :param value: :class:`~basyx.aas.model.base.ValueDataType` object - :param value_type: Corresponding :class:`~basyx.aas.model.base.DataTypeDefXsd` - :param tag: tag of the serialized :class:`~basyx.aas.model.base.ValueDataType` object - :return: Serialized :class:`~lxml.etree._Element` object - """ - # todo: add "{NS_XSI+"type": "xs:"+model.datatypes.XSD_TYPE_NAMES[value_type]}" as attribute, if the schema allows - # it - # TODO: if this is ever changed, check value_reference_pair_to_xml() - return _generate_element(tag, - text=model.datatypes.xsd_repr(value)) - - -def lang_string_set_to_xml(obj: model.LangStringSet, tag: str) -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.LangStringSet` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.LangStringSet` - :param tag: Namespace+Tag name of the returned XML element. - :return: Serialized :class:`~lxml.etree._Element` object - """ - LANG_STRING_SET_TAGS: Dict[Type[model.LangStringSet], str] = {k: NS_AAS + v for k, v in { - model.MultiLanguageNameType: "langStringNameType", - model.MultiLanguageTextType: "langStringTextType", - model.DefinitionTypeIEC61360: "langStringDefinitionTypeIec61360", - model.PreferredNameTypeIEC61360: "langStringPreferredNameTypeIec61360", - model.ShortNameTypeIEC61360: "langStringShortNameTypeIec61360" - }.items()} - et_lss = _generate_element(name=tag) - for language, text in obj.items(): - et_ls = _generate_element(name=LANG_STRING_SET_TAGS[type(obj)]) - et_ls.append(_generate_element(name=NS_AAS + "language", text=language)) - et_ls.append(_generate_element(name=NS_AAS + "text", text=text)) - et_lss.append(et_ls) - return et_lss - - -def administrative_information_to_xml(obj: model.AdministrativeInformation, - tag: str = NS_AAS+"administration") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.AdministrativeInformation` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.AdministrativeInformation` - :param tag: Namespace+Tag of the serialized element. Default is ``aas:administration`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_administration = abstract_classes_to_xml(tag, obj) - if obj.version: - et_administration.append(_generate_element(name=NS_AAS + "version", text=obj.version)) - if obj.revision: - et_administration.append(_generate_element(name=NS_AAS + "revision", text=obj.revision)) - if obj.creator: - et_administration.append(reference_to_xml(obj.creator, tag=NS_AAS + "creator")) - if obj.template_id: - et_administration.append(_generate_element(name=NS_AAS + "templateId", text=obj.template_id)) - return et_administration - - -def data_element_to_xml(obj: model.DataElement) -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.DataElement` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.DataElement` - :return: Serialized :class:`~lxml.etree._Element` object - """ - if isinstance(obj, model.MultiLanguageProperty): - return multi_language_property_to_xml(obj) - if isinstance(obj, model.Property): - return property_to_xml(obj) - if isinstance(obj, model.Range): - return range_to_xml(obj) - if isinstance(obj, model.Blob): - return blob_to_xml(obj) - if isinstance(obj, model.File): - return file_to_xml(obj) - if isinstance(obj, model.ReferenceElement): - return reference_element_to_xml(obj) - raise AssertionError(f"Type {obj.__class__.__name__} is not yet supported by the XML serialization!") - - -def key_to_xml(obj: model.Key, tag: str = NS_AAS+"key") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.Key` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.Key` - :param tag: Namespace+Tag of the returned element. Default is ``aas:key`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_key = _generate_element(tag) - et_key.append(_generate_element(name=NS_AAS + "type", text=_generic.KEY_TYPES[obj.type])) - et_key.append(_generate_element(name=NS_AAS + "value", text=obj.value)) - return et_key - - -def reference_to_xml(obj: model.Reference, tag: str = NS_AAS+"reference") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.Reference` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.Reference` - :param tag: Namespace+Tag of the returned element. Default is ``aas:reference`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_reference = _generate_element(tag) - et_reference.append(_generate_element(NS_AAS + "type", text=_generic.REFERENCE_TYPES[obj.__class__])) - if obj.referred_semantic_id is not None: - et_reference.append(reference_to_xml(obj.referred_semantic_id, NS_AAS + "referredSemanticId")) - et_keys = _generate_element(name=NS_AAS + "keys") - for aas_key in obj.key: - et_keys.append(key_to_xml(aas_key)) - et_reference.append(et_keys) - - return et_reference - - -def qualifier_to_xml(obj: model.Qualifier, tag: str = NS_AAS+"qualifier") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.Qualifier` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.Qualifier` - :param tag: Namespace+Tag of the serialized ElementTree object. Default is ``aas:qualifier`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_qualifier = abstract_classes_to_xml(tag, obj) - et_qualifier.append(_generate_element(NS_AAS + "kind", text=_generic.QUALIFIER_KIND[obj.kind])) - et_qualifier.append(_generate_element(NS_AAS + "type", text=obj.type)) - et_qualifier.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) - if obj.value: - et_qualifier.append(_value_to_xml(obj.value, obj.value_type)) - if obj.value_id: - et_qualifier.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) - return et_qualifier - - -def extension_to_xml(obj: model.Extension, tag: str = NS_AAS+"extension") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.Extension` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.Extension` - :param tag: Namespace+Tag of the serialized ElementTree object. Default is ``aas:extension`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_extension = abstract_classes_to_xml(tag, obj) - et_extension.append(_generate_element(NS_AAS + "name", text=obj.name)) - if obj.value_type: - et_extension.append(_generate_element(NS_AAS + "valueType", - text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) - if obj.value: - et_extension.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore # (value_type could be None) - if len(obj.refers_to) > 0: - refers_to = _generate_element(NS_AAS+"refersTo") - for reference in obj.refers_to: - refers_to.append(reference_to_xml(reference, NS_AAS+"reference")) - et_extension.append(refers_to) - return et_extension - - -def value_reference_pair_to_xml(obj: model.ValueReferencePair, - tag: str = NS_AAS+"valueReferencePair") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.ValueReferencePair` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.ValueReferencePair` - :param tag: Namespace+Tag of the serialized element. Default is ``aas:valueReferencePair`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_vrp = _generate_element(tag) - # TODO: value_type isn't used at all by _value_to_xml(), thus we can ignore the type here for now - et_vrp.append(_generate_element(NS_AAS+"value", text=obj.value)) # type: ignore - et_vrp.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) - return et_vrp - - -def value_list_to_xml(obj: model.ValueList, - tag: str = NS_AAS+"valueList") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.ValueList` to XML - - todo: couldn't find it in the official schema, so guessing how to implement serialization - - :param obj: Object of class :class:`~basyx.aas.model.base.ValueList` - :param tag: Namespace+Tag of the serialized element. Default is ``aas:valueList`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_value_list = _generate_element(tag) - et_value_reference_pairs = _generate_element(NS_AAS+"valueReferencePairs") - for aas_reference_pair in obj: - et_value_reference_pairs.append(value_reference_pair_to_xml(aas_reference_pair, NS_AAS+"valueReferencePair")) - et_value_list.append(et_value_reference_pairs) - return et_value_list - - -# ############################################################## -# transformation functions to serialize classes from model.aas -# ############################################################## - - -def specific_asset_id_to_xml(obj: model.SpecificAssetId, tag: str = NS_AAS + "specifidAssetId") \ - -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.SpecificAssetId` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.SpecificAssetId` - :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:identifierKeyValuePair`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_asset_information = abstract_classes_to_xml(tag, obj) - et_asset_information.append(_generate_element(name=NS_AAS + "name", text=obj.name)) - et_asset_information.append(_generate_element(name=NS_AAS + "value", text=obj.value)) - if obj.external_subject_id: - et_asset_information.append(reference_to_xml(obj.external_subject_id, NS_AAS + "externalSubjectId")) - - return et_asset_information - - -def asset_information_to_xml(obj: model.AssetInformation, tag: str = NS_AAS+"assetInformation") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.aas.AssetInformation` to XML - - :param obj: Object of class :class:`~basyx.aas.model.aas.AssetInformation` - :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:assetInformation`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_asset_information = abstract_classes_to_xml(tag, obj) - et_asset_information.append(_generate_element(name=NS_AAS + "assetKind", text=_generic.ASSET_KIND[obj.asset_kind])) - if obj.global_asset_id: - et_asset_information.append(_generate_element(name=NS_AAS + "globalAssetId", text=obj.global_asset_id)) - if obj.specific_asset_id: - et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") - for specific_asset_id in obj.specific_asset_id: - et_specific_asset_id.append(specific_asset_id_to_xml(specific_asset_id, NS_AAS + "specificAssetId")) - et_asset_information.append(et_specific_asset_id) - if obj.asset_type: - et_asset_information.append(_generate_element(name=NS_AAS + "assetType", text=obj.asset_type)) - if obj.default_thumbnail: - et_asset_information.append(resource_to_xml(obj.default_thumbnail, NS_AAS+"defaultThumbnail")) - - return et_asset_information - - -def concept_description_to_xml(obj: model.ConceptDescription, - tag: str = NS_AAS+"conceptDescription") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.concept.ConceptDescription` to XML - - :param obj: Object of class :class:`~basyx.aas.model.concept.ConceptDescription` - :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:conceptDescription`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_concept_description = abstract_classes_to_xml(tag, obj) - if obj.is_case_of: - et_is_case_of = _generate_element(NS_AAS+"isCaseOf") - for reference in obj.is_case_of: - et_is_case_of.append(reference_to_xml(reference, NS_AAS+"reference")) - et_concept_description.append(et_is_case_of) - return et_concept_description - - -def embedded_data_specification_to_xml(obj: model.EmbeddedDataSpecification, - tag: str = NS_AAS+"embeddedDataSpecification") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.EmbeddedDataSpecification` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.EmbeddedDataSpecification` - :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:embeddedDataSpecification`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_embedded_data_specification = abstract_classes_to_xml(tag, obj) - et_embedded_data_specification.append(reference_to_xml(obj.data_specification, tag=NS_AAS + "dataSpecification")) - et_embedded_data_specification.append(data_specification_content_to_xml(obj.data_specification_content)) - return et_embedded_data_specification - - -def data_specification_content_to_xml(obj: model.DataSpecificationContent, - tag: str = NS_AAS+"dataSpecificationContent") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.DataSpecificationContent` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.DataSpecificationContent` - :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:dataSpecificationContent`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_data_specification_content = abstract_classes_to_xml(tag, obj) - if isinstance(obj, model.DataSpecificationIEC61360): - et_data_specification_content.append(data_specification_iec61360_to_xml(obj)) - else: - raise TypeError(f"Serialization of {obj.__class__} to XML is not supported!") - return et_data_specification_content - - -def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360, - tag: str = NS_AAS+"dataSpecificationIec61360") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.DataSpecificationIEC61360` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.DataSpecificationIEC61360` - :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:dataSpecificationIec61360`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_data_specification_iec61360 = abstract_classes_to_xml(tag, obj) - et_data_specification_iec61360.append(lang_string_set_to_xml(obj.preferred_name, NS_AAS + "preferredName")) - if obj.short_name is not None: - et_data_specification_iec61360.append(lang_string_set_to_xml(obj.short_name, NS_AAS + "shortName")) - if obj.unit is not None: - et_data_specification_iec61360.append(_generate_element(NS_AAS + "unit", text=obj.unit)) - if obj.unit_id is not None: - et_data_specification_iec61360.append(reference_to_xml(obj.unit_id, NS_AAS + "unitId")) - if obj.source_of_definition is not None: - et_data_specification_iec61360.append(_generate_element(NS_AAS + "sourceOfDefinition", - text=obj.source_of_definition)) - if obj.symbol is not None: - et_data_specification_iec61360.append(_generate_element(NS_AAS + "symbol", text=obj.symbol)) - if obj.data_type is not None: - et_data_specification_iec61360.append(_generate_element(NS_AAS + "dataType", - text=_generic.IEC61360_DATA_TYPES[obj.data_type])) - if obj.definition is not None: - et_data_specification_iec61360.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition")) - - if obj.value_format is not None: - et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat", text=obj.value_format)) - # this can be either None or an empty set, both of which are equivalent to the bool false - # thus we don't check 'is not None' for this property - if obj.value_list: - et_data_specification_iec61360.append(value_list_to_xml(obj.value_list)) - if obj.value is not None: - et_data_specification_iec61360.append(_generate_element(NS_AAS + "value", text=obj.value)) - if obj.level_types: - et_level_types = _generate_element(NS_AAS + "levelType") - for k, v in _generic.IEC61360_LEVEL_TYPES.items(): - et_level_types.append(_generate_element(NS_AAS + v, text=boolean_to_xml(k in obj.level_types))) - et_data_specification_iec61360.append(et_level_types) - return et_data_specification_iec61360 - - -def asset_administration_shell_to_xml(obj: model.AssetAdministrationShell, - tag: str = NS_AAS+"assetAdministrationShell") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.aas.AssetAdministrationShell` to XML - - :param obj: Object of class :class:`~basyx.aas.model.aas.AssetAdministrationShell` - :param tag: Namespace+Tag of the ElementTree object. Default is ``aas:assetAdministrationShell`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_aas = abstract_classes_to_xml(tag, obj) - if obj.derived_from: - et_aas.append(reference_to_xml(obj.derived_from, tag=NS_AAS+"derivedFrom")) - et_aas.append(asset_information_to_xml(obj.asset_information, tag=NS_AAS + "assetInformation")) - if obj.submodel: - et_submodels = _generate_element(NS_AAS + "submodels") - for reference in obj.submodel: - et_submodels.append(reference_to_xml(reference, tag=NS_AAS+"reference")) - et_aas.append(et_submodels) - return et_aas - - -# ############################################################## -# transformation functions to serialize classes from model.submodel -# ############################################################## - - -def submodel_element_to_xml(obj: model.SubmodelElement) -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElement` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` - :return: Serialized :class:`~lxml.etree._Element` object - """ - if isinstance(obj, model.DataElement): - return data_element_to_xml(obj) - if isinstance(obj, model.BasicEventElement): - return basic_event_element_to_xml(obj) - if isinstance(obj, model.Capability): - return capability_to_xml(obj) - if isinstance(obj, model.Entity): - return entity_to_xml(obj) - if isinstance(obj, model.Operation): - return operation_to_xml(obj) - if isinstance(obj, model.AnnotatedRelationshipElement): - return annotated_relationship_element_to_xml(obj) - if isinstance(obj, model.RelationshipElement): - return relationship_element_to_xml(obj) - if isinstance(obj, model.SubmodelElementCollection): - return submodel_element_collection_to_xml(obj) - if isinstance(obj, model.SubmodelElementList): - return submodel_element_list_to_xml(obj) - raise AssertionError(f"Type {obj.__class__.__name__} is not yet supported by the XML serialization!") - - -def submodel_to_xml(obj: model.Submodel, - tag: str = NS_AAS+"submodel") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.Submodel` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.Submodel` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodel`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_submodel = abstract_classes_to_xml(tag, obj) - if obj.submodel_element: - et_submodel_elements = _generate_element(NS_AAS + "submodelElements") - for submodel_element in obj.submodel_element: - et_submodel_elements.append(submodel_element_to_xml(submodel_element)) - et_submodel.append(et_submodel_elements) - return et_submodel - - -def property_to_xml(obj: model.Property, - tag: str = NS_AAS+"property") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.Property` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.Property` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:property`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_property = abstract_classes_to_xml(tag, obj) - et_property.append(_generate_element(NS_AAS + "valueType", text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) - if obj.value is not None: - et_property.append(_value_to_xml(obj.value, obj.value_type)) - if obj.value_id: - et_property.append(reference_to_xml(obj.value_id, NS_AAS + "valueId")) - return et_property - - -def multi_language_property_to_xml(obj: model.MultiLanguageProperty, - tag: str = NS_AAS+"multiLanguageProperty") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.MultiLanguageProperty` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.MultiLanguageProperty` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:multiLanguageProperty`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_multi_language_property = abstract_classes_to_xml(tag, obj) - if obj.value: - et_multi_language_property.append(lang_string_set_to_xml(obj.value, tag=NS_AAS + "value")) - if obj.value_id: - et_multi_language_property.append(reference_to_xml(obj.value_id, NS_AAS+"valueId")) - return et_multi_language_property - - -def range_to_xml(obj: model.Range, - tag: str = NS_AAS+"range") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.Range` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.Range` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:range`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_range = abstract_classes_to_xml(tag, obj) - et_range.append(_generate_element(name=NS_AAS + "valueType", - text=model.datatypes.XSD_TYPE_NAMES[obj.value_type])) - if obj.min is not None: - et_range.append(_value_to_xml(obj.min, obj.value_type, tag=NS_AAS + "min")) - if obj.max is not None: - et_range.append(_value_to_xml(obj.max, obj.value_type, tag=NS_AAS + "max")) - return et_range - - -def blob_to_xml(obj: model.Blob, - tag: str = NS_AAS+"blob") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.Blob` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.Blob` - :param tag: Namespace+Tag of the serialized element. Default is ``aas:blob`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_blob = abstract_classes_to_xml(tag, obj) - et_value = etree.Element(NS_AAS + "value") - if obj.value is not None: - et_value.text = base64.b64encode(obj.value).decode() - et_blob.append(et_value) - et_blob.append(_generate_element(NS_AAS + "contentType", text=obj.content_type)) - return et_blob - - -def file_to_xml(obj: model.File, - tag: str = NS_AAS+"file") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.File` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.File` - :param tag: Namespace+Tag of the serialized element. Default is ``aas:file`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_file = abstract_classes_to_xml(tag, obj) - if obj.value: - et_file.append(_generate_element(NS_AAS + "value", text=obj.value)) - et_file.append(_generate_element(NS_AAS + "contentType", text=obj.content_type)) - return et_file - - -def resource_to_xml(obj: model.Resource, - tag: str = NS_AAS+"resource") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.base.Resource` to XML - - :param obj: Object of class :class:`~basyx.aas.model.base.Resource` - :param tag: Namespace+Tag of the serialized element. Default is ``aas:resource`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_resource = abstract_classes_to_xml(tag, obj) - et_resource.append(_generate_element(NS_AAS + "path", text=obj.path)) - if obj.content_type: - et_resource.append(_generate_element(NS_AAS + "contentType", text=obj.content_type)) - return et_resource - - -def reference_element_to_xml(obj: model.ReferenceElement, - tag: str = NS_AAS+"referenceElement") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.ReferenceElement` to XMl - - :param obj: Object of class :class:`~basyx.aas.model.submodel.ReferenceElement` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:referenceElement`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_reference_element = abstract_classes_to_xml(tag, obj) - if obj.value: - et_reference_element.append(reference_to_xml(obj.value, NS_AAS+"value")) - return et_reference_element - - -def submodel_element_collection_to_xml(obj: model.SubmodelElementCollection, - tag: str = NS_AAS+"submodelElementCollection") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElementCollection` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElementCollection` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodelElementCollection`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_submodel_element_collection = abstract_classes_to_xml(tag, obj) - if obj.value: - et_value = _generate_element(NS_AAS + "value") - for submodel_element in obj.value: - et_value.append(submodel_element_to_xml(submodel_element)) - et_submodel_element_collection.append(et_value) - return et_submodel_element_collection - - -def submodel_element_list_to_xml(obj: model.SubmodelElementList, - tag: str = NS_AAS+"submodelElementList") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.SubmodelElementList` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElementList` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:submodelElementList`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_submodel_element_list = abstract_classes_to_xml(tag, obj) - et_submodel_element_list.append(_generate_element(NS_AAS + "orderRelevant", boolean_to_xml(obj.order_relevant))) - if obj.semantic_id_list_element is not None: - et_submodel_element_list.append(reference_to_xml(obj.semantic_id_list_element, - NS_AAS + "semanticIdListElement")) - et_submodel_element_list.append(_generate_element(NS_AAS + "typeValueListElement", _generic.KEY_TYPES[ - model.KEY_TYPES_CLASSES[obj.type_value_list_element]])) - if obj.value_type_list_element is not None: - et_submodel_element_list.append(_generate_element(NS_AAS + "valueTypeListElement", - model.datatypes.XSD_TYPE_NAMES[obj.value_type_list_element])) - if len(obj.value) > 0: - et_value = _generate_element(NS_AAS + "value") - for se in obj.value: - et_value.append(submodel_element_to_xml(se)) - et_submodel_element_list.append(et_value) - return et_submodel_element_list - - -def relationship_element_to_xml(obj: model.RelationshipElement, - tag: str = NS_AAS+"relationshipElement") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.RelationshipElement` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.RelationshipElement` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:relationshipElement`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_relationship_element = abstract_classes_to_xml(tag, obj) - et_relationship_element.append(reference_to_xml(obj.first, NS_AAS+"first")) - et_relationship_element.append(reference_to_xml(obj.second, NS_AAS+"second")) - return et_relationship_element - - -def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElement, - tag: str = NS_AAS+"annotatedRelationshipElement") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement` - :param tag: Namespace+Tag of the serialized element (optional): Default is ``aas:annotatedRelationshipElement`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_annotated_relationship_element = relationship_element_to_xml(obj, tag) - if obj.annotation: - et_annotations = _generate_element(name=NS_AAS + "annotations") - for data_element in obj.annotation: - et_annotations.append(data_element_to_xml(data_element)) - et_annotated_relationship_element.append(et_annotations) - return et_annotated_relationship_element - - -def operation_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"operationVariable") -> etree._Element: - """ - Serialization of :class:`~basyx.aas.model.submodel.SubmodelElement` to the XML OperationVariable representation - Since we don't implement the ``OperationVariable`` class, which is just a wrapper for a single - :class:`~basyx.aas.model.submodel.SubmodelElement`, elements are serialized as the ``aas:value`` child of an - ``aas:operationVariable`` element. - - :param obj: Object of class :class:`~basyx.aas.model.submodel.SubmodelElement` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:operationVariable`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_operation_variable = _generate_element(tag) - et_value = _generate_element(NS_AAS+"value") - et_value.append(submodel_element_to_xml(obj)) - et_operation_variable.append(et_value) - return et_operation_variable - - -def operation_to_xml(obj: model.Operation, - tag: str = NS_AAS+"operation") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.Operation` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.Operation` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:operation`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_operation = abstract_classes_to_xml(tag, obj) - for tag, nss in ((NS_AAS+"inputVariables", obj.input_variable), - (NS_AAS+"outputVariables", obj.output_variable), - (NS_AAS+"inoutputVariables", obj.in_output_variable)): - if nss: - et_variables = _generate_element(tag) - for submodel_element in nss: - et_variables.append(operation_variable_to_xml(submodel_element)) - et_operation.append(et_variables) - return et_operation - - -def capability_to_xml(obj: model.Capability, - tag: str = NS_AAS+"capability") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.Capability` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.Capability` - :param tag: Namespace+Tag of the serialized element, default is ``aas:capability`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - return abstract_classes_to_xml(tag, obj) - - -def entity_to_xml(obj: model.Entity, - tag: str = NS_AAS+"entity") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.Entity` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.Entity` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:entity`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_entity = abstract_classes_to_xml(tag, obj) - if obj.statement: - et_statements = _generate_element(NS_AAS + "statements") - for statement in obj.statement: - et_statements.append(submodel_element_to_xml(statement)) - et_entity.append(et_statements) - et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type])) - if obj.global_asset_id: - et_entity.append(_generate_element(NS_AAS + "globalAssetId", text=obj.global_asset_id)) - if obj.specific_asset_id: - et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") - for specific_asset_id in obj.specific_asset_id: - et_specific_asset_id.append(specific_asset_id_to_xml(specific_asset_id, NS_AAS + "specificAssetId")) - et_entity.append(et_specific_asset_id) - return et_entity - - -def basic_event_element_to_xml(obj: model.BasicEventElement, tag: str = NS_AAS+"basicEventElement") -> etree._Element: - """ - Serialization of objects of class :class:`~basyx.aas.model.submodel.BasicEventElement` to XML - - :param obj: Object of class :class:`~basyx.aas.model.submodel.BasicEventElement` - :param tag: Namespace+Tag of the serialized element (optional). Default is ``aas:basicEventElement`` - :return: Serialized :class:`~lxml.etree._Element` object - """ - et_basic_event_element = abstract_classes_to_xml(tag, obj) - et_basic_event_element.append(reference_to_xml(obj.observed, NS_AAS+"observed")) - et_basic_event_element.append(_generate_element(NS_AAS +"direction", text=_generic.DIRECTION[obj.direction])) - et_basic_event_element.append(_generate_element(NS_AAS +"state", text=_generic.STATE_OF_EVENT[obj.state])) - if obj.message_topic is not None: - et_basic_event_element.append(_generate_element(NS_AAS+"messageTopic", text=obj.message_topic)) - if obj.message_broker is not None: - et_basic_event_element.append(reference_to_xml(obj.message_broker, NS_AAS+"messageBroker")) - if obj.last_update is not None: - et_basic_event_element.append(_generate_element(NS_AAS+"lastUpdate", - text=model.datatypes.xsd_repr(obj.last_update))) - if obj.min_interval is not None: - et_basic_event_element.append(_generate_element(NS_AAS+"minInterval", - text=model.datatypes.xsd_repr(obj.min_interval))) - if obj.max_interval is not None: - et_basic_event_element.append(_generate_element(NS_AAS+"maxInterval", - text=model.datatypes.xsd_repr(obj.max_interval))) - return et_basic_event_element - - -# ############################################################## -# general functions -# ############################################################## - def _write_element(file: _generic.PathOrBinaryIO, element: etree._Element, **kwargs) -> None: etree.ElementTree(element).write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) -def object_to_xml_element(obj: object) -> etree._Element: - """ - Serialize a single object to an :class:`~lxml.etree._Element`. - - :param obj: The object to serialize - """ - serialization_func: Callable[..., etree._Element] - - if isinstance(obj, model.Key): - serialization_func = key_to_xml - elif isinstance(obj, model.Reference): - serialization_func = reference_to_xml - elif isinstance(obj, model.Reference): - serialization_func = reference_to_xml - elif isinstance(obj, model.AdministrativeInformation): - serialization_func = administrative_information_to_xml - elif isinstance(obj, model.Qualifier): - serialization_func = qualifier_to_xml - elif isinstance(obj, model.AnnotatedRelationshipElement): - serialization_func = annotated_relationship_element_to_xml - elif isinstance(obj, model.BasicEventElement): - serialization_func = basic_event_element_to_xml - elif isinstance(obj, model.Blob): - serialization_func = blob_to_xml - elif isinstance(obj, model.Capability): - serialization_func = capability_to_xml - elif isinstance(obj, model.Entity): - serialization_func = entity_to_xml - elif isinstance(obj, model.Extension): - serialization_func = extension_to_xml - elif isinstance(obj, model.File): - serialization_func = file_to_xml - elif isinstance(obj, model.Resource): - serialization_func = resource_to_xml - elif isinstance(obj, model.MultiLanguageProperty): - serialization_func = multi_language_property_to_xml - elif isinstance(obj, model.Operation): - serialization_func = operation_to_xml - elif isinstance(obj, model.Property): - serialization_func = property_to_xml - elif isinstance(obj, model.Range): - serialization_func = range_to_xml - elif isinstance(obj, model.ReferenceElement): - serialization_func = reference_element_to_xml - elif isinstance(obj, model.RelationshipElement): - serialization_func = relationship_element_to_xml - elif isinstance(obj, model.SubmodelElementCollection): - serialization_func = submodel_element_collection_to_xml - elif isinstance(obj, model.SubmodelElementList): - serialization_func = submodel_element_list_to_xml - elif isinstance(obj, model.AssetAdministrationShell): - serialization_func = asset_administration_shell_to_xml - elif isinstance(obj, model.AssetInformation): - serialization_func = asset_information_to_xml - elif isinstance(obj, model.SpecificAssetId): - serialization_func = specific_asset_id_to_xml - elif isinstance(obj, model.Submodel): - serialization_func = submodel_to_xml - elif isinstance(obj, model.ValueReferencePair): - serialization_func = value_reference_pair_to_xml - elif isinstance(obj, model.ConceptDescription): - serialization_func = concept_description_to_xml - elif isinstance(obj, model.LangStringSet): - serialization_func = lang_string_set_to_xml - elif isinstance(obj, model.EmbeddedDataSpecification): - serialization_func = embedded_data_specification_to_xml - elif isinstance(obj, model.DataSpecificationIEC61360): - serialization_func = data_specification_iec61360_to_xml - # generic serialization using the functions for abstract classes - elif isinstance(obj, model.DataElement): - serialization_func = data_element_to_xml - elif isinstance(obj, model.SubmodelElement): - serialization_func = submodel_to_xml - elif isinstance(obj, model.DataSpecificationContent): - serialization_func = data_specification_content_to_xml - # type aliases - elif isinstance(obj, model.ValueList): - serialization_func = value_list_to_xml - else: - raise ValueError(f"{obj!r} cannot be serialized!") - - return serialization_func(obj) - - -def write_aas_xml_element(file: _generic.PathOrBinaryIO, obj: object, **kwargs) -> None: - """ - Serialize a single object to XML. Namespace declarations are added to the object itself, as there is no surrounding - environment element. - - :param file: A filename or file-like object to write the XML-serialized data to - :param obj: The object to serialize - :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree._ElementTree.write` - """ - return _write_element(file, object_to_xml_element(obj), **kwargs) - - -def object_store_to_xml_element(data: model.AbstractObjectStore) -> etree._Element: +def object_store_to_xml_element(data: ObjectStore) -> etree._Element: """ Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree._Element`. This function is used internally by :meth:`write_aas_xml_file` and shouldn't be called directly for most use-cases. - :param data: :class:`ObjectStore ` which contains different objects of + :param data: :class:`ObjectStore ` which contains different objects of the AAS meta model which should be serialized to an XML file """ # separate different kind of objects @@ -991,31 +40,31 @@ def object_store_to_xml_element(data: model.AbstractObjectStore) -> etree._Eleme if asset_administration_shells: et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") for aas_obj in asset_administration_shells: - et_asset_administration_shells.append(asset_administration_shell_to_xml(aas_obj)) + et_asset_administration_shells.append( + etree.fromstring(aas_xmlization.to_str(aas_obj))) root.append(et_asset_administration_shells) if submodels: et_submodels = etree.Element(NS_AAS + "submodels") for sub_obj in submodels: - et_submodels.append(submodel_to_xml(sub_obj)) + et_submodels.append(etree.fromstring(aas_xmlization.to_str(sub_obj))) root.append(et_submodels) if concept_descriptions: et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") for con_obj in concept_descriptions: - et_concept_descriptions.append(concept_description_to_xml(con_obj)) + et_concept_descriptions.append(etree.fromstring(aas_xmlization.to_str(con_obj))) root.append(et_concept_descriptions) - return root def write_aas_xml_file(file: _generic.PathOrBinaryIO, - data: model.AbstractObjectStore, + data: ObjectStore, **kwargs) -> None: """ Write a set of AAS objects to an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 :param file: A filename or file-like object to write the XML-serialized data to - :param data: :class:`ObjectStore ` which contains different objects of + :param data: :class:`ObjectStore ` which contains different objects of the AAS meta model which should be serialized to an XML file :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree._ElementTree.write` """ From a882d8187bac2e25f53133a5af9788175ab84ea7 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 5 Dec 2019 10:38:36 +0100 Subject: [PATCH 275/474] model.adapter: first version of json serialization --- test/adapter/__init__.py | 0 test/adapter/test_json.py | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 test/adapter/__init__.py create mode 100644 test/adapter/test_json.py diff --git a/test/adapter/__init__.py b/test/adapter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/adapter/test_json.py b/test/adapter/test_json.py new file mode 100644 index 0000000..c76e6a8 --- /dev/null +++ b/test/adapter/test_json.py @@ -0,0 +1,13 @@ +import unittest +import json + +from aas import model +from aas.adapter import json_adapter + + +class JsonSerializationTest(unittest.TestCase): + def test_serialize_Object(self): + test_object = model.Property("test_id_short", "string", category="PARAMETER", + description={"en-us": "Germany", "de": "Deutschland"}) + json_data = json.dumps(test_object, cls=json_adapter.AASToJsonEncoder) + print(json_data) From 25ddaa4da7d0a83269a0a47b8c32fa068f6fa3d9 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 5 Dec 2019 17:22:39 +0100 Subject: [PATCH 276/474] adapter: restructuring --- test/adapter/json/__init__.py | 0 test/adapter/{ => json}/test_json.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 test/adapter/json/__init__.py rename test/adapter/{ => json}/test_json.py (71%) diff --git a/test/adapter/json/__init__.py b/test/adapter/json/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/adapter/test_json.py b/test/adapter/json/test_json.py similarity index 71% rename from test/adapter/test_json.py rename to test/adapter/json/test_json.py index c76e6a8..2001640 100644 --- a/test/adapter/test_json.py +++ b/test/adapter/json/test_json.py @@ -2,12 +2,12 @@ import json from aas import model -from aas.adapter import json_adapter +from aas.adapter.json import json_serialization class JsonSerializationTest(unittest.TestCase): def test_serialize_Object(self): test_object = model.Property("test_id_short", "string", category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) - json_data = json.dumps(test_object, cls=json_adapter.AASToJsonEncoder) + json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) print(json_data) From f9c4731d849811c8760f5b4d54e525f56722d504 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Fri, 6 Dec 2019 08:56:15 +0100 Subject: [PATCH 277/474] test.adapter.json: first test for validation --- test/adapter/json/test_json.py | 13 ------ test/adapter/json/test_json_serialization.py | 45 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 13 deletions(-) delete mode 100644 test/adapter/json/test_json.py create mode 100644 test/adapter/json/test_json_serialization.py diff --git a/test/adapter/json/test_json.py b/test/adapter/json/test_json.py deleted file mode 100644 index 2001640..0000000 --- a/test/adapter/json/test_json.py +++ /dev/null @@ -1,13 +0,0 @@ -import unittest -import json - -from aas import model -from aas.adapter.json import json_serialization - - -class JsonSerializationTest(unittest.TestCase): - def test_serialize_Object(self): - test_object = model.Property("test_id_short", "string", category="PARAMETER", - description={"en-us": "Germany", "de": "Deutschland"}) - json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) - print(json_data) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py new file mode 100644 index 0000000..85984a2 --- /dev/null +++ b/test/adapter/json/test_json_serialization.py @@ -0,0 +1,45 @@ +import unittest +import json +import os + +from aas import model +from aas.adapter.json import json_serialization +from jsonschema import validate # type: ignore + + +class JsonSerializationTest(unittest.TestCase): + + def test_serialize_Object(self): + test_object = model.Property("test_id_short", "string", category="PARAMETER", + description={"en-us": "Germany", "de": "Deutschland"}) + json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) + print(json_data) + + def test_validate_serialization(self): + asset_key = [model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM)] + asset_reference = model.Reference(asset_key, model.Asset) + aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) + submodel_key = [model.Key(model.KeyElements.SUBMODEL, True, "submodel", model.KeyType.CUSTOM)] + submodel_reference = model.Reference(submodel_key, model.Submodel) + # submodel_identifier = model.Identifier("SM1", model.IdentifierType.CUSTOM) + # submodel = model.Submodel(submodel_identifier) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_=[submodel_reference]) + + # serialize object to json + test_aas_json_data = json.dumps(test_aas, cls=json_serialization.AASToJsonEncoder) + test_submodel_data = "" + test_asset_data = "" + test_concept_description_data = "" + json_data_new = '{"assetAdministrationShells": [' + test_aas_json_data + '], ' \ + '"submodels": [' + test_submodel_data + '], ' \ + '"assets": [' + test_asset_data + '], ' \ + '"conceptDescriptions": [' + test_concept_description_data + ']}' + json_data_new2 = json.loads(json_data_new) + + # load schema + with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + schema_data = json_file.read() + aas_schema = json.loads(schema_data) + + # validate serialization against schema + validate(instance=json_data_new2, schema=aas_schema) From 651914ddef85a23f396244321a041babfafd7b0e Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 6 Dec 2019 09:25:03 +0100 Subject: [PATCH 278/474] adapter.json: Clean up test --- test/adapter/json/test_json_serialization.py | 39 ++++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 85984a2..746b561 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -9,37 +9,36 @@ class JsonSerializationTest(unittest.TestCase): - def test_serialize_Object(self): + def test_serialize_Object(self) -> None: test_object = model.Property("test_id_short", "string", category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) - print(json_data) - def test_validate_serialization(self): + def test_validate_serialization(self) -> None: asset_key = [model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM)] asset_reference = model.Reference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = [model.Key(model.KeyElements.SUBMODEL, True, "submodel", model.KeyType.CUSTOM)] - submodel_reference = model.Reference(submodel_key, model.Submodel) - # submodel_identifier = model.Identifier("SM1", model.IdentifierType.CUSTOM) - # submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_=[submodel_reference]) + submodel_key = model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM) + submodel_identifier = submodel_key.get_identifier() + assert(submodel_identifier is not None) + submodel_reference = model.Reference([submodel_key], model.Submodel) + # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which + # must be a Reference. (This seems to be a bug in the JSONSchema.) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference([], model.Referable)) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) # serialize object to json - test_aas_json_data = json.dumps(test_aas, cls=json_serialization.AASToJsonEncoder) - test_submodel_data = "" - test_asset_data = "" - test_concept_description_data = "" - json_data_new = '{"assetAdministrationShells": [' + test_aas_json_data + '], ' \ - '"submodels": [' + test_submodel_data + '], ' \ - '"assets": [' + test_asset_data + '], ' \ - '"conceptDescriptions": [' + test_concept_description_data + ']}' - json_data_new2 = json.loads(json_data_new) + json_data = json.dumps({ + 'assetAdministrationShells': [test_aas], + 'submodels': [submodel], + 'assets': [], + 'conceptDescriptions': [], + }, cls=json_serialization.AASToJsonEncoder) + json_data_new = json.loads(json_data) # load schema with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: - schema_data = json_file.read() - aas_schema = json.loads(schema_data) + aas_schema = json.load(json_file) # validate serialization against schema - validate(instance=json_data_new2, schema=aas_schema) + validate(instance=json_data_new, schema=aas_schema) From be6a98e3a8cb40bf3d784b13ae514abbf062b33c Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 10 Dec 2019 15:32:02 +0100 Subject: [PATCH 279/474] adapter.json: Add first version of json deserialization functionality --- test/adapter/json/test_json_serialization.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 746b561..05417b8 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -3,7 +3,7 @@ import os from aas import model -from aas.adapter.json import json_serialization +from aas.adapter.json import json_serialization, json_deserialization from jsonschema import validate # type: ignore @@ -36,6 +36,10 @@ def test_validate_serialization(self) -> None: }, cls=json_serialization.AASToJsonEncoder) json_data_new = json.loads(json_data) + # try deserializing the json string with help of the json_deserialization module + # TODO move to own test + json_data_new2 = json.loads(json_data, cls=json_deserialization.AASFromJsonDecoder) + # load schema with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: aas_schema = json.load(json_file) From c9954de913c4f052c140d91b4ebdafb13b27bdfe Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 10 Dec 2019 18:20:08 +0100 Subject: [PATCH 280/474] adapter.json: Add read_json_aas_file() --- test/adapter/json/test_json_serialization.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 05417b8..a1cb7e4 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,3 +1,4 @@ +import io import unittest import json import os @@ -36,9 +37,10 @@ def test_validate_serialization(self) -> None: }, cls=json_serialization.AASToJsonEncoder) json_data_new = json.loads(json_data) - # try deserializing the json string with help of the json_deserialization module + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module # TODO move to own test - json_data_new2 = json.loads(json_data, cls=json_deserialization.AASFromJsonDecoder) + json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) # load schema with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: From 6af2976cc08ae503b2469fc047ef4234f52e19f1 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Fri, 13 Dec 2019 17:52:15 +0100 Subject: [PATCH 281/474] aas.examples: add example for a nearly complete asset administration shell with all its elements --- test/adapter/json/test_json_serialization.py | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index a1cb7e4..2d3b2ff 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -7,6 +7,8 @@ from aas.adapter.json import json_serialization, json_deserialization from jsonschema import validate # type: ignore +from aas.examples import example_create_aas + class JsonSerializationTest(unittest.TestCase): @@ -48,3 +50,27 @@ def test_validate_serialization(self) -> None: # validate serialization against schema validate(instance=json_data_new, schema=aas_schema) + + def test_full_example_serialization(self) -> None: + asset = example_create_aas.create_example_asset() + concept_description = example_create_aas.create_example_concept_description() + concept_dictionary = example_create_aas.create_example_concept_dictionary() + submodel = example_create_aas.create_example_submodel() + asset_administration_shell = example_create_aas.create_example_asset_administration_shell( + concept_dictionary) + + # serialize object to json + json_data = json.dumps({ + 'assetAdministrationShells': [asset_administration_shell], + 'submodels': [submodel], + 'assets': [asset], + 'conceptDescriptions': [concept_description], + }, cls=json_serialization.AASToJsonEncoder) + json_data_new = json.loads(json_data) + + # load schema + with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + aas_schema = json.load(json_file) + + # validate serialization against schema + validate(instance=json_data_new, schema=aas_schema) From 6a667565c796bd111ab4966de053511b6e8dcdc8 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 16 Dec 2019 10:16:42 +0100 Subject: [PATCH 282/474] adapter.json: Fix usage of Reference and new AASReference --- test/adapter/json/test_json_serialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 2d3b2ff..8de8b24 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -19,15 +19,15 @@ def test_serialize_Object(self) -> None: def test_validate_serialization(self) -> None: asset_key = [model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM)] - asset_reference = model.Reference(asset_key, model.Asset) + asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM) submodel_identifier = submodel_key.get_identifier() assert(submodel_identifier is not None) - submodel_reference = model.Reference([submodel_key], model.Submodel) + submodel_reference = model.AASReference([submodel_key], model.Submodel) # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference([], model.Referable)) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference([])) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) # serialize object to json From 3fa0e4393530c319d90c85c29fffc2a0590e5a75 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 16 Dec 2019 10:24:56 +0100 Subject: [PATCH 283/474] Add license headers to new files --- test/adapter/json/test_json_serialization.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 8de8b24..cf9a2d6 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,3 +1,14 @@ +# Copyright 2019 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + import io import unittest import json From d1b9448d9f1966eacea24c7a3a1ca05bc3692cbf Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 16 Dec 2019 10:53:18 +0100 Subject: [PATCH 284/474] test: Add json deserialization to the full example test --- test/adapter/json/test_json_serialization.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index cf9a2d6..cd44f6c 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -79,6 +79,11 @@ def test_full_example_serialization(self) -> None: }, cls=json_serialization.AASToJsonEncoder) json_data_new = json.loads(json_data) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + # TODO move to own test + json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) + # load schema with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: aas_schema = json.load(json_file) From 6bea50b5e9477469b4da3bf4197529963aa15316 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 16 Dec 2019 11:00:24 +0100 Subject: [PATCH 285/474] json.serialization: add write_aas_to_json_file function --- test/adapter/json/test_json_serialization.py | 32 +++++++------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index cd44f6c..eb23d3b 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -63,30 +63,20 @@ def test_validate_serialization(self) -> None: validate(instance=json_data_new, schema=aas_schema) def test_full_example_serialization(self) -> None: - asset = example_create_aas.create_example_asset() - concept_description = example_create_aas.create_example_concept_description() - concept_dictionary = example_create_aas.create_example_concept_dictionary() - submodel = example_create_aas.create_example_submodel() - asset_administration_shell = example_create_aas.create_example_asset_administration_shell( - concept_dictionary) + data = example_create_aas.create_full_example() + with open(os.path.join(os.path.dirname(__file__), 'test_full_example.json'), 'w') as json_file: + json_serialization.write_aas_to_json_file(file=json_file, data=data, append=False) - # serialize object to json - json_data = json.dumps({ - 'assetAdministrationShells': [asset_administration_shell], - 'submodels': [submodel], - 'assets': [asset], - 'conceptDescriptions': [concept_description], - }, cls=json_serialization.AASToJsonEncoder) - json_data_new = json.loads(json_data) + with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + aas_json_schema = json.load(json_file) + + with open(os.path.join(os.path.dirname(__file__), 'test_full_example.json'), 'r') as json_file: + json_data = json.load(json_file) + + # validate serialization against schema + validate(instance=json_data, schema=aas_json_schema) # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module # TODO move to own test json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) - - # load schema - with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: - aas_schema = json.load(json_file) - - # validate serialization against schema - validate(instance=json_data_new, schema=aas_schema) From c5cff06d975dc8d341c8cb91b84d36d11e2acbb8 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 16 Dec 2019 11:19:30 +0100 Subject: [PATCH 286/474] adapter.json.json_serialization: add modul docstring --- test/adapter/json/test_full_example.json | 1 + test/adapter/json/test_json_serialization.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 test/adapter/json/test_full_example.json diff --git a/test/adapter/json/test_full_example.json b/test/adapter/json/test_full_example.json new file mode 100644 index 0000000..68cf5f4 --- /dev/null +++ b/test/adapter/json/test_full_example.json @@ -0,0 +1 @@ +{"assetAdministrationShells": [[{"idShort": "TestAssetAdministrationShell", "description": [{"language": "en-us", "text": "An Example Asset Administration Shell for the test application"}, {"language": "de", "text": "Ein Beispiel-Verwaltungsschale f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "AssetAdministrationShell"}, "identification": {"id": "https://acplt.org/Test_AssetAdministrationShell", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "asset": {"keys": [{"type": "Asset", "idType": "IRDI", "value": "https://acplt.org/Test_Asset", "local": false}]}, "submodels": [{"keys": [{"type": "Submodel", "idType": "IRDI", "value": "https://acplt.org/Test_Submodel", "local": false}]}], "conceptDictionaries": [{"idShort": "TestConceptDictionary", "description": [{"language": "en-us", "text": "An example concept dictionary for the test application"}, {"language": "de", "text": "Ein Beispiel-ConceptDictionary f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "ConceptDictionary"}, "conceptDescriptions": [{"keys": [{"type": "ConceptDescription", "idType": "IRDI", "value": "https://acplt.org/Test_ConceptDescription", "local": false}]}]}]}]], "submodels": [[{"idShort": "Identification", "description": [{"language": "en-us", "text": "An example asset identification submodel for the test application"}, {"language": "de", "text": "Ein Beispiel-Identifikations-Submodel f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "Submodel"}, "identification": {"id": "http://acplt.org/Submodels/Assets/TestAsset/Identification", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "embeddedDataSpecification": [{"keys": [{"type": "Asset", "idType": "IRDI", "value": "http://acplt.org/DataSpecifications/Submodels/AssetIdentification", "local": false}]}], "semanticId": {"keys": [{"type": "Submodel", "idType": "IRDI", "value": "http://acplt.org/SubmodelTemplates/AssetIdentification", "local": false}]}, "submodelElements": [{"idShort": "ManufacturerName", "description": [{"language": "en-us", "text": "Legally valid designation of the natural or judicial person which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into circulation."}, {"language": "de", "text": "Bezeichnung f\u00fcr eine nat\u00fcrliche oder juristische Person, die f\u00fcr die Auslegung, Herstellung und Verpackung sowie die Etikettierung eines Produkts im Hinblick auf das 'Inverkehrbringen' im eigenen Namen verantwortlich ist"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "0173-1#02-AAO677#002", "local": false}]}, "value": "ACPLT", "valueType": "string"}, {"idShort": "InstanceId", "description": [{"language": "en-us", "text": "Legally valid designation of the natural or judicial person which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into circulation."}, {"language": "de", "text": "Bezeichnung f\u00fcr eine nat\u00fcrliche oder juristische Person, die f\u00fcr die Auslegung, Herstellung und Verpackung sowie die Etikettierung eines Produkts im Hinblick auf das 'Inverkehrbringen' im eigenen Namen verantwortlich ist"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRI", "value": "http://opcfoundation.org/UA/DI/1.1/DeviceType/Serialnumber", "local": false}]}, "value": "978-8234-234-342", "valueType": "string"}]}, {"idShort": "TestSubmodel", "description": [{"language": "en-us", "text": "An example submodel for the test application"}, {"language": "de", "text": "Ein Beispiel-Teilmodell f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "Submodel"}, "identification": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "embeddedDataSpecification": [{"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/DataSpecifications/AssetTypes/TestAsset", "local": false}]}], "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/SubmodelTemplates/ExampleSubmodel", "local": false}]}, "submodelElements": [{"idShort": "ExampleRelationshipElement", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example RelationshipElement object"}, {"language": "de", "text": "Beispiel RelationshipElement Element"}], "modelType": {"name": "RelationshipElement"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/RelationshipElements/ExampleRelationshipElement", "local": false}]}, "first": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}, "second": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}}, {"idShort": "ExampleAnnotatedRelationshipElement", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example AnnotatedRelationshipElement object"}, {"language": "de", "text": "Beispiel AnnotatedRelationshipElement Element"}], "modelType": {"name": "AnnotatedRelationshipElement"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/RelationshipElements/ExampleAnnotatedRelationshipElement", "local": false}]}, "first": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}, "second": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}}, {"idShort": "ExampleOperation", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Operation object"}, {"language": "de", "text": "Beispiel Operation Element"}], "modelType": {"name": "Operation"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Operations/ExampleOperation", "local": false}]}, "inputVariable": [{"idShort": "ExampleInputOperationVariable", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example ExampleInputOperationVariable object"}, {"language": "de", "text": "Beispiel ExampleInputOperationVariable Element"}], "modelType": {"name": "OperationVariable"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Operations/ExampleInputOperationVariable", "local": false}]}, "value": {"idShort": "ExampleProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example Property object"}, {"language": "de", "text": "Beispiel Property Element"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Properties/ExampleProperty", "local": false}]}, "value": "exampleValue", "valueType": "string"}}], "outputVariable": [{"idShort": "ExampleOutputOperationVariable", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example OutputOperationVariable object"}, {"language": "de", "text": "Beispiel OutputOperationVariable Element"}], "modelType": {"name": "OperationVariable"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Operations/ExampleOutputOperationVariable", "local": false}]}, "value": {"idShort": "ExampleProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example Property object"}, {"language": "de", "text": "Beispiel Property Element"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Properties/ExampleProperty", "local": false}]}, "value": "exampleValue", "valueType": "string"}}], "inoutputVariable": [{"idShort": "ExampleInOutputOperationVariable", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example InOutputOperationVariable object"}, {"language": "de", "text": "Beispiel InOutputOperationVariable Element"}], "modelType": {"name": "OperationVariable"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Operations/ExampleInOutputOperationVariable", "local": false}]}, "value": {"idShort": "ExampleProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example Property object"}, {"language": "de", "text": "Beispiel Property Element"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Properties/ExampleProperty", "local": false}]}, "value": "exampleValue", "valueType": "string"}}]}, {"idShort": "ExampleCapability", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Capability object"}, {"language": "de", "text": "Beispiel Capability Element"}], "modelType": {"name": "Capability"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Capabilities/ExampleCapability", "local": false}]}}, {"idShort": "ExampleBasicEvent", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example BasicEvent object"}, {"language": "de", "text": "Beispiel BasicEvent Element"}], "modelType": {"name": "BasicEvent"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Events/ExampleBasicEvent", "local": false}]}, "observed": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}}, {"idShort": "ExampleSubmodelCollectionOrdered", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example SubmodelElementCollectionOrdered object"}, {"language": "de", "text": "Beispiel SubmodelElementCollectionOrdered Element"}], "modelType": {"name": "SubmodelElementCollection"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/SubmodelElementCollections/ExampleSubmodelElementCollectionOrdered", "local": false}]}, "value": [{"idShort": "ExampleProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example Property object"}, {"language": "de", "text": "Beispiel Property Element"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Properties/ExampleProperty", "local": false}]}, "value": "exampleValue", "valueType": "string"}, {"idShort": "ExampleMultiLanguageProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example MultiLanguageProperty object"}, {"language": "de", "text": "Beispiel MulitLanguageProperty Element"}], "modelType": {"name": "MultiLanguageProperty"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/MultiLanguageProperties/ExampleMultiLanguageProperty", "local": false}]}, "value": [{"language": "en-us", "text": "Example value of a MultiLanguageProperty element"}, {"language": "de", "text": "Beispielswert f\u00fcr ein MulitLanguageProperty-Element"}]}, {"idShort": "ExampleRange", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Range object"}, {"language": "de", "text": "Beispiel Range Element"}], "modelType": {"name": "Range"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Ranges/ExampleRange", "local": false}]}, "valueType": "int", "min": "0", "max": "100"}], "odered": true}, {"idShort": "ExampleSubmodelCollectionUnordered", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example SubmodelElementCollectionUnordered object"}, {"language": "de", "text": "Beispiel SubmodelElementCollectionUnordered Element"}], "modelType": {"name": "SubmodelElementCollection"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/SubmodelElementCollections/ExampleSubmodelElementCollectionUnordered", "local": false}]}, "value": [{"idShort": "ExampleBlob", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Blob object"}, {"language": "de", "text": "Beispiel Blob Element"}], "modelType": {"name": "Blob"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Blobs/ExampleBlob", "local": false}]}, "mimeType": "application/pdf", "value": "AQIDBAU="}, {"idShort": "ExampleFile", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example File object"}, {"language": "de", "text": "Beispiel File Element"}], "modelType": {"name": "File"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Files/ExampleFile", "local": false}]}, "value": "/TestFile.pdf", "mimeType": "application/pdf"}, {"idShort": "ExampleReferenceElement", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Reference Element object"}, {"language": "de", "text": "Beispiel Reference Element Element"}], "modelType": {"name": "ReferenceElement"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/ReferenceElements/ExampleReferenceElement", "local": false}]}, "value": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}}], "odered": false}]}]], "assets": [[{"idShort": "Test_Asset", "description": [{"language": "en-us", "text": "An example asset for the test application"}, {"language": "de", "text": "Ein Beispiel-Asset f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "Asset"}, "identification": {"id": "https://acplt.org/Test_Asset", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "embeddedDataSpecification": [{"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/DataSpecifications/AssetTypes/TestAsset", "local": false}]}], "kind": "Instance", "assetIdentificationModel": {"keys": [{"type": "Submodel", "idType": "IRDI", "value": "http://acplt.org/Submodels/Assets/TestAsset/Identification", "local": false}]}}]], "conceptDescriptions": [[{"idShort": "TestConceptDescription", "description": [{"language": "en-us", "text": "An example concept description for the test application"}, {"language": "de", "text": "Ein Beispiel-ConceptDescription f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "ConceptDescription"}, "identification": {"id": "https://acplt.org/Test_ConceptDescription", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "embeddedDataSpecification": [{"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/DataSpecifications/ConceptDescriptions/TestConceptDescription", "local": false}]}]}]]} \ No newline at end of file diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index eb23d3b..71e5a6b 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -64,14 +64,14 @@ def test_validate_serialization(self) -> None: def test_full_example_serialization(self) -> None: data = example_create_aas.create_full_example() - with open(os.path.join(os.path.dirname(__file__), 'test_full_example.json'), 'w') as json_file: - json_serialization.write_aas_to_json_file(file=json_file, data=data, append=False) + file = io.StringIO() + json_serialization.write_aas_to_json_file(file=file, data=data) with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: aas_json_schema = json.load(json_file) - with open(os.path.join(os.path.dirname(__file__), 'test_full_example.json'), 'r') as json_file: - json_data = json.load(json_file) + file.seek(0) + json_data = json.load(file) # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) @@ -79,4 +79,5 @@ def test_full_example_serialization(self) -> None: # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module # TODO move to own test - json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) From fd59c7b0c80c6944acaeb5cc7bf143719be5b5ce Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 16 Dec 2019 14:27:31 +0100 Subject: [PATCH 287/474] example: add example for creation of a submodel template including test --- test/adapter/json/test_json_serialization.py | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 71e5a6b..68120a9 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -18,7 +18,7 @@ from aas.adapter.json import json_serialization, json_deserialization from jsonschema import validate # type: ignore -from aas.examples import example_create_aas +from aas.examples import example_create_aas, example_create_submodel_template class JsonSerializationTest(unittest.TestCase): @@ -81,3 +81,24 @@ def test_full_example_serialization(self) -> None: # TODO move to own test file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + def test_submodel_template_serialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_create_submodel_template.create_example_submodel_template()) + file = io.StringIO() + json_serialization.write_aas_to_json_file(file=file, data=data) + + with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + aas_json_schema = json.load(json_file) + + file.seek(0) + json_data = json.load(file) + + # validate serialization against schema + validate(instance=json_data, schema=aas_json_schema) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + # TODO move to own test + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) From 3eba7360766548ca304dcb83f493b401474eab9b Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 16 Dec 2019 15:47:39 +0100 Subject: [PATCH 288/474] json: add function to create an example asset administration shell with only mandatory attributes, make semanticId optional --- test/adapter/json/test_json_serialization.py | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 68120a9..723c809 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -18,7 +18,7 @@ from aas.adapter.json import json_serialization, json_deserialization from jsonschema import validate # type: ignore -from aas.examples import example_create_aas, example_create_submodel_template +from aas.examples import example_create_aas, example_create_submodel_template, example_create_aas_mandatory_attributes class JsonSerializationTest(unittest.TestCase): @@ -102,3 +102,23 @@ def test_submodel_template_serialization(self) -> None: # TODO move to own test file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + def test_full_empty_example_serialization(self) -> None: + data = example_create_aas_mandatory_attributes.create_full_example() + file = io.StringIO() + json_serialization.write_aas_to_json_file(file=file, data=data) + + with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + aas_json_schema = json.load(json_file) + + file.seek(0) + json_data = json.load(file) + + # validate serialization against schema + validate(instance=json_data, schema=aas_json_schema) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + # TODO move to own test + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) From 8ff994829220dbeb16e3c8c5e130363f68a83e39 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 16 Dec 2019 17:30:11 +0100 Subject: [PATCH 289/474] example: add example for testing missing object-attribute-combinations --- test/adapter/json/test_json_serialization.py | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 723c809..da211dc 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -18,7 +18,8 @@ from aas.adapter.json import json_serialization, json_deserialization from jsonschema import validate # type: ignore -from aas.examples import example_create_aas, example_create_submodel_template, example_create_aas_mandatory_attributes +from aas.examples import example_create_aas, example_create_submodel_template, \ + example_create_aas_mandatory_attributes, example_test_serialization class JsonSerializationTest(unittest.TestCase): @@ -122,3 +123,23 @@ def test_full_empty_example_serialization(self) -> None: # TODO move to own test file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + def test_missing_serialization(self) -> None: + data = example_test_serialization.create_full_example() + file = io.StringIO() + json_serialization.write_aas_to_json_file(file=file, data=data) + + with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + aas_json_schema = json.load(json_file) + + file.seek(0) + json_data = json.load(file) + + # validate serialization against schema + validate(instance=json_data, schema=aas_json_schema) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + # TODO move to own test + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) From cb9cb2539d5612b831b17e5363379463804efa12 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 16 Dec 2019 17:30:35 +0100 Subject: [PATCH 290/474] adapter.json: Rename json_serialization.write_aas_to_json_file, improve docs --- test/adapter/json/test_json_serialization.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index da211dc..ceb7d17 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -66,7 +66,7 @@ def test_validate_serialization(self) -> None: def test_full_example_serialization(self) -> None: data = example_create_aas.create_full_example() file = io.StringIO() - json_serialization.write_aas_to_json_file(file=file, data=data) + json_serialization.write_aas_json_file(file=file, data=data) with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: aas_json_schema = json.load(json_file) @@ -87,7 +87,7 @@ def test_submodel_template_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_create_submodel_template.create_example_submodel_template()) file = io.StringIO() - json_serialization.write_aas_to_json_file(file=file, data=data) + json_serialization.write_aas_json_file(file=file, data=data) with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: aas_json_schema = json.load(json_file) @@ -100,14 +100,13 @@ def test_submodel_template_serialization(self) -> None: # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module - # TODO move to own test file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) def test_full_empty_example_serialization(self) -> None: data = example_create_aas_mandatory_attributes.create_full_example() file = io.StringIO() - json_serialization.write_aas_to_json_file(file=file, data=data) + json_serialization.write_aas_json_file(file=file, data=data) with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: aas_json_schema = json.load(json_file) @@ -127,7 +126,7 @@ def test_full_empty_example_serialization(self) -> None: def test_missing_serialization(self) -> None: data = example_test_serialization.create_full_example() file = io.StringIO() - json_serialization.write_aas_to_json_file(file=file, data=data) + json_serialization.write_aas_json_file(file=file, data=data) with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: aas_json_schema = json.load(json_file) From 2ad9583b842ce60e1b4d5bb69799d97fd0196922 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 16 Dec 2019 17:31:27 +0100 Subject: [PATCH 291/474] test: Add more tests for JSON deserialization --- .../adapter/json/test_json_deserialization.py | 80 +++++++++++++++++++ test/adapter/json/test_json_serialization.py | 12 +-- 2 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 test/adapter/json/test_json_deserialization.py diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py new file mode 100644 index 0000000..b660a2f --- /dev/null +++ b/test/adapter/json/test_json_deserialization.py @@ -0,0 +1,80 @@ +# Copyright 2019 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +""" +Additional tests for the adapter.json.json_deserialization module. + +Deserialization is also somehow tested in the serialization tests -- at least, we get to know if exceptions are raised +when trying to reconstruct the serialized data structure. This module additionally tests error behaviour and verifies +deserialization results. +""" +import io +import logging +import unittest +from aas.adapter.json import json_deserialization + + +class JsonDeserializationTest(unittest.TestCase): + def test_file_format_missing_list(self) -> None: + data = """ +{ + "assetAdministrationShells": [], + "assets": [], + "conceptDescriptions": [] +}""" + with self.assertRaisesRegex(KeyError, r"submodels"): + json_deserialization.read_json_aas_file(io.StringIO(data), False) + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: + json_deserialization.read_json_aas_file(io.StringIO(data), True) + self.assertIn("submodels", cm.output[0]) + + def test_file_format_wrong_list(self) -> None: + data = """ +{ + "assetAdministrationShells": [], + "assets": [], + "conceptDescriptions": [], + "submodels": [ + { + "modelType": { + "name": "Asset" + }, + "identification": { + "id": "https://acplt.org/Test_Asset", + "idType": "IRI" + }, + "kind": "Instance" + } + ] +}""" + with self.assertRaisesRegex(TypeError, r"submodels.*Asset"): + json_deserialization.read_json_aas_file(io.StringIO(data), False) + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: + json_deserialization.read_json_aas_file(io.StringIO(data), True) + self.assertIn("submodels", cm.output[0]) + self.assertIn("Asset", cm.output[0]) + + def test_file_format_unknown_object(self) -> None: + data = """ +{ + "assetAdministrationShells": [], + "assets": [], + "conceptDescriptions": [], + "submodels": [ + "foo" + ] +}""" + with self.assertRaisesRegex(TypeError, r"submodels.*'foo'"): + json_deserialization.read_json_aas_file(io.StringIO(data), False) + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: + json_deserialization.read_json_aas_file(io.StringIO(data), True) + self.assertIn("submodels", cm.output[0]) + self.assertIn("'foo'", cm.output[0]) + diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index ceb7d17..ea88089 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -51,11 +51,6 @@ def test_validate_serialization(self) -> None: }, cls=json_serialization.AASToJsonEncoder) json_data_new = json.loads(json_data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - # TODO move to own test - json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) - # load schema with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: aas_schema = json.load(json_file) @@ -63,6 +58,10 @@ def test_validate_serialization(self) -> None: # validate serialization against schema validate(instance=json_data_new, schema=aas_schema) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) + def test_full_example_serialization(self) -> None: data = example_create_aas.create_full_example() file = io.StringIO() @@ -79,7 +78,6 @@ def test_full_example_serialization(self) -> None: # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module - # TODO move to own test file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) @@ -119,7 +117,6 @@ def test_full_empty_example_serialization(self) -> None: # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module - # TODO move to own test file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) @@ -139,6 +136,5 @@ def test_missing_serialization(self) -> None: # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module - # TODO move to own test file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) From 4aab9b7b73002a49a97cc9c1dae387e68c90d2da Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 16 Dec 2019 18:14:00 +0100 Subject: [PATCH 292/474] test: Fix codestyle --- .../adapter/json/test_json_deserialization.py | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index b660a2f..58c7d22 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -24,11 +24,11 @@ class JsonDeserializationTest(unittest.TestCase): def test_file_format_missing_list(self) -> None: data = """ -{ - "assetAdministrationShells": [], - "assets": [], - "conceptDescriptions": [] -}""" + { + "assetAdministrationShells": [], + "assets": [], + "conceptDescriptions": [] + }""" with self.assertRaisesRegex(KeyError, r"submodels"): json_deserialization.read_json_aas_file(io.StringIO(data), False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: @@ -37,23 +37,23 @@ def test_file_format_missing_list(self) -> None: def test_file_format_wrong_list(self) -> None: data = """ -{ - "assetAdministrationShells": [], - "assets": [], - "conceptDescriptions": [], - "submodels": [ - { - "modelType": { - "name": "Asset" - }, - "identification": { - "id": "https://acplt.org/Test_Asset", - "idType": "IRI" - }, - "kind": "Instance" - } - ] -}""" + { + "assetAdministrationShells": [], + "assets": [], + "conceptDescriptions": [], + "submodels": [ + { + "modelType": { + "name": "Asset" + }, + "identification": { + "id": "https://acplt.org/Test_Asset", + "idType": "IRI" + }, + "kind": "Instance" + } + ] + }""" with self.assertRaisesRegex(TypeError, r"submodels.*Asset"): json_deserialization.read_json_aas_file(io.StringIO(data), False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: @@ -63,18 +63,17 @@ def test_file_format_wrong_list(self) -> None: def test_file_format_unknown_object(self) -> None: data = """ -{ - "assetAdministrationShells": [], - "assets": [], - "conceptDescriptions": [], - "submodels": [ - "foo" - ] -}""" + { + "assetAdministrationShells": [], + "assets": [], + "conceptDescriptions": [], + "submodels": [ + "foo" + ] + }""" with self.assertRaisesRegex(TypeError, r"submodels.*'foo'"): json_deserialization.read_json_aas_file(io.StringIO(data), False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: json_deserialization.read_json_aas_file(io.StringIO(data), True) self.assertIn("submodels", cm.output[0]) self.assertIn("'foo'", cm.output[0]) - From bbf8b6eec79b992a577651a10db9738ee88aa516 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 17 Dec 2019 08:55:20 +0100 Subject: [PATCH 293/474] examples: update docstrings and move example for test to test modul --- .../json/example_test_serialization.py | 485 ++++++++++++++++++ test/adapter/json/test_json_serialization.py | 3 +- 2 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 test/adapter/json/example_test_serialization.py diff --git a/test/adapter/json/example_test_serialization.py b/test/adapter/json/example_test_serialization.py new file mode 100644 index 0000000..de0145c --- /dev/null +++ b/test/adapter/json/example_test_serialization.py @@ -0,0 +1,485 @@ +# Copyright 2019 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +""" +Module for the creation of a object store with missing object attribute combination for testing the serialization + +""" +from aas import model + + +def create_full_example() -> model.DictObjectStore: + """ + creates an object store containing an example asset identification submodel, an example asset, an example submodel, + an example concept description and an example asset administration shell + + :return: object store + """ + obj_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + obj_store.add(create_example_asset()) + obj_store.add(create_example_submodel()) + obj_store.add(create_example_concept_description()) + obj_store.add(create_example_asset_administration_shell(create_example_concept_dictionary())) + return obj_store + + +def create_example_asset() -> model.Asset: + """ + creates an example asset which holds references to the example asset identification submodel + + :return: example asset + """ + asset = model.Asset( + kind=model.AssetKind.INSTANCE, + identification=model.Identifier(id_='https://acplt.org/Test_Asset', + id_type=model.IdentifierType.IRI), + id_short='Test_Asset', + category=None, + description={'en-us': 'An example asset for the test application', + 'de': 'Ein Beispiel-Asset für eine Test-Anwendung'}, + parent=None, + administration=model.AdministrativeInformation(), + data_specification={model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/DataSpecifications/AssetTypes/' + 'TestAsset', + id_type=model.KeyType.IRDI)])}, + asset_identification_model=None, + bill_of_material=None) + return asset + + +def create_example_submodel() -> model.Submodel: + """ + creates an example submodel containing all kind of SubmodelElement objects + + :return: example submodel + """ + formula = model.Formula() + + qualifier = model.Qualifier( + type_='http://acplt.org/Qualifier/ExampleQualifier', + value_type='string') + + submodel_element_property = model.Property( + id_short='ExampleProperty', + value_type='string', + value='exampleValue', + value_id=None, # TODO + category='CONSTANT', + description={'en-us': 'Example Property object', + 'de': 'Beispiel Property Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Properties/ExampleProperty', + id_type=model.KeyType.IRDI)]), + qualifier={formula, qualifier}, + kind=model.ModelingKind.INSTANCE) + + submodel_element_multi_language_property = model.MultiLanguageProperty( + id_short='ExampleMultiLanguageProperty', + value={'en-us': 'Example value of a MultiLanguageProperty element', + 'de': 'Beispielswert für ein MulitLanguageProperty-Element'}, + value_id=None, # TODO + category='CONSTANT', + description={'en-us': 'Example MultiLanguageProperty object', + 'de': 'Beispiel MulitLanguageProperty Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/MultiLanguageProperties/' + 'ExampleMultiLanguageProperty', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_range = model.Range( + id_short='ExampleRange', + value_type='int', + min_='0', + max_='100', + category='PARAMETER', + description={'en-us': 'Example Range object', + 'de': 'Beispiel Range Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Ranges/ExampleRange', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_blob = model.Blob( + id_short='ExampleBlob', + mime_type='application/pdf', + value=bytearray(b'\x01\x02\x03\x04\x05'), + category='PARAMETER', + description={'en-us': 'Example Blob object', + 'de': 'Beispiel Blob Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Blobs/ExampleBlob', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_file = model.File( + id_short='ExampleFile', + mime_type='application/pdf', + value='/TestFile.pdf', + category='PARAMETER', + description={'en-us': 'Example File object', + 'de': 'Beispiel File Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Files/ExampleFile', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_reference_element = model.ReferenceElement( + id_short='ExampleReferenceElement', + value=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + local=True, + value='ExampleProperty', + id_type=model.KeyType.IDSHORT)], + model.Property), + category='PARAMETER', + description={'en-us': 'Example Reference Element object', + 'de': 'Beispiel Reference Element Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/ReferenceElements/ExampleReferenceElement', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_relationship_element = model.RelationshipElement( + id_short='ExampleRelationshipElement', + first=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + local=True, + value='ExampleProperty', + id_type=model.KeyType.IDSHORT)], + model.Property), + second=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + local=True, + value='ExampleProperty', + id_type=model.KeyType.IDSHORT)], + model.Property), + category='PARAMETER', + description={'en-us': 'Example RelationshipElement object', + 'de': 'Beispiel RelationshipElement Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/RelationshipElements/' + 'ExampleRelationshipElement', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_annotated_relationship_element = model.AnnotatedRelationshipElement( + id_short='ExampleAnnotatedRelationshipElement', + first=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + local=True, + value='ExampleProperty', + id_type=model.KeyType.IDSHORT)], + model.Property), + second=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + local=True, + value='ExampleProperty', + id_type=model.KeyType.IDSHORT)], + model.Property), + annotation={model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + local=True, + value='ExampleProperty', + id_type=model.KeyType.IDSHORT)], + model.Property)}, + category='PARAMETER', + description={'en-us': 'Example AnnotatedRelationshipElement object', + 'de': 'Beispiel AnnotatedRelationshipElement Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/RelationshipElements/' + 'ExampleAnnotatedRelationshipElement', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_operation_variable_input = model.OperationVariable( + id_short='ExampleInputOperationVariable', + value=submodel_element_property, + category='PARAMETER', + description={'en-us': 'Example ExampleInputOperationVariable object', + 'de': 'Beispiel ExampleInputOperationVariable Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Operations/' + 'ExampleInputOperationVariable', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_operation_variable_output = model.OperationVariable( + id_short='ExampleOutputOperationVariable', + value=submodel_element_property, + category='PARAMETER', + description={'en-us': 'Example OutputOperationVariable object', + 'de': 'Beispiel OutputOperationVariable Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Operations/' + 'ExampleOutputOperationVariable', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_operation_variable_in_output = model.OperationVariable( + id_short='ExampleInOutputOperationVariable', + value=submodel_element_property, + category='PARAMETER', + description={'en-us': 'Example InOutputOperationVariable object', + 'de': 'Beispiel InOutputOperationVariable Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Operations/' + 'ExampleInOutputOperationVariable', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_operation = model.Operation( + id_short='ExampleOperation', + input_variable={submodel_element_operation_variable_input}, + output_variable={submodel_element_operation_variable_output}, + in_output_variable={submodel_element_operation_variable_in_output}, + category='PARAMETER', + description={'en-us': 'Example Operation object', + 'de': 'Beispiel Operation Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Operations/' + 'ExampleOperation', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_capability = model.Capability( + id_short='ExampleCapability', + category='PARAMETER', + description={'en-us': 'Example Capability object', + 'de': 'Beispiel Capability Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Capabilities/' + 'ExampleCapability', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_basic_event = model.BasicEvent( + id_short='ExampleBasicEvent', + observed=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + local=True, + value='ExampleProperty', + id_type=model.KeyType.IDSHORT)], + model.Property), + category='PARAMETER', + description={'en-us': 'Example BasicEvent object', + 'de': 'Beispiel BasicEvent Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Events/' + 'ExampleBasicEvent', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_submodel_element_collection_ordered = model.SubmodelElementCollectionOrdered( + id_short='ExampleSubmodelCollectionOrdered', + value=(submodel_element_property, + submodel_element_multi_language_property, + submodel_element_range), + category='PARAMETER', + description={'en-us': 'Example SubmodelElementCollectionOrdered object', + 'de': 'Beispiel SubmodelElementCollectionOrdered Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/SubmodelElementCollections/' + 'ExampleSubmodelElementCollectionOrdered', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel_element_submodel_element_collection_unordered = model.SubmodelElementCollectionUnordered( + id_short='ExampleSubmodelCollectionUnordered', + value=(submodel_element_blob, + submodel_element_file, + submodel_element_reference_element), + category='PARAMETER', + description={'en-us': 'Example SubmodelElementCollectionUnordered object', + 'de': 'Beispiel SubmodelElementCollectionUnordered Element'}, + parent=None, + data_specification=None, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/SubmodelElementCollections/' + 'ExampleSubmodelElementCollectionUnordered', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + + submodel = model.Submodel( + identification=model.Identifier(id_='https://acplt.org/Test_Submodel', + id_type=model.IdentifierType.IRI), + submodel_element=(submodel_element_relationship_element, + submodel_element_annotated_relationship_element, + submodel_element_operation, + submodel_element_capability, + submodel_element_basic_event, + submodel_element_submodel_element_collection_ordered, + submodel_element_submodel_element_collection_unordered), + id_short='TestSubmodel', + category=None, + description={'en-us': 'An example submodel for the test application', + 'de': 'Ein Beispiel-Teilmodell für eine Test-Anwendung'}, + parent=None, + administration=model.AdministrativeInformation(version='0.9', + revision='0'), + data_specification={model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/DataSpecifications/AssetTypes/' + 'TestAsset', + id_type=model.KeyType.IRDI)])}, + semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/SubmodelTemplates/' + 'ExampleSubmodel', + id_type=model.KeyType.IRDI)]), + qualifier=None, + kind=model.ModelingKind.INSTANCE) + return submodel + + +def create_example_concept_description() -> model.ConceptDescription: + """ + creates an example concept description + + :return: example concept description + """ + concept_description = model.ConceptDescription( + identification=model.Identifier(id_='https://acplt.org/Test_ConceptDescription', + id_type=model.IdentifierType.IRI), + is_case_of=None, + id_short='TestConceptDescription', + category=None, + description={'en-us': 'An example concept description for the test application', + 'de': 'Ein Beispiel-ConceptDescription für eine Test-Anwendung'}, + parent=None, + administration=model.AdministrativeInformation(version='0.9', + revision='0'), + data_specification={model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/DataSpecifications/' + 'ConceptDescriptions/TestConceptDescription', + id_type=model.KeyType.IRDI)])}) + return concept_description + + +def create_example_concept_dictionary() -> model.ConceptDictionary: + """ + creates an example concept dictionary containing an reference to the example concept description + + :return: example concept dictionary + """ + concept_dictionary = model.ConceptDictionary( + id_short='TestConceptDictionary', + category=None, + description={'en-us': 'An example concept dictionary for the test application', + 'de': 'Ein Beispiel-ConceptDictionary für eine Test-Anwendung'}, + parent=None, + concept_description={model.AASReference([model.Key(type_=model.KeyElements.CONCEPT_DESCRIPTION, + local=False, + value='https://acplt.org/Test_ConceptDescription', + id_type=model.KeyType.IRDI)], + model.ConceptDescription)}) + return concept_dictionary + + +def create_example_asset_administration_shell(concept_dictionary: model.ConceptDictionary) -> \ + model.AssetAdministrationShell: + """ + creates an example asset administration shell containing references to the example asset and example submodel + + :return: example asset administration shell + """ + view = model.View( + id_short='ExampleView', + contained_element={model.AASReference([model.Key(type_=model.KeyElements.SUBMODEL, + local=False, + value='https://acplt.org/Test_Submodel', + id_type=model.KeyType.IRDI)], + model.Submodel)}) + view_2 = model.View( + id_short='ExampleView2') + + asset_administration_shell = model.AssetAdministrationShell( + asset=model.AASReference([model.Key(type_=model.KeyElements.ASSET, + local=False, + value='https://acplt.org/Test_Asset', + id_type=model.KeyType.IRDI)], + model.Asset), + identification=model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', + id_type=model.IdentifierType.IRI), + id_short='TestAssetAdministrationShell', + category=None, + description={'en-us': 'An Example Asset Administration Shell for the test application', + 'de': 'Ein Beispiel-Verwaltungsschale für eine Test-Anwendung'}, + parent=None, + administration=model.AdministrativeInformation(version='0.9', + revision='0'), + data_specification=None, + security_=None, + submodel_={model.AASReference([model.Key(type_=model.KeyElements.SUBMODEL, + local=False, + value='https://acplt.org/Test_Submodel', + id_type=model.KeyType.IRDI)], + model.Submodel)}, + concept_dictionary=[concept_dictionary], + view=[view, view_2], + derived_from=None) + return asset_administration_shell diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index ea88089..a1afb3b 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -19,7 +19,8 @@ from jsonschema import validate # type: ignore from aas.examples import example_create_aas, example_create_submodel_template, \ - example_create_aas_mandatory_attributes, example_test_serialization + example_create_aas_mandatory_attributes +from test.adapter.json import example_test_serialization class JsonSerializationTest(unittest.TestCase): From df735ce626ebdf14d9e3206fe76ceaa983a67239 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 17 Dec 2019 09:47:10 +0100 Subject: [PATCH 294/474] test.adapter.json: delete not used example --- test/adapter/json/test_full_example.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test/adapter/json/test_full_example.json diff --git a/test/adapter/json/test_full_example.json b/test/adapter/json/test_full_example.json deleted file mode 100644 index 68cf5f4..0000000 --- a/test/adapter/json/test_full_example.json +++ /dev/null @@ -1 +0,0 @@ -{"assetAdministrationShells": [[{"idShort": "TestAssetAdministrationShell", "description": [{"language": "en-us", "text": "An Example Asset Administration Shell for the test application"}, {"language": "de", "text": "Ein Beispiel-Verwaltungsschale f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "AssetAdministrationShell"}, "identification": {"id": "https://acplt.org/Test_AssetAdministrationShell", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "asset": {"keys": [{"type": "Asset", "idType": "IRDI", "value": "https://acplt.org/Test_Asset", "local": false}]}, "submodels": [{"keys": [{"type": "Submodel", "idType": "IRDI", "value": "https://acplt.org/Test_Submodel", "local": false}]}], "conceptDictionaries": [{"idShort": "TestConceptDictionary", "description": [{"language": "en-us", "text": "An example concept dictionary for the test application"}, {"language": "de", "text": "Ein Beispiel-ConceptDictionary f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "ConceptDictionary"}, "conceptDescriptions": [{"keys": [{"type": "ConceptDescription", "idType": "IRDI", "value": "https://acplt.org/Test_ConceptDescription", "local": false}]}]}]}]], "submodels": [[{"idShort": "Identification", "description": [{"language": "en-us", "text": "An example asset identification submodel for the test application"}, {"language": "de", "text": "Ein Beispiel-Identifikations-Submodel f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "Submodel"}, "identification": {"id": "http://acplt.org/Submodels/Assets/TestAsset/Identification", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "embeddedDataSpecification": [{"keys": [{"type": "Asset", "idType": "IRDI", "value": "http://acplt.org/DataSpecifications/Submodels/AssetIdentification", "local": false}]}], "semanticId": {"keys": [{"type": "Submodel", "idType": "IRDI", "value": "http://acplt.org/SubmodelTemplates/AssetIdentification", "local": false}]}, "submodelElements": [{"idShort": "ManufacturerName", "description": [{"language": "en-us", "text": "Legally valid designation of the natural or judicial person which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into circulation."}, {"language": "de", "text": "Bezeichnung f\u00fcr eine nat\u00fcrliche oder juristische Person, die f\u00fcr die Auslegung, Herstellung und Verpackung sowie die Etikettierung eines Produkts im Hinblick auf das 'Inverkehrbringen' im eigenen Namen verantwortlich ist"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "0173-1#02-AAO677#002", "local": false}]}, "value": "ACPLT", "valueType": "string"}, {"idShort": "InstanceId", "description": [{"language": "en-us", "text": "Legally valid designation of the natural or judicial person which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into circulation."}, {"language": "de", "text": "Bezeichnung f\u00fcr eine nat\u00fcrliche oder juristische Person, die f\u00fcr die Auslegung, Herstellung und Verpackung sowie die Etikettierung eines Produkts im Hinblick auf das 'Inverkehrbringen' im eigenen Namen verantwortlich ist"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRI", "value": "http://opcfoundation.org/UA/DI/1.1/DeviceType/Serialnumber", "local": false}]}, "value": "978-8234-234-342", "valueType": "string"}]}, {"idShort": "TestSubmodel", "description": [{"language": "en-us", "text": "An example submodel for the test application"}, {"language": "de", "text": "Ein Beispiel-Teilmodell f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "Submodel"}, "identification": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "embeddedDataSpecification": [{"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/DataSpecifications/AssetTypes/TestAsset", "local": false}]}], "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/SubmodelTemplates/ExampleSubmodel", "local": false}]}, "submodelElements": [{"idShort": "ExampleRelationshipElement", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example RelationshipElement object"}, {"language": "de", "text": "Beispiel RelationshipElement Element"}], "modelType": {"name": "RelationshipElement"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/RelationshipElements/ExampleRelationshipElement", "local": false}]}, "first": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}, "second": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}}, {"idShort": "ExampleAnnotatedRelationshipElement", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example AnnotatedRelationshipElement object"}, {"language": "de", "text": "Beispiel AnnotatedRelationshipElement Element"}], "modelType": {"name": "AnnotatedRelationshipElement"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/RelationshipElements/ExampleAnnotatedRelationshipElement", "local": false}]}, "first": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}, "second": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}}, {"idShort": "ExampleOperation", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Operation object"}, {"language": "de", "text": "Beispiel Operation Element"}], "modelType": {"name": "Operation"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Operations/ExampleOperation", "local": false}]}, "inputVariable": [{"idShort": "ExampleInputOperationVariable", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example ExampleInputOperationVariable object"}, {"language": "de", "text": "Beispiel ExampleInputOperationVariable Element"}], "modelType": {"name": "OperationVariable"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Operations/ExampleInputOperationVariable", "local": false}]}, "value": {"idShort": "ExampleProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example Property object"}, {"language": "de", "text": "Beispiel Property Element"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Properties/ExampleProperty", "local": false}]}, "value": "exampleValue", "valueType": "string"}}], "outputVariable": [{"idShort": "ExampleOutputOperationVariable", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example OutputOperationVariable object"}, {"language": "de", "text": "Beispiel OutputOperationVariable Element"}], "modelType": {"name": "OperationVariable"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Operations/ExampleOutputOperationVariable", "local": false}]}, "value": {"idShort": "ExampleProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example Property object"}, {"language": "de", "text": "Beispiel Property Element"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Properties/ExampleProperty", "local": false}]}, "value": "exampleValue", "valueType": "string"}}], "inoutputVariable": [{"idShort": "ExampleInOutputOperationVariable", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example InOutputOperationVariable object"}, {"language": "de", "text": "Beispiel InOutputOperationVariable Element"}], "modelType": {"name": "OperationVariable"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Operations/ExampleInOutputOperationVariable", "local": false}]}, "value": {"idShort": "ExampleProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example Property object"}, {"language": "de", "text": "Beispiel Property Element"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Properties/ExampleProperty", "local": false}]}, "value": "exampleValue", "valueType": "string"}}]}, {"idShort": "ExampleCapability", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Capability object"}, {"language": "de", "text": "Beispiel Capability Element"}], "modelType": {"name": "Capability"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Capabilities/ExampleCapability", "local": false}]}}, {"idShort": "ExampleBasicEvent", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example BasicEvent object"}, {"language": "de", "text": "Beispiel BasicEvent Element"}], "modelType": {"name": "BasicEvent"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Events/ExampleBasicEvent", "local": false}]}, "observed": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}}, {"idShort": "ExampleSubmodelCollectionOrdered", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example SubmodelElementCollectionOrdered object"}, {"language": "de", "text": "Beispiel SubmodelElementCollectionOrdered Element"}], "modelType": {"name": "SubmodelElementCollection"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/SubmodelElementCollections/ExampleSubmodelElementCollectionOrdered", "local": false}]}, "value": [{"idShort": "ExampleProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example Property object"}, {"language": "de", "text": "Beispiel Property Element"}], "modelType": {"name": "Property"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Properties/ExampleProperty", "local": false}]}, "value": "exampleValue", "valueType": "string"}, {"idShort": "ExampleMultiLanguageProperty", "category": "CONSTANT", "description": [{"language": "en-us", "text": "Example MultiLanguageProperty object"}, {"language": "de", "text": "Beispiel MulitLanguageProperty Element"}], "modelType": {"name": "MultiLanguageProperty"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/MultiLanguageProperties/ExampleMultiLanguageProperty", "local": false}]}, "value": [{"language": "en-us", "text": "Example value of a MultiLanguageProperty element"}, {"language": "de", "text": "Beispielswert f\u00fcr ein MulitLanguageProperty-Element"}]}, {"idShort": "ExampleRange", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Range object"}, {"language": "de", "text": "Beispiel Range Element"}], "modelType": {"name": "Range"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Ranges/ExampleRange", "local": false}]}, "valueType": "int", "min": "0", "max": "100"}], "odered": true}, {"idShort": "ExampleSubmodelCollectionUnordered", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example SubmodelElementCollectionUnordered object"}, {"language": "de", "text": "Beispiel SubmodelElementCollectionUnordered Element"}], "modelType": {"name": "SubmodelElementCollection"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/SubmodelElementCollections/ExampleSubmodelElementCollectionUnordered", "local": false}]}, "value": [{"idShort": "ExampleBlob", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Blob object"}, {"language": "de", "text": "Beispiel Blob Element"}], "modelType": {"name": "Blob"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Blobs/ExampleBlob", "local": false}]}, "mimeType": "application/pdf", "value": "AQIDBAU="}, {"idShort": "ExampleFile", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example File object"}, {"language": "de", "text": "Beispiel File Element"}], "modelType": {"name": "File"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/Files/ExampleFile", "local": false}]}, "value": "/TestFile.pdf", "mimeType": "application/pdf"}, {"idShort": "ExampleReferenceElement", "category": "PARAMETER", "description": [{"language": "en-us", "text": "Example Reference Element object"}, {"language": "de", "text": "Beispiel Reference Element Element"}], "modelType": {"name": "ReferenceElement"}, "semanticId": {"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/ReferenceElements/ExampleReferenceElement", "local": false}]}, "value": {"keys": [{"type": "Property", "idType": "IdShort", "value": "ExampleProperty", "local": true}]}}], "odered": false}]}]], "assets": [[{"idShort": "Test_Asset", "description": [{"language": "en-us", "text": "An example asset for the test application"}, {"language": "de", "text": "Ein Beispiel-Asset f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "Asset"}, "identification": {"id": "https://acplt.org/Test_Asset", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "embeddedDataSpecification": [{"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/DataSpecifications/AssetTypes/TestAsset", "local": false}]}], "kind": "Instance", "assetIdentificationModel": {"keys": [{"type": "Submodel", "idType": "IRDI", "value": "http://acplt.org/Submodels/Assets/TestAsset/Identification", "local": false}]}}]], "conceptDescriptions": [[{"idShort": "TestConceptDescription", "description": [{"language": "en-us", "text": "An example concept description for the test application"}, {"language": "de", "text": "Ein Beispiel-ConceptDescription f\u00fcr eine Test-Anwendung"}], "modelType": {"name": "ConceptDescription"}, "identification": {"id": "https://acplt.org/Test_ConceptDescription", "idType": "IRI"}, "AdministrativeInformation": {"version": "0.9", "revision": "0"}, "embeddedDataSpecification": [{"keys": [{"type": "GlobalReference", "idType": "IRDI", "value": "http://acplt.org/DataSpecifications/ConceptDescriptions/TestConceptDescription", "local": false}]}]}]]} \ No newline at end of file From 1a3938112933780d2970c002f1683b6e5590bb41 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 17 Dec 2019 15:09:39 +0100 Subject: [PATCH 295/474] test: Add more expected failure tests for json deserialization --- .../adapter/json/test_json_deserialization.py | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 58c7d22..e61ccb4 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -16,9 +16,11 @@ deserialization results. """ import io +import json import logging import unittest from aas.adapter.json import json_deserialization +from aas import model class JsonDeserializationTest(unittest.TestCase): @@ -68,7 +70,7 @@ def test_file_format_unknown_object(self) -> None: "assets": [], "conceptDescriptions": [], "submodels": [ - "foo" + { "x": "foo" } ] }""" with self.assertRaisesRegex(TypeError, r"submodels.*'foo'"): @@ -77,3 +79,83 @@ def test_file_format_unknown_object(self) -> None: json_deserialization.read_json_aas_file(io.StringIO(data), True) self.assertIn("submodels", cm.output[0]) self.assertIn("'foo'", cm.output[0]) + + def test_broken_asset(self) -> None: + data = """ + [ + { + "modelType": {"name": "Asset"}, + "kind": "Instance" + }, + { + "modelType": {"name": "Asset"}, + "identification": ["https://acplt.org/Test_Asset_broken_id", "IRI"], + "kind": "Instance" + }, + { + "modelType": {"name": "Asset"}, + "identification": {"id": "https://acplt.org/Test_Asset", "idType": "IRI"}, + "kind": "Instance" + } + ]""" + # In strict mode, we should catch an exception + with self.assertRaisesRegex(KeyError, r"identification"): + json.loads(data, cls=json_deserialization.StrictAASFromJsonDecoder) + + # In failsafe mode, we should get a log entry and the first Asset entry should be returned as untouched dict + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: + parsed_data = json.loads(data, cls=json_deserialization.AASFromJsonDecoder) + self.assertIn("identification", cm.output[0]) + self.assertIsInstance(parsed_data, list) + self.assertEqual(3, len(parsed_data)) + + self.assertIsInstance(parsed_data[0], dict) + self.assertIsInstance(parsed_data[1], dict) + self.assertIsInstance(parsed_data[2], model.Asset) + self.assertEqual("https://acplt.org/Test_Asset", parsed_data[2].identification.id) + + def test_wrong_submodel_element_type(self) -> None: + data = """ + [ + { + "modelType": {"name": "Submodel"}, + "identification": { + "id": "http://acplt.org/Submodels/Assets/TestAsset/Identification", + "idType": "IRI" + }, + "submodelElements": [ + { + "modelType": {"name": "Asset"}, + "identification": {"id": "https://acplt.org/Test_Asset", "idType": "IRI"}, + "kind": "Instance" + }, + { + "modelType": "Broken modelType" + }, + { + "modelType": {"name": "Capability"}, + "idShort": "TestCapability" + } + ] + } + ]""" + # In strict mode, we should catch an exception for the unexpected Asset within the Submodel + # The broken object should not raise an exception, but log a warning, even in strict mode. + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: + with self.assertRaisesRegex(TypeError, r"SubmodelElement.*Asset"): + json.loads(data, cls=json_deserialization.StrictAASFromJsonDecoder) + self.assertIn("modelType", cm.output[0]) + + # In failsafe mode, we should get a log entries for the broken object and the wrong type of the first two + # submodelElements + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: + parsed_data = json.loads(data, cls=json_deserialization.AASFromJsonDecoder) + self.assertGreaterEqual(len(cm.output), 3) + self.assertIn("SubmodelElement", cm.output[1]) + self.assertIn("SubmodelElement", cm.output[2]) + + self.assertIsInstance(parsed_data[0], model.Submodel) + self.assertEqual(1, len(parsed_data[0].submodel_element)) + cap = parsed_data[0].submodel_element.pop() + self.assertIsInstance(cap, model.Capability) + self.assertEqual("TestCapability", cap.id_short) From cc4e9c6494f4bd5b7eb2fd5222604937650d3d6d Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 17 Dec 2019 17:31:58 +0100 Subject: [PATCH 296/474] model.base: make key of Reference immutable (convert to Tuple) for creating a hash to use __eq__ --- .../json/example_test_serialization.py | 125 +++++++++--------- test/adapter/json/test_json_serialization.py | 10 +- 2 files changed, 68 insertions(+), 67 deletions(-) diff --git a/test/adapter/json/example_test_serialization.py b/test/adapter/json/example_test_serialization.py index de0145c..87b5416 100644 --- a/test/adapter/json/example_test_serialization.py +++ b/test/adapter/json/example_test_serialization.py @@ -13,6 +13,7 @@ """ from aas import model +from typing import Any, Set def create_full_example() -> model.DictObjectStore: @@ -46,11 +47,11 @@ def create_example_asset() -> model.Asset: 'de': 'Ein Beispiel-Asset für eine Test-Anwendung'}, parent=None, administration=model.AdministrativeInformation(), - data_specification={model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + data_specification={model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/DataSpecifications/AssetTypes/' 'TestAsset', - id_type=model.KeyType.IRDI)])}, + id_type=model.KeyType.IRDI),))}, asset_identification_model=None, bill_of_material=None) return asset @@ -78,10 +79,10 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel Property Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Properties/ExampleProperty', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier={formula, qualifier}, kind=model.ModelingKind.INSTANCE) @@ -95,11 +96,11 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel MulitLanguageProperty Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/MultiLanguageProperties/' 'ExampleMultiLanguageProperty', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -113,10 +114,10 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel Range Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Ranges/ExampleRange', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -129,10 +130,10 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel Blob Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Blobs/ExampleBlob', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -145,84 +146,84 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel File Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Files/ExampleFile', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) submodel_element_reference_element = model.ReferenceElement( id_short='ExampleReferenceElement', - value=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + value=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, local=True, value='ExampleProperty', - id_type=model.KeyType.IDSHORT)], + id_type=model.KeyType.IDSHORT),), model.Property), category='PARAMETER', description={'en-us': 'Example Reference Element object', 'de': 'Beispiel Reference Element Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/ReferenceElements/ExampleReferenceElement', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) submodel_element_relationship_element = model.RelationshipElement( id_short='ExampleRelationshipElement', - first=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + first=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, local=True, value='ExampleProperty', - id_type=model.KeyType.IDSHORT)], + id_type=model.KeyType.IDSHORT),), model.Property), - second=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + second=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, local=True, value='ExampleProperty', - id_type=model.KeyType.IDSHORT)], + id_type=model.KeyType.IDSHORT),), model.Property), category='PARAMETER', description={'en-us': 'Example RelationshipElement object', 'de': 'Beispiel RelationshipElement Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/RelationshipElements/' 'ExampleRelationshipElement', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) submodel_element_annotated_relationship_element = model.AnnotatedRelationshipElement( id_short='ExampleAnnotatedRelationshipElement', - first=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + first=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, local=True, value='ExampleProperty', - id_type=model.KeyType.IDSHORT)], + id_type=model.KeyType.IDSHORT),), model.Property), - second=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + second=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, local=True, value='ExampleProperty', - id_type=model.KeyType.IDSHORT)], + id_type=model.KeyType.IDSHORT),), model.Property), - annotation={model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + annotation={model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, local=True, value='ExampleProperty', - id_type=model.KeyType.IDSHORT)], + id_type=model.KeyType.IDSHORT),), model.Property)}, category='PARAMETER', description={'en-us': 'Example AnnotatedRelationshipElement object', 'de': 'Beispiel AnnotatedRelationshipElement Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/RelationshipElements/' 'ExampleAnnotatedRelationshipElement', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -234,11 +235,11 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel ExampleInputOperationVariable Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Operations/' 'ExampleInputOperationVariable', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -250,11 +251,11 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel OutputOperationVariable Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Operations/' 'ExampleOutputOperationVariable', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -266,11 +267,11 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel InOutputOperationVariable Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Operations/' 'ExampleInOutputOperationVariable', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -284,11 +285,11 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel Operation Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Operations/' 'ExampleOperation', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -299,31 +300,31 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel Capability Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Capabilities/' 'ExampleCapability', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) submodel_element_basic_event = model.BasicEvent( id_short='ExampleBasicEvent', - observed=model.AASReference([model.Key(type_=model.KeyElements.PROPERTY, + observed=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, local=True, value='ExampleProperty', - id_type=model.KeyType.IDSHORT)], + id_type=model.KeyType.IDSHORT),), model.Property), category='PARAMETER', description={'en-us': 'Example BasicEvent object', 'de': 'Beispiel BasicEvent Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/Events/' 'ExampleBasicEvent', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -337,11 +338,11 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel SubmodelElementCollectionOrdered Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/SubmodelElementCollections/' 'ExampleSubmodelElementCollectionOrdered', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -355,11 +356,11 @@ def create_example_submodel() -> model.Submodel: 'de': 'Beispiel SubmodelElementCollectionUnordered Element'}, parent=None, data_specification=None, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/SubmodelElementCollections/' 'ExampleSubmodelElementCollectionUnordered', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) @@ -380,16 +381,16 @@ def create_example_submodel() -> model.Submodel: parent=None, administration=model.AdministrativeInformation(version='0.9', revision='0'), - data_specification={model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + data_specification={model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/DataSpecifications/AssetTypes/' 'TestAsset', - id_type=model.KeyType.IRDI)])}, - semantic_id=model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + id_type=model.KeyType.IRDI),))}, + semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/SubmodelTemplates/' 'ExampleSubmodel', - id_type=model.KeyType.IRDI)]), + id_type=model.KeyType.IRDI),)), qualifier=None, kind=model.ModelingKind.INSTANCE) return submodel @@ -412,11 +413,11 @@ def create_example_concept_description() -> model.ConceptDescription: parent=None, administration=model.AdministrativeInformation(version='0.9', revision='0'), - data_specification={model.Reference([model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + data_specification={model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, local=False, value='http://acplt.org/DataSpecifications/' 'ConceptDescriptions/TestConceptDescription', - id_type=model.KeyType.IRDI)])}) + id_type=model.KeyType.IRDI),))}) return concept_description @@ -432,10 +433,10 @@ def create_example_concept_dictionary() -> model.ConceptDictionary: description={'en-us': 'An example concept dictionary for the test application', 'de': 'Ein Beispiel-ConceptDictionary für eine Test-Anwendung'}, parent=None, - concept_description={model.AASReference([model.Key(type_=model.KeyElements.CONCEPT_DESCRIPTION, + concept_description={model.AASReference((model.Key(type_=model.KeyElements.CONCEPT_DESCRIPTION, local=False, value='https://acplt.org/Test_ConceptDescription', - id_type=model.KeyType.IRDI)], + id_type=model.KeyType.IRDI),), model.ConceptDescription)}) return concept_dictionary @@ -449,19 +450,19 @@ def create_example_asset_administration_shell(concept_dictionary: model.ConceptD """ view = model.View( id_short='ExampleView', - contained_element={model.AASReference([model.Key(type_=model.KeyElements.SUBMODEL, + contained_element={model.AASReference((model.Key(type_=model.KeyElements.SUBMODEL, local=False, value='https://acplt.org/Test_Submodel', - id_type=model.KeyType.IRDI)], + id_type=model.KeyType.IRDI),), model.Submodel)}) view_2 = model.View( id_short='ExampleView2') asset_administration_shell = model.AssetAdministrationShell( - asset=model.AASReference([model.Key(type_=model.KeyElements.ASSET, + asset=model.AASReference((model.Key(type_=model.KeyElements.ASSET, local=False, value='https://acplt.org/Test_Asset', - id_type=model.KeyType.IRDI)], + id_type=model.KeyType.IRDI),), model.Asset), identification=model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', id_type=model.IdentifierType.IRI), @@ -474,10 +475,10 @@ def create_example_asset_administration_shell(concept_dictionary: model.ConceptD revision='0'), data_specification=None, security_=None, - submodel_={model.AASReference([model.Key(type_=model.KeyElements.SUBMODEL, + submodel_={model.AASReference((model.Key(type_=model.KeyElements.SUBMODEL, local=False, value='https://acplt.org/Test_Submodel', - id_type=model.KeyType.IRDI)], + id_type=model.KeyType.IRDI),), model.Submodel)}, concept_dictionary=[concept_dictionary], view=[view, view_2], diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index a1afb3b..07ae24c 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -31,16 +31,16 @@ def test_serialize_Object(self) -> None: json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) def test_validate_serialization(self) -> None: - asset_key = [model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM)] + asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM) - submodel_identifier = submodel_key.get_identifier() + submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_identifier = submodel_key[0].get_identifier() assert(submodel_identifier is not None) - submodel_reference = model.AASReference([submodel_key], model.Submodel) + submodel_reference = model.AASReference(submodel_key, model.Submodel) # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference([])) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) # serialize object to json From 66477aca2ed9951e641358eb22bd7bb070e7c55e Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Thu, 19 Dec 2019 16:43:53 +0100 Subject: [PATCH 297/474] test: Make JSON schema validating tests optional --- test/adapter/json/test_json_serialization.py | 106 +++++++++++++------ 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 07ae24c..31a08e3 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -23,13 +23,83 @@ from test.adapter.json import example_test_serialization -class JsonSerializationTest(unittest.TestCase): +JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') + +class JsonSerializationDeserializationTest(unittest.TestCase): def test_serialize_Object(self) -> None: test_object = model.Property("test_id_short", "string", category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) + def test_validate_serialization(self) -> None: + asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_reference = model.AASReference(asset_key, model.Asset) + aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_identifier = submodel_key[0].get_identifier() + assert(submodel_identifier is not None) + submodel_reference = model.AASReference(submodel_key, model.Submodel) + submodel = model.Submodel(submodel_identifier) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + + # serialize object to json + json_data = json.dumps({ + 'assetAdministrationShells': [test_aas], + 'submodels': [submodel], + 'assets': [], + 'conceptDescriptions': [], + }, cls=json_serialization.AASToJsonEncoder) + json_data_new = json.loads(json_data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) + + def test_full_example_serialization(self) -> None: + data = example_create_aas.create_full_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + def test_submodel_template_serialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_create_submodel_template.create_example_submodel_template()) + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + def test_full_empty_example_serialization(self) -> None: + data = example_create_aas_mandatory_attributes.create_full_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + def test_missing_serialization(self) -> None: + data = example_test_serialization.create_full_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + +@unittest.skipUnless(os.path.exists(JSON_SCHEMA_FILE), "JSON Schema not found for validation") +class JsonSerializationSchemaTest(unittest.TestCase): def test_validate_serialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) @@ -53,22 +123,18 @@ def test_validate_serialization(self) -> None: json_data_new = json.loads(json_data) # load schema - with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_schema = json.load(json_file) # validate serialization against schema validate(instance=json_data_new, schema=aas_schema) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) - def test_full_example_serialization(self) -> None: data = example_create_aas.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) - with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) file.seek(0) @@ -77,18 +143,13 @@ def test_full_example_serialization(self) -> None: # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - def test_submodel_template_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_create_submodel_template.create_example_submodel_template()) file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) - with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) file.seek(0) @@ -97,17 +158,12 @@ def test_submodel_template_serialization(self) -> None: # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - def test_full_empty_example_serialization(self) -> None: data = example_create_aas_mandatory_attributes.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) - with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) file.seek(0) @@ -116,17 +172,12 @@ def test_full_empty_example_serialization(self) -> None: # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - def test_missing_serialization(self) -> None: data = example_test_serialization.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) - with open(os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json'), 'r') as json_file: + with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) file.seek(0) @@ -134,8 +185,3 @@ def test_missing_serialization(self) -> None: # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) From 949a2436f2b37cf37461c036926616cf4eb231fc Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 16 Dec 2019 14:47:08 +0100 Subject: [PATCH 298/474] model: Fix incompliant inheritance of OperationVariable from SubmodelElement This includes reverting fc9f4ae6b3 and 1ba9dff08b and some modifications to JSON serialization. --- .../json/example_test_serialization.py | 51 +++---------------- 1 file changed, 6 insertions(+), 45 deletions(-) diff --git a/test/adapter/json/example_test_serialization.py b/test/adapter/json/example_test_serialization.py index 87b5416..8b0a3f5 100644 --- a/test/adapter/json/example_test_serialization.py +++ b/test/adapter/json/example_test_serialization.py @@ -228,58 +228,19 @@ def create_example_submodel() -> model.Submodel: kind=model.ModelingKind.INSTANCE) submodel_element_operation_variable_input = model.OperationVariable( - id_short='ExampleInputOperationVariable', - value=submodel_element_property, - category='PARAMETER', - description={'en-us': 'Example ExampleInputOperationVariable object', - 'de': 'Beispiel ExampleInputOperationVariable Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Operations/' - 'ExampleInputOperationVariable', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) + value=submodel_element_property) submodel_element_operation_variable_output = model.OperationVariable( - id_short='ExampleOutputOperationVariable', - value=submodel_element_property, - category='PARAMETER', - description={'en-us': 'Example OutputOperationVariable object', - 'de': 'Beispiel OutputOperationVariable Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Operations/' - 'ExampleOutputOperationVariable', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) + value=submodel_element_property) submodel_element_operation_variable_in_output = model.OperationVariable( - id_short='ExampleInOutputOperationVariable', - value=submodel_element_property, - category='PARAMETER', - description={'en-us': 'Example InOutputOperationVariable object', - 'de': 'Beispiel InOutputOperationVariable Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Operations/' - 'ExampleInOutputOperationVariable', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) + value=submodel_element_property) submodel_element_operation = model.Operation( id_short='ExampleOperation', - input_variable={submodel_element_operation_variable_input}, - output_variable={submodel_element_operation_variable_output}, - in_output_variable={submodel_element_operation_variable_in_output}, + input_variable=[submodel_element_operation_variable_input], + output_variable=[submodel_element_operation_variable_output], + in_output_variable=[submodel_element_operation_variable_in_output], category='PARAMETER', description={'en-us': 'Example Operation object', 'de': 'Beispiel Operation Element'}, From dc6dfae51d1c19bd9f7a854098da95c7d4da120f Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Fri, 20 Dec 2019 13:14:25 +0100 Subject: [PATCH 299/474] test: add tests for examples --- test/adapter/json/test_json_serialization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 31a08e3..8b26132 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -20,13 +20,14 @@ from aas.examples import example_create_aas, example_create_submodel_template, \ example_create_aas_mandatory_attributes +from test._helper.helpers import ExampleHelper from test.adapter.json import example_test_serialization JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') -class JsonSerializationDeserializationTest(unittest.TestCase): +class JsonSerializationDeserializationTest(ExampleHelper): def test_serialize_Object(self) -> None: test_object = model.Property("test_id_short", "string", category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) From 401c2b06ba231b17a2f0079a049bd7d8f7b15466 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 20 Dec 2019 13:35:51 +0100 Subject: [PATCH 300/474] adapter.json: Fix missing attributes of AAS and Asset in JSON deserialization --- test/adapter/json/test_json_serialization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 8b26132..e7ae88a 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -66,6 +66,7 @@ def test_full_example_serialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + self.assert_full_example(json_object_store) def test_submodel_template_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() From e32cd2f13a6e087d49c3152bfe1f58c7c037e13d Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 20 Dec 2019 14:24:06 +0100 Subject: [PATCH 301/474] adapter.json: Add parameter to deserialization's constructor methods --- .../adapter/json/test_json_deserialization.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index e61ccb4..1de7c18 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -159,3 +159,29 @@ def test_wrong_submodel_element_type(self) -> None: cap = parsed_data[0].submodel_element.pop() self.assertIsInstance(cap, model.Capability) self.assertEqual("TestCapability", cap.id_short) + + +class JsonDeserializationDerivingTest(unittest.TestCase): + def test_asset_constructor_overriding(self) -> None: + class EnhancedAsset(model.Asset): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.enhanced_attribute = "fancy!" + + class EnhancedAASDecoder(json_deserialization.AASFromJsonDecoder): + @classmethod + def _construct_asset(cls, dct): + return super()._construct_asset(dct, object_class=EnhancedAsset) + + data = """ + [ + { + "modelType": {"name": "Asset"}, + "identification": {"id": "https://acplt.org/Test_Asset", "idType": "IRI"}, + "kind": "Instance" + } + ]""" + parsed_data = json.loads(data, cls=EnhancedAASDecoder) + self.assertEqual(1, len(parsed_data)) + self.assertIsInstance(parsed_data[0], EnhancedAsset) + self.assertEqual(parsed_data[0].enhanced_attribute, "fancy!") From e12ed03a31424cc032f49db56de497e3f64fb51a Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 7 Jan 2020 13:13:21 +0100 Subject: [PATCH 302/474] aas.examples, test: restructuring and renaming of packages, modules and functions --- .../json/example_test_serialization.py | 447 ------------------ test/adapter/json/test_json_serialization.py | 70 +-- ...test_json_serialization_deserialization.py | 93 ++++ 3 files changed, 104 insertions(+), 506 deletions(-) delete mode 100644 test/adapter/json/example_test_serialization.py create mode 100644 test/adapter/json/test_json_serialization_deserialization.py diff --git a/test/adapter/json/example_test_serialization.py b/test/adapter/json/example_test_serialization.py deleted file mode 100644 index 8b0a3f5..0000000 --- a/test/adapter/json/example_test_serialization.py +++ /dev/null @@ -1,447 +0,0 @@ -# Copyright 2019 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -""" -Module for the creation of a object store with missing object attribute combination for testing the serialization - -""" -from aas import model -from typing import Any, Set - - -def create_full_example() -> model.DictObjectStore: - """ - creates an object store containing an example asset identification submodel, an example asset, an example submodel, - an example concept description and an example asset administration shell - - :return: object store - """ - obj_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - obj_store.add(create_example_asset()) - obj_store.add(create_example_submodel()) - obj_store.add(create_example_concept_description()) - obj_store.add(create_example_asset_administration_shell(create_example_concept_dictionary())) - return obj_store - - -def create_example_asset() -> model.Asset: - """ - creates an example asset which holds references to the example asset identification submodel - - :return: example asset - """ - asset = model.Asset( - kind=model.AssetKind.INSTANCE, - identification=model.Identifier(id_='https://acplt.org/Test_Asset', - id_type=model.IdentifierType.IRI), - id_short='Test_Asset', - category=None, - description={'en-us': 'An example asset for the test application', - 'de': 'Ein Beispiel-Asset für eine Test-Anwendung'}, - parent=None, - administration=model.AdministrativeInformation(), - data_specification={model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/DataSpecifications/AssetTypes/' - 'TestAsset', - id_type=model.KeyType.IRDI),))}, - asset_identification_model=None, - bill_of_material=None) - return asset - - -def create_example_submodel() -> model.Submodel: - """ - creates an example submodel containing all kind of SubmodelElement objects - - :return: example submodel - """ - formula = model.Formula() - - qualifier = model.Qualifier( - type_='http://acplt.org/Qualifier/ExampleQualifier', - value_type='string') - - submodel_element_property = model.Property( - id_short='ExampleProperty', - value_type='string', - value='exampleValue', - value_id=None, # TODO - category='CONSTANT', - description={'en-us': 'Example Property object', - 'de': 'Beispiel Property Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Properties/ExampleProperty', - id_type=model.KeyType.IRDI),)), - qualifier={formula, qualifier}, - kind=model.ModelingKind.INSTANCE) - - submodel_element_multi_language_property = model.MultiLanguageProperty( - id_short='ExampleMultiLanguageProperty', - value={'en-us': 'Example value of a MultiLanguageProperty element', - 'de': 'Beispielswert für ein MulitLanguageProperty-Element'}, - value_id=None, # TODO - category='CONSTANT', - description={'en-us': 'Example MultiLanguageProperty object', - 'de': 'Beispiel MulitLanguageProperty Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/MultiLanguageProperties/' - 'ExampleMultiLanguageProperty', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_range = model.Range( - id_short='ExampleRange', - value_type='int', - min_='0', - max_='100', - category='PARAMETER', - description={'en-us': 'Example Range object', - 'de': 'Beispiel Range Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Ranges/ExampleRange', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_blob = model.Blob( - id_short='ExampleBlob', - mime_type='application/pdf', - value=bytearray(b'\x01\x02\x03\x04\x05'), - category='PARAMETER', - description={'en-us': 'Example Blob object', - 'de': 'Beispiel Blob Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Blobs/ExampleBlob', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_file = model.File( - id_short='ExampleFile', - mime_type='application/pdf', - value='/TestFile.pdf', - category='PARAMETER', - description={'en-us': 'Example File object', - 'de': 'Beispiel File Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Files/ExampleFile', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_reference_element = model.ReferenceElement( - id_short='ExampleReferenceElement', - value=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, - local=True, - value='ExampleProperty', - id_type=model.KeyType.IDSHORT),), - model.Property), - category='PARAMETER', - description={'en-us': 'Example Reference Element object', - 'de': 'Beispiel Reference Element Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/ReferenceElements/ExampleReferenceElement', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_relationship_element = model.RelationshipElement( - id_short='ExampleRelationshipElement', - first=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, - local=True, - value='ExampleProperty', - id_type=model.KeyType.IDSHORT),), - model.Property), - second=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, - local=True, - value='ExampleProperty', - id_type=model.KeyType.IDSHORT),), - model.Property), - category='PARAMETER', - description={'en-us': 'Example RelationshipElement object', - 'de': 'Beispiel RelationshipElement Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/RelationshipElements/' - 'ExampleRelationshipElement', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_annotated_relationship_element = model.AnnotatedRelationshipElement( - id_short='ExampleAnnotatedRelationshipElement', - first=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, - local=True, - value='ExampleProperty', - id_type=model.KeyType.IDSHORT),), - model.Property), - second=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, - local=True, - value='ExampleProperty', - id_type=model.KeyType.IDSHORT),), - model.Property), - annotation={model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, - local=True, - value='ExampleProperty', - id_type=model.KeyType.IDSHORT),), - model.Property)}, - category='PARAMETER', - description={'en-us': 'Example AnnotatedRelationshipElement object', - 'de': 'Beispiel AnnotatedRelationshipElement Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/RelationshipElements/' - 'ExampleAnnotatedRelationshipElement', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_operation_variable_input = model.OperationVariable( - value=submodel_element_property) - - submodel_element_operation_variable_output = model.OperationVariable( - value=submodel_element_property) - - submodel_element_operation_variable_in_output = model.OperationVariable( - value=submodel_element_property) - - submodel_element_operation = model.Operation( - id_short='ExampleOperation', - input_variable=[submodel_element_operation_variable_input], - output_variable=[submodel_element_operation_variable_output], - in_output_variable=[submodel_element_operation_variable_in_output], - category='PARAMETER', - description={'en-us': 'Example Operation object', - 'de': 'Beispiel Operation Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Operations/' - 'ExampleOperation', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_capability = model.Capability( - id_short='ExampleCapability', - category='PARAMETER', - description={'en-us': 'Example Capability object', - 'de': 'Beispiel Capability Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Capabilities/' - 'ExampleCapability', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_basic_event = model.BasicEvent( - id_short='ExampleBasicEvent', - observed=model.AASReference((model.Key(type_=model.KeyElements.PROPERTY, - local=True, - value='ExampleProperty', - id_type=model.KeyType.IDSHORT),), - model.Property), - category='PARAMETER', - description={'en-us': 'Example BasicEvent object', - 'de': 'Beispiel BasicEvent Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Events/' - 'ExampleBasicEvent', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_submodel_element_collection_ordered = model.SubmodelElementCollectionOrdered( - id_short='ExampleSubmodelCollectionOrdered', - value=(submodel_element_property, - submodel_element_multi_language_property, - submodel_element_range), - category='PARAMETER', - description={'en-us': 'Example SubmodelElementCollectionOrdered object', - 'de': 'Beispiel SubmodelElementCollectionOrdered Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/SubmodelElementCollections/' - 'ExampleSubmodelElementCollectionOrdered', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel_element_submodel_element_collection_unordered = model.SubmodelElementCollectionUnordered( - id_short='ExampleSubmodelCollectionUnordered', - value=(submodel_element_blob, - submodel_element_file, - submodel_element_reference_element), - category='PARAMETER', - description={'en-us': 'Example SubmodelElementCollectionUnordered object', - 'de': 'Beispiel SubmodelElementCollectionUnordered Element'}, - parent=None, - data_specification=None, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/SubmodelElementCollections/' - 'ExampleSubmodelElementCollectionUnordered', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - - submodel = model.Submodel( - identification=model.Identifier(id_='https://acplt.org/Test_Submodel', - id_type=model.IdentifierType.IRI), - submodel_element=(submodel_element_relationship_element, - submodel_element_annotated_relationship_element, - submodel_element_operation, - submodel_element_capability, - submodel_element_basic_event, - submodel_element_submodel_element_collection_ordered, - submodel_element_submodel_element_collection_unordered), - id_short='TestSubmodel', - category=None, - description={'en-us': 'An example submodel for the test application', - 'de': 'Ein Beispiel-Teilmodell für eine Test-Anwendung'}, - parent=None, - administration=model.AdministrativeInformation(version='0.9', - revision='0'), - data_specification={model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/DataSpecifications/AssetTypes/' - 'TestAsset', - id_type=model.KeyType.IRDI),))}, - semantic_id=model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/SubmodelTemplates/' - 'ExampleSubmodel', - id_type=model.KeyType.IRDI),)), - qualifier=None, - kind=model.ModelingKind.INSTANCE) - return submodel - - -def create_example_concept_description() -> model.ConceptDescription: - """ - creates an example concept description - - :return: example concept description - """ - concept_description = model.ConceptDescription( - identification=model.Identifier(id_='https://acplt.org/Test_ConceptDescription', - id_type=model.IdentifierType.IRI), - is_case_of=None, - id_short='TestConceptDescription', - category=None, - description={'en-us': 'An example concept description for the test application', - 'de': 'Ein Beispiel-ConceptDescription für eine Test-Anwendung'}, - parent=None, - administration=model.AdministrativeInformation(version='0.9', - revision='0'), - data_specification={model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/DataSpecifications/' - 'ConceptDescriptions/TestConceptDescription', - id_type=model.KeyType.IRDI),))}) - return concept_description - - -def create_example_concept_dictionary() -> model.ConceptDictionary: - """ - creates an example concept dictionary containing an reference to the example concept description - - :return: example concept dictionary - """ - concept_dictionary = model.ConceptDictionary( - id_short='TestConceptDictionary', - category=None, - description={'en-us': 'An example concept dictionary for the test application', - 'de': 'Ein Beispiel-ConceptDictionary für eine Test-Anwendung'}, - parent=None, - concept_description={model.AASReference((model.Key(type_=model.KeyElements.CONCEPT_DESCRIPTION, - local=False, - value='https://acplt.org/Test_ConceptDescription', - id_type=model.KeyType.IRDI),), - model.ConceptDescription)}) - return concept_dictionary - - -def create_example_asset_administration_shell(concept_dictionary: model.ConceptDictionary) -> \ - model.AssetAdministrationShell: - """ - creates an example asset administration shell containing references to the example asset and example submodel - - :return: example asset administration shell - """ - view = model.View( - id_short='ExampleView', - contained_element={model.AASReference((model.Key(type_=model.KeyElements.SUBMODEL, - local=False, - value='https://acplt.org/Test_Submodel', - id_type=model.KeyType.IRDI),), - model.Submodel)}) - view_2 = model.View( - id_short='ExampleView2') - - asset_administration_shell = model.AssetAdministrationShell( - asset=model.AASReference((model.Key(type_=model.KeyElements.ASSET, - local=False, - value='https://acplt.org/Test_Asset', - id_type=model.KeyType.IRDI),), - model.Asset), - identification=model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', - id_type=model.IdentifierType.IRI), - id_short='TestAssetAdministrationShell', - category=None, - description={'en-us': 'An Example Asset Administration Shell for the test application', - 'de': 'Ein Beispiel-Verwaltungsschale für eine Test-Anwendung'}, - parent=None, - administration=model.AdministrativeInformation(version='0.9', - revision='0'), - data_specification=None, - security_=None, - submodel_={model.AASReference((model.Key(type_=model.KeyElements.SUBMODEL, - local=False, - value='https://acplt.org/Test_Submodel', - id_type=model.KeyType.IRDI),), - model.Submodel)}, - concept_dictionary=[concept_dictionary], - view=[view, view_2], - derived_from=None) - return asset_administration_shell diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index e7ae88a..a5e79b5 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -18,22 +18,20 @@ from aas.adapter.json import json_serialization, json_deserialization from jsonschema import validate # type: ignore -from aas.examples import example_create_aas, example_create_submodel_template, \ - example_create_aas_mandatory_attributes -from test._helper.helpers import ExampleHelper -from test.adapter.json import example_test_serialization - +from aas.helpers import example_aas, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas_missing_attributes +from test._helper.testCase_for_example_aas import ExampleHelper JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') -class JsonSerializationDeserializationTest(ExampleHelper): - def test_serialize_Object(self) -> None: +class JsonSerializationTest(ExampleHelper): + def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", "string", category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) - def test_validate_serialization(self) -> None: + def test_random_object_serialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) @@ -53,56 +51,10 @@ def test_validate_serialization(self) -> None: }, cls=json_serialization.AASToJsonEncoder) json_data_new = json.loads(json_data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) - - def test_full_example_serialization(self) -> None: - data = example_create_aas.create_full_example() - file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - self.assert_full_example(json_object_store) - - def test_submodel_template_serialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_create_submodel_template.create_example_submodel_template()) - file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - - def test_full_empty_example_serialization(self) -> None: - data = example_create_aas_mandatory_attributes.create_full_example() - file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - - def test_missing_serialization(self) -> None: - data = example_test_serialization.create_full_example() - file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - @unittest.skipUnless(os.path.exists(JSON_SCHEMA_FILE), "JSON Schema not found for validation") class JsonSerializationSchemaTest(unittest.TestCase): - def test_validate_serialization(self) -> None: + def test_random_object_serialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) @@ -132,7 +84,7 @@ def test_validate_serialization(self) -> None: validate(instance=json_data_new, schema=aas_schema) def test_full_example_serialization(self) -> None: - data = example_create_aas.create_full_example() + data = example_aas.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) @@ -147,7 +99,7 @@ def test_full_example_serialization(self) -> None: def test_submodel_template_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_create_submodel_template.create_example_submodel_template()) + data.add(example_submodel_template.create_example_submodel_template()) file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) @@ -161,7 +113,7 @@ def test_submodel_template_serialization(self) -> None: validate(instance=json_data, schema=aas_json_schema) def test_full_empty_example_serialization(self) -> None: - data = example_create_aas_mandatory_attributes.create_full_example() + data = example_aas_mandatory_attributes.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) @@ -175,7 +127,7 @@ def test_full_empty_example_serialization(self) -> None: validate(instance=json_data, schema=aas_json_schema) def test_missing_serialization(self) -> None: - data = example_test_serialization.create_full_example() + data = example_aas_missing_attributes.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py new file mode 100644 index 0000000..dd3e511 --- /dev/null +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -0,0 +1,93 @@ +# Copyright 2019 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import io +import unittest +import json +import os + +from aas import model +from aas.adapter.json import json_serialization, json_deserialization +from jsonschema import validate # type: ignore + +from aas.helpers import example_aas, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas_missing_attributes +from test._helper.testCase_for_example_aas import ExampleHelper + +JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') + + +class JsonSerializationDeserializationTest(ExampleHelper): + def test_random_object_serialization_deserialization(self) -> None: + asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_reference = model.AASReference(asset_key, model.Asset) + aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_identifier = submodel_key[0].get_identifier() + assert(submodel_identifier is not None) + submodel_reference = model.AASReference(submodel_key, model.Submodel) + submodel = model.Submodel(submodel_identifier) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + + # serialize object to json + json_data = json.dumps({ + 'assetAdministrationShells': [test_aas], + 'submodels': [submodel], + 'assets': [], + 'conceptDescriptions': [], + }, cls=json_serialization.AASToJsonEncoder) + json_data_new = json.loads(json_data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) + + def test_full_example_serialization_deserialization(self) -> None: + data = example_aas.create_full_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + self.assert_full_example(json_object_store) + + def test_submodel_template_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_submodel_template.create_example_submodel_template()) + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + def test_full_empty_example_serialization_deserialization(self) -> None: + data = example_aas_mandatory_attributes.create_full_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + def test_missing_serialization_deserialization(self) -> None: + data = example_aas_missing_attributes.create_full_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) From 65267d86a02d0d3147f1cfc7d235ee4934bbd9cd Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 8 Jan 2020 18:30:17 +0100 Subject: [PATCH 303/474] test: Prevent aasJSONSchema from being added to git repository --- test/adapter/json/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test/adapter/json/.gitignore diff --git a/test/adapter/json/.gitignore b/test/adapter/json/.gitignore new file mode 100644 index 0000000..785a074 --- /dev/null +++ b/test/adapter/json/.gitignore @@ -0,0 +1,2 @@ +# JSON schema should not be added to the Git Repository due to license concerns +aasJSONSchemaV2.0.json From febed7102b58153faf62360e9537cd2ec206e0ba Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 20 Jan 2020 09:54:01 +0100 Subject: [PATCH 304/474] Remove unused imports, make imports within the packages relative --- test/adapter/json/test_json_serialization.py | 2 +- test/adapter/json/test_json_serialization_deserialization.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index a5e79b5..c400f24 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -20,7 +20,7 @@ from aas.helpers import example_aas, example_submodel_template, \ example_aas_mandatory_attributes, example_aas_missing_attributes -from test._helper.testCase_for_example_aas import ExampleHelper +from ..._helper.testCase_for_example_aas import ExampleHelper JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index dd3e511..f0a168e 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -10,17 +10,15 @@ # specific language governing permissions and limitations under the License. import io -import unittest import json import os from aas import model from aas.adapter.json import json_serialization, json_deserialization -from jsonschema import validate # type: ignore from aas.helpers import example_aas, example_submodel_template, \ example_aas_mandatory_attributes, example_aas_missing_attributes -from test._helper.testCase_for_example_aas import ExampleHelper +from ..._helper.testCase_for_example_aas import ExampleHelper JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') From 82126f5e9ad92850b0f12d0dddf8a30e7526b0ef Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 20 Jan 2020 10:53:36 +0100 Subject: [PATCH 305/474] add test function for example_aas_mandatory_attributes --- .../test_json_serialization_deserialization.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index f0a168e..9f357f0 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -18,7 +18,8 @@ from aas.helpers import example_aas, example_submodel_template, \ example_aas_mandatory_attributes, example_aas_missing_attributes -from ..._helper.testCase_for_example_aas import ExampleHelper +from test._helper.testCase_for_example_aas import ExampleHelper +from test._helper.testCase_for_example_aas_mandatory_attributes import ExampleHelper as ExampleHelperMandatory JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -89,3 +90,16 @@ def test_missing_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + + +class JsonSerializationDeserializationTest2(ExampleHelperMandatory): + def test_example_mandatory_attributes_serialization_deserialization(self) -> None: + data = example_aas_mandatory_attributes.create_full_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + self.assert_full_example(json_object_store) \ No newline at end of file From 05eb8c22e75297e76dbdab25118483535c1fff2a Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 20 Jan 2020 11:09:43 +0100 Subject: [PATCH 306/474] fixed build error --- test/adapter/json/test_json_serialization_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 9f357f0..664a35f 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -102,4 +102,4 @@ def test_example_mandatory_attributes_serialization_deserialization(self) -> Non # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - self.assert_full_example(json_object_store) \ No newline at end of file + self.assert_full_example(json_object_store) From b5b08840054eb3fcf861c6cce2b767f60682ac53 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 8 Jan 2020 18:29:25 +0100 Subject: [PATCH 307/474] adapter.couchdb: Add first draft of CouchDB database store --- test/adapter/test_couchdb.py | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/adapter/test_couchdb.py diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py new file mode 100644 index 0000000..3b315f8 --- /dev/null +++ b/test/adapter/test_couchdb.py @@ -0,0 +1,45 @@ +# Copyright 2019 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import os + +from aas import model +from aas.adapter import couchdb + +from aas.examples import example_create_aas +from .._helper.helpers import ExampleHelper + + +JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') + + +class CouchDBTest(ExampleHelper): + def test_example_submodel_storing(self) -> None: + example_submodel = example_create_aas.create_example_submodel() + + # Create CouchDB store, login and check database + db = couchdb.CouchDBDatabase("http://localhost:5984", "aas_test") + db.login("aas_test", "aas_test") + # TODO create clean database before test + db.check_database() + + # Add exmaple submodel + db.add(example_submodel) + + # Restore example submodel and check data + submodel_restored = db.get_identifiable( + model.Identifier(id_='https://acplt.org/Test_Submodel', id_type=model.IdentifierType.IRI)) + + db.discard(submodel_restored) + db.logout() + + assert(isinstance(submodel_restored, model.Submodel)) + self.assert_example_submodel(submodel_restored) From f5b23561910fd9db1b07165ad02b787eb65f00e0 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Thu, 9 Jan 2020 17:03:59 +0100 Subject: [PATCH 308/474] adapter.couchdb: Add major parts of functionality and tests --- test/adapter/test_couchdb.py | 154 +++++++++++++++++++++++++++++++---- 1 file changed, 140 insertions(+), 14 deletions(-) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index 3b315f8..cbb2811 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -8,8 +8,13 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. - +import base64 +import concurrent.futures +import configparser import os +import unittest +import urllib.request +import urllib.error from aas import model from aas.adapter import couchdb @@ -18,28 +23,149 @@ from .._helper.helpers import ExampleHelper -JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') +TEST_CONFIG = configparser.ConfigParser() +TEST_CONFIG.read((os.path.join(os.path.dirname(__file__), "..", "test_config.default.ini"), + os.path.join(os.path.dirname(__file__), "..", "test_config.ini"))) -class CouchDBTest(ExampleHelper): - def test_example_submodel_storing(self) -> None: - example_submodel = example_create_aas.create_example_submodel() +# Check if CouchDB database is avalable. Otherwise, skip tests. +try: + request = urllib.request.Request( + "{}/{}".format(TEST_CONFIG['couchdb']['url'], TEST_CONFIG['couchdb']['database']), + headers={ + 'Authorization': 'Basic %s' % base64.b64encode( + ('%s:%s' % (TEST_CONFIG['couchdb']['user'], TEST_CONFIG['couchdb']['password'])) + .encode('ascii')).decode("ascii") + }, + method='HEAD') + urllib.request.urlopen(request) + COUCHDB_OKAY = True + COUCHDB_ERROR = None +except urllib.error.URLError as e: + COUCHDB_OKAY = False + COUCHDB_ERROR = e + +@unittest.skipUnless(COUCHDB_OKAY, "No CouchDB is reachable at {}/{}: {}".format(TEST_CONFIG['couchdb']['url'], + TEST_CONFIG['couchdb']['database'], + COUCHDB_ERROR)) +class CouchDBTest(ExampleHelper): + def setUp(self) -> None: # Create CouchDB store, login and check database - db = couchdb.CouchDBDatabase("http://localhost:5984", "aas_test") - db.login("aas_test", "aas_test") + self.db = couchdb.CouchDBObjectStore(TEST_CONFIG['couchdb']['url'], TEST_CONFIG['couchdb']['database']) + self.db.login(TEST_CONFIG['couchdb']['user'], TEST_CONFIG['couchdb']['password']) # TODO create clean database before test - db.check_database() + self.db.check_database() + + def tearDown(self) -> None: + self.db.clear() + self.db.logout() + + def test_example_submodel_storing(self) -> None: + example_submodel = example_create_aas.create_example_submodel() # Add exmaple submodel - db.add(example_submodel) + self.db.add(example_submodel) + self.assertEqual(1, len(self.db)) + self.assertIn(example_submodel, self.db) # Restore example submodel and check data - submodel_restored = db.get_identifiable( + submodel_restored = self.db.get_identifiable( model.Identifier(id_='https://acplt.org/Test_Submodel', id_type=model.IdentifierType.IRI)) - - db.discard(submodel_restored) - db.logout() - assert(isinstance(submodel_restored, model.Submodel)) self.assert_example_submodel(submodel_restored) + + # Delete example submodel + self.db.discard(submodel_restored) + self.assertNotIn(example_submodel, self.db) + + def test_iterating(self) -> None: + example_data = example_create_aas.create_full_example() + + # Add all objects + for item in example_data: + self.db.add(item) + + self.assertEqual(6, len(self.db)) + + # Iterate objects, add them to a DictObjectStore and check them + retrieved_data_store: model.registry.DictObjectStore[model.Identifiable] = model.registry.DictObjectStore() + for item in self.db: + retrieved_data_store.add(item) + self.assert_full_example(retrieved_data_store) + + def test_parallel_iterating(self) -> None: + example_data = example_create_aas.create_full_example() + ids = [item.identification for item in example_data] + + # Add objects via thread pool executor + with concurrent.futures.ThreadPoolExecutor() as pool: + result = pool.map(self.db.add, example_data) + list(result) # Iterate Executor result to raise exceptions + + self.assertEqual(6, len(self.db)) + + # Retrieve objects via thread pool executor + with concurrent.futures.ThreadPoolExecutor() as pool: + retrieved_objects = pool.map(self.db.get_identifiable, ids) + + retrieved_data_store: model.registry.DictObjectStore[model.Identifiable] = model.registry.DictObjectStore() + for item in retrieved_objects: + retrieved_data_store.add(item) + self.assertEqual(6, len(retrieved_data_store)) + self.assert_full_example(retrieved_data_store) + + # Delete objects via thread pool executor + with concurrent.futures.ThreadPoolExecutor() as pool: + result = pool.map(self.db.discard, example_data) + list(result) # Iterate Executor result to raise exceptions + + self.assertEqual(0, len(self.db)) + + def test_key_errors(self) -> None: + # Double adding an object should raise a KeyError + example_submodel = example_create_aas.create_example_submodel() + self.db.add(example_submodel) + with self.assertRaises(KeyError): + self.db.add(example_submodel) + + # Querying a deleted object should raise a KeyError + self.db.get_identifiable(model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) + self.db.discard(example_submodel) + with self.assertRaises(KeyError): + self.db.get_identifiable(model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) + + def test_editing(self) -> None: + example_submodel = example_create_aas.create_example_submodel() + self.db.add(example_submodel) + + # Retrieve submodel from database and change ExampleCapability's semanticId + submodel = self.db.get_identifiable( + model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) + assert(isinstance(submodel, couchdb.CouchDBSubmodel)) + capability = submodel.submodel_element.get_referable('ExampleCapability') + capability.semantic_id = model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, + local=False, + value='http://acplt.org/Capabilities/AnotherCapability', + id_type=model.KeyType.IRDI),)) + + # Commit changes + submodel.commit_changes() + + # Change ExampleSubmodelCollectionOrdered's description + collection = submodel.submodel_element.get_referable('ExampleSubmodelCollectionOrdered') + collection.description['de'] = "Eine sehr wichtige Sammlung von Elementen" # type: ignore + + # Commit changes + submodel.commit_changes() + + # Check version in database + new_submodel = self.db.get_identifiable( + model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) + assert(isinstance(new_submodel, couchdb.CouchDBSubmodel)) + capability = new_submodel.submodel_element.get_referable('ExampleCapability') + assert(isinstance(capability, model.Capability)) + self.assertEqual('http://acplt.org/Capabilities/AnotherCapability', + capability.semantic_id.key[0].value) # type: ignore + collection = new_submodel.submodel_element.get_referable('ExampleSubmodelCollectionOrdered') + self.assertEqual("Eine sehr wichtige Sammlung von Elementen", collection.description['de']) # type: ignore From ec147e85f1127d5ee416a4dcbeb4dba5aa032513 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 10 Jan 2020 09:57:56 +0100 Subject: [PATCH 309/474] test: Add CI test for adapter.couchdb using GitLab CI services This includes adding the helper script test/_helper/setup_testdb.py for configuring the CouchDB server. --- test/adapter/test_couchdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index cbb2811..2e75ca1 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -18,8 +18,8 @@ from aas import model from aas.adapter import couchdb - from aas.examples import example_create_aas + from .._helper.helpers import ExampleHelper From 5a4ed08b0ba86c1fca25a2518e104abbf1003236 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 10 Jan 2020 14:44:32 +0100 Subject: [PATCH 310/474] adapter.couchdb: Improve documentation and exception handling --- test/adapter/test_couchdb.py | 37 ++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index 2e75ca1..3ce8f44 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -11,6 +11,7 @@ import base64 import concurrent.futures import configparser +import copy import os import unittest import urllib.request @@ -54,7 +55,6 @@ def setUp(self) -> None: # Create CouchDB store, login and check database self.db = couchdb.CouchDBObjectStore(TEST_CONFIG['couchdb']['url'], TEST_CONFIG['couchdb']['database']) self.db.login(TEST_CONFIG['couchdb']['user'], TEST_CONFIG['couchdb']['password']) - # TODO create clean database before test self.db.check_database() def tearDown(self) -> None: @@ -130,11 +130,44 @@ def test_key_errors(self) -> None: self.db.add(example_submodel) # Querying a deleted object should raise a KeyError - self.db.get_identifiable(model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) + retrieved_submodel = self.db.get_identifiable( + model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) self.db.discard(example_submodel) with self.assertRaises(KeyError): self.db.get_identifiable(model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) + # Double deleting should also raise a KeyError + with self.assertRaises(KeyError): + self.db.discard(retrieved_submodel) + + def test_conflict_errors(self) -> None: + # Preperation: add object and retrieve it from the database + example_submodel = example_create_aas.create_example_submodel() + self.db.add(example_submodel) + retrieved_submodel = self.db.get_identifiable( + model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) + + # Simulate a concurrent modification + remote_modified_submodel = copy.copy(retrieved_submodel) + remote_modified_submodel.id_short = "newIdShort" + remote_modified_submodel.commit_changes() + + # Committing changes to the retrieved object should now raise a conflict error + retrieved_submodel.id_short = "myOtherNewIdShort" + with self.assertRaises(couchdb.CouchDBConflictError): + retrieved_submodel.commit_changes() + + # Deleting the submodel with safe_delete should also raise a conflict error. Deletion without safe_delete should + # work + with self.assertRaises(couchdb.CouchDBConflictError): + self.db.discard(retrieved_submodel, True) + self.db.discard(retrieved_submodel, False) + self.assertEqual(0, len(self.db)) + + # Committing after delition should also raise a conflict error + with self.assertRaises(couchdb.CouchDBConflictError): + retrieved_submodel.commit_changes() + def test_editing(self) -> None: example_submodel = example_create_aas.create_example_submodel() self.db.add(example_submodel) From 66db1b1367bd2a304e619515592f7c3b819fd7b2 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 17 Jan 2020 16:07:57 +0100 Subject: [PATCH 311/474] adapter.couchdb: Add more docstrings, remove TODOs --- test/adapter/test_couchdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index 3ce8f44..c80e72c 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -164,7 +164,7 @@ def test_conflict_errors(self) -> None: self.db.discard(retrieved_submodel, False) self.assertEqual(0, len(self.db)) - # Committing after delition should also raise a conflict error + # Committing after deletion should also raise a conflict error with self.assertRaises(couchdb.CouchDBConflictError): retrieved_submodel.commit_changes() From 15642efb1096560461c3aff348cefed908593e17 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 20 Jan 2020 15:29:48 +0100 Subject: [PATCH 312/474] test._helper, adapter.json: add test for example_aas_missing_attributes and fix view deserialisation in json --- .../json/test_json_serialization_deserialization.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 664a35f..443a7c9 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -20,6 +20,7 @@ example_aas_mandatory_attributes, example_aas_missing_attributes from test._helper.testCase_for_example_aas import ExampleHelper from test._helper.testCase_for_example_aas_mandatory_attributes import ExampleHelper as ExampleHelperMandatory +from test._helper.testCase_for_example_aas_missing_attributes import ExampleHelper as ExampleHelperMissing JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -103,3 +104,15 @@ def test_example_mandatory_attributes_serialization_deserialization(self) -> Non file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) self.assert_full_example(json_object_store) + + +class JsonSerializationDeserializationTest3(ExampleHelperMissing): + def test_example_missing_attributes_serialization_deserialization(self) -> None: + data = example_aas_missing_attributes.create_full_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + self.assert_full_example(json_object_store) From 62175d08c778dc92fce3eea7fd1e2cc391674fec Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 20 Jan 2020 15:34:55 +0100 Subject: [PATCH 313/474] test, aas: some replacement and renaming of files --- test/adapter/json/test_json_serialization.py | 6 +++--- ...test_json_serialization_deserialization.py | 4 ++-- test/adapter/test_couchdb.py | 20 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index c400f24..60c406f 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -15,11 +15,11 @@ import os from aas import model -from aas.adapter.json import json_serialization, json_deserialization +from aas.adapter.json import json_serialization from jsonschema import validate # type: ignore -from aas.helpers import example_aas, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas_missing_attributes +from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas from ..._helper.testCase_for_example_aas import ExampleHelper JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 443a7c9..8e0e3e9 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -16,8 +16,8 @@ from aas import model from aas.adapter.json import json_serialization, json_deserialization -from aas.helpers import example_aas, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas_missing_attributes +from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas from test._helper.testCase_for_example_aas import ExampleHelper from test._helper.testCase_for_example_aas_mandatory_attributes import ExampleHelper as ExampleHelperMandatory from test._helper.testCase_for_example_aas_missing_attributes import ExampleHelper as ExampleHelperMissing diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index c80e72c..7efc355 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -19,9 +19,9 @@ from aas import model from aas.adapter import couchdb -from aas.examples import example_create_aas +from aas.examples.data import example_aas -from .._helper.helpers import ExampleHelper +from .._helper.testCase_for_example_aas import ExampleHelper TEST_CONFIG = configparser.ConfigParser() @@ -62,7 +62,7 @@ def tearDown(self) -> None: self.db.logout() def test_example_submodel_storing(self) -> None: - example_submodel = example_create_aas.create_example_submodel() + example_submodel = example_aas.create_example_submodel() # Add exmaple submodel self.db.add(example_submodel) @@ -80,7 +80,7 @@ def test_example_submodel_storing(self) -> None: self.assertNotIn(example_submodel, self.db) def test_iterating(self) -> None: - example_data = example_create_aas.create_full_example() + example_data = example_aas.create_full_example() # Add all objects for item in example_data: @@ -89,13 +89,13 @@ def test_iterating(self) -> None: self.assertEqual(6, len(self.db)) # Iterate objects, add them to a DictObjectStore and check them - retrieved_data_store: model.registry.DictObjectStore[model.Identifiable] = model.registry.DictObjectStore() + retrieved_data_store: model.provider.DictObjectStore[model.Identifiable] = model.provider.DictObjectStore() for item in self.db: retrieved_data_store.add(item) self.assert_full_example(retrieved_data_store) def test_parallel_iterating(self) -> None: - example_data = example_create_aas.create_full_example() + example_data = example_aas.create_full_example() ids = [item.identification for item in example_data] # Add objects via thread pool executor @@ -109,7 +109,7 @@ def test_parallel_iterating(self) -> None: with concurrent.futures.ThreadPoolExecutor() as pool: retrieved_objects = pool.map(self.db.get_identifiable, ids) - retrieved_data_store: model.registry.DictObjectStore[model.Identifiable] = model.registry.DictObjectStore() + retrieved_data_store: model.provider.DictObjectStore[model.Identifiable] = model.provider.DictObjectStore() for item in retrieved_objects: retrieved_data_store.add(item) self.assertEqual(6, len(retrieved_data_store)) @@ -124,7 +124,7 @@ def test_parallel_iterating(self) -> None: def test_key_errors(self) -> None: # Double adding an object should raise a KeyError - example_submodel = example_create_aas.create_example_submodel() + example_submodel = example_aas.create_example_submodel() self.db.add(example_submodel) with self.assertRaises(KeyError): self.db.add(example_submodel) @@ -142,7 +142,7 @@ def test_key_errors(self) -> None: def test_conflict_errors(self) -> None: # Preperation: add object and retrieve it from the database - example_submodel = example_create_aas.create_example_submodel() + example_submodel = example_aas.create_example_submodel() self.db.add(example_submodel) retrieved_submodel = self.db.get_identifiable( model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) @@ -169,7 +169,7 @@ def test_conflict_errors(self) -> None: retrieved_submodel.commit_changes() def test_editing(self) -> None: - example_submodel = example_create_aas.create_example_submodel() + example_submodel = example_aas.create_example_submodel() self.db.add(example_submodel) # Retrieve submodel from database and change ExampleCapability's semanticId From 4e2042b432710efd2949a73eb8fd9bceb9d038f0 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 20 Jan 2020 16:29:14 +0100 Subject: [PATCH 314/474] test: add test function for example_submodel_template --- .../test_json_serialization_deserialization.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 8e0e3e9..eaf509e 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -21,6 +21,7 @@ from test._helper.testCase_for_example_aas import ExampleHelper from test._helper.testCase_for_example_aas_mandatory_attributes import ExampleHelper as ExampleHelperMandatory from test._helper.testCase_for_example_aas_missing_attributes import ExampleHelper as ExampleHelperMissing +from test._helper.testCase_for_example_submodel_template import ExampleHelper as ExampleHelperSubmodelTemplate JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -116,3 +117,16 @@ def test_example_missing_attributes_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) self.assert_full_example(json_object_store) + + +class JsonSerializationDeserializationTest4(ExampleHelperSubmodelTemplate): + def test_example_submodel_template_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_submodel_template.create_example_submodel_template()) + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + self.assert_full_example(json_object_store) From 167d3b99bdb81292b5aa778fb5cc2789d9964922 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 20 Jan 2020 17:46:47 +0100 Subject: [PATCH 315/474] aas, test: add test function for example concept description --- .../test_json_serialization_deserialization.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index eaf509e..db30705 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -17,11 +17,12 @@ from aas.adapter.json import json_serialization, json_deserialization from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas + example_aas_mandatory_attributes, example_aas, example_concept_description from test._helper.testCase_for_example_aas import ExampleHelper from test._helper.testCase_for_example_aas_mandatory_attributes import ExampleHelper as ExampleHelperMandatory from test._helper.testCase_for_example_aas_missing_attributes import ExampleHelper as ExampleHelperMissing from test._helper.testCase_for_example_submodel_template import ExampleHelper as ExampleHelperSubmodelTemplate +from test._helper.testCase_for_example_concept_description import ExampleHelper as ExampleHelperConceptDescription JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -130,3 +131,16 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) self.assert_full_example(json_object_store) + + +class JsonSerializationDeserializationTest5(ExampleHelperConceptDescription): + def test_example_iec61360_concept_description_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_concept_description.create_iec61360_concept_description()) + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization + # module + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + self.assert_full_example(json_object_store) From 5f8d9c745b65d11a4a193f1b2a9047099a538c5c Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 21 Jan 2020 10:53:11 +0100 Subject: [PATCH 316/474] aas, test: restructure of test functions --- test/adapter/json/test_json_serialization.py | 3 +- ...test_json_serialization_deserialization.py | 101 ++++++++++-------- test/adapter/test_couchdb.py | 11 +- 3 files changed, 62 insertions(+), 53 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 60c406f..b5d677b 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -20,12 +20,11 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas -from ..._helper.testCase_for_example_aas import ExampleHelper JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') -class JsonSerializationTest(ExampleHelper): +class JsonSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", "string", category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index db30705..872dc2a 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -12,22 +12,21 @@ import io import json import os +import unittest from aas import model from aas.adapter.json import json_serialization, json_deserialization from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description -from test._helper.testCase_for_example_aas import ExampleHelper -from test._helper.testCase_for_example_aas_mandatory_attributes import ExampleHelper as ExampleHelperMandatory -from test._helper.testCase_for_example_aas_missing_attributes import ExampleHelper as ExampleHelperMissing -from test._helper.testCase_for_example_submodel_template import ExampleHelper as ExampleHelperSubmodelTemplate -from test._helper.testCase_for_example_concept_description import ExampleHelper as ExampleHelperConceptDescription +from test._helper import testCase_for_example_aas, testCase_for_example_aas_mandatory_attributes, \ + testCase_for_example_aas_missing_attributes, testCase_for_example_concept_description, \ + testCase_for_example_submodel_template JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') -class JsonSerializationDeserializationTest(ExampleHelper): +class JsonSerializationDeserializationTest(unittest.TestCase): def test_random_object_serialization_deserialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) @@ -52,7 +51,7 @@ def test_random_object_serialization_deserialization(self) -> None: # module json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) - def test_full_example_serialization_deserialization(self) -> None: + def test_example_serialization_deserialization(self) -> None: data = example_aas.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) @@ -61,20 +60,11 @@ def test_full_example_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - self.assert_full_example(json_object_store) + testCase_for_example_aas.assert_full_example(self, json_object_store) - def test_submodel_template_serialization_deserialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_submodel_template.create_example_submodel_template()) - file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - def test_full_empty_example_serialization_deserialization(self) -> None: +class JsonSerializationDeserializationTest2(unittest.TestCase): + def test_example_mandatory_attributes_serialization_deserialization(self) -> None: data = example_aas_mandatory_attributes.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) @@ -83,64 +73,85 @@ def test_full_empty_example_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - - def test_missing_serialization_deserialization(self) -> None: - data = example_aas_missing_attributes.create_full_example() - file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + testCase_for_example_aas_mandatory_attributes.assert_full_example(self, json_object_store) -class JsonSerializationDeserializationTest2(ExampleHelperMandatory): - def test_example_mandatory_attributes_serialization_deserialization(self) -> None: - data = example_aas_mandatory_attributes.create_full_example() +class JsonSerializationDeserializationTest3(unittest.TestCase): + def test_example_missing_attributes_serialization_deserialization(self) -> None: + data = example_aas_missing_attributes.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - self.assert_full_example(json_object_store) + testCase_for_example_aas_missing_attributes.assert_full_example(self, json_object_store) -class JsonSerializationDeserializationTest3(ExampleHelperMissing): - def test_example_missing_attributes_serialization_deserialization(self) -> None: - data = example_aas_missing_attributes.create_full_example() +class JsonSerializationDeserializationTest4(unittest.TestCase): + def test_example_submodel_template_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_submodel_template.create_example_submodel_template()) file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - self.assert_full_example(json_object_store) + testCase_for_example_submodel_template.assert_full_example(self, json_object_store) -class JsonSerializationDeserializationTest4(ExampleHelperSubmodelTemplate): - def test_example_submodel_template_serialization_deserialization(self) -> None: +class JsonSerializationDeserializationTest5(unittest.TestCase): + def test_example_iec61360_concept_description_serialization_deserialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_submodel_template.create_example_submodel_template()) + data.add(example_concept_description.create_iec61360_concept_description()) file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - self.assert_full_example(json_object_store) + testCase_for_example_concept_description.assert_full_example(self, json_object_store) -class JsonSerializationDeserializationTest5(ExampleHelperConceptDescription): - def test_example_iec61360_concept_description_serialization_deserialization(self) -> None: +class JsonSerializationDeserializationTest6(unittest.TestCase): + def test_all_examples_serialization_deserialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + # add object from example_aas: + data.add(example_aas.create_example_asset_identification_submodel()) + data.add(example_aas.create_example_bill_of_material_submodel()) + data.add(example_aas.create_example_asset()) + data.add(example_aas.create_example_submodel()) + data.add(example_aas.create_example_concept_description()) + data.add(example_aas.create_example_asset_administration_shell( + example_aas.create_example_concept_dictionary())) + # add objects from example_aas_mandatory_attributes: + data.add(example_aas_mandatory_attributes.create_example_asset()) + data.add(example_aas_mandatory_attributes.create_example_submodel()) + data.add(example_aas_mandatory_attributes.create_example_empty_submodel()) + data.add(example_aas_mandatory_attributes.create_example_concept_description()) + data.add(example_aas_mandatory_attributes.create_example_asset_administration_shell( + example_aas_mandatory_attributes.create_example_concept_dictionary())) + data.add(example_aas_mandatory_attributes.create_example_empty_asset_administration_shell()) + # add objects from example_aas_missing_attributes: + data.add(example_aas_missing_attributes.create_example_asset()) + data.add(example_aas_missing_attributes.create_example_submodel()) + data.add(example_aas_missing_attributes.create_example_concept_description()) + data.add(example_aas_missing_attributes.create_example_asset_administration_shell( + example_aas_missing_attributes.create_example_concept_dictionary())) + # add objects from example_concept_description: data.add(example_concept_description.create_iec61360_concept_description()) + # add objects from example_submodel_template: + data.add(example_submodel_template.create_example_submodel_template()) + file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - self.assert_full_example(json_object_store) + testCase_for_example_aas.assert_full_example(self, json_object_store, False) + testCase_for_example_aas_mandatory_attributes.assert_full_example(self, json_object_store, False) + testCase_for_example_aas_missing_attributes.assert_full_example(self, json_object_store, False) + testCase_for_example_concept_description.assert_full_example(self, json_object_store, False) + testCase_for_example_submodel_template.assert_full_example(self, json_object_store, False) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index 7efc355..ad1abe8 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -20,8 +20,7 @@ from aas import model from aas.adapter import couchdb from aas.examples.data import example_aas - -from .._helper.testCase_for_example_aas import ExampleHelper +from test._helper.testCase_for_example_aas import * TEST_CONFIG = configparser.ConfigParser() @@ -50,7 +49,7 @@ @unittest.skipUnless(COUCHDB_OKAY, "No CouchDB is reachable at {}/{}: {}".format(TEST_CONFIG['couchdb']['url'], TEST_CONFIG['couchdb']['database'], COUCHDB_ERROR)) -class CouchDBTest(ExampleHelper): +class CouchDBTest(unittest.TestCase): def setUp(self) -> None: # Create CouchDB store, login and check database self.db = couchdb.CouchDBObjectStore(TEST_CONFIG['couchdb']['url'], TEST_CONFIG['couchdb']['database']) @@ -73,7 +72,7 @@ def test_example_submodel_storing(self) -> None: submodel_restored = self.db.get_identifiable( model.Identifier(id_='https://acplt.org/Test_Submodel', id_type=model.IdentifierType.IRI)) assert(isinstance(submodel_restored, model.Submodel)) - self.assert_example_submodel(submodel_restored) + assert_example_submodel(self, submodel_restored) # Delete example submodel self.db.discard(submodel_restored) @@ -92,7 +91,7 @@ def test_iterating(self) -> None: retrieved_data_store: model.provider.DictObjectStore[model.Identifiable] = model.provider.DictObjectStore() for item in self.db: retrieved_data_store.add(item) - self.assert_full_example(retrieved_data_store) + assert_full_example(self, retrieved_data_store) def test_parallel_iterating(self) -> None: example_data = example_aas.create_full_example() @@ -113,7 +112,7 @@ def test_parallel_iterating(self) -> None: for item in retrieved_objects: retrieved_data_store.add(item) self.assertEqual(6, len(retrieved_data_store)) - self.assert_full_example(retrieved_data_store) + assert_full_example(self, retrieved_data_store) # Delete objects via thread pool executor with concurrent.futures.ThreadPoolExecutor() as pool: From 7488c735dda411c977142224e94ba4d78bb3720c Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Sat, 8 Feb 2020 19:38:14 +0100 Subject: [PATCH 317/474] test.xml: add initial files --- test/adapter/xml/__init__.py | 0 test/adapter/xml/test_xml_serialization.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/adapter/xml/__init__.py create mode 100644 test/adapter/xml/test_xml_serialization.py diff --git a/test/adapter/xml/__init__.py b/test/adapter/xml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py new file mode 100644 index 0000000..e69de29 From 7dd984e7b081f265e5fe296fbfe3e7ae25a99a6e Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Sat, 8 Feb 2020 20:25:49 +0100 Subject: [PATCH 318/474] test.adapter.xml: add .gitignore file to ignore AAS.xsd --- test/adapter/xml/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test/adapter/xml/.gitignore diff --git a/test/adapter/xml/.gitignore b/test/adapter/xml/.gitignore new file mode 100644 index 0000000..353aee2 --- /dev/null +++ b/test/adapter/xml/.gitignore @@ -0,0 +1,2 @@ +# XML schema should not be added to the Git repository due to license concerns +AAS.xsd \ No newline at end of file From f3b8cd5baad98117dd9a6ceb03b891c7da5a1c85 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Sat, 8 Feb 2020 20:36:55 +0100 Subject: [PATCH 319/474] test.adapter.xml: add the other two schemas to the .gitignore --- test/adapter/xml/.gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/adapter/xml/.gitignore b/test/adapter/xml/.gitignore index 353aee2..a1a70f3 100644 --- a/test/adapter/xml/.gitignore +++ b/test/adapter/xml/.gitignore @@ -1,2 +1,4 @@ # XML schema should not be added to the Git repository due to license concerns -AAS.xsd \ No newline at end of file +AAS.xsd +AAS_ABAC.xsd +IEC61360.xsd \ No newline at end of file From 842875ccd2dc7b2ba25e9a093fd622507fdc1a6e Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Sat, 8 Feb 2020 21:29:55 +0100 Subject: [PATCH 320/474] test.adapter.xml: add test_xml_serialization.py --- test/adapter/xml/test_xml_serialization.py | 135 +++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index e69de29..7406b55 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -0,0 +1,135 @@ +# Copyright 2019 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +import io +import unittest +from lxml import etree # type: ignore # todo: put lxml in project requirements? +import os + +from aas import model +from aas.adapter.xml import xml_serialization + +from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas + +XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'AAS.xsd') + + +class XMLSerializationTest(unittest.TestCase): + def test_serialize_object(self) -> None: + test_object = model.Property("test_id_short", + "string", + category="PARAMETER", + description={"en-us": "Germany", "de": "Deutschland"}) + xml_data = xml_serialization.property_to_xml(test_object, xml_serialization.NS_AAS, "test_object") + # todo: is this a correct way to test it? + + def test_random_object_serialization(self) -> None: + asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_reference = model.AASReference(asset_key, model.Asset) + aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_identifier = submodel_key[0].get_identifier() + assert (submodel_identifier is not None) + submodel_reference = model.AASReference(submodel_key, model.Submodel) + submodel = model.Submodel(submodel_identifier) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + + test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + test_data.add(test_aas) + test_data.add(submodel) + + test_file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=test_file, data=test_data) + + +@unittest.skipUnless(os.path.exists(XML_SCHEMA_FILE), "JSON Schema not found for validation") +class JsonSerializationSchemaTest(unittest.TestCase): + def test_random_object_serialization(self) -> None: + asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_reference = model.AASReference(asset_key, model.Asset) + aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_identifier = submodel_key[0].get_identifier() + assert(submodel_identifier is not None) + submodel_reference = model.AASReference(submodel_key, model.Submodel) + # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which + # must be a Reference. (This seems to be a bug in the JSONSchema.) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + + # serialize object to xml + test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + test_data.add(test_aas) + test_data.add(submodel) + + test_file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=test_file, data=test_data) + + # load schema + aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) + + # validate serialization against schema + parser = etree.XMLParser(schema=aas_schema) + test_file.seek(0) + root = etree.parse(test_file, parser=parser) + + def test_full_example_serialization(self) -> None: + data = example_aas.create_full_example() + file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=file, data=data) + + # load schema + aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) + + # validate serialization against schema + parser = etree.XMLParser(schema=aas_schema) + file.seek(0) + root = etree.parse(file, parser=parser) + + def test_submodel_template_serialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_submodel_template.create_example_submodel_template()) + file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=file, data=data) + + # load schema + aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) + + # validate serialization against schema + parser = etree.XMLParser(schema=aas_schema) + file.seek(0) + root = etree.parse(file, parser=parser) + + def test_full_empty_example_serialization(self) -> None: + data = example_aas_mandatory_attributes.create_full_example() + file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=file, data=data) + + # load schema + aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) + + # validate serialization against schema + parser = etree.XMLParser(schema=aas_schema) + file.seek(0) + root = etree.parse(file, parser=parser) + + def test_missing_serialization(self) -> None: + data = example_aas_missing_attributes.create_full_example() + file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=file, data=data) + + # load schema + aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) + + # validate serialization against schema + parser = etree.XMLParser(schema=aas_schema) + file.seek(0) + root = etree.parse(file, parser=parser) From 3691ca5e53d571a32d1704eb11ddb748505c8419 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Sat, 22 Feb 2020 11:05:23 +0100 Subject: [PATCH 321/474] test.adapter.xml: change name of class XMLSerializationSchemaTest was wrongly named JSONSerializationSchemaTest beforehand (due to copy-paste from json test) --- test/adapter/xml/test_xml_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 7406b55..0fa660d 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -51,7 +51,7 @@ def test_random_object_serialization(self) -> None: @unittest.skipUnless(os.path.exists(XML_SCHEMA_FILE), "JSON Schema not found for validation") -class JsonSerializationSchemaTest(unittest.TestCase): +class XMLSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) From 27c8789ac2009316dc5909cd59de28fc6e39df75 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Sat, 22 Feb 2020 12:42:16 +0100 Subject: [PATCH 322/474] test.adapter.xml: remove json specific comments from serialization test --- test/adapter/xml/test_xml_serialization.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 0fa660d..5c731e3 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -50,7 +50,7 @@ def test_random_object_serialization(self) -> None: xml_serialization.write_aas_xml_file(file=test_file, data=test_data) -@unittest.skipUnless(os.path.exists(XML_SCHEMA_FILE), "JSON Schema not found for validation") +@unittest.skipUnless(os.path.exists(XML_SCHEMA_FILE), "XML Schema not found for validation") class XMLSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) @@ -60,8 +60,6 @@ def test_random_object_serialization(self) -> None: submodel_identifier = submodel_key[0].get_identifier() assert(submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) - # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which - # must be a Reference. (This seems to be a bug in the JSONSchema.) submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) From f3aa08dc293418a5bf4eb9e68a0aa978483b6511 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 23 Jan 2020 14:03:22 +0100 Subject: [PATCH 323/474] AASDataChecker: create AASDataChecker for easier testing of examples --- ...test_json_serialization_deserialization.py | 33 ++++++++++++------- test/adapter/test_couchdb.py | 6 ++-- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 872dc2a..226faba 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -22,7 +22,7 @@ from test._helper import testCase_for_example_aas, testCase_for_example_aas_mandatory_attributes, \ testCase_for_example_aas_missing_attributes, testCase_for_example_concept_description, \ testCase_for_example_submodel_template - +from aas.examples.data._helper import AASDataChecker JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -60,7 +60,8 @@ def test_example_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - testCase_for_example_aas.assert_full_example(self, json_object_store) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + testCase_for_example_aas.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest2(unittest.TestCase): @@ -73,7 +74,8 @@ def test_example_mandatory_attributes_serialization_deserialization(self) -> Non # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - testCase_for_example_aas_mandatory_attributes.assert_full_example(self, json_object_store) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest3(unittest.TestCase): @@ -85,7 +87,8 @@ def test_example_missing_attributes_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - testCase_for_example_aas_missing_attributes.assert_full_example(self, json_object_store) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest4(unittest.TestCase): @@ -98,7 +101,8 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - testCase_for_example_submodel_template.assert_full_example(self, json_object_store) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + testCase_for_example_submodel_template.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest5(unittest.TestCase): @@ -111,7 +115,8 @@ def test_example_iec61360_concept_description_serialization_deserialization(self # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - testCase_for_example_concept_description.assert_full_example(self, json_object_store) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + testCase_for_example_concept_description.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest6(unittest.TestCase): @@ -150,8 +155,14 @@ def test_all_examples_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - testCase_for_example_aas.assert_full_example(self, json_object_store, False) - testCase_for_example_aas_mandatory_attributes.assert_full_example(self, json_object_store, False) - testCase_for_example_aas_missing_attributes.assert_full_example(self, json_object_store, False) - testCase_for_example_concept_description.assert_full_example(self, json_object_store, False) - testCase_for_example_submodel_template.assert_full_example(self, json_object_store, False) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + testCase_for_example_aas.assert_full_example(checker, json_object_store, False) + testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store, False) + testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) + testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) + testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) + for result in checker.failed_checks: + print(result) + + for result in checker.successful_checks: + print(result) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index ad1abe8..e2c22f9 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -91,7 +91,8 @@ def test_iterating(self) -> None: retrieved_data_store: model.provider.DictObjectStore[model.Identifiable] = model.provider.DictObjectStore() for item in self.db: retrieved_data_store.add(item) - assert_full_example(self, retrieved_data_store) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + assert_full_example(checker, retrieved_data_store) def test_parallel_iterating(self) -> None: example_data = example_aas.create_full_example() @@ -112,7 +113,8 @@ def test_parallel_iterating(self) -> None: for item in retrieved_objects: retrieved_data_store.add(item) self.assertEqual(6, len(retrieved_data_store)) - assert_full_example(self, retrieved_data_store) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + assert_full_example(checker, retrieved_data_store) # Delete objects via thread pool executor with concurrent.futures.ThreadPoolExecutor() as pool: From 777a487f811ef1f4453f74a679660deb4d341e2a Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 23 Jan 2020 14:10:47 +0100 Subject: [PATCH 324/474] fix build error in test of couchDB --- test/adapter/test_couchdb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index e2c22f9..e61dff2 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -72,7 +72,8 @@ def test_example_submodel_storing(self) -> None: submodel_restored = self.db.get_identifiable( model.Identifier(id_='https://acplt.org/Test_Submodel', id_type=model.IdentifierType.IRI)) assert(isinstance(submodel_restored, model.Submodel)) - assert_example_submodel(self, submodel_restored) + checker = AASDataChecker(raise_immediately=False) # TODO set to True + assert_example_submodel(checker, submodel_restored) # Delete example submodel self.db.discard(submodel_restored) From 2c5951de1c280446bd23a3e855c567ca9fc9cb28 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 23 Jan 2020 14:47:50 +0100 Subject: [PATCH 325/474] AASDataChecker: add check of Value List Elements --- test/adapter/json/test_json_serialization_deserialization.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 226faba..0323436 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -161,8 +161,3 @@ def test_all_examples_serialization_deserialization(self) -> None: testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) - for result in checker.failed_checks: - print(result) - - for result in checker.successful_checks: - print(result) From 093a8e4c36425cc9cd79dd059ca9ebb2f95a246a Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 23 Jan 2020 16:31:45 +0100 Subject: [PATCH 326/474] DataChecker: fixed check of annotation attribute of AnnotatedRelationshipElements --- .../json/test_json_serialization_deserialization.py | 12 ++++++------ test/adapter/test_couchdb.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 0323436..bd07438 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -60,7 +60,7 @@ def test_example_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) testCase_for_example_aas.assert_full_example(checker, json_object_store) @@ -74,7 +74,7 @@ def test_example_mandatory_attributes_serialization_deserialization(self) -> Non # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store) @@ -87,7 +87,7 @@ def test_example_missing_attributes_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store) @@ -101,7 +101,7 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) testCase_for_example_submodel_template.assert_full_example(checker, json_object_store) @@ -115,7 +115,7 @@ def test_example_iec61360_concept_description_serialization_deserialization(self # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) testCase_for_example_concept_description.assert_full_example(checker, json_object_store) @@ -155,7 +155,7 @@ def test_all_examples_serialization_deserialization(self) -> None: # module file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) testCase_for_example_aas.assert_full_example(checker, json_object_store, False) testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store, False) testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index e61dff2..ac2aed4 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -72,7 +72,7 @@ def test_example_submodel_storing(self) -> None: submodel_restored = self.db.get_identifiable( model.Identifier(id_='https://acplt.org/Test_Submodel', id_type=model.IdentifierType.IRI)) assert(isinstance(submodel_restored, model.Submodel)) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) assert_example_submodel(checker, submodel_restored) # Delete example submodel @@ -92,7 +92,7 @@ def test_iterating(self) -> None: retrieved_data_store: model.provider.DictObjectStore[model.Identifiable] = model.provider.DictObjectStore() for item in self.db: retrieved_data_store.add(item) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) assert_full_example(checker, retrieved_data_store) def test_parallel_iterating(self) -> None: @@ -114,7 +114,7 @@ def test_parallel_iterating(self) -> None: for item in retrieved_objects: retrieved_data_store.add(item) self.assertEqual(6, len(retrieved_data_store)) - checker = AASDataChecker(raise_immediately=False) # TODO set to True + checker = AASDataChecker(raise_immediately=True) assert_full_example(checker, retrieved_data_store) # Delete objects via thread pool executor From f61c0451595ef6d020202946bd3ecacedc74b839 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Sun, 26 Jan 2020 10:00:44 +0100 Subject: [PATCH 327/474] model: make description to empy dict instead of None --- .../json/test_json_serialization_deserialization.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index bd07438..fe12322 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -153,11 +153,18 @@ def test_all_examples_serialization_deserialization(self) -> None: json_serialization.write_aas_json_file(file=file, data=data) # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) + file2 = open('myresult.json', 'r', encoding='utf-8') + file2.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file2, failsafe=False) + checker = AASDataChecker(raise_immediately=False) testCase_for_example_aas.assert_full_example(checker, json_object_store, False) testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store, False) testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) + + for result in checker.failed_checks: + print(result) + + for result in checker.successful_checks: + print(result) From 0cc13ccc90007ce7597b5fa30e42c1cec98c4670 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Sun, 26 Jan 2020 10:06:42 +0100 Subject: [PATCH 328/474] fixed build error --- .../json/test_json_serialization_deserialization.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index fe12322..bd07438 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -153,18 +153,11 @@ def test_all_examples_serialization_deserialization(self) -> None: json_serialization.write_aas_json_file(file=file, data=data) # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization # module - file2 = open('myresult.json', 'r', encoding='utf-8') - file2.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file2, failsafe=False) - checker = AASDataChecker(raise_immediately=False) + file.seek(0) + json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + checker = AASDataChecker(raise_immediately=True) testCase_for_example_aas.assert_full_example(checker, json_object_store, False) testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store, False) testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) - - for result in checker.failed_checks: - print(result) - - for result in checker.successful_checks: - print(result) From 18d03f9aebf3b51d234babd705595b7f99fbb7b8 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 24 Jan 2020 10:12:12 +0100 Subject: [PATCH 329/474] Update example data and tests for native value types --- test/adapter/json/test_json_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index b5d677b..d032f4b 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -26,7 +26,7 @@ class JsonSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: - test_object = model.Property("test_id_short", "string", category="PARAMETER", + test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) From 19928db3d69e68ad051832f1bbe4168f69b68e07 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Wed, 29 Jan 2020 12:40:02 +0100 Subject: [PATCH 330/474] examples: integrate check functions into example files --- ...test_json_serialization_deserialization.py | 23 ++++++++----------- test/adapter/test_couchdb.py | 21 ++++++++--------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index bd07438..2e59776 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -19,9 +19,6 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description -from test._helper import testCase_for_example_aas, testCase_for_example_aas_mandatory_attributes, \ - testCase_for_example_aas_missing_attributes, testCase_for_example_concept_description, \ - testCase_for_example_submodel_template from aas.examples.data._helper import AASDataChecker JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -61,7 +58,7 @@ def test_example_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_aas.assert_full_example(checker, json_object_store) + example_aas.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest2(unittest.TestCase): @@ -75,7 +72,7 @@ def test_example_mandatory_attributes_serialization_deserialization(self) -> Non file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store) + example_aas_mandatory_attributes.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest3(unittest.TestCase): @@ -88,7 +85,7 @@ def test_example_missing_attributes_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store) + example_aas_missing_attributes.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest4(unittest.TestCase): @@ -102,7 +99,7 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_submodel_template.assert_full_example(checker, json_object_store) + example_submodel_template.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest5(unittest.TestCase): @@ -116,7 +113,7 @@ def test_example_iec61360_concept_description_serialization_deserialization(self file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_concept_description.assert_full_example(checker, json_object_store) + example_concept_description.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest6(unittest.TestCase): @@ -156,8 +153,8 @@ def test_all_examples_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_aas.assert_full_example(checker, json_object_store, False) - testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store, False) - testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) - testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) - testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) + example_aas.check_full_example(checker, json_object_store, False) + example_aas_mandatory_attributes.check_full_example(checker, json_object_store, False) + example_aas_missing_attributes.check_full_example(checker, json_object_store, False) + example_concept_description.check_full_example(checker, json_object_store, False) + example_submodel_template.check_full_example(checker, json_object_store, False) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index ac2aed4..9c1be81 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -19,8 +19,7 @@ from aas import model from aas.adapter import couchdb -from aas.examples.data import example_aas -from test._helper.testCase_for_example_aas import * +from aas.examples.data.example_aas import * TEST_CONFIG = configparser.ConfigParser() @@ -61,7 +60,7 @@ def tearDown(self) -> None: self.db.logout() def test_example_submodel_storing(self) -> None: - example_submodel = example_aas.create_example_submodel() + example_submodel = create_example_submodel() # Add exmaple submodel self.db.add(example_submodel) @@ -73,14 +72,14 @@ def test_example_submodel_storing(self) -> None: model.Identifier(id_='https://acplt.org/Test_Submodel', id_type=model.IdentifierType.IRI)) assert(isinstance(submodel_restored, model.Submodel)) checker = AASDataChecker(raise_immediately=True) - assert_example_submodel(checker, submodel_restored) + check_example_submodel(checker, submodel_restored) # Delete example submodel self.db.discard(submodel_restored) self.assertNotIn(example_submodel, self.db) def test_iterating(self) -> None: - example_data = example_aas.create_full_example() + example_data = create_full_example() # Add all objects for item in example_data: @@ -93,10 +92,10 @@ def test_iterating(self) -> None: for item in self.db: retrieved_data_store.add(item) checker = AASDataChecker(raise_immediately=True) - assert_full_example(checker, retrieved_data_store) + check_full_example(checker, retrieved_data_store) def test_parallel_iterating(self) -> None: - example_data = example_aas.create_full_example() + example_data = create_full_example() ids = [item.identification for item in example_data] # Add objects via thread pool executor @@ -115,7 +114,7 @@ def test_parallel_iterating(self) -> None: retrieved_data_store.add(item) self.assertEqual(6, len(retrieved_data_store)) checker = AASDataChecker(raise_immediately=True) - assert_full_example(checker, retrieved_data_store) + check_full_example(checker, retrieved_data_store) # Delete objects via thread pool executor with concurrent.futures.ThreadPoolExecutor() as pool: @@ -126,7 +125,7 @@ def test_parallel_iterating(self) -> None: def test_key_errors(self) -> None: # Double adding an object should raise a KeyError - example_submodel = example_aas.create_example_submodel() + example_submodel = create_example_submodel() self.db.add(example_submodel) with self.assertRaises(KeyError): self.db.add(example_submodel) @@ -144,7 +143,7 @@ def test_key_errors(self) -> None: def test_conflict_errors(self) -> None: # Preperation: add object and retrieve it from the database - example_submodel = example_aas.create_example_submodel() + example_submodel = create_example_submodel() self.db.add(example_submodel) retrieved_submodel = self.db.get_identifiable( model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) @@ -171,7 +170,7 @@ def test_conflict_errors(self) -> None: retrieved_submodel.commit_changes() def test_editing(self) -> None: - example_submodel = example_aas.create_example_submodel() + example_submodel = create_example_submodel() self.db.add(example_submodel) # Retrieve submodel from database and change ExampleCapability's semanticId From 53dc0f2277dacb2b4b0986f0427807abf8da5494 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Wed, 29 Jan 2020 15:19:39 +0100 Subject: [PATCH 331/474] add tests and fixed code errors --- ...test_json_serialization_deserialization.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 2e59776..5a472c3 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -19,6 +19,9 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description +from test._helper import testCase_for_example_aas, testCase_for_example_aas_mandatory_attributes, \ + testCase_for_example_aas_missing_attributes, testCase_for_example_concept_description, \ + testCase_for_example_submodel_template from aas.examples.data._helper import AASDataChecker JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -58,7 +61,7 @@ def test_example_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, json_object_store) + testCase_for_example_aas.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest2(unittest.TestCase): @@ -72,7 +75,7 @@ def test_example_mandatory_attributes_serialization_deserialization(self) -> Non file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - example_aas_mandatory_attributes.check_full_example(checker, json_object_store) + testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest3(unittest.TestCase): @@ -85,7 +88,7 @@ def test_example_missing_attributes_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - example_aas_missing_attributes.check_full_example(checker, json_object_store) + testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest4(unittest.TestCase): @@ -99,7 +102,7 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - example_submodel_template.check_full_example(checker, json_object_store) + testCase_for_example_submodel_template.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest5(unittest.TestCase): @@ -113,7 +116,7 @@ def test_example_iec61360_concept_description_serialization_deserialization(self file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - example_concept_description.check_full_example(checker, json_object_store) + testCase_for_example_concept_description.assert_full_example(checker, json_object_store) class JsonSerializationDeserializationTest6(unittest.TestCase): @@ -153,8 +156,9 @@ def test_all_examples_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, json_object_store, False) - example_aas_mandatory_attributes.check_full_example(checker, json_object_store, False) - example_aas_missing_attributes.check_full_example(checker, json_object_store, False) - example_concept_description.check_full_example(checker, json_object_store, False) - example_submodel_template.check_full_example(checker, json_object_store, False) + testCase_for_example_aas.assert_full_example(checker, json_object_store, False) + testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store, False) + testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) + testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) + testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) + self.assertEqual(963, sum(1 for _ in checker.successful_checks)) From 76287be835062dc49057b9c204bf77fff39240f4 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Wed, 29 Jan 2020 16:02:52 +0100 Subject: [PATCH 332/474] add tests and fix code errors --- test/adapter/json/test_json_serialization_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 5a472c3..c0e58d8 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -161,4 +161,4 @@ def test_all_examples_serialization_deserialization(self) -> None: testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) - self.assertEqual(963, sum(1 for _ in checker.successful_checks)) + self.assertEqual(1042, sum(1 for _ in checker.successful_checks)) From 34b79e7e26f196f1340fecfc2a53ba34524fe70d Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Wed, 29 Jan 2020 17:14:06 +0100 Subject: [PATCH 333/474] fix commits to #234547 --- test/adapter/json/test_json_serialization_deserialization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index c0e58d8..bd07438 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -161,4 +161,3 @@ def test_all_examples_serialization_deserialization(self) -> None: testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) - self.assertEqual(1042, sum(1 for _ in checker.successful_checks)) From add9b7673fdf8993fa305f1eee79d9d89aaafd05 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 30 Jan 2020 13:43:24 +0100 Subject: [PATCH 334/474] make imports relative if possible #41' --- test/adapter/test_couchdb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index 9c1be81..10a9b39 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -17,7 +17,6 @@ import urllib.request import urllib.error -from aas import model from aas.adapter import couchdb from aas.examples.data.example_aas import * From e4ffe92fccf90bb8aad6895889e764dae142ec84 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 3 Feb 2020 15:51:11 +0100 Subject: [PATCH 335/474] test: add exception tests for test_couchdb.py --- test/adapter/test_couchdb.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index 10a9b39..5e262ab 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -126,19 +126,25 @@ def test_key_errors(self) -> None: # Double adding an object should raise a KeyError example_submodel = create_example_submodel() self.db.add(example_submodel) - with self.assertRaises(KeyError): + with self.assertRaises(KeyError) as cm: self.db.add(example_submodel) + self.assertEqual("'Identifiable with id Identifier(IRI=https://acplt.org/Test_Submodel) already exists in " + "CouchDB database'", str(cm.exception)) # Querying a deleted object should raise a KeyError retrieved_submodel = self.db.get_identifiable( model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) self.db.discard(example_submodel) - with self.assertRaises(KeyError): + with self.assertRaises(KeyError) as cm: self.db.get_identifiable(model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) + self.assertEqual("'No Identifiable with id IRI-https://acplt.org/Test_Submodel found in CouchDB database'", + str(cm.exception)) # Double deleting should also raise a KeyError - with self.assertRaises(KeyError): + with self.assertRaises(KeyError) as cm: self.db.discard(retrieved_submodel) + self.assertEqual("'No AAS object with id Identifier(IRI=https://acplt.org/Test_Submodel) exists in " + "CouchDB database'", str(cm.exception)) def test_conflict_errors(self) -> None: # Preperation: add object and retrieve it from the database @@ -154,19 +160,25 @@ def test_conflict_errors(self) -> None: # Committing changes to the retrieved object should now raise a conflict error retrieved_submodel.id_short = "myOtherNewIdShort" - with self.assertRaises(couchdb.CouchDBConflictError): + with self.assertRaises(couchdb.CouchDBConflictError) as cm: retrieved_submodel.commit_changes() + self.assertEqual("Could not commit changes to id Identifier(IRI=https://acplt.org/Test_Submodel) due to a " + "concurrent modification in the database.", str(cm.exception)) # Deleting the submodel with safe_delete should also raise a conflict error. Deletion without safe_delete should # work - with self.assertRaises(couchdb.CouchDBConflictError): + with self.assertRaises(couchdb.CouchDBConflictError) as cm: self.db.discard(retrieved_submodel, True) + self.assertEqual("Object with id Identifier(IRI=https://acplt.org/Test_Submodel) has been modified in the " + "database since the version requested to be deleted.", str(cm.exception)) self.db.discard(retrieved_submodel, False) self.assertEqual(0, len(self.db)) # Committing after deletion should also raise a conflict error - with self.assertRaises(couchdb.CouchDBConflictError): + with self.assertRaises(couchdb.CouchDBConflictError) as cm: retrieved_submodel.commit_changes() + self.assertEqual("Could not commit changes to id Identifier(IRI=https://acplt.org/Test_Submodel) due to a " + "concurrent modification in the database.", str(cm.exception)) def test_editing(self) -> None: example_submodel = create_example_submodel() From 47d92ce97ac9fd32e194a9da09c370e0d3d64c55 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Wed, 19 Feb 2020 12:04:21 +0100 Subject: [PATCH 336/474] examples: add logger for failsafe mode and fixing failsafe mode --- ...test_json_serialization_deserialization.py | 57 ++----------------- 1 file changed, 5 insertions(+), 52 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index bd07438..a1837f0 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -19,9 +19,6 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description -from test._helper import testCase_for_example_aas, testCase_for_example_aas_mandatory_attributes, \ - testCase_for_example_aas_missing_attributes, testCase_for_example_concept_description, \ - testCase_for_example_submodel_template from aas.examples.data._helper import AASDataChecker JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -61,7 +58,7 @@ def test_example_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_aas.assert_full_example(checker, json_object_store) + example_aas.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest2(unittest.TestCase): @@ -75,7 +72,7 @@ def test_example_mandatory_attributes_serialization_deserialization(self) -> Non file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store) + example_aas_mandatory_attributes.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest3(unittest.TestCase): @@ -88,7 +85,7 @@ def test_example_missing_attributes_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store) + example_aas_missing_attributes.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest4(unittest.TestCase): @@ -102,7 +99,7 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_submodel_template.assert_full_example(checker, json_object_store) + example_submodel_template.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest5(unittest.TestCase): @@ -116,48 +113,4 @@ def test_example_iec61360_concept_description_serialization_deserialization(self file.seek(0) json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) - testCase_for_example_concept_description.assert_full_example(checker, json_object_store) - - -class JsonSerializationDeserializationTest6(unittest.TestCase): - def test_all_examples_serialization_deserialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - # add object from example_aas: - data.add(example_aas.create_example_asset_identification_submodel()) - data.add(example_aas.create_example_bill_of_material_submodel()) - data.add(example_aas.create_example_asset()) - data.add(example_aas.create_example_submodel()) - data.add(example_aas.create_example_concept_description()) - data.add(example_aas.create_example_asset_administration_shell( - example_aas.create_example_concept_dictionary())) - # add objects from example_aas_mandatory_attributes: - data.add(example_aas_mandatory_attributes.create_example_asset()) - data.add(example_aas_mandatory_attributes.create_example_submodel()) - data.add(example_aas_mandatory_attributes.create_example_empty_submodel()) - data.add(example_aas_mandatory_attributes.create_example_concept_description()) - data.add(example_aas_mandatory_attributes.create_example_asset_administration_shell( - example_aas_mandatory_attributes.create_example_concept_dictionary())) - data.add(example_aas_mandatory_attributes.create_example_empty_asset_administration_shell()) - # add objects from example_aas_missing_attributes: - data.add(example_aas_missing_attributes.create_example_asset()) - data.add(example_aas_missing_attributes.create_example_submodel()) - data.add(example_aas_missing_attributes.create_example_concept_description()) - data.add(example_aas_missing_attributes.create_example_asset_administration_shell( - example_aas_missing_attributes.create_example_concept_dictionary())) - # add objects from example_concept_description: - data.add(example_concept_description.create_iec61360_concept_description()) - # add objects from example_submodel_template: - data.add(example_submodel_template.create_example_submodel_template()) - - file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - testCase_for_example_aas.assert_full_example(checker, json_object_store, False) - testCase_for_example_aas_mandatory_attributes.assert_full_example(checker, json_object_store, False) - testCase_for_example_aas_missing_attributes.assert_full_example(checker, json_object_store, False) - testCase_for_example_concept_description.assert_full_example(checker, json_object_store, False) - testCase_for_example_submodel_template.assert_full_example(checker, json_object_store, False) + example_concept_description.check_full_example(checker, json_object_store) From faa65a0eae372d3bd8b78087f810d428fbfdb606 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Fri, 28 Feb 2020 15:37:11 +0100 Subject: [PATCH 337/474] update xml_serialization and test with changes that appeared through merge with master --- test/adapter/xml/test_xml_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 5c731e3..ae8de8d 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -25,7 +25,7 @@ class XMLSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", - "string", + model.datatypes.String, category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) xml_data = xml_serialization.property_to_xml(test_object, xml_serialization.NS_AAS, "test_object") From 9d18dc1851949266c8a91bce3671093c81d066f0 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Wed, 4 Mar 2020 14:31:10 +0100 Subject: [PATCH 338/474] test.adapter.xml: remove resolved todo --- test/adapter/xml/test_xml_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index ae8de8d..2c6549f 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -10,7 +10,7 @@ # specific language governing permissions and limitations under the License. import io import unittest -from lxml import etree # type: ignore # todo: put lxml in project requirements? +from lxml import etree # type: ignore import os from aas import model From e328acec2f55bb708c36442634ff966f22be046a Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Mon, 16 Mar 2020 16:04:23 +0100 Subject: [PATCH 339/474] test.adapter.xml: update test_functions after parameter change in xml_serialization --- test/adapter/xml/test_xml_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 2c6549f..ac4ea82 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -28,7 +28,7 @@ def test_serialize_object(self) -> None: model.datatypes.String, category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) - xml_data = xml_serialization.property_to_xml(test_object, xml_serialization.NS_AAS, "test_object") + xml_data = xml_serialization.property_to_xml(test_object, xml_serialization.NS_AAS+"test_object") # todo: is this a correct way to test it? def test_random_object_serialization(self) -> None: From 6cbe4ae2ba45ad0a824d98af119465ef74cf0a22 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 17 Mar 2020 12:11:07 +0100 Subject: [PATCH 340/474] test: Add IEC61360ConceptDescription to json/xml serialization tests --- test/adapter/json/test_json_serialization.py | 17 ++++++++++++++++- test/adapter/xml/test_xml_serialization.py | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index d032f4b..2947279 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -19,7 +19,7 @@ from jsonschema import validate # type: ignore from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas + example_aas_mandatory_attributes, example_aas, example_concept_description JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -138,3 +138,18 @@ def test_missing_serialization(self) -> None: # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) + + def test_concept_description(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_concept_description.create_iec61360_concept_description()) + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + with open(JSON_SCHEMA_FILE, 'r') as json_file: + aas_json_schema = json.load(json_file) + + file.seek(0) + json_data = json.load(file) + + # validate serialization against schema + validate(instance=json_data, schema=aas_json_schema) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index ac4ea82..d9ad656 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -17,7 +17,7 @@ from aas.adapter.xml import xml_serialization from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas + example_aas_mandatory_attributes, example_aas, example_concept_description XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'AAS.xsd') @@ -131,3 +131,17 @@ def test_missing_serialization(self) -> None: parser = etree.XMLParser(schema=aas_schema) file.seek(0) root = etree.parse(file, parser=parser) + + def test_concept_description(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_concept_description.create_iec61360_concept_description()) + file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=file, data=data) + + # load schema + aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) + + # validate serialization against schema + parser = etree.XMLParser(schema=aas_schema) + file.seek(0) + root = etree.parse(file, parser=parser) From 0cc2f444f537f0654ab218d16e64fc834e28d342 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 12 Mar 2020 13:54:52 +0100 Subject: [PATCH 341/474] compliance_tool: add unittests for compliance_tool script --- test/adapter/json/test_json_serialization.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 2947279..230501f 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -19,7 +19,7 @@ from jsonschema import validate # type: ignore from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, example_concept_description + example_aas_mandatory_attributes, example_aas, create_example, example_concept_description JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') @@ -82,7 +82,7 @@ def test_random_object_serialization(self) -> None: # validate serialization against schema validate(instance=json_data_new, schema=aas_schema) - def test_full_example_serialization(self) -> None: + def test_aas_example_serialization(self) -> None: data = example_aas.create_full_example() file = io.StringIO() json_serialization.write_aas_json_file(file=file, data=data) @@ -139,7 +139,7 @@ def test_missing_serialization(self) -> None: # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) - def test_concept_description(self) -> None: + def test_concept_description_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_concept_description.create_iec61360_concept_description()) file = io.StringIO() @@ -153,3 +153,17 @@ def test_concept_description(self) -> None: # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) + + def test_full_example_serialization(self) -> None: + data = create_example() + file = io.StringIO() + json_serialization.write_aas_json_file(file=file, data=data) + + with open(JSON_SCHEMA_FILE, 'r') as json_file: + aas_json_schema = json.load(json_file) + + file.seek(0) + json_data = json.load(file) + + # validate serialization against schema + validate(instance=json_data, schema=aas_json_schema) From db9134e37244729cfc492d62f63002d93d235235 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 6 Mar 2020 13:59:30 +0100 Subject: [PATCH 342/474] adapter.aasx: Add first draft of AASX reading/writing --- test/adapter/aasx/test_aasx.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/adapter/aasx/test_aasx.py diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py new file mode 100644 index 0000000..e8f8581 --- /dev/null +++ b/test/adapter/aasx/test_aasx.py @@ -0,0 +1,23 @@ +# Copyright 2019 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import unittest +from aas import model +from aas.adapter import aasx + + +class TestAASXUtils(unittest.TestCase): + def test_name_friendlyfier(self) -> None: + friendlyfier = aasx.NameFriendlyfier() + name1 = friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS-a", model.IdentifierType.IRI)) + self.assertEqual("http___example_com_AAS_a", name1) + name2 = friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS+a", model.IdentifierType.IRI)) + self.assertEqual("http___example_com_AAS_a_1", name2) From 0c337431095ff3dc35427a9e425240b98c6477ef Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 18 Mar 2020 12:52:59 +0100 Subject: [PATCH 343/474] Add aas.util.traversal --- test/adapter/aasx/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/adapter/aasx/__init__.py diff --git a/test/adapter/aasx/__init__.py b/test/adapter/aasx/__init__.py new file mode 100644 index 0000000..e69de29 From 8cab92d69edbbee43ffcdffeebe6cf2586c7db9c Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 24 Mar 2020 11:53:56 +0100 Subject: [PATCH 344/474] test: Add test for AASXReader and AASXWriter, improve coverage report --- test/adapter/aasx/TestFile.pdf | Bin 0 -> 8178 bytes test/adapter/aasx/test_aasx.py | 57 ++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 test/adapter/aasx/TestFile.pdf diff --git a/test/adapter/aasx/TestFile.pdf b/test/adapter/aasx/TestFile.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2bccbec5f60ea7a8f51e5fd41eda019cf27a08d7 GIT binary patch literal 8178 zcma)>Wl&sQv+qOj0fGjX!JV1G-6as*B{+lY;1)a(Jdoh-7Ti6!ySoGk?rt~FIqzHd zRK2(A++FKK_gcNX|JAGahh0BfWl3pP2pboGc4DS?0l)zS1077P0fK@6kUZ4h!o?EE z#R>e^0{}@|*}6bsK#;Vpu?tiZYU*GH1qcfRoLyj0V>^Jyk~3g)%DqOpa;AopgIx_p zRt;RuImyAvp;gH_!2zjPMv+wsn*+~8Vw`WmFgD-5*}+Er4S?F4{VTy=>!0Gh{~-bb zgm7?k{aX?{kc*F#h!_ z5Qu4SDc*3QlYZ%4o-x)IRHTS{>*5ppMv~P3!=PB+PN$J5I(ou0Nm$} zQz8op5ZJ5`61}=LbI6_eT$Ck3LVz%|xMP7kCY+ID&Jdi4`AP2?6T#u_^YSjB|81NR zSX)<<)ZZJ<(T|>IGIL)6UU0J`EjH8K2boeV!&0deaUDSrVe@VOYd~PDal7N2i1UC@ zgy+KTOaUx}x4hJ8mHyN#?*raG3ka;CyWh93IsyZ*HapvCtdQn`G^Be#)Mg;>|=unY>BC>Q*hQUu9QP}9`g^{mev(imj zaEt4+TNV4K;l8g}{--g#cL9F8?4sK`XvlXj3NVsNng!I?fTzsj7v|ru{b!jvFFbLt z?qbRmG#81fr-`qxPThi71O6g!1g(54J>dT2p0kTS^US<`(W`xMv?1OKlpL{gm7(ZFQ42LErOxrz8Z$=gV z1`&`}r1_B-2f)MyUcV9^QHZp<(KE<93$O2IU=W{q#UJ1U!sbJ#~M_|I89f!PO zvqb;1CrN7tr}DIYHI4EhV?s}Xj#Tl}Ft+xTmWe%4aIZ8l$L7{BKo!0QaAS5TsLR}Gh6*eX%j|}$u#ILL zwiLV`zFTo|xLT+_mrMT6wuox~fp?PS7u{1YihH!_E(0|^s%Fv;b$_q^OxW+o7w~lr z=H3y%Y+zPwl;n9}mL8hPZL~DJg<(p#Cl7}cWYxrs_Caw~U;sC-IiaK*Os=jCzO?V^ zP|Ws!CJLF@CfY2evN73*hNYetrPRaL*9LiF^N(aiX-@0hIX^P)NO9HP5GqPES0g=o z5ZgB0+3_AOkr2!-_W54o6WsQ*s8vf;j0a!?m>aGumlColei+oO3Y`XH=1|O2xXoX& z)CX&~Djz7DI9&&!ST^ePkIE0{*9g<~89%ad>UGvxRR9$3h2!`rzeq76tUi}`jiBFu z$3$9>qqL_!f-c65ynFEi<>guTZ;W=K%<@jfEOBoMiVtn9oF8wer~A8Rak*Zp3&`N{m`?Y*x>{+&!)7BfSjz;a-=o5~mC(FVfkY?TEnvB~1Kw8Jz z3cB~)kzGhvMC2T|%c|7*5?}tn{m*C3 zFw~6NbY+mGsn`1g`Pcc|6mV5IUAKhr+l$E3n%hPW&4RDx%RH|0Nq$16nr-GLUP<0U zjsaPAK1B}Swx2hsw6Lmsc0(k+S%IMBSm|K-txZBhbnj*!zaS9h$p~$^+t`RwJ}f+6 z(Dyf)&*$9I+nj^zj|y8xy1g$LtiX;i9#h2%(byXw7TV!wHKT$H$m8=8N&Xu5d*|Bq z_c`GX3!Hk`u+dWH-W1;^!Acm_^`FJq4)bV8NzL8TG;aZ|B+>0cH%s)nSZoF%oJ5oB zoLY%P1Y-6RDJq*vg1c<`T+x;+D*k(EPpcNDlfut!vZgfU-@36GS+o>;Kh`;NXThXG ztL1||s@udB3NuBKBd9m5(BkMkfg10c$?b(FxF=eL`3tzbSfL<;+M+#x%gSsFF4O&4 z`?^-VcKJs2rDJWC_^b7+DSNe8sK6{B*%r%D1#Gp{B;IWib3Bxi`!%x3z9GHoXy86> z+&E7|qrxDc*_DHj;aIk?d^&#d)*M$)#H)sW+aV7KM+9AM33zrak^)P~Qi6VHYLNs^ zUI+N#zn;&?NBI+;e{WYs(^G};l2%i64sg4<^?!|8Hea(57%6D~EY+plmDtU{`XbsQ zd^>w{SdH84&a8Movf(+mrSF?g{cNGRk=SYVI#tDN8kSr9<7VONDzjB$U0N8%pKnb#lQZeJelh5%L4Xh_{84So z{`7L}5f7iXV)7&Co#57zATjSB^lf{Pr@(^3-mTL~ZW1f;JU8a|rA#KPmbStxone*h zg}^xQfQDFktYRL2JiaU92eK8rj1+k7Y`lJ(KF-qKy@IvBxftGJ(3^yu8aHr-Z&a$3J&HF7H<{wD|WZ zg&Eb($DlQy_;dY=%t{(*G1)ji*=^JeEXg5+)ihChwYu6=_A}N+y`xW#>U?Ok7}AIF zi_?)9BK27*BBrv|IVjpwh60HI<}tPg8-9P|ng!V(7?g*6ImrkU6-St*z3>a=g{=H4 z2Zu43mToKj>2-7$9-amica(6%aGJUB;-ALfaEgn8OM04Z!`XV)qJi6rt3QZ)8n65}p9aj3y zdDHvOFguHl3lB1)D~iH3G=LXVC1qPHCDSC_V(e@xmWpbLCMB|i@>xRLT!vC~uYjE${pUi4X^wY{L0C9$4-E0SIc+I77o zgQv>m50>7$@GUZ)Ee|}|*>TNn_v!X#Vu}=MpJdE-mKgYyF&$c~OoA_(47m3lCUrlYVS?YlV-da?bb* z8s}Z&gH@F$pQeC5sX2f--fq5*wlB+{PcpzVE)8CjYYsjYiL_zq&ekln@eQ*zNy zVXMF+b6PIBNAo$=KSeqji{mr-)|zBKEBB;*{RQIDW!Xe4fxIIFcVS2ODE6$1F!EEz zp;MF)BsZ;hu%HPH=;h*zr2Yo|ntO3yb5ap2Q27;8kJ=_?4{n|#Ca;Id@6!KNLrpy> zIjw#k{uC_yd2_#IS^V1|ar*aqkx3PXK$RcQ5fSe+Z1Od8kofHTx6*!8P~Z(=p|5a< z5q+Kal}Zv~$((iEiIL+7- z)IG}jn)eIalSd>`qiCvCDZ5}33;M!0Zw6~qW0}1nzm3Z z5sFy1A-(S$1qT_p- zwOD%*7|NNTR2fUqLm{u-OGxGie=UJJ$_eAS8N+e=;!2$&!&ryo!CZoe>OjPNt7fgl zX4G__&8+1*mwj+JSy=BpTE1JiRAIUOLf1rGW_Mar<)%nRlHZH( z$}G>Pcql7;_UIN~NsLwlsZ6YSqU3&`tYvvW+WBV@$o&rw+11d6t!$z3y*Xd7E14L5 z)l7S4{q?TJffkHbwOuU3F=Wa}P<^bSm145KeR)}GHH9AMgH5A~%f$AxFLJu#VG-$f zJg~I%Pa84v{EVppu{)Wnx zl{W5+VB?9RSN_OS)dzQP>$;ibva)PGi#AWf zAtyZ}JuU!Oaf}uupHlk;{Q3KzfYGNegTD#MejSx`Ws_KlyL&p+nG{ z8F|BZXOp@Lq*t5z7mN4XKdGWTlU$GV6D6qqzJh-i**0&05?K_Pb_%%``z$MTCmq8} zKgXx72}k~(ST$_3O|IB5WS2GL>)uJs2#Hu{o3e*en~iO5&NcJL zAb!*)u(ujnsE&@1w0Y@ef`T*!I`Yn8mh(jedGCj{c2M#3A#~Rz&kYze@*d2Ly(Hc7 zQf1+ARh8t3Ok6KjI91Q2ZRx%si%_uCy@vIPSSTl|BPzLDb(zm`k&%om2^ zti{4H=VKkBgyM6OBb+5d?^AP2IbuoxZc?~Apk(lQ-14k%qF$H545Pc&i}<186Tf&6 zX4xY3x}g$}-I432fy!R@azU%BfR5Hq4Ia6NzJvnHeX!{_FY8#mVK>q0n0v?ovsLS= zO%d>u72{_hR*pHld5(rLR9)tn5SL0No>$Sm$X%HXYQV|c{(pX6(Htt>+C8*l4C{R`5$_BU#r_|dHsyay>74IO)?sLYZT|-{_ThP*)T3TW{N)I#AaqTHt8x%bCj*g?#JK{5ntB z8xb(%-HZ0iREG3t+^CefC%0rZcO61RHU2SIFn_O< zpL7l@3iI{7+iAQJinYO~OgBObQ60o4C4n$xYS3ME0{_gVHFb@W1i=tr0<#jV9h0Q) z#uh9XmwoR!vT?e27HnVM2dB;1-;i_{-B`7&dN&&1S!fpBr*c%l z;du2X3frI^ccs9pC_3tqrOl-BZC0x{+a2on1t#S?r6E|2mi>)q23!z-fsxKDHM_by zj6T)jYs7qMOx^U~5?-6e=L7JXJzI?l7b8KAiI~`h4K@zpWrcns*E<-8S6JD*#~eDg z8V-NbC6Ur9Iphi__Tuz>Db&36r#8=a~5Q@PKYi>lq(Cmk#v%tb%!Xo;A-bYoHE{^}fliP@P0t z+pl?p!f&*cg9@~f!)(YXX<-g!v)K2$W8oU&zt(0)7f?>BQ$d!URTPVdX&-S6(p$dDH2xi zN`?m)IwXFg89KA_bFpD{jW^$G{TAc&$X4+M=R*OB0N2UAw&lhj99rJhJ99R6v{6q! zr;CuqoxaXrLi3<<-^zS%OdqD~#ULwy9cMy6TuohLB0NDfA~DFN=_KKkU zjMq|Ps&M<`OgXgkBW85@fQLzalY8h>7Ld( zqSQn!mLwnKF2BFmBVrsTKCge|L}VVH$Qw#|*JN}qceLD$qJQ^8tN>;_XC&+5m~nkPho$t9|a zq56o`$6$L@vhSdnX#1)*^hkA37gKx$MQ3qlfr#F8rn08Xo>rx!v-Xrjq=q2H__1)u zC3btIRe3oF@l#{_S{%FCSqL#*dBkSARifNHaWdNWeh9tiAZC>K@2}gup(L)ip%u6E z$>#H)0?0|1%h)r?7ozg8s@QSA4HoL#zk7kcKF}QxCK_;6^^;&pr!KzVj zJTO8{C3}@xI# zBV7V(%?2za@d?8o4d0YzQvrv~Rr#8@;c=P@w?YAQww#QG38<0EeiBy<88C4mnz5e1 zD8l~CEC8&b`q(OqUIDNrTU^I>AgzYeP%`2fA1f1<<6p7xU#n8Ob&!n&vJV5I_v+-! z4n0II3H}Th0cu>vT*}ra!H*EcJ)`7EgM0s}nfnhO$K(=4sxTwC- zQn4!haRQwnXKnDQ=v4YLMCgea(*(+JnvwM0jP&t5@uYF8r>*dk_?w=iTZe(N7Pkb! zq1#|-d>Jrzl0<>>Q(+qV6z$aa_KpD^99uywXJbV7R`iu6jH`Fi%_MR23s%-zxqwH%3D3h&luYxAHI%D zY@Gb128tsb>Sr1d=KS6M7LF=@?vW)1FPPR&GXN2QI4Ovp)PKP=wq!j|Drwp+A=-8F zCR;sg)cZFZ^wjFpmvV#}QNzY^@bq1oRKQ@hijQG2Krx;tC%xTwH)->_Q@NtAexb)d z;hPJZ1krNAY@AesBVuhsPdTM@oyw5IuUzU|e|DLmSB&S$11*KYfd?*}@Tu%zJ`3|~ z)9N*u2#XNimh0>>B}{mM_Zk&cCGDP)-Q{mF!IRL!w?BR?Fl)yr0rcx+Dqd9D6?3F(a96o}?SE*nuYe z>ZkEvqK1A^AySv}&D`183=J1@Wi$oAb;e|)^N209G6ZZ-gB>w6L2R>asV;eKjSL~cG0LZL6v?e~Z3 zi{lBN2}oq=dM3EC-`Ku=ou~QX(;?Ann7&M_iwPIyImc9|kyURCtQhjOUd4IP>lL0> zR@3J{xf%rWKfszY%)!jn^e@a~a5OVlhidMcat8ig|E-Ays0oER|1AyVV1sZ%xcJz)xmelRxc{y7zZ3%H zt=ynMb})nwWCnEuNkUDmjO`hLHjc(Fmd;RPpxeJvaI^hQ@=plkKgjZ5sFCY`bVS|L z5eft;nOLj2*a1L_K%RdXBFxzZ$oYQ(BL^q%|2GVUsulTPEv?NlNa~Wm)I`G1PGE{x$X@h26BK1R(k9E`Rb`{F(wg^t(8#+qKz?j@d zs-h`MG9gkA{^M)snP3{Y6d6J|WPkYPgQaF=e7V|;JyD(m>ugvWyHQ#5$K5+;=z(IU z-7VfgQX+-t;Ib10q(q?qy<8nOS zEYz9Gdt2tzCtpCC!0Lj{$K|O$a;J*M_9{^Ib@`C=g03a=(9t6k^ zg#6u8b#QP2Lco9DSN$j4+yVGs-^o9YBv4Nf43Xyed+Xd>Tv8kmK54Fhh7imL5tD$3 zNlWqa@CpO}_mIC;{teT?{~90Q{|=~4jg0Qbdpd_ude9@$pU_29LR5A|!psp(=%l|e v0=YS`Y9MkQ#zx--#@@WK&qTQX&#pMT7{gpVV1N6-!^^`5prw^kk_P-Ay3<)0 literal 0 HcmV?d00001 diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index e8f8581..9358909 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -8,10 +8,17 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. - +import datetime +import hashlib +import io +import os +import tempfile import unittest + +import pyecma376_2 from aas import model from aas.adapter import aasx +from aas.examples.data import example_aas, _helper class TestAASXUtils(unittest.TestCase): @@ -21,3 +28,51 @@ def test_name_friendlyfier(self) -> None: self.assertEqual("http___example_com_AAS_a", name1) name2 = friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS+a", model.IdentifierType.IRI)) self.assertEqual("http___example_com_AAS_a_1", name2) + + +class AASXWriterTest(unittest.TestCase): + def test_writing_reading_example_aas(self) -> None: + # Create example data and file_store + data = example_aas.create_full_example() + files = aasx.DictSupplementaryFileContainer() + with open(os.path.join(os.path.dirname(__file__), 'TestFile.pdf'), 'rb') as f: + files.add_file("/TestFile.pdf", f, "application/pdf") + f.seek(0) + + # Create OPC/AASX core properties + cp = pyecma376_2.OPCCoreProperties() + cp.created = datetime.datetime.now() + cp.creator = "PyI40AAS Testing Framework" + + # Write AASX file + fd, filename = tempfile.mkstemp(suffix=".aasx") + os.close(fd) + with aasx.AASXWriter(filename) as writer: + writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', + id_type=model.IdentifierType.IRI), + data, files) + writer.write_core_properties(cp) + + # Read AASX file + new_data = model.DictObjectStore() + new_files = aasx.DictSupplementaryFileContainer() + with aasx.AASXReader(filename) as reader: + reader.read_into(new_data, new_files) + new_cp = reader.get_core_properties() + + # Check AAS objects + checker = _helper.AASDataChecker(raise_immediately=True) + example_aas.check_full_example(checker, new_data) + + # Check core properties + self.assertEqual(new_cp.created, cp.created) + self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") + self.assertIsNone(new_cp.lastModifiedBy) + + # Check files + self.assertEqual(new_files.get_content_type("/TestFile.pdf"), "application/pdf") + file_content = io.BytesIO() + new_files.write_file("/TestFile.pdf", file_content) + self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), "78450a66f59d74c073bf6858db340090ea72a8b1") + + os.unlink(filename) From 21e4854cc9973099cf9cca68d34118c75a9c18e7 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 24 Mar 2020 16:35:06 +0100 Subject: [PATCH 345/474] adapter.aasx: Add type annotation to fix mypy type check --- test/adapter/aasx/test_aasx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 9358909..6cf3bdc 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -54,7 +54,7 @@ def test_writing_reading_example_aas(self) -> None: writer.write_core_properties(cp) # Read AASX file - new_data = model.DictObjectStore() + new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() new_files = aasx.DictSupplementaryFileContainer() with aasx.AASXReader(filename) as reader: reader.read_into(new_data, new_files) From a1fbdc40300f2af4084761047511d58096e14b7d Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 25 Mar 2020 09:59:09 +0100 Subject: [PATCH 346/474] adapter.aasx: Rework SupplementaryFileContainer to detect equal files, add test --- test/adapter/aasx/test_aasx.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 6cf3bdc..8d81894 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -29,6 +29,34 @@ def test_name_friendlyfier(self) -> None: name2 = friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS+a", model.IdentifierType.IRI)) self.assertEqual("http___example_com_AAS_a_1", name2) + def test_supplementary_file_container(self) -> None: + container = aasx.DictSupplementaryFileContainer() + with open(os.path.join(os.path.dirname(__file__), 'TestFile.pdf'), 'rb') as f: + new_name = container.add_file("/TestFile.pdf", f, "application/pdf") + # Name should not be modified, since there is no conflict + self.assertEqual("/TestFile.pdf", new_name) + f.seek(0) + container.add_file("/TestFile.pdf", f, "application/pdf") + # Name should not be modified, since there is still no conflict + self.assertEqual("/TestFile.pdf", new_name) + + with open(__file__, 'rb') as f: + new_name = container.add_file("/TestFile.pdf", f, "application/pdf") + # Now, we have a conflict + self.assertNotEqual("/TestFile.pdf", new_name) + self.assertIn(new_name, container) + + # Check metadata + self.assertEqual("application/pdf", container.get_content_type("/TestFile.pdf")) + self.assertEqual("b18229b24a4ee92c6c2b6bc6a8018563b17472f1150d35d5a5945afeb447ed44", + container.get_sha256("/TestFile.pdf").hex()) + self.assertIn("/TestFile.pdf", container) + + # Check contents + file_content = io.BytesIO() + container.write_file("/TestFile.pdf", file_content) + self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), "78450a66f59d74c073bf6858db340090ea72a8b1") + class AASXWriterTest(unittest.TestCase): def test_writing_reading_example_aas(self) -> None: From 2b4d7fe46427e0c41b5beb83aa53242dd7be20e9 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 9 Apr 2020 15:58:22 +0200 Subject: [PATCH 347/474] json: update to schema 2.0.1 --- test/adapter/json/.gitignore | 2 +- test/adapter/json/test_json_serialization.py | 2 +- test/adapter/json/test_json_serialization_deserialization.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/adapter/json/.gitignore b/test/adapter/json/.gitignore index 785a074..949e766 100644 --- a/test/adapter/json/.gitignore +++ b/test/adapter/json/.gitignore @@ -1,2 +1,2 @@ # JSON schema should not be added to the Git Repository due to license concerns -aasJSONSchemaV2.0.json +aasJSONSchema*.json \ No newline at end of file diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 230501f..0a86fab 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -21,7 +21,7 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, create_example, example_concept_description -JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') +JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') class JsonSerializationTest(unittest.TestCase): diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index a1837f0..cce19f4 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -20,7 +20,7 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description from aas.examples.data._helper import AASDataChecker -JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchemaV2.0.json') +JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') class JsonSerializationDeserializationTest(unittest.TestCase): From 1d270dc2b43e12f73d8c3b76507936b2abde28ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 9 Apr 2020 16:03:16 +0200 Subject: [PATCH 348/474] adapter: re-publish reader/writer functions Re-publish reader/writer functions and relevant classes from adapter.json and adapter.xml modules. Rename read_json_aas_file to read_aas_json_file. Change json and xml module imports accordingly. Close #65 --- .../adapter/json/test_json_deserialization.py | 24 +++++----- test/adapter/json/test_json_serialization.py | 20 ++++----- ...test_json_serialization_deserialization.py | 44 ++++++++----------- test/adapter/xml/test_xml_serialization.py | 16 +++---- 4 files changed, 49 insertions(+), 55 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 1de7c18..13caee0 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -19,7 +19,7 @@ import json import logging import unittest -from aas.adapter.json import json_deserialization +from aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, read_aas_json_file from aas import model @@ -32,9 +32,9 @@ def test_file_format_missing_list(self) -> None: "conceptDescriptions": [] }""" with self.assertRaisesRegex(KeyError, r"submodels"): - json_deserialization.read_json_aas_file(io.StringIO(data), False) + read_aas_json_file(io.StringIO(data), False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - json_deserialization.read_json_aas_file(io.StringIO(data), True) + read_aas_json_file(io.StringIO(data), True) self.assertIn("submodels", cm.output[0]) def test_file_format_wrong_list(self) -> None: @@ -57,9 +57,9 @@ def test_file_format_wrong_list(self) -> None: ] }""" with self.assertRaisesRegex(TypeError, r"submodels.*Asset"): - json_deserialization.read_json_aas_file(io.StringIO(data), False) + read_aas_json_file(io.StringIO(data), False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - json_deserialization.read_json_aas_file(io.StringIO(data), True) + read_aas_json_file(io.StringIO(data), True) self.assertIn("submodels", cm.output[0]) self.assertIn("Asset", cm.output[0]) @@ -74,9 +74,9 @@ def test_file_format_unknown_object(self) -> None: ] }""" with self.assertRaisesRegex(TypeError, r"submodels.*'foo'"): - json_deserialization.read_json_aas_file(io.StringIO(data), False) + read_aas_json_file(io.StringIO(data), False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - json_deserialization.read_json_aas_file(io.StringIO(data), True) + read_aas_json_file(io.StringIO(data), True) self.assertIn("submodels", cm.output[0]) self.assertIn("'foo'", cm.output[0]) @@ -100,11 +100,11 @@ def test_broken_asset(self) -> None: ]""" # In strict mode, we should catch an exception with self.assertRaisesRegex(KeyError, r"identification"): - json.loads(data, cls=json_deserialization.StrictAASFromJsonDecoder) + json.loads(data, cls=StrictAASFromJsonDecoder) # In failsafe mode, we should get a log entry and the first Asset entry should be returned as untouched dict with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - parsed_data = json.loads(data, cls=json_deserialization.AASFromJsonDecoder) + parsed_data = json.loads(data, cls=AASFromJsonDecoder) self.assertIn("identification", cm.output[0]) self.assertIsInstance(parsed_data, list) self.assertEqual(3, len(parsed_data)) @@ -143,13 +143,13 @@ def test_wrong_submodel_element_type(self) -> None: # The broken object should not raise an exception, but log a warning, even in strict mode. with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: with self.assertRaisesRegex(TypeError, r"SubmodelElement.*Asset"): - json.loads(data, cls=json_deserialization.StrictAASFromJsonDecoder) + json.loads(data, cls=StrictAASFromJsonDecoder) self.assertIn("modelType", cm.output[0]) # In failsafe mode, we should get a log entries for the broken object and the wrong type of the first two # submodelElements with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - parsed_data = json.loads(data, cls=json_deserialization.AASFromJsonDecoder) + parsed_data = json.loads(data, cls=AASFromJsonDecoder) self.assertGreaterEqual(len(cm.output), 3) self.assertIn("SubmodelElement", cm.output[1]) self.assertIn("SubmodelElement", cm.output[2]) @@ -168,7 +168,7 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.enhanced_attribute = "fancy!" - class EnhancedAASDecoder(json_deserialization.AASFromJsonDecoder): + class EnhancedAASDecoder(AASFromJsonDecoder): @classmethod def _construct_asset(cls, dct): return super()._construct_asset(dct, object_class=EnhancedAsset) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 0a86fab..112a2b6 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -15,7 +15,7 @@ import os from aas import model -from aas.adapter.json import json_serialization +from aas.adapter.json import AASToJsonEncoder, write_aas_json_file from jsonschema import validate # type: ignore from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ @@ -28,7 +28,7 @@ class JsonSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER", description={"en-us": "Germany", "de": "Deutschland"}) - json_data = json.dumps(test_object, cls=json_serialization.AASToJsonEncoder) + json_data = json.dumps(test_object, cls=AASToJsonEncoder) def test_random_object_serialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) @@ -47,7 +47,7 @@ def test_random_object_serialization(self) -> None: 'submodels': [submodel], 'assets': [], 'conceptDescriptions': [], - }, cls=json_serialization.AASToJsonEncoder) + }, cls=AASToJsonEncoder) json_data_new = json.loads(json_data) @@ -72,7 +72,7 @@ def test_random_object_serialization(self) -> None: 'submodels': [submodel], 'assets': [], 'conceptDescriptions': [], - }, cls=json_serialization.AASToJsonEncoder) + }, cls=AASToJsonEncoder) json_data_new = json.loads(json_data) # load schema @@ -85,7 +85,7 @@ def test_random_object_serialization(self) -> None: def test_aas_example_serialization(self) -> None: data = example_aas.create_full_example() file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) + write_aas_json_file(file=file, data=data) with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) @@ -100,7 +100,7 @@ def test_submodel_template_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_submodel_template.create_example_submodel_template()) file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) + write_aas_json_file(file=file, data=data) with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) @@ -114,7 +114,7 @@ def test_submodel_template_serialization(self) -> None: def test_full_empty_example_serialization(self) -> None: data = example_aas_mandatory_attributes.create_full_example() file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) + write_aas_json_file(file=file, data=data) with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) @@ -128,7 +128,7 @@ def test_full_empty_example_serialization(self) -> None: def test_missing_serialization(self) -> None: data = example_aas_missing_attributes.create_full_example() file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) + write_aas_json_file(file=file, data=data) with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) @@ -143,7 +143,7 @@ def test_concept_description_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_concept_description.create_iec61360_concept_description()) file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) + write_aas_json_file(file=file, data=data) with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) @@ -157,7 +157,7 @@ def test_concept_description_serialization(self) -> None: def test_full_example_serialization(self) -> None: data = create_example() file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) + write_aas_json_file(file=file, data=data) with open(JSON_SCHEMA_FILE, 'r') as json_file: aas_json_schema = json.load(json_file) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index cce19f4..980348b 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -15,7 +15,7 @@ import unittest from aas import model -from aas.adapter.json import json_serialization, json_deserialization +from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description @@ -41,22 +41,20 @@ def test_random_object_serialization_deserialization(self) -> None: 'submodels': [submodel], 'assets': [], 'conceptDescriptions': [], - }, cls=json_serialization.AASToJsonEncoder) + }, cls=AASToJsonEncoder) json_data_new = json.loads(json_data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module - json_object_store = json_deserialization.read_json_aas_file(io.StringIO(json_data), failsafe=False) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module + json_object_store = read_aas_json_file(io.StringIO(json_data), failsafe=False) def test_example_serialization_deserialization(self) -> None: data = example_aas.create_full_example() file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) + write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + json_object_store = read_aas_json_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) example_aas.check_full_example(checker, json_object_store) @@ -65,12 +63,11 @@ class JsonSerializationDeserializationTest2(unittest.TestCase): def test_example_mandatory_attributes_serialization_deserialization(self) -> None: data = example_aas_mandatory_attributes.create_full_example() file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) + write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + json_object_store = read_aas_json_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) example_aas_mandatory_attributes.check_full_example(checker, json_object_store) @@ -79,11 +76,10 @@ class JsonSerializationDeserializationTest3(unittest.TestCase): def test_example_missing_attributes_serialization_deserialization(self) -> None: data = example_aas_missing_attributes.create_full_example() file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module + write_aas_json_file(file=file, data=data) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + json_object_store = read_aas_json_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) example_aas_missing_attributes.check_full_example(checker, json_object_store) @@ -93,11 +89,10 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_submodel_template.create_example_submodel_template()) file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module + write_aas_json_file(file=file, data=data) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + json_object_store = read_aas_json_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) example_submodel_template.check_full_example(checker, json_object_store) @@ -107,10 +102,9 @@ def test_example_iec61360_concept_description_serialization_deserialization(self data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_concept_description.create_iec61360_concept_description()) file = io.StringIO() - json_serialization.write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json_deserialization - # module + write_aas_json_file(file=file, data=data) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module file.seek(0) - json_object_store = json_deserialization.read_json_aas_file(file, failsafe=False) + json_object_store = read_aas_json_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) example_concept_description.check_full_example(checker, json_object_store) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index d9ad656..8b0676b 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -14,7 +14,7 @@ import os from aas import model -from aas.adapter.xml import xml_serialization +from aas.adapter.xml import write_aas_xml_file, xml_serialization from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description @@ -47,7 +47,7 @@ def test_random_object_serialization(self) -> None: test_data.add(submodel) test_file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=test_file, data=test_data) + write_aas_xml_file(file=test_file, data=test_data) @unittest.skipUnless(os.path.exists(XML_SCHEMA_FILE), "XML Schema not found for validation") @@ -69,7 +69,7 @@ def test_random_object_serialization(self) -> None: test_data.add(submodel) test_file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=test_file, data=test_data) + write_aas_xml_file(file=test_file, data=test_data) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) @@ -82,7 +82,7 @@ def test_random_object_serialization(self) -> None: def test_full_example_serialization(self) -> None: data = example_aas.create_full_example() file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file=file, data=data) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) @@ -96,7 +96,7 @@ def test_submodel_template_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_submodel_template.create_example_submodel_template()) file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file=file, data=data) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) @@ -109,7 +109,7 @@ def test_submodel_template_serialization(self) -> None: def test_full_empty_example_serialization(self) -> None: data = example_aas_mandatory_attributes.create_full_example() file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file=file, data=data) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) @@ -122,7 +122,7 @@ def test_full_empty_example_serialization(self) -> None: def test_missing_serialization(self) -> None: data = example_aas_missing_attributes.create_full_example() file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file=file, data=data) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) @@ -136,7 +136,7 @@ def test_concept_description(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() data.add(example_concept_description.create_iec61360_concept_description()) file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file=file, data=data) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) From 36018cb2cb4a0744088f8bd20b54161ec9a4fb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Apr 2020 19:19:45 +0200 Subject: [PATCH 349/474] test: add serialize and deserialize test for xml --- .../test_xml_serialization_deserialization.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/adapter/xml/test_xml_serialization_deserialization.py diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py new file mode 100644 index 0000000..bf09064 --- /dev/null +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -0,0 +1,61 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import io +import unittest + +from aas import model +from aas.adapter.xml import xml_serialization, xml_deserialization + +from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas, example_concept_description +from aas.examples.data._helper import AASDataChecker + + +def _serialize_and_deserialize(data: model.DictObjectStore) -> model.DictObjectStore: + file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=file, data=data) + + # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml_deserialization + # module + file.seek(0) + return xml_deserialization.read_xml_aas_file(file, failsafe=False) + + +class XMLSerializationDeserializationTest(unittest.TestCase): + def test_example_serialization_deserialization(self) -> None: + object_store = _serialize_and_deserialize(example_aas.create_full_example()) + checker = AASDataChecker(raise_immediately=True) + example_aas.check_full_example(checker, object_store) + + def test_example_mandatory_attributes_serialization_deserialization(self) -> None: + object_store = _serialize_and_deserialize(example_aas_mandatory_attributes.create_full_example()) + checker = AASDataChecker(raise_immediately=True) + example_aas_mandatory_attributes.check_full_example(checker, object_store) + + def test_example_missing_attributes_serialization_deserialization(self) -> None: + object_store = _serialize_and_deserialize(example_aas_missing_attributes.create_full_example()) + checker = AASDataChecker(raise_immediately=True) + example_aas_missing_attributes.check_full_example(checker, object_store) + + def test_example_submodel_template_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_submodel_template.create_example_submodel_template()) + object_store = _serialize_and_deserialize(data) + checker = AASDataChecker(raise_immediately=True) + example_submodel_template.check_full_example(checker, object_store) + + def test_example_iec61360_concept_description_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_concept_description.create_iec61360_concept_description()) + object_store = _serialize_and_deserialize(data) + checker = AASDataChecker(raise_immediately=True) + example_concept_description.check_full_example(checker, object_store) From 17d15af65a7dd94dd63c7a0535f055284feb5109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 9 Apr 2020 23:53:36 +0200 Subject: [PATCH 350/474] adapter.xml: rename read_xml_aas_file to read_aas_xml_file Re-publish read_aas_xml_file in adapter.xml.__init__. --- test/adapter/xml/test_xml_serialization_deserialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index bf09064..de6aecd 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -13,7 +13,7 @@ import unittest from aas import model -from aas.adapter.xml import xml_serialization, xml_deserialization +from aas.adapter.xml import write_aas_xml_file, read_aas_xml_file from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description @@ -22,12 +22,12 @@ def _serialize_and_deserialize(data: model.DictObjectStore) -> model.DictObjectStore: file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file=file, data=data) # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml_deserialization # module file.seek(0) - return xml_deserialization.read_xml_aas_file(file, failsafe=False) + return read_aas_xml_file(file, failsafe=False) class XMLSerializationDeserializationTest(unittest.TestCase): From e1c4879f83a300cd25e17479a88345e40b4aa47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 10 Apr 2020 01:47:20 +0200 Subject: [PATCH 351/474] test: add xml_deserialization tests --- test/adapter/xml/test_xml_deserialization.py | 261 ++++++++++++++++++ .../test_xml_serialization_deserialization.py | 3 +- 2 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 test/adapter/xml/test_xml_deserialization.py diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py new file mode 100644 index 0000000..beb2cff --- /dev/null +++ b/test/adapter/xml/test_xml_deserialization.py @@ -0,0 +1,261 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import io +import logging +import unittest + +from aas.adapter.xml import read_aas_xml_file +from lxml import etree # type: ignore +from typing import Iterable, Type, Union + + +def _xml_wrap(xml: str) -> str: + return \ + """""" \ + """""" \ + + xml + """""" + + +def _root_cause(exception: BaseException) -> BaseException: + while exception.__cause__ is not None: + exception = exception.__cause__ + return exception + + +class XMLDeserializationTest(unittest.TestCase): + def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], error_type: Type[BaseException], + log_level: int) -> None: + """ + Runs read_xml_aas_file in failsafe mode and checks if each string is contained in the first message logged. + Then runs it in non-failsafe mode and checks if each string is contained in the first error raised. + + :param xml: The xml document to parse. + :param strings: One or more strings to match. + :param error_type: The expected error type. + :param log_level: The log level on which the string is expected. + """ + if isinstance(strings, str): + strings = [strings] + bytes_io = io.BytesIO(xml.encode("utf-8")) + with self.assertLogs(logging.getLogger(), level=log_level) as log_ctx: + read_aas_xml_file(bytes_io, True) + for s in strings: + self.assertIn(s, log_ctx.output[0]) + with self.assertRaises(error_type) as err_ctx: + read_aas_xml_file(bytes_io, False) + cause = _root_cause(err_ctx.exception) + for s in strings: + self.assertIn(s, str(cause)) + + def test_malformed_xml(self) -> None: + xml = ( + "invalid xml", + _xml_wrap("<<>>><<<<<"), + _xml_wrap("") + ) + for s in xml: + bytes_io = io.BytesIO(s.encode("utf-8")) + with self.assertRaises(etree.XMLSyntaxError): + read_aas_xml_file(bytes_io, False) + with self.assertLogs(logging.getLogger(), level=logging.ERROR): + read_aas_xml_file(bytes_io, True) + + def test_invalid_list_name(self) -> None: + xml = _xml_wrap("") + self._assertInExceptionAndLog(xml, "aas:invalidList", TypeError, logging.WARNING) + + def test_invalid_element_in_list(self) -> None: + xml = _xml_wrap(""" + + + + """) + self._assertInExceptionAndLog(xml, ["aas:invalidElement", "aas:assets"], KeyError, logging.WARNING) + + def test_missing_identification_attribute(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_asset + Instance + + + """) + self._assertInExceptionAndLog(xml, "idType", KeyError, logging.ERROR) + + def test_invalid_identification_attribute_value(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_asset + Instance + + + """) + self._assertInExceptionAndLog(xml, ["idType", "invalid"], ValueError, logging.ERROR) + + def test_missing_asset_kind(self) -> None: + xml = _xml_wrap(""" + + + + + """) + self._assertInExceptionAndLog(xml, "aas:kind", KeyError, logging.ERROR) + + def test_missing_asset_kind_text(self) -> None: + xml = _xml_wrap(""" + + + + + + """) + self._assertInExceptionAndLog(xml, "aas:kind", KeyError, logging.ERROR) + + def test_invalid_asset_kind_text(self) -> None: + xml = _xml_wrap(""" + + + invalidKind + + + """) + self._assertInExceptionAndLog(xml, ["aas:kind", "invalidKind"], ValueError, logging.ERROR) + + def test_invalid_boolean(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_asset + + + http://acplt.org/test_ref + + + + + """) + self._assertInExceptionAndLog(xml, "False", ValueError, logging.ERROR) + + def test_no_modeling_kind(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + + """) + # should get parsed successfully + read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + + def test_reference_kind_mismatch(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_aas + + + http://acplt.org/test_ref + + + + + """) + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: + read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + self.assertIn("GLOBAL_REFERENCE", context.output[0]) + self.assertIn("IRI=http://acplt.org/test_ref", context.output[0]) + self.assertIn("Asset", context.output[0]) + + def test_invalid_submodel_element(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + + + + """) + self._assertInExceptionAndLog(xml, "aas:invalidSubmodelElement", KeyError, logging.ERROR) + + def test_invalid_constraint(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + + + + + """) + self._assertInExceptionAndLog(xml, "aas:invalidConstraint", KeyError, logging.ERROR) + + def test_operation_variable_no_submodel_element(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + test_operation + + + + + + + + + + """) + self._assertInExceptionAndLog(xml, "aas:value", KeyError, logging.ERROR) + + def test_operation_variable_too_many_submodel_elements(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + test_operation + + + + + test_file + application/problem+xml + + + test_file2 + application/problem+xml + + + + + + + + + """) + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: + read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + self.assertIn("aas:value", context.output[0]) diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index de6aecd..093de30 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -24,8 +24,7 @@ def _serialize_and_deserialize(data: model.DictObjectStore) -> model.DictObjectS file = io.BytesIO() write_aas_xml_file(file=file, data=data) - # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml_deserialization - # module + # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml module file.seek(0) return read_aas_xml_file(file, failsafe=False) From 506a51bf4bb22b05a09c65ec7ee6ca57d4fbc7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 14 Apr 2020 14:10:00 +0200 Subject: [PATCH 352/474] adapter: explicitly define INSTANCE as ModelingKind default value test this behavior in the respective test remove TODO docstring from _construct_concept_description() --- test/adapter/xml/test_xml_deserialization.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index beb2cff..7949306 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -13,6 +13,7 @@ import logging import unittest +from aas import model from aas.adapter.xml import read_aas_xml_file from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -161,7 +162,11 @@ def test_no_modeling_kind(self) -> None: """) # should get parsed successfully - read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + object_store = read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + # modeling kind should default to INSTANCE + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel._kind, model.ModelingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: xml = _xml_wrap(""" From ce5ff087a3ed6341a883b2d0a6ff9eed5f79aa24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 14 Apr 2020 15:55:18 +0200 Subject: [PATCH 353/474] test.xml: don't access protected property _kind --- test/adapter/xml/test_xml_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 7949306..533eb8f 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -166,7 +166,7 @@ def test_no_modeling_kind(self) -> None: # modeling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel._kind, model.ModelingKind.INSTANCE) + self.assertEqual(submodel.kind, model.ModelingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: xml = _xml_wrap(""" From 00bd0ca769adefa382ef8948b90b555a8890987e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 17 Apr 2020 04:23:31 +0200 Subject: [PATCH 354/474] set correct copyright year for all files add set_copyright_year.sh script to etc/scripts --- test/adapter/aasx/test_aasx.py | 2 +- test/adapter/json/test_json_deserialization.py | 2 +- test/adapter/json/test_json_serialization.py | 2 +- test/adapter/json/test_json_serialization_deserialization.py | 2 +- test/adapter/test_couchdb.py | 2 +- test/adapter/xml/test_xml_serialization.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 8d81894..75d81ec 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -1,4 +1,4 @@ -# Copyright 2019 PyI40AAS Contributors +# Copyright 2020 PyI40AAS Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 13caee0..e636eb0 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -1,4 +1,4 @@ -# Copyright 2019 PyI40AAS Contributors +# Copyright 2020 PyI40AAS Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 112a2b6..ec84177 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,4 +1,4 @@ -# Copyright 2019 PyI40AAS Contributors +# Copyright 2020 PyI40AAS Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 980348b..fc8e41f 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -1,4 +1,4 @@ -# Copyright 2019 PyI40AAS Contributors +# Copyright 2020 PyI40AAS Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index 5e262ab..6b0a2a2 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -1,4 +1,4 @@ -# Copyright 2019 PyI40AAS Contributors +# Copyright 2020 PyI40AAS Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 8b0676b..1360d53 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -1,4 +1,4 @@ -# Copyright 2019 PyI40AAS Contributors +# Copyright 2020 PyI40AAS Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at From 0a2eaeedcdc6c2c6b52f39a37684cc058ebfe722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 17 Apr 2020 14:56:00 +0200 Subject: [PATCH 355/474] adapter: specify type of objects in DictObjectStore for xml/json deserialize functions Previously mypy would not know the type of the objects in the DictObjectStore, which would result in mypy infering Any and not checking whether attributes exist for objects in the object store. --- test/adapter/xml/test_xml_deserialization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 533eb8f..2b0695e 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -166,6 +166,7 @@ def test_no_modeling_kind(self) -> None: # modeling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) + assert(isinstance(submodel, model.Submodel)) # to make mypy happy self.assertEqual(submodel.kind, model.ModelingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: From 8ca3babc9b1f14e96f2b49f56af13537ebba6ff4 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 17 Apr 2020 12:50:52 +0200 Subject: [PATCH 356/474] test: Fix occasional test failures due to rounding error in test_aasx --- test/adapter/aasx/test_aasx.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 75d81ec..d7bb6e3 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -93,7 +93,10 @@ def test_writing_reading_example_aas(self) -> None: example_aas.check_full_example(checker, new_data) # Check core properties - self.assertEqual(new_cp.created, cp.created) + assert(isinstance(cp.created, datetime.datetime)) # to make mypy happy + self.assertIsInstance(new_cp.created, datetime.datetime) + assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy + self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") self.assertIsNone(new_cp.lastModifiedBy) From ddc3faf350802941fa02cfebcd30107c16f7bd63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 23 Apr 2020 19:53:37 +0200 Subject: [PATCH 357/474] adapter.xml: change for current schema revert this commit should our suggestions get accepted see #56 #57 --- test/adapter/xml/test_xml_deserialization.py | 74 ++++++++++++-------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 2b0695e..4c924f2 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -189,12 +189,16 @@ def test_reference_kind_mismatch(self) -> None: self.assertIn("Asset", context.output[0]) def test_invalid_submodel_element(self) -> None: + # TODO: simplify this should our suggestion regarding the XML schema get accepted + # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 xml = _xml_wrap(""" http://acplt.org/test_submodel - + + + @@ -202,33 +206,41 @@ def test_invalid_submodel_element(self) -> None: self._assertInExceptionAndLog(xml, "aas:invalidSubmodelElement", KeyError, logging.ERROR) def test_invalid_constraint(self) -> None: + # TODO: simplify this should our suggestion regarding the XML schema get accepted + # https://git.rwth-aachen.de/acplt/pyaas/-/issues/56 xml = _xml_wrap(""" http://acplt.org/test_submodel - - - + + + + + """) self._assertInExceptionAndLog(xml, "aas:invalidConstraint", KeyError, logging.ERROR) def test_operation_variable_no_submodel_element(self) -> None: + # TODO: simplify this should our suggestion regarding the XML schema get accepted + # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 xml = _xml_wrap(""" http://acplt.org/test_submodel - - test_operation - - - - - - + + + test_operation + + + + + + + @@ -236,28 +248,32 @@ def test_operation_variable_no_submodel_element(self) -> None: self._assertInExceptionAndLog(xml, "aas:value", KeyError, logging.ERROR) def test_operation_variable_too_many_submodel_elements(self) -> None: + # TODO: simplify this should our suggestion regarding the XML schema get accepted + # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 xml = _xml_wrap(""" http://acplt.org/test_submodel - - test_operation - - - - - test_file - application/problem+xml - - - test_file2 - application/problem+xml - - - - - + + + test_operation + + + + + test_file + application/problem+xml + + + test_file2 + application/problem+xml + + + + + + From 6041e0e01b4f03c76515f343130e4fac5f6917f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 May 2020 16:21:20 +0200 Subject: [PATCH 358/474] adapter.xml.xml_deserialization: support schema v2.0.1 change tests accordingly --- test/adapter/xml/test_xml_deserialization.py | 30 ++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 4c924f2..27c523b 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -214,9 +214,7 @@ def test_invalid_constraint(self) -> None: http://acplt.org/test_submodel - - - + @@ -235,9 +233,7 @@ def test_operation_variable_no_submodel_element(self) -> None: test_operation - - - + @@ -259,18 +255,16 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: test_operation - - - - test_file - application/problem+xml - - - test_file2 - application/problem+xml - - - + + + test_file + application/problem+xml + + + test_file2 + application/problem+xml + + From b34048d54caefad87b93d81b146080a5b9330176 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Mon, 27 Apr 2020 13:32:15 +0200 Subject: [PATCH 359/474] compliance_tool: Add xml extension including tests adapter.xml.xml_serialization: Remove attribute "xsi:type" from element "value" in _value_to_xml adapter.xml.xml_serialization: Add wrapping around statements in entity_to_xml adapter.xml.xml_deserialization: add parsing submodelElement wrapper to _construct_entity and _construct_submodel_element_collection --- .../test_json_serialization_deserialization.py | 14 +++++++++++++- test/adapter/xml/.gitignore | 1 + .../xml/test_xml_serialization_deserialization.py | 8 +++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index fc8e41f..d9e5047 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -18,7 +18,7 @@ from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, example_concept_description + example_aas_mandatory_attributes, example_aas, example_concept_description, create_example from aas.examples.data._helper import AASDataChecker JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') @@ -108,3 +108,15 @@ def test_example_iec61360_concept_description_serialization_deserialization(self json_object_store = read_aas_json_file(file, failsafe=False) checker = AASDataChecker(raise_immediately=True) example_concept_description.check_full_example(checker, json_object_store) + + +class JsonSerializationDeserializationTest6(unittest.TestCase): + def test_example_all_examples_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = create_example() + file = io.StringIO() + write_aas_json_file(file=file, data=data) + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module + file.seek(0) + json_object_store = read_aas_json_file(file, failsafe=False) + checker = AASDataChecker(raise_immediately=True) + checker.check_object_store(json_object_store, data) diff --git a/test/adapter/xml/.gitignore b/test/adapter/xml/.gitignore index a1a70f3..7ac9398 100644 --- a/test/adapter/xml/.gitignore +++ b/test/adapter/xml/.gitignore @@ -1,4 +1,5 @@ # XML schema should not be added to the Git repository due to license concerns AAS.xsd +aasXMLSchema*.xsd AAS_ABAC.xsd IEC61360.xsd \ No newline at end of file diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index 093de30..44041ec 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -16,7 +16,7 @@ from aas.adapter.xml import write_aas_xml_file, read_aas_xml_file from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, example_concept_description + example_aas_mandatory_attributes, example_aas, example_concept_description, create_example from aas.examples.data._helper import AASDataChecker @@ -58,3 +58,9 @@ def test_example_iec61360_concept_description_serialization_deserialization(self object_store = _serialize_and_deserialize(data) checker = AASDataChecker(raise_immediately=True) example_concept_description.check_full_example(checker, object_store) + + def test_example_all_examples_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = create_example() + object_store = _serialize_and_deserialize(data) + checker = AASDataChecker(raise_immediately=True) + checker.check_object_store(object_store, data) From ef1bb350827b6794ea3c05fd580fa82c746d9ad7 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 30 Apr 2020 09:05:44 +0200 Subject: [PATCH 360/474] test.adapter.xml: delete aasXMLSchema*.xsd from gitignore --- test/adapter/xml/.gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/test/adapter/xml/.gitignore b/test/adapter/xml/.gitignore index 7ac9398..a1a70f3 100644 --- a/test/adapter/xml/.gitignore +++ b/test/adapter/xml/.gitignore @@ -1,5 +1,4 @@ # XML schema should not be added to the Git repository due to license concerns AAS.xsd -aasXMLSchema*.xsd AAS_ABAC.xsd IEC61360.xsd \ No newline at end of file From 862ae20b44387487459a93b121928595f76ebe6c Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 26 May 2020 11:11:43 +0200 Subject: [PATCH 361/474] adapter, cli: add json schema and change path --- test/adapter/json/test_json_serialization.py | 5 +++-- .../adapter/json/test_json_serialization_deserialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index ec84177..c903620 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -13,6 +13,7 @@ import unittest import json import os +from os.path import dirname from aas import model from aas.adapter.json import AASToJsonEncoder, write_aas_json_file @@ -21,7 +22,8 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, create_example, example_concept_description -JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') +JSON_SCHEMA_FILE = os.path.join(dirname(dirname(dirname(dirname(__file__)))), + 'aas', 'adapter', 'json', 'aasJSONSchema.json') class JsonSerializationTest(unittest.TestCase): @@ -51,7 +53,6 @@ def test_random_object_serialization(self) -> None: json_data_new = json.loads(json_data) -@unittest.skipUnless(os.path.exists(JSON_SCHEMA_FILE), "JSON Schema not found for validation") class JsonSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index d9e5047..88e31a4 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -13,6 +13,7 @@ import json import os import unittest +from os.path import dirname from aas import model from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file @@ -20,7 +21,6 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description, create_example from aas.examples.data._helper import AASDataChecker -JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') class JsonSerializationDeserializationTest(unittest.TestCase): diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 1360d53..e9febab 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -10,6 +10,8 @@ # specific language governing permissions and limitations under the License. import io import unittest +from os.path import dirname + from lxml import etree # type: ignore import os @@ -19,7 +21,8 @@ from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description -XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'AAS.xsd') +XML_SCHEMA_FILE = os.path.join(dirname(dirname(dirname(dirname(__file__)))), + 'aas', 'adapter', 'xml', 'AAS.xsd') class XMLSerializationTest(unittest.TestCase): @@ -50,7 +53,6 @@ def test_random_object_serialization(self) -> None: write_aas_xml_file(file=test_file, data=test_data) -@unittest.skipUnless(os.path.exists(XML_SCHEMA_FILE), "XML Schema not found for validation") class XMLSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) From 57c2b51591f18213ee2259658ad28c41af7e7a73 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 27 May 2020 10:13:09 +0200 Subject: [PATCH 362/474] adapter: Improve path handling of Schema files --- test/adapter/json/test_json_serialization.py | 7 +------ test/adapter/xml/test_xml_serialization.py | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index c903620..1ab4fd3 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -12,19 +12,14 @@ import io import unittest import json -import os -from os.path import dirname from aas import model -from aas.adapter.json import AASToJsonEncoder, write_aas_json_file +from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE from jsonschema import validate # type: ignore from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, create_example, example_concept_description -JSON_SCHEMA_FILE = os.path.join(dirname(dirname(dirname(dirname(__file__)))), - 'aas', 'adapter', 'json', 'aasJSONSchema.json') - class JsonSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index e9febab..3df5e42 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -10,20 +10,15 @@ # specific language governing permissions and limitations under the License. import io import unittest -from os.path import dirname from lxml import etree # type: ignore -import os from aas import model -from aas.adapter.xml import write_aas_xml_file, xml_serialization +from aas.adapter.xml import write_aas_xml_file, xml_serialization, XML_SCHEMA_FILE from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description -XML_SCHEMA_FILE = os.path.join(dirname(dirname(dirname(dirname(__file__)))), - 'aas', 'adapter', 'xml', 'AAS.xsd') - class XMLSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: From 5a225b93916093efc0a649f63b2377e27aca4e7c Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 27 May 2020 10:13:25 +0200 Subject: [PATCH 363/474] test: Remove .gitignore files for Schema files --- test/adapter/json/.gitignore | 2 -- test/adapter/xml/.gitignore | 4 ---- 2 files changed, 6 deletions(-) delete mode 100644 test/adapter/json/.gitignore delete mode 100644 test/adapter/xml/.gitignore diff --git a/test/adapter/json/.gitignore b/test/adapter/json/.gitignore deleted file mode 100644 index 949e766..0000000 --- a/test/adapter/json/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# JSON schema should not be added to the Git Repository due to license concerns -aasJSONSchema*.json \ No newline at end of file diff --git a/test/adapter/xml/.gitignore b/test/adapter/xml/.gitignore deleted file mode 100644 index a1a70f3..0000000 --- a/test/adapter/xml/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# XML schema should not be added to the Git repository due to license concerns -AAS.xsd -AAS_ABAC.xsd -IEC61360.xsd \ No newline at end of file From 2b3f15fb45acb95955a6daa022cf444cac04f329 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 13 Oct 2020 13:39:32 +0200 Subject: [PATCH 364/474] test.adapter.test_couchdb: Make tests be skipped Since adapter.couchd will be deprecated soon, these tests are only here for reference and will be deleted together with adapter.couchdb, after the adapter.couchdb is implemented in backend.couchdb --- test/adapter/test_couchdb.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py index 6b0a2a2..9e2d795 100644 --- a/test/adapter/test_couchdb.py +++ b/test/adapter/test_couchdb.py @@ -43,6 +43,8 @@ COUCHDB_OKAY = False COUCHDB_ERROR = e +COUCHDB_OKAY = False + @unittest.skipUnless(COUCHDB_OKAY, "No CouchDB is reachable at {}/{}: {}".format(TEST_CONFIG['couchdb']['url'], TEST_CONFIG['couchdb']['database'], From a5867fccdc49eea68e4844e37553c4e736dc0db4 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Wed, 28 Oct 2020 12:31:44 +0100 Subject: [PATCH 365/474] adapter: Remove couchdb and according tests. The functionality will now be found in `backend.couchdb` --- test/adapter/test_couchdb.py | 218 ----------------------------------- 1 file changed, 218 deletions(-) delete mode 100644 test/adapter/test_couchdb.py diff --git a/test/adapter/test_couchdb.py b/test/adapter/test_couchdb.py deleted file mode 100644 index 9e2d795..0000000 --- a/test/adapter/test_couchdb.py +++ /dev/null @@ -1,218 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -import base64 -import concurrent.futures -import configparser -import copy -import os -import unittest -import urllib.request -import urllib.error - -from aas.adapter import couchdb -from aas.examples.data.example_aas import * - - -TEST_CONFIG = configparser.ConfigParser() -TEST_CONFIG.read((os.path.join(os.path.dirname(__file__), "..", "test_config.default.ini"), - os.path.join(os.path.dirname(__file__), "..", "test_config.ini"))) - - -# Check if CouchDB database is avalable. Otherwise, skip tests. -try: - request = urllib.request.Request( - "{}/{}".format(TEST_CONFIG['couchdb']['url'], TEST_CONFIG['couchdb']['database']), - headers={ - 'Authorization': 'Basic %s' % base64.b64encode( - ('%s:%s' % (TEST_CONFIG['couchdb']['user'], TEST_CONFIG['couchdb']['password'])) - .encode('ascii')).decode("ascii") - }, - method='HEAD') - urllib.request.urlopen(request) - COUCHDB_OKAY = True - COUCHDB_ERROR = None -except urllib.error.URLError as e: - COUCHDB_OKAY = False - COUCHDB_ERROR = e - -COUCHDB_OKAY = False - - -@unittest.skipUnless(COUCHDB_OKAY, "No CouchDB is reachable at {}/{}: {}".format(TEST_CONFIG['couchdb']['url'], - TEST_CONFIG['couchdb']['database'], - COUCHDB_ERROR)) -class CouchDBTest(unittest.TestCase): - def setUp(self) -> None: - # Create CouchDB store, login and check database - self.db = couchdb.CouchDBObjectStore(TEST_CONFIG['couchdb']['url'], TEST_CONFIG['couchdb']['database']) - self.db.login(TEST_CONFIG['couchdb']['user'], TEST_CONFIG['couchdb']['password']) - self.db.check_database() - - def tearDown(self) -> None: - self.db.clear() - self.db.logout() - - def test_example_submodel_storing(self) -> None: - example_submodel = create_example_submodel() - - # Add exmaple submodel - self.db.add(example_submodel) - self.assertEqual(1, len(self.db)) - self.assertIn(example_submodel, self.db) - - # Restore example submodel and check data - submodel_restored = self.db.get_identifiable( - model.Identifier(id_='https://acplt.org/Test_Submodel', id_type=model.IdentifierType.IRI)) - assert(isinstance(submodel_restored, model.Submodel)) - checker = AASDataChecker(raise_immediately=True) - check_example_submodel(checker, submodel_restored) - - # Delete example submodel - self.db.discard(submodel_restored) - self.assertNotIn(example_submodel, self.db) - - def test_iterating(self) -> None: - example_data = create_full_example() - - # Add all objects - for item in example_data: - self.db.add(item) - - self.assertEqual(6, len(self.db)) - - # Iterate objects, add them to a DictObjectStore and check them - retrieved_data_store: model.provider.DictObjectStore[model.Identifiable] = model.provider.DictObjectStore() - for item in self.db: - retrieved_data_store.add(item) - checker = AASDataChecker(raise_immediately=True) - check_full_example(checker, retrieved_data_store) - - def test_parallel_iterating(self) -> None: - example_data = create_full_example() - ids = [item.identification for item in example_data] - - # Add objects via thread pool executor - with concurrent.futures.ThreadPoolExecutor() as pool: - result = pool.map(self.db.add, example_data) - list(result) # Iterate Executor result to raise exceptions - - self.assertEqual(6, len(self.db)) - - # Retrieve objects via thread pool executor - with concurrent.futures.ThreadPoolExecutor() as pool: - retrieved_objects = pool.map(self.db.get_identifiable, ids) - - retrieved_data_store: model.provider.DictObjectStore[model.Identifiable] = model.provider.DictObjectStore() - for item in retrieved_objects: - retrieved_data_store.add(item) - self.assertEqual(6, len(retrieved_data_store)) - checker = AASDataChecker(raise_immediately=True) - check_full_example(checker, retrieved_data_store) - - # Delete objects via thread pool executor - with concurrent.futures.ThreadPoolExecutor() as pool: - result = pool.map(self.db.discard, example_data) - list(result) # Iterate Executor result to raise exceptions - - self.assertEqual(0, len(self.db)) - - def test_key_errors(self) -> None: - # Double adding an object should raise a KeyError - example_submodel = create_example_submodel() - self.db.add(example_submodel) - with self.assertRaises(KeyError) as cm: - self.db.add(example_submodel) - self.assertEqual("'Identifiable with id Identifier(IRI=https://acplt.org/Test_Submodel) already exists in " - "CouchDB database'", str(cm.exception)) - - # Querying a deleted object should raise a KeyError - retrieved_submodel = self.db.get_identifiable( - model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) - self.db.discard(example_submodel) - with self.assertRaises(KeyError) as cm: - self.db.get_identifiable(model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) - self.assertEqual("'No Identifiable with id IRI-https://acplt.org/Test_Submodel found in CouchDB database'", - str(cm.exception)) - - # Double deleting should also raise a KeyError - with self.assertRaises(KeyError) as cm: - self.db.discard(retrieved_submodel) - self.assertEqual("'No AAS object with id Identifier(IRI=https://acplt.org/Test_Submodel) exists in " - "CouchDB database'", str(cm.exception)) - - def test_conflict_errors(self) -> None: - # Preperation: add object and retrieve it from the database - example_submodel = create_example_submodel() - self.db.add(example_submodel) - retrieved_submodel = self.db.get_identifiable( - model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) - - # Simulate a concurrent modification - remote_modified_submodel = copy.copy(retrieved_submodel) - remote_modified_submodel.id_short = "newIdShort" - remote_modified_submodel.commit_changes() - - # Committing changes to the retrieved object should now raise a conflict error - retrieved_submodel.id_short = "myOtherNewIdShort" - with self.assertRaises(couchdb.CouchDBConflictError) as cm: - retrieved_submodel.commit_changes() - self.assertEqual("Could not commit changes to id Identifier(IRI=https://acplt.org/Test_Submodel) due to a " - "concurrent modification in the database.", str(cm.exception)) - - # Deleting the submodel with safe_delete should also raise a conflict error. Deletion without safe_delete should - # work - with self.assertRaises(couchdb.CouchDBConflictError) as cm: - self.db.discard(retrieved_submodel, True) - self.assertEqual("Object with id Identifier(IRI=https://acplt.org/Test_Submodel) has been modified in the " - "database since the version requested to be deleted.", str(cm.exception)) - self.db.discard(retrieved_submodel, False) - self.assertEqual(0, len(self.db)) - - # Committing after deletion should also raise a conflict error - with self.assertRaises(couchdb.CouchDBConflictError) as cm: - retrieved_submodel.commit_changes() - self.assertEqual("Could not commit changes to id Identifier(IRI=https://acplt.org/Test_Submodel) due to a " - "concurrent modification in the database.", str(cm.exception)) - - def test_editing(self) -> None: - example_submodel = create_example_submodel() - self.db.add(example_submodel) - - # Retrieve submodel from database and change ExampleCapability's semanticId - submodel = self.db.get_identifiable( - model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) - assert(isinstance(submodel, couchdb.CouchDBSubmodel)) - capability = submodel.submodel_element.get_referable('ExampleCapability') - capability.semantic_id = model.Reference((model.Key(type_=model.KeyElements.GLOBAL_REFERENCE, - local=False, - value='http://acplt.org/Capabilities/AnotherCapability', - id_type=model.KeyType.IRDI),)) - - # Commit changes - submodel.commit_changes() - - # Change ExampleSubmodelCollectionOrdered's description - collection = submodel.submodel_element.get_referable('ExampleSubmodelCollectionOrdered') - collection.description['de'] = "Eine sehr wichtige Sammlung von Elementen" # type: ignore - - # Commit changes - submodel.commit_changes() - - # Check version in database - new_submodel = self.db.get_identifiable( - model.Identifier('https://acplt.org/Test_Submodel', model.IdentifierType.IRI)) - assert(isinstance(new_submodel, couchdb.CouchDBSubmodel)) - capability = new_submodel.submodel_element.get_referable('ExampleCapability') - assert(isinstance(capability, model.Capability)) - self.assertEqual('http://acplt.org/Capabilities/AnotherCapability', - capability.semantic_id.key[0].value) # type: ignore - collection = new_submodel.submodel_element.get_referable('ExampleSubmodelCollectionOrdered') - self.assertEqual("Eine sehr wichtige Sammlung von Elementen", collection.description['de']) # type: ignore From b0ad89c10b24adaa87eba109e4b050323e90e6e6 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Thu, 24 Sep 2020 15:10:58 +0200 Subject: [PATCH 366/474] adapter.aasx: Allow chosing between XML and JSON serialization when writing AASX packages --- test/adapter/aasx/test_aasx.py | 71 ++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index d7bb6e3..7ac5b3d 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -73,37 +73,40 @@ def test_writing_reading_example_aas(self) -> None: cp.creator = "PyI40AAS Testing Framework" # Write AASX file - fd, filename = tempfile.mkstemp(suffix=".aasx") - os.close(fd) - with aasx.AASXWriter(filename) as writer: - writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', - id_type=model.IdentifierType.IRI), - data, files) - writer.write_core_properties(cp) - - # Read AASX file - new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - new_files = aasx.DictSupplementaryFileContainer() - with aasx.AASXReader(filename) as reader: - reader.read_into(new_data, new_files) - new_cp = reader.get_core_properties() - - # Check AAS objects - checker = _helper.AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, new_data) - - # Check core properties - assert(isinstance(cp.created, datetime.datetime)) # to make mypy happy - self.assertIsInstance(new_cp.created, datetime.datetime) - assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy - self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) - self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") - self.assertIsNone(new_cp.lastModifiedBy) - - # Check files - self.assertEqual(new_files.get_content_type("/TestFile.pdf"), "application/pdf") - file_content = io.BytesIO() - new_files.write_file("/TestFile.pdf", file_content) - self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), "78450a66f59d74c073bf6858db340090ea72a8b1") - - os.unlink(filename) + for write_json in (False, True): + with self.subTest(write_json=write_json): + fd, filename = tempfile.mkstemp(suffix=".aasx") + os.close(fd) + with aasx.AASXWriter(filename) as writer: + writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', + id_type=model.IdentifierType.IRI), + data, files, write_json=write_json) + writer.write_core_properties(cp) + + # Read AASX file + new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + new_files = aasx.DictSupplementaryFileContainer() + with aasx.AASXReader(filename) as reader: + reader.read_into(new_data, new_files) + new_cp = reader.get_core_properties() + + # Check AAS objects + checker = _helper.AASDataChecker(raise_immediately=True) + example_aas.check_full_example(checker, new_data) + + # Check core properties + assert(isinstance(cp.created, datetime.datetime)) # to make mypy happy + self.assertIsInstance(new_cp.created, datetime.datetime) + assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy + self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) + self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") + self.assertIsNone(new_cp.lastModifiedBy) + + # Check files + self.assertEqual(new_files.get_content_type("/TestFile.pdf"), "application/pdf") + file_content = io.BytesIO() + new_files.write_file("/TestFile.pdf", file_content) + self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), + "78450a66f59d74c073bf6858db340090ea72a8b1") + + os.unlink(filename) From c14d486df4bed84f9c42f19819268f24744d7735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Oct 2020 22:21:53 +0200 Subject: [PATCH 367/474] adapter.xml.xml_deserialization: add read_aas_xml_file_into() function read_aas_xml_file() is now nothing more than a wrapper function, fix tests accordingly --- test/adapter/xml/test_xml_deserialization.py | 72 +++++++++++++++++--- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 27c523b..ee1fb69 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -14,7 +14,7 @@ import unittest from aas import model -from aas.adapter.xml import read_aas_xml_file +from aas.adapter.xml import read_aas_xml_file, read_aas_xml_file_into from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -52,11 +52,11 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], strings = [strings] bytes_io = io.BytesIO(xml.encode("utf-8")) with self.assertLogs(logging.getLogger(), level=log_level) as log_ctx: - read_aas_xml_file(bytes_io, True) + read_aas_xml_file(bytes_io, failsafe=True) for s in strings: self.assertIn(s, log_ctx.output[0]) with self.assertRaises(error_type) as err_ctx: - read_aas_xml_file(bytes_io, False) + read_aas_xml_file(bytes_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: self.assertIn(s, str(cause)) @@ -70,9 +70,9 @@ def test_malformed_xml(self) -> None: for s in xml: bytes_io = io.BytesIO(s.encode("utf-8")) with self.assertRaises(etree.XMLSyntaxError): - read_aas_xml_file(bytes_io, False) + read_aas_xml_file(bytes_io, failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.ERROR): - read_aas_xml_file(bytes_io, True) + read_aas_xml_file(bytes_io, failsafe=True) def test_invalid_list_name(self) -> None: xml = _xml_wrap("") @@ -162,7 +162,7 @@ def test_no_modeling_kind(self) -> None: """) # should get parsed successfully - object_store = read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + object_store = read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) # modeling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) @@ -183,10 +183,9 @@ def test_reference_kind_mismatch(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: - read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) - self.assertIn("GLOBAL_REFERENCE", context.output[0]) - self.assertIn("IRI=http://acplt.org/test_ref", context.output[0]) - self.assertIn("Asset", context.output[0]) + read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) + for s in ("GLOBAL_REFERENCE", "RI=http://acplt.org/test_ref", "Asset"): + self.assertIn(s, context.output[0]) def test_invalid_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted @@ -273,5 +272,56 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: - read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) self.assertIn("aas:value", context.output[0]) + + def test_duplicate_identifier(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_aas + + + http://acplt.org/asset_ref + + + + + + + http://acplt.org/test_aas + + + + """) + self._assertInExceptionAndLog(xml, "duplicate identifier", KeyError, logging.ERROR) + + def test_duplicate_identifier_object_store(self) -> None: + def get_clean_store() -> model.DictObjectStore: + store: model.DictObjectStore = model.DictObjectStore() + submodel = model.Submodel(model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI)) + store.add(submodel) + return store + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + + """) + bytes_io = io.BytesIO(xml.encode("utf-8")) + + object_store = get_clean_store() + read_aas_xml_file_into(object_store, bytes_io, replace_existing=True, ignore_existing=False) + self.assertIsInstance(object_store.pop(), model.Submodel) + + object_store = get_clean_store() + with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: + read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) + self.assertIn("already exists in the object store", log_ctx.output[0]) + + with self.assertRaises(KeyError) as err_ctx: + read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) + cause = _root_cause(err_ctx.exception) + self.assertIn("already exists in the object store", str(cause)) From f98695d8aa75c13d8cdeebb4756b78e77cf46380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 8 Oct 2020 22:26:34 +0200 Subject: [PATCH 368/474] adapter.xml.xml_deserialization: add read_aas_xml_element() move xml document parsing to a separate function _parse_xml_document() allow passing keyword arguments to the xml parser --- test/adapter/xml/test_xml_deserialization.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index ee1fb69..b5ea015 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -14,7 +14,7 @@ import unittest from aas import model -from aas.adapter.xml import read_aas_xml_file, read_aas_xml_file_into +from aas.adapter.xml import AASFromXmlDecoder, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -325,3 +325,15 @@ def get_clean_store() -> model.DictObjectStore: read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) cause = _root_cause(err_ctx.exception) self.assertIn("already exists in the object store", str(cause)) + + def test_read_aas_xml_element(self) -> None: + xml = """ + + http://acplt.org/test_submodel + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + submodel = read_aas_xml_element(bytes_io, AASFromXmlDecoder.construct_submodel, failsafe=False) + self.assertIsInstance(submodel, model.Submodel) From e0f7be46b235fe839eec2340de10b1b59c03a898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 26 Oct 2020 15:21:14 +0100 Subject: [PATCH 369/474] adapter.xml.xml_deserialization: make failsafe and stripped class variables add enum XMLConstructables to specify which element to construct in read_aas_xml_element() read_aas_xml_file_into(): support specifying a decoder class add strict, stripped and a strict+stripped decoder class remove _constructor_name_to_typename() helper function add tests for parsing stripped xml elements --- test/adapter/xml/test_xml_deserialization.py | 180 +++++++++++++++++-- 1 file changed, 166 insertions(+), 14 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index b5ea015..f8606cb 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -14,7 +14,7 @@ import unittest from aas import model -from aas.adapter.xml import AASFromXmlDecoder, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element +from aas.adapter.xml import XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -166,7 +166,7 @@ def test_no_modeling_kind(self) -> None: # modeling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) - assert(isinstance(submodel, model.Submodel)) # to make mypy happy + assert isinstance(submodel, model.Submodel) # to make mypy happy self.assertEqual(submodel.kind, model.ModelingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: @@ -184,12 +184,12 @@ def test_reference_kind_mismatch(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - for s in ("GLOBAL_REFERENCE", "RI=http://acplt.org/test_ref", "Asset"): + for s in ("GLOBAL_REFERENCE", "IRI=http://acplt.org/test_ref", "Asset"): self.assertIn(s, context.output[0]) def test_invalid_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" @@ -206,7 +206,7 @@ def test_invalid_submodel_element(self) -> None: def test_invalid_constraint(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyaas/-/issues/56 + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" @@ -222,7 +222,7 @@ def test_invalid_constraint(self) -> None: def test_operation_variable_no_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" @@ -244,7 +244,7 @@ def test_operation_variable_no_submodel_element(self) -> None: def test_operation_variable_too_many_submodel_elements(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" @@ -297,15 +297,19 @@ def test_duplicate_identifier(self) -> None: self._assertInExceptionAndLog(xml, "duplicate identifier", KeyError, logging.ERROR) def test_duplicate_identifier_object_store(self) -> None: + sm_id = model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI) + def get_clean_store() -> model.DictObjectStore: store: model.DictObjectStore = model.DictObjectStore() - submodel = model.Submodel(model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI)) - store.add(submodel) + submodel_ = model.Submodel(sm_id, id_short="test123") + store.add(submodel_) return store + xml = _xml_wrap(""" http://acplt.org/test_submodel + test456 @@ -313,18 +317,30 @@ def get_clean_store() -> model.DictObjectStore: bytes_io = io.BytesIO(xml.encode("utf-8")) object_store = get_clean_store() - read_aas_xml_file_into(object_store, bytes_io, replace_existing=True, ignore_existing=False) - self.assertIsInstance(object_store.pop(), model.Submodel) + identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=True, ignore_existing=False) + self.assertEqual(identifiers.pop(), sm_id) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test456") object_store = get_clean_store() with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: - read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) + identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) + self.assertEqual(len(identifiers), 0) self.assertIn("already exists in the object store", log_ctx.output[0]) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test123") + object_store = get_clean_store() with self.assertRaises(KeyError) as err_ctx: - read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) + identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) + self.assertEqual(len(identifiers), 0) cause = _root_cause(err_ctx.exception) self.assertIn("already exists in the object store", str(cause)) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test123") def test_read_aas_xml_element(self) -> None: xml = """ @@ -335,5 +351,141 @@ def test_read_aas_xml_element(self) -> None: """ bytes_io = io.BytesIO(xml.encode("utf-8")) - submodel = read_aas_xml_element(bytes_io, AASFromXmlDecoder.construct_submodel, failsafe=False) + submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL) + self.assertIsInstance(submodel, model.Submodel) + + def test_stripped_qualifiable(self) -> None: + xml = """ + + http://acplt.org/test_stripped_submodel + + + + test_operation + + + test_qualifier + string + + + + + + + + test_qualifier + string + + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # check if XML with constraints can be parsed successfully + submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False) + self.assertIsInstance(submodel, model.Submodel) + assert isinstance(submodel, model.Submodel) + self.assertEqual(len(submodel.qualifier), 1) + + # check if constraints are ignored in stripped mode + submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False, stripped=True) self.assertIsInstance(submodel, model.Submodel) + assert isinstance(submodel, model.Submodel) + self.assertEqual(len(submodel.qualifier), 0) + + def test_stripped_annotated_relationship_element(self) -> None: + xml = """ + + test_annotated_relationship_element + + + test_ref + + + + + test_ref + + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # XML schema requires annotations to be present, so parsing should fail + with self.assertRaises(KeyError): + read_aas_xml_element(bytes_io, XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT, failsafe=False) + + # check if it can be parsed in stripped mode + read_aas_xml_element(bytes_io, XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT, failsafe=False, stripped=True) + + def test_stripped_entity(self) -> None: + xml = """ + + test_entity + CoManagedEntity + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # XML schema requires statements to be present, so parsing should fail + with self.assertRaises(KeyError): + read_aas_xml_element(bytes_io, XMLConstructables.ENTITY, failsafe=False) + + # check if it can be parsed in stripped mode + read_aas_xml_element(bytes_io, XMLConstructables.ENTITY, failsafe=False, stripped=True) + + def test_stripped_submodel_element_collection(self) -> None: + xml = """ + + test_collection + false + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # XML schema requires statements to be present, so parsing should fail + with self.assertRaises(KeyError): + read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False) + + # check if it can be parsed in stripped mode + read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False, stripped=True) + + def test_stripped_asset_administration_shell(self) -> None: + xml = """ + + http://acplt.org/test_aas + + + http://acplt.org/test_ref + + + + + + http://acplt.org/test_ref + + + + + + test_view + + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # check if XML with constraints can be parsed successfully + aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) + self.assertIsInstance(aas, model.AssetAdministrationShell) + assert isinstance(aas, model.AssetAdministrationShell) + self.assertEqual(len(aas.submodel), 1) + self.assertEqual(len(aas.view), 1) + + # check if constraints are ignored in stripped mode + aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, + stripped=True) + self.assertIsInstance(aas, model.AssetAdministrationShell) + assert isinstance(aas, model.AssetAdministrationShell) + self.assertEqual(len(aas.submodel), 0) + self.assertEqual(len(aas.view), 0) From b3343b35ebf5b742b856d60d46e6e4e69d40d2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 27 Oct 2020 00:01:01 +0100 Subject: [PATCH 370/474] adapter.xml.xml_deserialization: change type_ to Referable test.adapter.xml.xml_deserialization: shorten code --- test/adapter/xml/test_xml_deserialization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index f8606cb..bba30f0 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -53,12 +53,11 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], bytes_io = io.BytesIO(xml.encode("utf-8")) with self.assertLogs(logging.getLogger(), level=log_level) as log_ctx: read_aas_xml_file(bytes_io, failsafe=True) - for s in strings: - self.assertIn(s, log_ctx.output[0]) with self.assertRaises(error_type) as err_ctx: read_aas_xml_file(bytes_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: + self.assertIn(s, log_ctx.output[0]) self.assertIn(s, str(cause)) def test_malformed_xml(self) -> None: From 91cb211d188f85245a7e796f6764da5d0a1e2078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Oct 2020 22:21:53 +0200 Subject: [PATCH 371/474] adapter.xml.xml_deserialization: add read_aas_xml_file_into() function read_aas_xml_file() is now nothing more than a wrapper function, fix tests accordingly --- test/adapter/xml/test_xml_deserialization.py | 193 ++----------------- 1 file changed, 15 insertions(+), 178 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index bba30f0..ee1fb69 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -14,7 +14,7 @@ import unittest from aas import model -from aas.adapter.xml import XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element +from aas.adapter.xml import read_aas_xml_file, read_aas_xml_file_into from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -53,11 +53,12 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], bytes_io = io.BytesIO(xml.encode("utf-8")) with self.assertLogs(logging.getLogger(), level=log_level) as log_ctx: read_aas_xml_file(bytes_io, failsafe=True) + for s in strings: + self.assertIn(s, log_ctx.output[0]) with self.assertRaises(error_type) as err_ctx: read_aas_xml_file(bytes_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: - self.assertIn(s, log_ctx.output[0]) self.assertIn(s, str(cause)) def test_malformed_xml(self) -> None: @@ -165,7 +166,7 @@ def test_no_modeling_kind(self) -> None: # modeling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) - assert isinstance(submodel, model.Submodel) # to make mypy happy + assert(isinstance(submodel, model.Submodel)) # to make mypy happy self.assertEqual(submodel.kind, model.ModelingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: @@ -183,12 +184,12 @@ def test_reference_kind_mismatch(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - for s in ("GLOBAL_REFERENCE", "IRI=http://acplt.org/test_ref", "Asset"): + for s in ("GLOBAL_REFERENCE", "RI=http://acplt.org/test_ref", "Asset"): self.assertIn(s, context.output[0]) def test_invalid_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 xml = _xml_wrap(""" @@ -205,7 +206,7 @@ def test_invalid_submodel_element(self) -> None: def test_invalid_constraint(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyaas/-/issues/56 xml = _xml_wrap(""" @@ -221,7 +222,7 @@ def test_invalid_constraint(self) -> None: def test_operation_variable_no_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 xml = _xml_wrap(""" @@ -243,7 +244,7 @@ def test_operation_variable_no_submodel_element(self) -> None: def test_operation_variable_too_many_submodel_elements(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 xml = _xml_wrap(""" @@ -296,19 +297,15 @@ def test_duplicate_identifier(self) -> None: self._assertInExceptionAndLog(xml, "duplicate identifier", KeyError, logging.ERROR) def test_duplicate_identifier_object_store(self) -> None: - sm_id = model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI) - def get_clean_store() -> model.DictObjectStore: store: model.DictObjectStore = model.DictObjectStore() - submodel_ = model.Submodel(sm_id, id_short="test123") - store.add(submodel_) + submodel = model.Submodel(model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI)) + store.add(submodel) return store - xml = _xml_wrap(""" http://acplt.org/test_submodel - test456 @@ -316,175 +313,15 @@ def get_clean_store() -> model.DictObjectStore: bytes_io = io.BytesIO(xml.encode("utf-8")) object_store = get_clean_store() - identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=True, ignore_existing=False) - self.assertEqual(identifiers.pop(), sm_id) - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test456") + read_aas_xml_file_into(object_store, bytes_io, replace_existing=True, ignore_existing=False) + self.assertIsInstance(object_store.pop(), model.Submodel) object_store = get_clean_store() with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: - identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) - self.assertEqual(len(identifiers), 0) + read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) self.assertIn("already exists in the object store", log_ctx.output[0]) - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test123") - object_store = get_clean_store() with self.assertRaises(KeyError) as err_ctx: - identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) - self.assertEqual(len(identifiers), 0) + read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) cause = _root_cause(err_ctx.exception) self.assertIn("already exists in the object store", str(cause)) - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test123") - - def test_read_aas_xml_element(self) -> None: - xml = """ - - http://acplt.org/test_submodel - - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL) - self.assertIsInstance(submodel, model.Submodel) - - def test_stripped_qualifiable(self) -> None: - xml = """ - - http://acplt.org/test_stripped_submodel - - - - test_operation - - - test_qualifier - string - - - - - - - - test_qualifier - string - - - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - # check if XML with constraints can be parsed successfully - submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False) - self.assertIsInstance(submodel, model.Submodel) - assert isinstance(submodel, model.Submodel) - self.assertEqual(len(submodel.qualifier), 1) - - # check if constraints are ignored in stripped mode - submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False, stripped=True) - self.assertIsInstance(submodel, model.Submodel) - assert isinstance(submodel, model.Submodel) - self.assertEqual(len(submodel.qualifier), 0) - - def test_stripped_annotated_relationship_element(self) -> None: - xml = """ - - test_annotated_relationship_element - - - test_ref - - - - - test_ref - - - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - # XML schema requires annotations to be present, so parsing should fail - with self.assertRaises(KeyError): - read_aas_xml_element(bytes_io, XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT, failsafe=False) - - # check if it can be parsed in stripped mode - read_aas_xml_element(bytes_io, XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT, failsafe=False, stripped=True) - - def test_stripped_entity(self) -> None: - xml = """ - - test_entity - CoManagedEntity - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - # XML schema requires statements to be present, so parsing should fail - with self.assertRaises(KeyError): - read_aas_xml_element(bytes_io, XMLConstructables.ENTITY, failsafe=False) - - # check if it can be parsed in stripped mode - read_aas_xml_element(bytes_io, XMLConstructables.ENTITY, failsafe=False, stripped=True) - - def test_stripped_submodel_element_collection(self) -> None: - xml = """ - - test_collection - false - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - # XML schema requires statements to be present, so parsing should fail - with self.assertRaises(KeyError): - read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False) - - # check if it can be parsed in stripped mode - read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False, stripped=True) - - def test_stripped_asset_administration_shell(self) -> None: - xml = """ - - http://acplt.org/test_aas - - - http://acplt.org/test_ref - - - - - - http://acplt.org/test_ref - - - - - - test_view - - - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - # check if XML with constraints can be parsed successfully - aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) - self.assertIsInstance(aas, model.AssetAdministrationShell) - assert isinstance(aas, model.AssetAdministrationShell) - self.assertEqual(len(aas.submodel), 1) - self.assertEqual(len(aas.view), 1) - - # check if constraints are ignored in stripped mode - aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, - stripped=True) - self.assertIsInstance(aas, model.AssetAdministrationShell) - assert isinstance(aas, model.AssetAdministrationShell) - self.assertEqual(len(aas.submodel), 0) - self.assertEqual(len(aas.view), 0) From 69ef74d483d313464ce2fdda5a23c89ade52d4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 8 Oct 2020 22:26:34 +0200 Subject: [PATCH 372/474] adapter.xml.xml_deserialization: add read_aas_xml_element() move xml document parsing to a separate function _parse_xml_document() allow passing keyword arguments to the xml parser --- test/adapter/xml/test_xml_deserialization.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index ee1fb69..b5ea015 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -14,7 +14,7 @@ import unittest from aas import model -from aas.adapter.xml import read_aas_xml_file, read_aas_xml_file_into +from aas.adapter.xml import AASFromXmlDecoder, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -325,3 +325,15 @@ def get_clean_store() -> model.DictObjectStore: read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) cause = _root_cause(err_ctx.exception) self.assertIn("already exists in the object store", str(cause)) + + def test_read_aas_xml_element(self) -> None: + xml = """ + + http://acplt.org/test_submodel + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + submodel = read_aas_xml_element(bytes_io, AASFromXmlDecoder.construct_submodel, failsafe=False) + self.assertIsInstance(submodel, model.Submodel) From a5de0e85633f14e3e2f84783792582b5d47b8e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 26 Oct 2020 15:21:14 +0100 Subject: [PATCH 373/474] adapter.xml.xml_deserialization: make failsafe and stripped class variables add enum XMLConstructables to specify which element to construct in read_aas_xml_element() read_aas_xml_file_into(): support specifying a decoder class add strict, stripped and a strict+stripped decoder class remove _constructor_name_to_typename() helper function add tests for parsing stripped xml elements --- test/adapter/xml/test_xml_deserialization.py | 180 +++++++++++++++++-- 1 file changed, 166 insertions(+), 14 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index b5ea015..f8606cb 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -14,7 +14,7 @@ import unittest from aas import model -from aas.adapter.xml import AASFromXmlDecoder, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element +from aas.adapter.xml import XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -166,7 +166,7 @@ def test_no_modeling_kind(self) -> None: # modeling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) - assert(isinstance(submodel, model.Submodel)) # to make mypy happy + assert isinstance(submodel, model.Submodel) # to make mypy happy self.assertEqual(submodel.kind, model.ModelingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: @@ -184,12 +184,12 @@ def test_reference_kind_mismatch(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - for s in ("GLOBAL_REFERENCE", "RI=http://acplt.org/test_ref", "Asset"): + for s in ("GLOBAL_REFERENCE", "IRI=http://acplt.org/test_ref", "Asset"): self.assertIn(s, context.output[0]) def test_invalid_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" @@ -206,7 +206,7 @@ def test_invalid_submodel_element(self) -> None: def test_invalid_constraint(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyaas/-/issues/56 + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" @@ -222,7 +222,7 @@ def test_invalid_constraint(self) -> None: def test_operation_variable_no_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" @@ -244,7 +244,7 @@ def test_operation_variable_no_submodel_element(self) -> None: def test_operation_variable_too_many_submodel_elements(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyaas/-/issues/57 + # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" @@ -297,15 +297,19 @@ def test_duplicate_identifier(self) -> None: self._assertInExceptionAndLog(xml, "duplicate identifier", KeyError, logging.ERROR) def test_duplicate_identifier_object_store(self) -> None: + sm_id = model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI) + def get_clean_store() -> model.DictObjectStore: store: model.DictObjectStore = model.DictObjectStore() - submodel = model.Submodel(model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI)) - store.add(submodel) + submodel_ = model.Submodel(sm_id, id_short="test123") + store.add(submodel_) return store + xml = _xml_wrap(""" http://acplt.org/test_submodel + test456 @@ -313,18 +317,30 @@ def get_clean_store() -> model.DictObjectStore: bytes_io = io.BytesIO(xml.encode("utf-8")) object_store = get_clean_store() - read_aas_xml_file_into(object_store, bytes_io, replace_existing=True, ignore_existing=False) - self.assertIsInstance(object_store.pop(), model.Submodel) + identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=True, ignore_existing=False) + self.assertEqual(identifiers.pop(), sm_id) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test456") object_store = get_clean_store() with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: - read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) + identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) + self.assertEqual(len(identifiers), 0) self.assertIn("already exists in the object store", log_ctx.output[0]) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test123") + object_store = get_clean_store() with self.assertRaises(KeyError) as err_ctx: - read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) + identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) + self.assertEqual(len(identifiers), 0) cause = _root_cause(err_ctx.exception) self.assertIn("already exists in the object store", str(cause)) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test123") def test_read_aas_xml_element(self) -> None: xml = """ @@ -335,5 +351,141 @@ def test_read_aas_xml_element(self) -> None: """ bytes_io = io.BytesIO(xml.encode("utf-8")) - submodel = read_aas_xml_element(bytes_io, AASFromXmlDecoder.construct_submodel, failsafe=False) + submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL) + self.assertIsInstance(submodel, model.Submodel) + + def test_stripped_qualifiable(self) -> None: + xml = """ + + http://acplt.org/test_stripped_submodel + + + + test_operation + + + test_qualifier + string + + + + + + + + test_qualifier + string + + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # check if XML with constraints can be parsed successfully + submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False) + self.assertIsInstance(submodel, model.Submodel) + assert isinstance(submodel, model.Submodel) + self.assertEqual(len(submodel.qualifier), 1) + + # check if constraints are ignored in stripped mode + submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False, stripped=True) self.assertIsInstance(submodel, model.Submodel) + assert isinstance(submodel, model.Submodel) + self.assertEqual(len(submodel.qualifier), 0) + + def test_stripped_annotated_relationship_element(self) -> None: + xml = """ + + test_annotated_relationship_element + + + test_ref + + + + + test_ref + + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # XML schema requires annotations to be present, so parsing should fail + with self.assertRaises(KeyError): + read_aas_xml_element(bytes_io, XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT, failsafe=False) + + # check if it can be parsed in stripped mode + read_aas_xml_element(bytes_io, XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT, failsafe=False, stripped=True) + + def test_stripped_entity(self) -> None: + xml = """ + + test_entity + CoManagedEntity + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # XML schema requires statements to be present, so parsing should fail + with self.assertRaises(KeyError): + read_aas_xml_element(bytes_io, XMLConstructables.ENTITY, failsafe=False) + + # check if it can be parsed in stripped mode + read_aas_xml_element(bytes_io, XMLConstructables.ENTITY, failsafe=False, stripped=True) + + def test_stripped_submodel_element_collection(self) -> None: + xml = """ + + test_collection + false + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # XML schema requires statements to be present, so parsing should fail + with self.assertRaises(KeyError): + read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False) + + # check if it can be parsed in stripped mode + read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False, stripped=True) + + def test_stripped_asset_administration_shell(self) -> None: + xml = """ + + http://acplt.org/test_aas + + + http://acplt.org/test_ref + + + + + + http://acplt.org/test_ref + + + + + + test_view + + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + # check if XML with constraints can be parsed successfully + aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) + self.assertIsInstance(aas, model.AssetAdministrationShell) + assert isinstance(aas, model.AssetAdministrationShell) + self.assertEqual(len(aas.submodel), 1) + self.assertEqual(len(aas.view), 1) + + # check if constraints are ignored in stripped mode + aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, + stripped=True) + self.assertIsInstance(aas, model.AssetAdministrationShell) + assert isinstance(aas, model.AssetAdministrationShell) + self.assertEqual(len(aas.submodel), 0) + self.assertEqual(len(aas.view), 0) From 8c47d79a5d3b3df20cd164b9d37153ebbd90fb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 27 Oct 2020 00:01:01 +0100 Subject: [PATCH 374/474] adapter.xml.xml_deserialization: change type_ to Referable test.adapter.xml.xml_deserialization: shorten code --- test/adapter/xml/test_xml_deserialization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index f8606cb..bba30f0 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -53,12 +53,11 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], bytes_io = io.BytesIO(xml.encode("utf-8")) with self.assertLogs(logging.getLogger(), level=log_level) as log_ctx: read_aas_xml_file(bytes_io, failsafe=True) - for s in strings: - self.assertIn(s, log_ctx.output[0]) with self.assertRaises(error_type) as err_ctx: read_aas_xml_file(bytes_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: + self.assertIn(s, log_ctx.output[0]) self.assertIn(s, str(cause)) def test_malformed_xml(self) -> None: From c58f6732e52b84a8b108b9f9ff2dda6396a808fb Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 7 Oct 2020 16:39:18 +0200 Subject: [PATCH 375/474] adapter.aasx: Add write_aas_objects() method Additionally, add the `submodel_split_parts` parameter for `write_aas()`. --- test/adapter/aasx/test_aasx.py | 72 +++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 7ac5b3d..aa54494 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -18,7 +18,7 @@ import pyecma376_2 from aas import model from aas.adapter import aasx -from aas.examples.data import example_aas, _helper +from aas.examples.data import example_aas, _helper, example_aas_mandatory_attributes class TestAASXUtils(unittest.TestCase): @@ -72,41 +72,69 @@ def test_writing_reading_example_aas(self) -> None: cp.created = datetime.datetime.now() cp.creator = "PyI40AAS Testing Framework" + # Write AASX file + for write_json in (False, True): + for submodel_split_parts in (False, True): + with self.subTest(write_json=write_json, submodel_split_parts=submodel_split_parts): + fd, filename = tempfile.mkstemp(suffix=".aasx") + os.close(fd) + with aasx.AASXWriter(filename) as writer: + writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', + id_type=model.IdentifierType.IRI), + data, files, write_json=write_json, submodel_split_parts=submodel_split_parts) + writer.write_core_properties(cp) + + # Read AASX file + new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + new_files = aasx.DictSupplementaryFileContainer() + with aasx.AASXReader(filename) as reader: + reader.read_into(new_data, new_files) + new_cp = reader.get_core_properties() + + # Check AAS objects + checker = _helper.AASDataChecker(raise_immediately=True) + example_aas.check_full_example(checker, new_data) + + # Check core properties + assert(isinstance(cp.created, datetime.datetime)) # to make mypy happy + self.assertIsInstance(new_cp.created, datetime.datetime) + assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy + self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) + self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") + self.assertIsNone(new_cp.lastModifiedBy) + + # Check files + self.assertEqual(new_files.get_content_type("/TestFile.pdf"), "application/pdf") + file_content = io.BytesIO() + new_files.write_file("/TestFile.pdf", file_content) + self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), + "78450a66f59d74c073bf6858db340090ea72a8b1") + + os.unlink(filename) + + def test_writing_reading_objects_single_part(self) -> None: + # Create example data and file_store + data = example_aas_mandatory_attributes.create_full_example() + files = aasx.DictSupplementaryFileContainer() + # Write AASX file for write_json in (False, True): with self.subTest(write_json=write_json): fd, filename = tempfile.mkstemp(suffix=".aasx") os.close(fd) with aasx.AASXWriter(filename) as writer: - writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', - id_type=model.IdentifierType.IRI), - data, files, write_json=write_json) - writer.write_core_properties(cp) + writer.write_aas_objects('/aasx/aasx.{}'.format('json' if write_json else 'xml'), + [obj.identification for obj in data], + data, files, write_json) # Read AASX file new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() new_files = aasx.DictSupplementaryFileContainer() with aasx.AASXReader(filename) as reader: reader.read_into(new_data, new_files) - new_cp = reader.get_core_properties() # Check AAS objects checker = _helper.AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, new_data) - - # Check core properties - assert(isinstance(cp.created, datetime.datetime)) # to make mypy happy - self.assertIsInstance(new_cp.created, datetime.datetime) - assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy - self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) - self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") - self.assertIsNone(new_cp.lastModifiedBy) - - # Check files - self.assertEqual(new_files.get_content_type("/TestFile.pdf"), "application/pdf") - file_content = io.BytesIO() - new_files.write_file("/TestFile.pdf", file_content) - self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), - "78450a66f59d74c073bf6858db340090ea72a8b1") + example_aas_mandatory_attributes.check_full_example(checker, new_data) os.unlink(filename) From 77def459d4063b0153adb1cc075f3923cbaa2d86 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Fri, 30 Oct 2020 10:03:59 +0100 Subject: [PATCH 376/474] test: Catch zipfile warnings during writing AASX file --- test/adapter/aasx/test_aasx.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index aa54494..f3f918e 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -14,6 +14,7 @@ import os import tempfile import unittest +import warnings import pyecma376_2 from aas import model @@ -78,11 +79,21 @@ def test_writing_reading_example_aas(self) -> None: with self.subTest(write_json=write_json, submodel_split_parts=submodel_split_parts): fd, filename = tempfile.mkstemp(suffix=".aasx") os.close(fd) - with aasx.AASXWriter(filename) as writer: - writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', - id_type=model.IdentifierType.IRI), - data, files, write_json=write_json, submodel_split_parts=submodel_split_parts) - writer.write_core_properties(cp) + + # Write AASX file + # the zipfile library reports errors as UserWarnings via the warnings library. Let's check for + # warnings + with warnings.catch_warnings(record=True) as w: + with aasx.AASXWriter(filename) as writer: + writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', + id_type=model.IdentifierType.IRI), + data, files, write_json=write_json, + submodel_split_parts=submodel_split_parts) + writer.write_core_properties(cp) + + assert isinstance(w, list) # This should be True due to the record=True parameter + self.assertEqual(0, len(w), f"Warnings were issued while writhing the AASX file: " + f"{[warning.message for warning in w]}") # Read AASX file new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() From 542547cc4d0a2de1add408f42fca40145f2dbb1c Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 2 Nov 2020 09:28:01 +0100 Subject: [PATCH 377/474] Fix typo in tests --- test/adapter/aasx/test_aasx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index f3f918e..7d13903 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -92,7 +92,7 @@ def test_writing_reading_example_aas(self) -> None: writer.write_core_properties(cp) assert isinstance(w, list) # This should be True due to the record=True parameter - self.assertEqual(0, len(w), f"Warnings were issued while writhing the AASX file: " + self.assertEqual(0, len(w), f"Warnings were issued while writing the AASX file: " f"{[warning.message for warning in w]}") # Read AASX file From 2382815eed6678313f84d85b5835fecf129c0698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Oct 2020 04:53:49 +0100 Subject: [PATCH 378/474] test.adapter.xml: improve xml deserialization tests add test for class derivation some minor things I noticed when writing the json deserialization tests --- test/adapter/xml/test_xml_deserialization.py | 43 +++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index bba30f0..c27aac2 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -14,7 +14,8 @@ import unittest from aas import model -from aas.adapter.xml import XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element +from aas.adapter.xml import StrictAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, \ + read_aas_xml_element from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -36,7 +37,7 @@ def _root_cause(exception: BaseException) -> BaseException: return exception -class XMLDeserializationTest(unittest.TestCase): +class XmlDeserializationTest(unittest.TestCase): def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], error_type: Type[BaseException], log_level: int) -> None: """ @@ -353,6 +354,8 @@ def test_read_aas_xml_element(self) -> None: submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL) self.assertIsInstance(submodel, model.Submodel) + +class XmlDeserializationStrippedObjectsTest(unittest.TestCase): def test_stripped_qualifiable(self) -> None: xml = """ @@ -385,12 +388,15 @@ def test_stripped_qualifiable(self) -> None: self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) self.assertEqual(len(submodel.qualifier), 1) + operation = submodel.submodel_element.pop() + self.assertEqual(len(operation.qualifier), 1) # check if constraints are ignored in stripped mode submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False, stripped=True) self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) self.assertEqual(len(submodel.qualifier), 0) + self.assertEqual(len(submodel.submodel_element), 0) def test_stripped_annotated_relationship_element(self) -> None: xml = """ @@ -442,7 +448,7 @@ def test_stripped_submodel_element_collection(self) -> None: """ bytes_io = io.BytesIO(xml.encode("utf-8")) - # XML schema requires statements to be present, so parsing should fail + # XML schema requires value to be present, so parsing should fail with self.assertRaises(KeyError): read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False) @@ -474,17 +480,44 @@ def test_stripped_asset_administration_shell(self) -> None: """ bytes_io = io.BytesIO(xml.encode("utf-8")) - # check if XML with constraints can be parsed successfully + # check if XML with submodelRef and views can be parsed successfully aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) self.assertEqual(len(aas.submodel), 1) self.assertEqual(len(aas.view), 1) - # check if constraints are ignored in stripped mode + # check if submodelRef and views are ignored in stripped mode aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, stripped=True) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) self.assertEqual(len(aas.submodel), 0) self.assertEqual(len(aas.view), 0) + + +class XmlDeserializationDerivingTest(unittest.TestCase): + def test_submodel_constructor_overriding(self) -> None: + class EnhancedSubmodel(model.Submodel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.enhanced_attribute = "fancy!" + + class EnhancedAASDecoder(StrictAASFromXmlDecoder): + @classmethod + def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **kwargs) \ + -> model.Submodel: + return super().construct_submodel(element, object_class=EnhancedSubmodel, **kwargs) + + xml = """ + + http://acplt.org/test_stripped_submodel + + + """ + bytes_io = io.BytesIO(xml.encode("utf-8")) + + submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, decoder=EnhancedAASDecoder) + self.assertIsInstance(submodel, EnhancedSubmodel) + assert isinstance(submodel, EnhancedSubmodel) + self.assertEqual(submodel.enhanced_attribute, "fancy!") From 2339693fa9795a1e9b2527d40a2d0460b0d8e5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Oct 2020 16:33:59 +0100 Subject: [PATCH 379/474] test.adapter.xml.xml_deserialization: make object_class default to EnhancedSubmodel --- test/adapter/xml/test_xml_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index c27aac2..1c3e474 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -505,9 +505,9 @@ def __init__(self, *args, **kwargs): class EnhancedAASDecoder(StrictAASFromXmlDecoder): @classmethod - def construct_submodel(cls, element: etree.Element, object_class=model.Submodel, **kwargs) \ + def construct_submodel(cls, element: etree.Element, object_class=EnhancedSubmodel, **kwargs) \ -> model.Submodel: - return super().construct_submodel(element, object_class=EnhancedSubmodel, **kwargs) + return super().construct_submodel(element, object_class=object_class, **kwargs) xml = """ From d99e767b62097048a7a5cc47e2430bce348ed0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Oct 2020 04:36:32 +0100 Subject: [PATCH 380/474] test.adapter.json: remove unused imports --- test/adapter/json/test_json_serialization_deserialization.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 88e31a4..5144539 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -11,9 +11,7 @@ import io import json -import os import unittest -from os.path import dirname from aas import model from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file From 6ee6c708a949a7178f20b6cbb03348a2e3fdfb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Oct 2020 04:44:15 +0100 Subject: [PATCH 381/474] adapter.json.json_deserialization: support deserializing stripped objects add read_aas_json_file_into() function fix #63 --- .../adapter/json/test_json_deserialization.py | 263 +++++++++++++++++- 1 file changed, 256 insertions(+), 7 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index e636eb0..fd7b113 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -19,7 +19,8 @@ import json import logging import unittest -from aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, read_aas_json_file +from aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder, \ + read_aas_json_file, read_aas_json_file_into from aas import model @@ -32,9 +33,9 @@ def test_file_format_missing_list(self) -> None: "conceptDescriptions": [] }""" with self.assertRaisesRegex(KeyError, r"submodels"): - read_aas_json_file(io.StringIO(data), False) + read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - read_aas_json_file(io.StringIO(data), True) + read_aas_json_file(io.StringIO(data), failsafe=True) self.assertIn("submodels", cm.output[0]) def test_file_format_wrong_list(self) -> None: @@ -57,9 +58,9 @@ def test_file_format_wrong_list(self) -> None: ] }""" with self.assertRaisesRegex(TypeError, r"submodels.*Asset"): - read_aas_json_file(io.StringIO(data), False) + read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - read_aas_json_file(io.StringIO(data), True) + read_aas_json_file(io.StringIO(data), failsafe=True) self.assertIn("submodels", cm.output[0]) self.assertIn("Asset", cm.output[0]) @@ -74,9 +75,9 @@ def test_file_format_unknown_object(self) -> None: ] }""" with self.assertRaisesRegex(TypeError, r"submodels.*'foo'"): - read_aas_json_file(io.StringIO(data), False) + read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - read_aas_json_file(io.StringIO(data), True) + read_aas_json_file(io.StringIO(data), failsafe=True) self.assertIn("submodels", cm.output[0]) self.assertIn("'foo'", cm.output[0]) @@ -160,6 +161,88 @@ def test_wrong_submodel_element_type(self) -> None: self.assertIsInstance(cap, model.Capability) self.assertEqual("TestCapability", cap.id_short) + def test_duplicate_identifier(self) -> None: + data = """ + { + "assetAdministrationShells": [{ + "modelType": {"name": "AssetAdministrationShell"}, + "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, + "asset": { + "keys": [{ + "idType": "IRI", + "local": false, + "type": "Asset", + "value": "http://acplt.org/test_aas" + }] + } + }], + "submodels": [{ + "modelType": {"name": "Submodel"}, + "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"} + }], + "assets": [], + "conceptDescriptions": [] + }""" + string_io = io.StringIO(data) + with self.assertLogs(logging.getLogger(), level=logging.ERROR) as cm: + read_aas_json_file(string_io, failsafe=True) + self.assertIn("duplicate identifier", cm.output[0]) + string_io.seek(0) + with self.assertRaisesRegex(KeyError, r"duplicate identifier"): + read_aas_json_file(string_io, failsafe=False) + + def test_duplicate_identifier_object_store(self) -> None: + sm_id = model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI) + + def get_clean_store() -> model.DictObjectStore: + store: model.DictObjectStore = model.DictObjectStore() + submodel_ = model.Submodel(sm_id, id_short="test123") + store.add(submodel_) + return store + + data = """ + { + "submodels": [{ + "modelType": {"name": "Submodel"}, + "identification": {"idType": "IRI", "id": "http://acplt.org/test_submodel"}, + "idShort": "test456" + }], + "assetAdministrationShells": [], + "assets": [], + "conceptDescriptions": [] + }""" + + string_io = io.StringIO(data) + + object_store = get_clean_store() + identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=True, ignore_existing=False) + self.assertEqual(identifiers.pop(), sm_id) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test456") + + string_io.seek(0) + + object_store = get_clean_store() + with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: + identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=False, ignore_existing=True) + self.assertEqual(len(identifiers), 0) + self.assertIn("already exists in the object store", log_ctx.output[0]) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test123") + + string_io.seek(0) + + object_store = get_clean_store() + with self.assertRaisesRegex(KeyError, r"already exists in the object store"): + identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=False, + ignore_existing=False) + self.assertEqual(len(identifiers), 0) + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel.id_short, "test123") + class JsonDeserializationDerivingTest(unittest.TestCase): def test_asset_constructor_overriding(self) -> None: @@ -185,3 +268,169 @@ def _construct_asset(cls, dct): self.assertEqual(1, len(parsed_data)) self.assertIsInstance(parsed_data[0], EnhancedAsset) self.assertEqual(parsed_data[0].enhanced_attribute, "fancy!") + + +class JsonDeserializationStrippedObjectsTest(unittest.TestCase): + def test_stripped_qualifiable(self) -> None: + data = """ + { + "modelType": {"name": "Submodel"}, + "identification": {"idType": "IRI", "id": "http://acplt.org/test_stripped_submodel"}, + "submodelElements": [{ + "modelType": {"name": "Operation"}, + "idShort": "test_operation", + "qualifiers": [{ + "modelType": {"name": "Qualifier"}, + "type": "test_qualifier", + "valueType": "string" + }] + }], + "qualifiers": [{ + "modelType": {"name": "Qualifier"}, + "type": "test_qualifier", + "valueType": "string" + }] + }""" + + # check if JSON with constraints can be parsed successfully + submodel = json.loads(data, cls=StrictAASFromJsonDecoder) + self.assertIsInstance(submodel, model.Submodel) + assert isinstance(submodel, model.Submodel) + self.assertEqual(len(submodel.qualifier), 1) + operation = submodel.submodel_element.pop() + self.assertEqual(len(operation.qualifier), 1) + + # check if constraints are ignored in stripped mode + submodel = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) + self.assertIsInstance(submodel, model.Submodel) + assert isinstance(submodel, model.Submodel) + self.assertEqual(len(submodel.qualifier), 0) + self.assertEqual(len(submodel.submodel_element), 0) + + def test_stripped_annotated_relationship_element(self) -> None: + data = """ + { + "modelType": {"name": "AnnotatedRelationshipElement"}, + "idShort": "test_annotated_relationship_element", + "first": { + "keys": [{ + "idType": "IdShort", + "local": true, + "type": "AnnotatedRelationshipElement", + "value": "test_ref" + }] + }, + "second": { + "keys": [{ + "idType": "IdShort", + "local": true, + "type": "AnnotatedRelationshipElement", + "value": "test_ref" + }] + }, + "annotation": [{ + "modelType": {"name": "MultiLanguageProperty"}, + "idShort": "test_multi_language_property" + }] + }""" + + # check if JSON with annotation can be parsed successfully + are = json.loads(data, cls=StrictAASFromJsonDecoder) + self.assertIsInstance(are, model.AnnotatedRelationshipElement) + assert isinstance(are, model.AnnotatedRelationshipElement) + self.assertEqual(len(are.annotation), 1) + + # check if annotation is ignored in stripped mode + are = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) + self.assertIsInstance(are, model.AnnotatedRelationshipElement) + assert isinstance(are, model.AnnotatedRelationshipElement) + self.assertEqual(len(are.annotation), 0) + + def test_stripped_entity(self) -> None: + data = """ + { + "modelType": {"name": "Entity"}, + "idShort": "test_entity", + "entityType": "CoManagedEntity", + "statements": [{ + "modelType": {"name": "MultiLanguageProperty"}, + "idShort": "test_multi_language_property" + }] + }""" + + # check if JSON with statements can be parsed successfully + entity = json.loads(data, cls=StrictAASFromJsonDecoder) + self.assertIsInstance(entity, model.Entity) + assert isinstance(entity, model.Entity) + self.assertEqual(len(entity.statement), 1) + + # check if statements is ignored in stripped mode + entity = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) + self.assertIsInstance(entity, model.Entity) + assert isinstance(entity, model.Entity) + self.assertEqual(len(entity.statement), 0) + + def test_stripped_submodel_element_collection(self) -> None: + data = """ + { + "modelType": {"name": "SubmodelElementCollection"}, + "idShort": "test_submodel_element_collection", + "ordered": false, + "value": [{ + "modelType": {"name": "MultiLanguageProperty"}, + "idShort": "test_multi_language_property" + }] + }""" + + # check if JSON with value can be parsed successfully + sec = json.loads(data, cls=StrictAASFromJsonDecoder) + self.assertIsInstance(sec, model.SubmodelElementCollectionUnordered) + assert isinstance(sec, model.SubmodelElementCollectionUnordered) + self.assertEqual(len(sec.value), 1) + + # check if value is ignored in stripped mode + sec = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) + self.assertIsInstance(sec, model.SubmodelElementCollectionUnordered) + assert isinstance(sec, model.SubmodelElementCollectionUnordered) + self.assertEqual(len(sec.value), 0) + + def test_stripped_asset_administration_shell(self) -> None: + data = """ + { + "modelType": {"name": "AssetAdministrationShell"}, + "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, + "asset": { + "keys": [{ + "idType": "IRI", + "local": false, + "type": "Asset", + "value": "http://acplt.org/test_aas" + }] + }, + "submodels": [{ + "keys": [{ + "idType": "IRI", + "local": false, + "type": "Submodel", + "value": "http://acplt.org/test_submodel" + }] + }], + "views": [{ + "modelType": {"name": "View"}, + "idShort": "test_view" + }] + }""" + + # check if JSON with submodels and views can be parsed successfully + aas = json.loads(data, cls=StrictAASFromJsonDecoder) + self.assertIsInstance(aas, model.AssetAdministrationShell) + assert isinstance(aas, model.AssetAdministrationShell) + self.assertEqual(len(aas.submodel), 1) + self.assertEqual(len(aas.view), 1) + + # check if submodels and views are ignored in stripped mode + aas = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) + self.assertIsInstance(aas, model.AssetAdministrationShell) + assert isinstance(aas, model.AssetAdministrationShell) + self.assertEqual(len(aas.submodel), 0) + self.assertEqual(len(aas.view), 0) From 365cd9a5de5ddab08fddb022937cf201ee3f2f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Oct 2020 04:45:15 +0100 Subject: [PATCH 382/474] adapter.json.json_serialization: support serializing stripped objects --- test/adapter/json/test_json_serialization.py | 73 +++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 1ab4fd3..19026cf 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -14,8 +14,9 @@ import json from aas import model -from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE +from aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE from jsonschema import validate # type: ignore +from typing import Set, Union from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, create_example, example_concept_description @@ -163,3 +164,73 @@ def test_full_example_serialization(self) -> None: # validate serialization against schema validate(instance=json_data, schema=aas_json_schema) + + +class JsonSerializationStrippedObjectsTest(unittest.TestCase): + def _checkNormalAndStripped(self, attributes: Union[Set[str], str], obj: object) -> None: + if isinstance(attributes, str): + attributes = {attributes} + + # attributes should be present when using the normal encoder, + # but must not be present when using the stripped encoder + for cls, assert_fn in ((AASToJsonEncoder, self.assertIn), (StrippedAASToJsonEncoder, self.assertNotIn)): + data = json.loads(json.dumps(obj, cls=cls)) + for attr in attributes: + assert_fn(attr, data) + + def test_stripped_qualifiable(self) -> None: + qualifier = model.Qualifier("test_qualifier", str) + operation = model.Operation("test_operation", qualifier={qualifier}) + submodel = model.Submodel( + model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI), + submodel_element=[operation], + qualifier={qualifier} + ) + + self._checkNormalAndStripped({"submodelElements", "qualifiers"}, submodel) + self._checkNormalAndStripped("qualifiers", operation) + + def test_stripped_annotated_relationship_element(self) -> None: + mlp = model.MultiLanguageProperty("test_multi_language_property") + ref = model.AASReference( + (model.Key(model.KeyElements.SUBMODEL, False, "http://acplt.org/test_ref", model.KeyType.IRI),), + model.Submodel + ) + are = model.AnnotatedRelationshipElement( + "test_annotated_relationship_element", + ref, + ref, + annotation=[mlp] + ) + + self._checkNormalAndStripped("annotation", are) + + def test_stripped_entity(self) -> None: + mlp = model.MultiLanguageProperty("test_multi_language_property") + entity = model.Entity("test_entity", model.EntityType.CO_MANAGED_ENTITY, statement=[mlp]) + + self._checkNormalAndStripped("statements", entity) + + def test_stripped_submodel_element_collection(self) -> None: + mlp = model.MultiLanguageProperty("test_multi_language_property") + sec = model.SubmodelElementCollectionOrdered("test_submodel_element_collection", value=[mlp]) + + self._checkNormalAndStripped("value", sec) + + def test_stripped_asset_administration_shell(self) -> None: + asset_ref = model.AASReference( + (model.Key(model.KeyElements.ASSET, False, "http://acplt.org/test_ref", model.KeyType.IRI),), + model.Asset + ) + submodel_ref = model.AASReference( + (model.Key(model.KeyElements.SUBMODEL, False, "http://acplt.org/test_ref", model.KeyType.IRI),), + model.Submodel + ) + aas = model.AssetAdministrationShell( + asset_ref, + model.Identifier("http://acplt.org/test_aas", model.IdentifierType.IRI), + submodel_={submodel_ref}, + view=[model.View("test_view")] + ) + + self._checkNormalAndStripped({"submodels", "views"}, aas) From 1c940ac52d71298834599b71f4140c6463dc63da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 13 Nov 2020 14:25:37 +0100 Subject: [PATCH 383/474] model.aas: remove underscore suffix from parameters submodel_ and security_ of AssetAdministrationShell.__init__ model.submodel: remove underscore suffix from parameters min_ and max_ of Range.__init__ adjust existing usages of these parameters fix #97 --- test/adapter/json/test_json_serialization.py | 6 +++--- .../adapter/json/test_json_serialization_deserialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 19026cf..e5b52eb 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -37,7 +37,7 @@ def test_random_object_serialization(self) -> None: assert(submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) # serialize object to json json_data = json.dumps({ @@ -61,7 +61,7 @@ def test_random_object_serialization(self) -> None: # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) # serialize object to json json_data = json.dumps({ @@ -229,7 +229,7 @@ def test_stripped_asset_administration_shell(self) -> None: aas = model.AssetAdministrationShell( asset_ref, model.Identifier("http://acplt.org/test_aas", model.IdentifierType.IRI), - submodel_={submodel_ref}, + submodel={submodel_ref}, view=[model.View("test_view")] ) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 5144539..9af99f8 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -31,7 +31,7 @@ def test_random_object_serialization_deserialization(self) -> None: assert(submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) # serialize object to json json_data = json.dumps({ diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 3df5e42..0fa5470 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -38,7 +38,7 @@ def test_random_object_serialization(self) -> None: assert (submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() test_data.add(test_aas) @@ -58,7 +58,7 @@ def test_random_object_serialization(self) -> None: assert(submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel_={submodel_reference}) + test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) # serialize object to xml test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() From 842f00b87c029eb15540e7b5d92a59e2688880a1 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 2 Feb 2021 10:16:07 +0100 Subject: [PATCH 384/474] Update license headers for EPL-2.0 dual-licensing Using license header template from Eclipse jetty project. --- test/adapter/aasx/test_aasx.py | 13 +++++-------- test/adapter/json/test_json_deserialization.py | 13 +++++-------- test/adapter/json/test_json_serialization.py | 13 +++++-------- .../json/test_json_serialization_deserialization.py | 13 +++++-------- test/adapter/xml/test_xml_deserialization.py | 13 +++++-------- test/adapter/xml/test_xml_serialization.py | 13 +++++-------- .../xml/test_xml_serialization_deserialization.py | 13 +++++-------- 7 files changed, 35 insertions(+), 56 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 7d13903..0ba4d92 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -1,13 +1,10 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 PyI40AAS Contributors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 import datetime import hashlib import io diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index fd7b113..e0fb436 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -1,13 +1,10 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 PyI40AAS Contributors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 """ Additional tests for the adapter.json.json_deserialization module. diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index e5b52eb..39f08cb 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,13 +1,10 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 PyI40AAS Contributors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 import io import unittest diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 9af99f8..3ef72a1 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -1,13 +1,10 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 PyI40AAS Contributors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 import io import json diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 1c3e474..dec1c37 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -1,13 +1,10 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 PyI40AAS Contributors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 import io import logging diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 0fa5470..4ae6226 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -1,13 +1,10 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 PyI40AAS Contributors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 import io import unittest diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index 44041ec..818a1e0 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -1,13 +1,10 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 PyI40AAS Contributors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 import io import unittest From a726d4008ccd015f4fdcac86e0249937e1da6462 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 15 Nov 2021 18:12:47 +0100 Subject: [PATCH 385/474] =?UTF-8?q?Rebrand=20project=20code=20PyI40AAS=20?= =?UTF-8?q?=E2=86=92=20Eclipse=20BaSyx=20Python=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/adapter/aasx/test_aasx.py | 6 +++--- test/adapter/json/test_json_deserialization.py | 2 +- test/adapter/json/test_json_serialization.py | 2 +- .../adapter/json/test_json_serialization_deserialization.py | 2 +- test/adapter/xml/test_xml_deserialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 2 +- test/adapter/xml/test_xml_serialization_deserialization.py | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 0ba4d92..b1abf10 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available @@ -68,7 +68,7 @@ def test_writing_reading_example_aas(self) -> None: # Create OPC/AASX core properties cp = pyecma376_2.OPCCoreProperties() cp.created = datetime.datetime.now() - cp.creator = "PyI40AAS Testing Framework" + cp.creator = "Eclipse BaSyx Python Testing Framework" # Write AASX file for write_json in (False, True): @@ -108,7 +108,7 @@ def test_writing_reading_example_aas(self) -> None: self.assertIsInstance(new_cp.created, datetime.datetime) assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) - self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") + self.assertEqual(new_cp.creator, "Eclipse BaSyx Python Testing Framework") self.assertIsNone(new_cp.lastModifiedBy) # Check files diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index e0fb436..7034e69 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 39f08cb..08a2f80 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 3ef72a1..1fb2299 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index dec1c37..77c9876 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 4ae6226..3b0ccd1 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index 818a1e0..71f62e4 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # # This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 # which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available From c85ebb90957e666f524d2249c7881c9e328b0090 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 15 Nov 2021 19:00:02 +0100 Subject: [PATCH 386/474] =?UTF-8?q?Move=20Python=20package=20aas=20?= =?UTF-8?q?=E2=86=92=20basyx.aas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/adapter/aasx/test_aasx.py | 6 +++--- test/adapter/json/test_json_deserialization.py | 4 ++-- test/adapter/json/test_json_serialization.py | 10 +++++----- .../json/test_json_serialization_deserialization.py | 10 +++++----- test/adapter/xml/test_xml_deserialization.py | 6 +++--- test/adapter/xml/test_xml_serialization.py | 8 ++++---- .../xml/test_xml_serialization_deserialization.py | 10 +++++----- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index b1abf10..a418a7c 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -14,9 +14,9 @@ import warnings import pyecma376_2 -from aas import model -from aas.adapter import aasx -from aas.examples.data import example_aas, _helper, example_aas_mandatory_attributes +from basyx.aas import model +from basyx.aas.adapter import aasx +from basyx.aas.examples.data import example_aas, example_aas_mandatory_attributes, _helper class TestAASXUtils(unittest.TestCase): diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 7034e69..894333c 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -16,9 +16,9 @@ import json import logging import unittest -from aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder, \ +from basyx.aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder, \ read_aas_json_file, read_aas_json_file_into -from aas import model +from basyx.aas import model class JsonDeserializationTest(unittest.TestCase): diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 08a2f80..9a0a44f 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -10,13 +10,13 @@ import unittest import json -from aas import model -from aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE +from basyx.aas import model +from basyx.aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE from jsonschema import validate # type: ignore from typing import Set, Union -from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, create_example, example_concept_description +from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ + example_aas_mandatory_attributes, example_submodel_template, create_example class JsonSerializationTest(unittest.TestCase): @@ -57,7 +57,7 @@ def test_random_object_serialization(self) -> None: submodel_reference = model.AASReference(submodel_key, model.Submodel) # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((), )) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) # serialize object to json diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 1fb2299..c735862 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -10,12 +10,12 @@ import json import unittest -from aas import model -from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file +from basyx.aas import model +from basyx.aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file -from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, example_concept_description, create_example -from aas.examples.data._helper import AASDataChecker +from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ + example_aas_mandatory_attributes, example_submodel_template, create_example +from basyx.aas.examples.data._helper import AASDataChecker class JsonSerializationDeserializationTest(unittest.TestCase): diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 77c9876..8ae6861 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -10,9 +10,9 @@ import logging import unittest -from aas import model -from aas.adapter.xml import StrictAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, \ - read_aas_xml_element +from basyx.aas import model +from basyx.aas.adapter.xml import StrictAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, \ + read_aas_xml_file_into, read_aas_xml_element from lxml import etree # type: ignore from typing import Iterable, Type, Union diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 3b0ccd1..9766469 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -10,11 +10,11 @@ from lxml import etree # type: ignore -from aas import model -from aas.adapter.xml import write_aas_xml_file, xml_serialization, XML_SCHEMA_FILE +from basyx.aas import model +from basyx.aas.adapter.xml import write_aas_xml_file, xml_serialization, XML_SCHEMA_FILE -from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, example_concept_description +from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ + example_submodel_template, example_aas_mandatory_attributes class XMLSerializationTest(unittest.TestCase): diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index 71f62e4..8dd6331 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -9,12 +9,12 @@ import io import unittest -from aas import model -from aas.adapter.xml import write_aas_xml_file, read_aas_xml_file +from basyx.aas import model +from basyx.aas.adapter.xml import write_aas_xml_file, read_aas_xml_file -from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, example_concept_description, create_example -from aas.examples.data._helper import AASDataChecker +from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ + example_aas_mandatory_attributes, example_submodel_template, create_example +from basyx.aas.examples.data._helper import AASDataChecker def _serialize_and_deserialize(data: model.DictObjectStore) -> model.DictObjectStore: From 6d671b606f7af60c840ecfff1b6348acba2f928e Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 2 Feb 2022 16:40:03 +0100 Subject: [PATCH 387/474] tests: Suppress MyPy warnings about TestCase.assertLogs MyPy complains that the returned from the assertLogs() context manager may be None. I assume this to be a MyPy or typeshed bug: The docs clearly state that the 'recording helper' is always returned: https://docs.python.org/3.10/library/unittest.html#unittest.TestCase.assertLogs Thus, I have added 'type: ignore's here, so that MyPy can inform us (via --warn-unused-ignores) when these are no longer needed -- in contrast to 'assert is not None' statements. As soon as this is the case, this commit can be reverted. --- .../adapter/json/test_json_deserialization.py | 24 +++++++++---------- test/adapter/xml/test_xml_deserialization.py | 8 +++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 894333c..66195a2 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -33,7 +33,7 @@ def test_file_format_missing_list(self) -> None: read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) + self.assertIn("submodels", cm.output[0]) # type: ignore def test_file_format_wrong_list(self) -> None: data = """ @@ -58,8 +58,8 @@ def test_file_format_wrong_list(self) -> None: read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) - self.assertIn("Asset", cm.output[0]) + self.assertIn("submodels", cm.output[0]) # type: ignore + self.assertIn("Asset", cm.output[0]) # type: ignore def test_file_format_unknown_object(self) -> None: data = """ @@ -75,8 +75,8 @@ def test_file_format_unknown_object(self) -> None: read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) - self.assertIn("'foo'", cm.output[0]) + self.assertIn("submodels", cm.output[0]) # type: ignore + self.assertIn("'foo'", cm.output[0]) # type: ignore def test_broken_asset(self) -> None: data = """ @@ -103,7 +103,7 @@ def test_broken_asset(self) -> None: # In failsafe mode, we should get a log entry and the first Asset entry should be returned as untouched dict with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: parsed_data = json.loads(data, cls=AASFromJsonDecoder) - self.assertIn("identification", cm.output[0]) + self.assertIn("identification", cm.output[0]) # type: ignore self.assertIsInstance(parsed_data, list) self.assertEqual(3, len(parsed_data)) @@ -142,15 +142,15 @@ def test_wrong_submodel_element_type(self) -> None: with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: with self.assertRaisesRegex(TypeError, r"SubmodelElement.*Asset"): json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIn("modelType", cm.output[0]) + self.assertIn("modelType", cm.output[0]) # type: ignore # In failsafe mode, we should get a log entries for the broken object and the wrong type of the first two # submodelElements with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: parsed_data = json.loads(data, cls=AASFromJsonDecoder) - self.assertGreaterEqual(len(cm.output), 3) - self.assertIn("SubmodelElement", cm.output[1]) - self.assertIn("SubmodelElement", cm.output[2]) + self.assertGreaterEqual(len(cm.output), 3) # type: ignore + self.assertIn("SubmodelElement", cm.output[1]) # type: ignore + self.assertIn("SubmodelElement", cm.output[2]) # type: ignore self.assertIsInstance(parsed_data[0], model.Submodel) self.assertEqual(1, len(parsed_data[0].submodel_element)) @@ -183,7 +183,7 @@ def test_duplicate_identifier(self) -> None: string_io = io.StringIO(data) with self.assertLogs(logging.getLogger(), level=logging.ERROR) as cm: read_aas_json_file(string_io, failsafe=True) - self.assertIn("duplicate identifier", cm.output[0]) + self.assertIn("duplicate identifier", cm.output[0]) # type: ignore string_io.seek(0) with self.assertRaisesRegex(KeyError, r"duplicate identifier"): read_aas_json_file(string_io, failsafe=False) @@ -224,7 +224,7 @@ def get_clean_store() -> model.DictObjectStore: with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=False, ignore_existing=True) self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) + self.assertIn("already exists in the object store", log_ctx.output[0]) # type: ignore submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) self.assertEqual(submodel.id_short, "test123") diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 8ae6861..4653656 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -55,7 +55,7 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], read_aas_xml_file(bytes_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: - self.assertIn(s, log_ctx.output[0]) + self.assertIn(s, log_ctx.output[0]) # type: ignore self.assertIn(s, str(cause)) def test_malformed_xml(self) -> None: @@ -182,7 +182,7 @@ def test_reference_kind_mismatch(self) -> None: with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) for s in ("GLOBAL_REFERENCE", "IRI=http://acplt.org/test_ref", "Asset"): - self.assertIn(s, context.output[0]) + self.assertIn(s, context.output[0]) # type: ignore def test_invalid_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted @@ -270,7 +270,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - self.assertIn("aas:value", context.output[0]) + self.assertIn("aas:value", context.output[0]) # type: ignore def test_duplicate_identifier(self) -> None: xml = _xml_wrap(""" @@ -324,7 +324,7 @@ def get_clean_store() -> model.DictObjectStore: with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) + self.assertIn("already exists in the object store", log_ctx.output[0]) # type: ignore submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) self.assertEqual(submodel.id_short, "test123") From 37a8eff5fc49e7c9fa9643d88fe600ef7c8cc71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 28 Oct 2021 19:43:43 +0200 Subject: [PATCH 388/474] minor codestyle improvements --- test/adapter/xml/test_xml_deserialization.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 4653656..7cd86fd 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -65,11 +65,7 @@ def test_malformed_xml(self) -> None: _xml_wrap("") ) for s in xml: - bytes_io = io.BytesIO(s.encode("utf-8")) - with self.assertRaises(etree.XMLSyntaxError): - read_aas_xml_file(bytes_io, failsafe=False) - with self.assertLogs(logging.getLogger(), level=logging.ERROR): - read_aas_xml_file(bytes_io, failsafe=True) + self._assertInExceptionAndLog(s, [], etree.XMLSyntaxError, logging.ERROR) def test_invalid_list_name(self) -> None: xml = _xml_wrap("") From f525c837486fde6cd5270c9b5bd3ae20873eddc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 19 Apr 2022 19:01:26 +0200 Subject: [PATCH 389/474] Update license headers for MIT license --- test/adapter/aasx/test_aasx.py | 7 +++---- test/adapter/json/test_json_deserialization.py | 7 +++---- test/adapter/json/test_json_serialization.py | 7 +++---- .../json/test_json_serialization_deserialization.py | 7 +++---- test/adapter/xml/test_xml_deserialization.py | 7 +++---- test/adapter/xml/test_xml_serialization.py | 7 +++---- test/adapter/xml/test_xml_serialization_deserialization.py | 7 +++---- 7 files changed, 21 insertions(+), 28 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index a418a7c..e126dd7 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT import datetime import hashlib import io diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 66195a2..30b9201 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ Additional tests for the adapter.json.json_deserialization module. diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 9a0a44f..f5bdba0 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT import io import unittest diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index c735862..b2e24ad 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT import io import json diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 7cd86fd..8597c14 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT import io import logging diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 9766469..982b70e 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT import io import unittest diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index 8dd6331..66931e8 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -1,10 +1,9 @@ # Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT import io import unittest From 0fd4cb171bc8009bc993d1b26f3a048f8a7fad8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 13 Aug 2022 17:23:59 +0200 Subject: [PATCH 390/474] add whitespace after assert keyword Since version 2.9.0, pycodestyle requires a whitespace after every keyword. --- test/adapter/aasx/test_aasx.py | 4 ++-- test/adapter/json/test_json_serialization.py | 4 ++-- test/adapter/json/test_json_serialization_deserialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index e126dd7..7584b40 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -103,9 +103,9 @@ def test_writing_reading_example_aas(self) -> None: example_aas.check_full_example(checker, new_data) # Check core properties - assert(isinstance(cp.created, datetime.datetime)) # to make mypy happy + assert isinstance(cp.created, datetime.datetime) # to make mypy happy self.assertIsInstance(new_cp.created, datetime.datetime) - assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy + assert isinstance(new_cp.created, datetime.datetime) # to make mypy happy self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) self.assertEqual(new_cp.creator, "Eclipse BaSyx Python Testing Framework") self.assertIsNone(new_cp.lastModifiedBy) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index f5bdba0..9ff99a1 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -30,7 +30,7 @@ def test_random_object_serialization(self) -> None: aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() - assert(submodel_identifier is not None) + assert submodel_identifier is not None submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) @@ -52,7 +52,7 @@ def test_random_object_serialization(self) -> None: aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() - assert(submodel_identifier is not None) + assert submodel_identifier is not None submodel_reference = model.AASReference(submodel_key, model.Submodel) # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index b2e24ad..d85228e 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -24,7 +24,7 @@ def test_random_object_serialization_deserialization(self) -> None: aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() - assert(submodel_identifier is not None) + assert submodel_identifier is not None submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 982b70e..fde361e 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -51,7 +51,7 @@ def test_random_object_serialization(self) -> None: aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() - assert(submodel_identifier is not None) + assert submodel_identifier is not None submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) From 07d6a6d74707ea43c0c32d1b1e1767ace862219b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 22 Oct 2022 01:00:47 +0200 Subject: [PATCH 391/474] model: prevent creating References without keys Close #31 --- test/adapter/json/test_json_serialization.py | 3 ++- test/adapter/xml/test_xml_serialization.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 9ff99a1..430b7ce 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -56,7 +56,8 @@ def test_random_object_serialization(self) -> None: submodel_reference = model.AASReference(submodel_key, model.Submodel) # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((), )) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((model.Key( + model.KeyElements.GLOBAL_REFERENCE, False, "http://acplt.org/TestSemanticId", model.KeyType.IRI),))) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) # serialize object to json diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index fde361e..30dbc49 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -53,7 +53,8 @@ def test_random_object_serialization(self) -> None: submodel_identifier = submodel_key[0].get_identifier() assert submodel_identifier is not None submodel_reference = model.AASReference(submodel_key, model.Submodel) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((model.Key( + model.KeyElements.GLOBAL_REFERENCE, False, "http://acplt.org/TestSemanticId", model.KeyType.IRI),))) test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) # serialize object to xml From 1b19aef7b191b3b9b8f65bf9eb65ad95ff184bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 3 Nov 2023 16:52:12 +0100 Subject: [PATCH 392/474] test.adapter.xml.xml_deserialization: remove unneeded 'type: ignore' comments --- test/adapter/xml/test_xml_deserialization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 8597c14..773e984 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -54,7 +54,7 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], read_aas_xml_file(bytes_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: - self.assertIn(s, log_ctx.output[0]) # type: ignore + self.assertIn(s, log_ctx.output[0]) self.assertIn(s, str(cause)) def test_malformed_xml(self) -> None: @@ -177,7 +177,7 @@ def test_reference_kind_mismatch(self) -> None: with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) for s in ("GLOBAL_REFERENCE", "IRI=http://acplt.org/test_ref", "Asset"): - self.assertIn(s, context.output[0]) # type: ignore + self.assertIn(s, context.output[0]) def test_invalid_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted @@ -265,7 +265,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - self.assertIn("aas:value", context.output[0]) # type: ignore + self.assertIn("aas:value", context.output[0]) def test_duplicate_identifier(self) -> None: xml = _xml_wrap(""" @@ -319,7 +319,7 @@ def get_clean_store() -> model.DictObjectStore: with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) # type: ignore + self.assertIn("already exists in the object store", log_ctx.output[0]) submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) self.assertEqual(submodel.id_short, "test123") From b13bd581bc46c28724ba29b0a9fc6cce9b7a2787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 3 Nov 2023 16:55:14 +0100 Subject: [PATCH 393/474] test.adapter.xml.xml_deserialization: make `OperationVariable` tests more explicit --- test/adapter/xml/test_xml_deserialization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 773e984..714dee1 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -232,7 +232,7 @@ def test_operation_variable_no_submodel_element(self) -> None: """) - self._assertInExceptionAndLog(xml, "aas:value", KeyError, logging.ERROR) + self._assertInExceptionAndLog(xml, ["aas:value", "has no submodel element"], KeyError, logging.ERROR) def test_operation_variable_too_many_submodel_elements(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted @@ -266,6 +266,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) self.assertIn("aas:value", context.output[0]) + self.assertIn("more than one submodel element", context.output[0]) def test_duplicate_identifier(self) -> None: xml = _xml_wrap(""" From e217e0ab9cea89870f33d4c3ea396fea37761700 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 24 Nov 2020 16:08:21 +0100 Subject: [PATCH 394/474] model, adapter, example: add constraint string not empty, adapt Asset to V30RC01, delete ConceptDictionary, delete key.local, adapt entity.asset --- .../adapter/json/test_json_deserialization.py | 47 ++++--- test/adapter/json/test_json_serialization.py | 51 ++++---- ...test_json_serialization_deserialization.py | 31 +++-- test/adapter/xml/test_xml_deserialization.py | 118 +++++++++++------- test/adapter/xml/test_xml_serialization.py | 39 +++--- 5 files changed, 170 insertions(+), 116 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 30b9201..2f861c2 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -163,14 +163,15 @@ def test_duplicate_identifier(self) -> None: "assetAdministrationShells": [{ "modelType": {"name": "AssetAdministrationShell"}, "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, - "asset": { - "keys": [{ - "idType": "IRI", - "local": false, - "type": "Asset", - "value": "http://acplt.org/test_aas" - }] - } + "assetInformation": { + "assetKind": "Instance", + "globalAssetId": { + "keys": [{ + "idType": "IRI", + "type": "Asset", + "value": "test_asset" + }] + }} }], "submodels": [{ "modelType": {"name": "Submodel"}, @@ -311,15 +312,13 @@ def test_stripped_annotated_relationship_element(self) -> None: "first": { "keys": [{ "idType": "IdShort", - "local": true, "type": "AnnotatedRelationshipElement", "value": "test_ref" }] }, "second": { "keys": [{ - "idType": "IdShort", - "local": true, + "idType": "IdShort", "type": "AnnotatedRelationshipElement", "value": "test_ref" }] @@ -348,6 +347,13 @@ def test_stripped_entity(self) -> None: "modelType": {"name": "Entity"}, "idShort": "test_entity", "entityType": "CoManagedEntity", + "globalAssetId": { + "keys": [{ + "idType": "IRI", + "type": "Asset", + "value": "test_asset" + }] + }, "statements": [{ "modelType": {"name": "MultiLanguageProperty"}, "idShort": "test_multi_language_property" @@ -395,18 +401,19 @@ def test_stripped_asset_administration_shell(self) -> None: { "modelType": {"name": "AssetAdministrationShell"}, "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, - "asset": { - "keys": [{ - "idType": "IRI", - "local": false, - "type": "Asset", - "value": "http://acplt.org/test_aas" - }] + "assetInformation": { + "assetKind": "Instance", + "globalAssetId": { + "keys": [{ + "idType": "IRI", + "type": "Asset", + "value": "test_asset" + }] + } }, "submodels": [{ "keys": [{ "idType": "IRI", - "local": false, "type": "Submodel", "value": "http://acplt.org/test_submodel" }] @@ -416,7 +423,7 @@ def test_stripped_asset_administration_shell(self) -> None: "idShort": "test_view" }] }""" - + print(data) # check if JSON with submodels and views can be parsed successfully aas = json.loads(data, cls=StrictAASFromJsonDecoder) self.assertIsInstance(aas, model.AssetAdministrationShell) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 430b7ce..38f23e3 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,21 +1,25 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright 2020 PyI40AAS Contributors # -# This program and the accompanying materials are made available under the terms of the MIT License, available in -# the LICENSE file of this project. +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# SPDX-License-Identifier: MIT +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. import io import unittest import json -from basyx.aas import model -from basyx.aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE +from aas import model +from aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE from jsonschema import validate # type: ignore from typing import Set, Union -from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ - example_aas_mandatory_attributes, example_submodel_template, create_example +from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas, create_example, example_concept_description class JsonSerializationTest(unittest.TestCase): @@ -25,15 +29,16 @@ def test_serialize_object(self) -> None: json_data = json.dumps(test_object, cls=AASToJsonEncoder) def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() - assert submodel_identifier is not None + assert(submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + aas_identifier, submodel_={submodel_reference}) # serialize object to json json_data = json.dumps({ @@ -47,18 +52,18 @@ def test_random_object_serialization(self) -> None: class JsonSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() - assert submodel_identifier is not None + assert(submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((model.Key( - model.KeyElements.GLOBAL_REFERENCE, False, "http://acplt.org/TestSemanticId", model.KeyType.IRI),))) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + aas_identifier, submodel_={submodel_reference}) # serialize object to json json_data = json.dumps({ @@ -190,7 +195,7 @@ def test_stripped_qualifiable(self) -> None: def test_stripped_annotated_relationship_element(self) -> None: mlp = model.MultiLanguageProperty("test_multi_language_property") ref = model.AASReference( - (model.Key(model.KeyElements.SUBMODEL, False, "http://acplt.org/test_ref", model.KeyType.IRI),), + (model.Key(model.KeyElements.SUBMODEL, "http://acplt.org/test_ref", model.KeyType.IRI),), model.Submodel ) are = model.AnnotatedRelationshipElement( @@ -216,17 +221,17 @@ def test_stripped_submodel_element_collection(self) -> None: def test_stripped_asset_administration_shell(self) -> None: asset_ref = model.AASReference( - (model.Key(model.KeyElements.ASSET, False, "http://acplt.org/test_ref", model.KeyType.IRI),), + (model.Key(model.KeyElements.ASSET, "http://acplt.org/test_ref", model.KeyType.IRI),), model.Asset ) submodel_ref = model.AASReference( - (model.Key(model.KeyElements.SUBMODEL, False, "http://acplt.org/test_ref", model.KeyType.IRI),), + (model.Key(model.KeyElements.SUBMODEL, "http://acplt.org/test_ref", model.KeyType.IRI),), model.Submodel ) aas = model.AssetAdministrationShell( - asset_ref, + model.AssetInformation(global_asset_id=asset_ref), model.Identifier("http://acplt.org/test_aas", model.IdentifierType.IRI), - submodel={submodel_ref}, + submodel_={submodel_ref}, view=[model.View("test_view")] ) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index d85228e..4ee13e5 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -1,33 +1,38 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright 2020 PyI40AAS Contributors # -# This program and the accompanying materials are made available under the terms of the MIT License, available in -# the LICENSE file of this project. +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# SPDX-License-Identifier: MIT +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. import io import json import unittest -from basyx.aas import model -from basyx.aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file +from aas import model +from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file -from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ - example_aas_mandatory_attributes, example_submodel_template, create_example -from basyx.aas.examples.data._helper import AASDataChecker +from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas, example_concept_description, create_example +from aas.examples.data._helper import AASDataChecker class JsonSerializationDeserializationTest(unittest.TestCase): def test_random_object_serialization_deserialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() - assert submodel_identifier is not None + assert(submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + aas_identifier, submodel_={submodel_reference}) # serialize object to json json_data = json.dumps({ diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 714dee1..2708656 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -102,32 +102,56 @@ def test_invalid_identification_attribute_value(self) -> None: def test_missing_asset_kind(self) -> None: xml = _xml_wrap(""" - - - - + + + http://acplt.org/test_aas + + + + http://acplt.org/asset_ref + + + + + """) - self._assertInExceptionAndLog(xml, "aas:kind", KeyError, logging.ERROR) + self._assertInExceptionAndLog(xml, "aas:assetKind", KeyError, logging.ERROR) def test_missing_asset_kind_text(self) -> None: xml = _xml_wrap(""" - - - - - + + + http://acplt.org/test_aas + + + + http://acplt.org/asset_ref + + + + + + """) - self._assertInExceptionAndLog(xml, "aas:kind", KeyError, logging.ERROR) + self._assertInExceptionAndLog(xml, "aas:assetKind", KeyError, logging.ERROR) def test_invalid_asset_kind_text(self) -> None: xml = _xml_wrap(""" - - - invalidKind - - + + + http://acplt.org/test_aas + + + + http://acplt.org/asset_ref + + + invalidKind + + + """) - self._assertInExceptionAndLog(xml, ["aas:kind", "invalidKind"], ValueError, logging.ERROR) + self._assertInExceptionAndLog(xml, ["aas:assetKind", "invalidKind"], ValueError, logging.ERROR) def test_invalid_boolean(self) -> None: xml = _xml_wrap(""" @@ -136,7 +160,7 @@ def test_invalid_boolean(self) -> None: http://acplt.org/test_asset - http://acplt.org/test_ref + http://acplt.org/test_ref @@ -168,7 +192,7 @@ def test_reference_kind_mismatch(self) -> None: http://acplt.org/test_aas - http://acplt.org/test_ref + http://acplt.org/test_ref @@ -273,16 +297,21 @@ def test_duplicate_identifier(self) -> None: http://acplt.org/test_aas - - - http://acplt.org/asset_ref - - + NotSet + + Instance + + + http://acplt.org/asset_ref + + + http://acplt.org/test_aas + NotSet @@ -337,7 +366,7 @@ def get_clean_store() -> model.DictObjectStore: def test_read_aas_xml_element(self) -> None: xml = """ - + http://acplt.org/test_submodel @@ -351,27 +380,27 @@ def test_read_aas_xml_element(self) -> None: class XmlDeserializationStrippedObjectsTest(unittest.TestCase): def test_stripped_qualifiable(self) -> None: xml = """ - + http://acplt.org/test_stripped_submodel test_operation - + test_qualifier string - + - + test_qualifier string - + """ bytes_io = io.BytesIO(xml.encode("utf-8")) @@ -393,16 +422,16 @@ def test_stripped_qualifiable(self) -> None: def test_stripped_annotated_relationship_element(self) -> None: xml = """ - + test_annotated_relationship_element - test_ref + test_ref - test_ref + test_ref @@ -418,7 +447,7 @@ def test_stripped_annotated_relationship_element(self) -> None: def test_stripped_entity(self) -> None: xml = """ - + test_entity CoManagedEntity @@ -434,7 +463,7 @@ def test_stripped_entity(self) -> None: def test_stripped_submodel_element_collection(self) -> None: xml = """ - + test_collection false @@ -450,17 +479,20 @@ def test_stripped_submodel_element_collection(self) -> None: def test_stripped_asset_administration_shell(self) -> None: xml = """ - + http://acplt.org/test_aas - - - http://acplt.org/test_ref - - + + Instance + + + http://acplt.org/test_ref + + + - http://acplt.org/test_ref + http://acplt.org/test_ref @@ -503,7 +535,7 @@ def construct_submodel(cls, element: etree.Element, object_class=EnhancedSubmode return super().construct_submodel(element, object_class=object_class, **kwargs) xml = """ - + http://acplt.org/test_stripped_submodel diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 30dbc49..347dca7 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -1,19 +1,23 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright 2020 PyI40AAS Contributors # -# This program and the accompanying materials are made available under the terms of the MIT License, available in -# the LICENSE file of this project. +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# SPDX-License-Identifier: MIT +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. import io import unittest from lxml import etree # type: ignore -from basyx.aas import model -from basyx.aas.adapter.xml import write_aas_xml_file, xml_serialization, XML_SCHEMA_FILE +from aas import model +from aas.adapter.xml import write_aas_xml_file, xml_serialization, XML_SCHEMA_FILE -from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ - example_submodel_template, example_aas_mandatory_attributes +from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas, example_concept_description class XMLSerializationTest(unittest.TestCase): @@ -26,15 +30,16 @@ def test_serialize_object(self) -> None: # todo: is this a correct way to test it? def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() assert (submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + aas_identifier, submodel_={submodel_reference}) test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() test_data.add(test_aas) @@ -46,16 +51,16 @@ def test_random_object_serialization(self) -> None: class XMLSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, True, "asset", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) asset_reference = model.AASReference(asset_key, model.Asset) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, True, "SM1", model.KeyType.CUSTOM),) + submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() - assert submodel_identifier is not None + assert(submodel_identifier is not None) submodel_reference = model.AASReference(submodel_key, model.Submodel) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((model.Key( - model.KeyElements.GLOBAL_REFERENCE, False, "http://acplt.org/TestSemanticId", model.KeyType.IRI),))) - test_aas = model.AssetAdministrationShell(asset_reference, aas_identifier, submodel={submodel_reference}) + submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + aas_identifier, submodel_={submodel_reference}) # serialize object to xml test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() From 67b864ad3e82145c144b5ef261ce97112e222383 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 24 Nov 2020 16:18:38 +0100 Subject: [PATCH 395/474] pycodestyle: fixed warnings --- test/adapter/json/test_json_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 2f861c2..4e8848e 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -318,7 +318,7 @@ def test_stripped_annotated_relationship_element(self) -> None: }, "second": { "keys": [{ - "idType": "IdShort", + "idType": "IdShort", "type": "AnnotatedRelationshipElement", "value": "test_ref" }] From 60b72c9543e0707ca7c6de816aa4f2c96aae5a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 1 Dec 2020 02:50:39 +0100 Subject: [PATCH 396/474] test.adapter.xml: fix xml_deserialization tests _xml_wrap: update xml namespace and remove unnecessary namespace declarations test_invalid_boolean: This test checks whether the xml deserialization raises an error when an invalid boolean is encountered. Because the `local` attribute was removed from reference keys, we can't use references to test this anymore. test_reference_kind_mismatch: This test checks if the xml deserialization logs a warning if the reference type doesn't match the expected type. Since asset references can now also be of type GlobalReference, we will now use a reference to an AssetAdministrationShell instead. test_invalid_constraint: The qualifier element has been renamed. --- test/adapter/xml/test_xml_deserialization.py | 42 ++++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 2708656..379556e 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -19,11 +19,7 @@ def _xml_wrap(xml: str) -> str: return \ """""" \ - """""" \ + """ """ \ + xml + """""" @@ -155,16 +151,19 @@ def test_invalid_asset_kind_text(self) -> None: def test_invalid_boolean(self) -> None: xml = _xml_wrap(""" - - - http://acplt.org/test_asset - - - http://acplt.org/test_ref - - - - + + + http://acplt.org/test_submodel + + + + False + collection + + + + + """) self._assertInExceptionAndLog(xml, "False", ValueError, logging.ERROR) @@ -190,11 +189,14 @@ def test_reference_kind_mismatch(self) -> None: http://acplt.org/test_aas - + + Instance + + http://acplt.org/test_ref - + """) @@ -221,16 +223,14 @@ def test_invalid_submodel_element(self) -> None: self._assertInExceptionAndLog(xml, "aas:invalidSubmodelElement", KeyError, logging.ERROR) def test_invalid_constraint(self) -> None: - # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" http://acplt.org/test_submodel - + - + """) From f977c63bfdcdd9de14f122be452b8fa192da3fd9 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 3 Dec 2020 14:19:16 +0100 Subject: [PATCH 397/474] submodel.entity: add typ for self.entity_type, add types for return values of getter and setter of entity_type, add constraint AASd-014 --- test/adapter/json/test_json_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 4e8848e..1c6d3e6 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -346,7 +346,7 @@ def test_stripped_entity(self) -> None: { "modelType": {"name": "Entity"}, "idShort": "test_entity", - "entityType": "CoManagedEntity", + "entityType": "SelfManagedEntity", "globalAssetId": { "keys": [{ "idType": "IRI", From a987b46b719054432df5be4838b2d4afbb28f5b1 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 8 Dec 2020 13:39:26 +0100 Subject: [PATCH 398/474] test.adapter.xml.test_xml_deserialization: Add "Template" to OperationVariable value (Constraint AASd-008) --- test/adapter/xml/test_xml_deserialization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 379556e..c0173a2 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -272,6 +272,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: + Template test_file application/problem+xml From a31d260e55f5b50e218cf89146ee66ae6c6a1f62 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 8 Dec 2020 15:19:01 +0100 Subject: [PATCH 399/474] test.adapter.json.test_json_serialization: Add categories to DataElements --- test/adapter/json/test_json_serialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 38f23e3..c503ea9 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -193,7 +193,7 @@ def test_stripped_qualifiable(self) -> None: self._checkNormalAndStripped("qualifiers", operation) def test_stripped_annotated_relationship_element(self) -> None: - mlp = model.MultiLanguageProperty("test_multi_language_property") + mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") ref = model.AASReference( (model.Key(model.KeyElements.SUBMODEL, "http://acplt.org/test_ref", model.KeyType.IRI),), model.Submodel @@ -208,13 +208,13 @@ def test_stripped_annotated_relationship_element(self) -> None: self._checkNormalAndStripped("annotation", are) def test_stripped_entity(self) -> None: - mlp = model.MultiLanguageProperty("test_multi_language_property") + mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") entity = model.Entity("test_entity", model.EntityType.CO_MANAGED_ENTITY, statement=[mlp]) self._checkNormalAndStripped("statements", entity) def test_stripped_submodel_element_collection(self) -> None: - mlp = model.MultiLanguageProperty("test_multi_language_property") + mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") sec = model.SubmodelElementCollectionOrdered("test_submodel_element_collection", value=[mlp]) self._checkNormalAndStripped("value", sec) From 7c4ff0ced7cf5b742e8449f6016f633ba43d3a60 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 8 Dec 2020 15:19:37 +0100 Subject: [PATCH 400/474] test.adapter.json.test_json_deserialization: Add category to DataElements --- test/adapter/json/test_json_deserialization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 1c6d3e6..ce37630 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -309,6 +309,7 @@ def test_stripped_annotated_relationship_element(self) -> None: { "modelType": {"name": "AnnotatedRelationshipElement"}, "idShort": "test_annotated_relationship_element", + "category": "PARAMETER", "first": { "keys": [{ "idType": "IdShort", @@ -325,7 +326,8 @@ def test_stripped_annotated_relationship_element(self) -> None: }, "annotation": [{ "modelType": {"name": "MultiLanguageProperty"}, - "idShort": "test_multi_language_property" + "idShort": "test_multi_language_property", + "category": "CONSTANT" }] }""" From aed4648774db30f11b1649448f90f5e39f7f53b2 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 8 Dec 2020 17:08:13 +0100 Subject: [PATCH 401/474] testfiles: add new test files for v3.0RC01 and fix minor error handling entity.specific_asset_id --- test/adapter/aasx/test_aasx.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 7584b40..e2942f9 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -56,6 +56,7 @@ def test_supplementary_file_container(self) -> None: class AASXWriterTest(unittest.TestCase): + @unittest.expectedFailure def test_writing_reading_example_aas(self) -> None: # Create example data and file_store data = example_aas.create_full_example() From ddc0f942a05beae6a0b95528ffdb5c7a45a9a6c4 Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 29 Dec 2020 15:53:10 +0100 Subject: [PATCH 402/474] bugfixes: fixes findings in merge request --- test/adapter/json/test_json_deserialization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index ce37630..d12055b 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -425,7 +425,6 @@ def test_stripped_asset_administration_shell(self) -> None: "idShort": "test_view" }] }""" - print(data) # check if JSON with submodels and views can be parsed successfully aas = json.loads(data, cls=StrictAASFromJsonDecoder) self.assertIsInstance(aas, model.AssetAdministrationShell) From 1118d8b1814bfcbbd73e332bc95fb42682b7b6ac Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Thu, 7 Jan 2021 15:59:18 +0100 Subject: [PATCH 403/474] model.namespace: create namespaces for unique id_short, semantic_id and type of qualifier including an generic namespaceSet --- test/adapter/json/test_json_serialization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index c503ea9..c022c65 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -182,11 +182,12 @@ def _checkNormalAndStripped(self, attributes: Union[Set[str], str], obj: object) def test_stripped_qualifiable(self) -> None: qualifier = model.Qualifier("test_qualifier", str) + qualifier2 = model.Qualifier("test_qualifier2", str) operation = model.Operation("test_operation", qualifier={qualifier}) submodel = model.Submodel( model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI), submodel_element=[operation], - qualifier={qualifier} + qualifier={qualifier2} ) self._checkNormalAndStripped({"submodelElements", "qualifiers"}, submodel) From c5d97c17b0ec4598fd40b12958de09ff9db1f9fa Mon Sep 17 00:00:00 2001 From: Torben Deppe Date: Tue, 12 Jan 2021 13:10:03 +0100 Subject: [PATCH 404/474] model.submodelelementcollection: add new types to support attribute allowDuplicates --- test/adapter/json/test_json_deserialization.py | 1 + test/adapter/xml/test_xml_deserialization.py | 1 + 2 files changed, 2 insertions(+) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index d12055b..894a045 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -380,6 +380,7 @@ def test_stripped_submodel_element_collection(self) -> None: "modelType": {"name": "SubmodelElementCollection"}, "idShort": "test_submodel_element_collection", "ordered": false, + "allowDuplicates": true, "value": [{ "modelType": {"name": "MultiLanguageProperty"}, "idShort": "test_multi_language_property" diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index c0173a2..9f4435d 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -466,6 +466,7 @@ def test_stripped_submodel_element_collection(self) -> None: xml = """ test_collection + true false """ From 6739cd3f3898970dfe9b9516be4d9bea32cf8ca8 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Mon, 4 Jan 2021 11:20:43 +0100 Subject: [PATCH 405/474] adapter.aasx: Update AASXWriter.write_aas() to DotAAS v3.0 spec --- test/adapter/aasx/test_aasx.py | 113 +++++++++++++++++---------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index e2942f9..f2d07ce 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -1,9 +1,13 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright 2020 PyI40AAS Contributors # -# This program and the accompanying materials are made available under the terms of the MIT License, available in -# the LICENSE file of this project. +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at # -# SPDX-License-Identifier: MIT +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. import datetime import hashlib import io @@ -13,9 +17,9 @@ import warnings import pyecma376_2 -from basyx.aas import model -from basyx.aas.adapter import aasx -from basyx.aas.examples.data import example_aas, example_aas_mandatory_attributes, _helper +from aas import model +from aas.adapter import aasx +from aas.examples.data import example_aas, _helper, example_aas_mandatory_attributes class TestAASXUtils(unittest.TestCase): @@ -56,7 +60,6 @@ def test_supplementary_file_container(self) -> None: class AASXWriterTest(unittest.TestCase): - @unittest.expectedFailure def test_writing_reading_example_aas(self) -> None: # Create example data and file_store data = example_aas.create_full_example() @@ -68,57 +71,55 @@ def test_writing_reading_example_aas(self) -> None: # Create OPC/AASX core properties cp = pyecma376_2.OPCCoreProperties() cp.created = datetime.datetime.now() - cp.creator = "Eclipse BaSyx Python Testing Framework" + cp.creator = "PyI40AAS Testing Framework" # Write AASX file for write_json in (False, True): - for submodel_split_parts in (False, True): - with self.subTest(write_json=write_json, submodel_split_parts=submodel_split_parts): - fd, filename = tempfile.mkstemp(suffix=".aasx") - os.close(fd) - - # Write AASX file - # the zipfile library reports errors as UserWarnings via the warnings library. Let's check for - # warnings - with warnings.catch_warnings(record=True) as w: - with aasx.AASXWriter(filename) as writer: - writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', - id_type=model.IdentifierType.IRI), - data, files, write_json=write_json, - submodel_split_parts=submodel_split_parts) - writer.write_core_properties(cp) - - assert isinstance(w, list) # This should be True due to the record=True parameter - self.assertEqual(0, len(w), f"Warnings were issued while writing the AASX file: " - f"{[warning.message for warning in w]}") - - # Read AASX file - new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - new_files = aasx.DictSupplementaryFileContainer() - with aasx.AASXReader(filename) as reader: - reader.read_into(new_data, new_files) - new_cp = reader.get_core_properties() - - # Check AAS objects - checker = _helper.AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, new_data) - - # Check core properties - assert isinstance(cp.created, datetime.datetime) # to make mypy happy - self.assertIsInstance(new_cp.created, datetime.datetime) - assert isinstance(new_cp.created, datetime.datetime) # to make mypy happy - self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) - self.assertEqual(new_cp.creator, "Eclipse BaSyx Python Testing Framework") - self.assertIsNone(new_cp.lastModifiedBy) - - # Check files - self.assertEqual(new_files.get_content_type("/TestFile.pdf"), "application/pdf") - file_content = io.BytesIO() - new_files.write_file("/TestFile.pdf", file_content) - self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), - "78450a66f59d74c073bf6858db340090ea72a8b1") - - os.unlink(filename) + with self.subTest(write_json=write_json): + fd, filename = tempfile.mkstemp(suffix=".aasx") + os.close(fd) + + # Write AASX file + # the zipfile library reports errors as UserWarnings via the warnings library. Let's check for + # warnings + with warnings.catch_warnings(record=True) as w: + with aasx.AASXWriter(filename) as writer: + writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', + id_type=model.IdentifierType.IRI), + data, files, write_json=write_json) + writer.write_core_properties(cp) + + assert isinstance(w, list) # This should be True due to the record=True parameter + self.assertEqual(0, len(w), f"Warnings were issued while writing the AASX file: " + f"{[warning.message for warning in w]}") + + # Read AASX file + new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + new_files = aasx.DictSupplementaryFileContainer() + with aasx.AASXReader(filename) as reader: + reader.read_into(new_data, new_files) + new_cp = reader.get_core_properties() + + # Check AAS objects + checker = _helper.AASDataChecker(raise_immediately=True) + example_aas.check_full_example(checker, new_data) + + # Check core properties + assert(isinstance(cp.created, datetime.datetime)) # to make mypy happy + self.assertIsInstance(new_cp.created, datetime.datetime) + assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy + self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) + self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") + self.assertIsNone(new_cp.lastModifiedBy) + + # Check files + self.assertEqual(new_files.get_content_type("/TestFile.pdf"), "application/pdf") + file_content = io.BytesIO() + new_files.write_file("/TestFile.pdf", file_content) + self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), + "78450a66f59d74c073bf6858db340090ea72a8b1") + + os.unlink(filename) def test_writing_reading_objects_single_part(self) -> None: # Create example data and file_store From 25815e6b929570d34ed59c23fe68b4525ef51782 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Wed, 13 Jan 2021 08:47:47 +0100 Subject: [PATCH 406/474] test: Remove outdated AASX test --- test/adapter/aasx/test_aasx.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index f2d07ce..6b9709a 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -84,6 +84,7 @@ def test_writing_reading_example_aas(self) -> None: # warnings with warnings.catch_warnings(record=True) as w: with aasx.AASXWriter(filename) as writer: + # TODO test writing multiple AAS writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', id_type=model.IdentifierType.IRI), data, files, write_json=write_json) @@ -120,30 +121,3 @@ def test_writing_reading_example_aas(self) -> None: "78450a66f59d74c073bf6858db340090ea72a8b1") os.unlink(filename) - - def test_writing_reading_objects_single_part(self) -> None: - # Create example data and file_store - data = example_aas_mandatory_attributes.create_full_example() - files = aasx.DictSupplementaryFileContainer() - - # Write AASX file - for write_json in (False, True): - with self.subTest(write_json=write_json): - fd, filename = tempfile.mkstemp(suffix=".aasx") - os.close(fd) - with aasx.AASXWriter(filename) as writer: - writer.write_aas_objects('/aasx/aasx.{}'.format('json' if write_json else 'xml'), - [obj.identification for obj in data], - data, files, write_json) - - # Read AASX file - new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - new_files = aasx.DictSupplementaryFileContainer() - with aasx.AASXReader(filename) as reader: - reader.read_into(new_data, new_files) - - # Check AAS objects - checker = _helper.AASDataChecker(raise_immediately=True) - example_aas_mandatory_attributes.check_full_example(checker, new_data) - - os.unlink(filename) From 8eb81223c4e870fb198525b6c9049dc8e409ffdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Nov 2021 17:04:00 +0100 Subject: [PATCH 407/474] test: fix dysfunctional xml deserialization test --- test/adapter/xml/test_xml_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 9f4435d..55d76e2 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -202,7 +202,7 @@ def test_reference_kind_mismatch(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - for s in ("GLOBAL_REFERENCE", "IRI=http://acplt.org/test_ref", "Asset"): + for s in ("GLOBAL_REFERENCE", "IRI=http://acplt.org/test_ref", "AssetAdministrationShell"): self.assertIn(s, context.output[0]) def test_invalid_submodel_element(self) -> None: From 34d7e6ac9fdce214e78723a1f6579d2a6075f6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 28 Oct 2021 05:47:07 +0200 Subject: [PATCH 408/474] remove Asset and all references to it --- .../adapter/json/test_json_deserialization.py | 121 ++++++++---------- test/adapter/json/test_json_serialization.py | 13 +- ...test_json_serialization_deserialization.py | 4 +- test/adapter/xml/test_xml_deserialization.py | 51 ++------ test/adapter/xml/test_xml_serialization.py | 8 +- 5 files changed, 80 insertions(+), 117 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 894a045..cb5a596 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -1,9 +1,10 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2020 PyI40AAS Contributors # -# This program and the accompanying materials are made available under the terms of the MIT License, available in -# the LICENSE file of this project. +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 """ Additional tests for the adapter.json.json_deserialization module. @@ -15,9 +16,9 @@ import json import logging import unittest -from basyx.aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder, \ +from aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder, \ read_aas_json_file, read_aas_json_file_into -from basyx.aas import model +from aas import model class JsonDeserializationTest(unittest.TestCase): @@ -25,14 +26,13 @@ def test_file_format_missing_list(self) -> None: data = """ { "assetAdministrationShells": [], - "assets": [], "conceptDescriptions": [] }""" with self.assertRaisesRegex(KeyError, r"submodels"): read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) # type: ignore + self.assertIn("submodels", cm.output[0]) def test_file_format_wrong_list(self) -> None: data = """ @@ -43,22 +43,24 @@ def test_file_format_wrong_list(self) -> None: "submodels": [ { "modelType": { - "name": "Asset" + "name": "AssetAdministrationShell" }, "identification": { "id": "https://acplt.org/Test_Asset", "idType": "IRI" }, - "kind": "Instance" + "assetInformation": { + "assetKind": "Instance" + } } ] }""" - with self.assertRaisesRegex(TypeError, r"submodels.*Asset"): + with self.assertRaisesRegex(TypeError, r"submodels.*AssetAdministrationShell"): read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) # type: ignore - self.assertIn("Asset", cm.output[0]) # type: ignore + self.assertIn("submodels", cm.output[0]) + self.assertIn("AssetAdministrationShell", cm.output[0]) def test_file_format_unknown_object(self) -> None: data = """ @@ -74,42 +76,39 @@ def test_file_format_unknown_object(self) -> None: read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) # type: ignore - self.assertIn("'foo'", cm.output[0]) # type: ignore + self.assertIn("submodels", cm.output[0]) + self.assertIn("'foo'", cm.output[0]) - def test_broken_asset(self) -> None: + def test_broken_submodel(self) -> None: data = """ [ { - "modelType": {"name": "Asset"}, - "kind": "Instance" + "modelType": {"name": "Submodel"} }, { - "modelType": {"name": "Asset"}, - "identification": ["https://acplt.org/Test_Asset_broken_id", "IRI"], - "kind": "Instance" + "modelType": {"name": "Submodel"}, + "identification": ["https://acplt.org/Test_Submodel_broken_id", "IRI"] }, { - "modelType": {"name": "Asset"}, - "identification": {"id": "https://acplt.org/Test_Asset", "idType": "IRI"}, - "kind": "Instance" + "modelType": {"name": "Submodel"}, + "identification": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} } ]""" # In strict mode, we should catch an exception with self.assertRaisesRegex(KeyError, r"identification"): json.loads(data, cls=StrictAASFromJsonDecoder) - # In failsafe mode, we should get a log entry and the first Asset entry should be returned as untouched dict + # In failsafe mode, we should get a log entry and the first Submodel entry should be returned as untouched dict with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: parsed_data = json.loads(data, cls=AASFromJsonDecoder) - self.assertIn("identification", cm.output[0]) # type: ignore + self.assertIn("identification", cm.output[0]) self.assertIsInstance(parsed_data, list) self.assertEqual(3, len(parsed_data)) self.assertIsInstance(parsed_data[0], dict) self.assertIsInstance(parsed_data[1], dict) - self.assertIsInstance(parsed_data[2], model.Asset) - self.assertEqual("https://acplt.org/Test_Asset", parsed_data[2].identification.id) + self.assertIsInstance(parsed_data[2], model.Submodel) + self.assertEqual("https://acplt.org/Test_Submodel", parsed_data[2].identification.id) def test_wrong_submodel_element_type(self) -> None: data = """ @@ -122,9 +121,8 @@ def test_wrong_submodel_element_type(self) -> None: }, "submodelElements": [ { - "modelType": {"name": "Asset"}, - "identification": {"id": "https://acplt.org/Test_Asset", "idType": "IRI"}, - "kind": "Instance" + "modelType": {"name": "Submodel"}, + "identification": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} }, { "modelType": "Broken modelType" @@ -136,20 +134,20 @@ def test_wrong_submodel_element_type(self) -> None: ] } ]""" - # In strict mode, we should catch an exception for the unexpected Asset within the Submodel + # In strict mode, we should catch an exception for the unexpected Submodel within the Submodel # The broken object should not raise an exception, but log a warning, even in strict mode. with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - with self.assertRaisesRegex(TypeError, r"SubmodelElement.*Asset"): + with self.assertRaisesRegex(TypeError, r"SubmodelElement.*Submodel"): json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIn("modelType", cm.output[0]) # type: ignore + self.assertIn("modelType", cm.output[0]) # In failsafe mode, we should get a log entries for the broken object and the wrong type of the first two # submodelElements with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: parsed_data = json.loads(data, cls=AASFromJsonDecoder) - self.assertGreaterEqual(len(cm.output), 3) # type: ignore - self.assertIn("SubmodelElement", cm.output[1]) # type: ignore - self.assertIn("SubmodelElement", cm.output[2]) # type: ignore + self.assertGreaterEqual(len(cm.output), 3) + self.assertIn("SubmodelElement", cm.output[1]) + self.assertIn("SubmodelElement", cm.output[2]) self.assertIsInstance(parsed_data[0], model.Submodel) self.assertEqual(1, len(parsed_data[0].submodel_element)) @@ -164,26 +162,19 @@ def test_duplicate_identifier(self) -> None: "modelType": {"name": "AssetAdministrationShell"}, "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, "assetInformation": { - "assetKind": "Instance", - "globalAssetId": { - "keys": [{ - "idType": "IRI", - "type": "Asset", - "value": "test_asset" - }] - }} + "assetKind": "Instance" + } }], "submodels": [{ "modelType": {"name": "Submodel"}, "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"} }], - "assets": [], "conceptDescriptions": [] }""" string_io = io.StringIO(data) with self.assertLogs(logging.getLogger(), level=logging.ERROR) as cm: read_aas_json_file(string_io, failsafe=True) - self.assertIn("duplicate identifier", cm.output[0]) # type: ignore + self.assertIn("duplicate identifier", cm.output[0]) string_io.seek(0) with self.assertRaisesRegex(KeyError, r"duplicate identifier"): read_aas_json_file(string_io, failsafe=False) @@ -205,7 +196,6 @@ def get_clean_store() -> model.DictObjectStore: "idShort": "test456" }], "assetAdministrationShells": [], - "assets": [], "conceptDescriptions": [] }""" @@ -224,7 +214,7 @@ def get_clean_store() -> model.DictObjectStore: with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=False, ignore_existing=True) self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) # type: ignore + self.assertIn("already exists in the object store", log_ctx.output[0]) submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) self.assertEqual(submodel.id_short, "test123") @@ -243,27 +233,26 @@ def get_clean_store() -> model.DictObjectStore: class JsonDeserializationDerivingTest(unittest.TestCase): def test_asset_constructor_overriding(self) -> None: - class EnhancedAsset(model.Asset): + class EnhancedSubmodel(model.Submodel): def __init__(self, **kwargs): super().__init__(**kwargs) self.enhanced_attribute = "fancy!" - class EnhancedAASDecoder(AASFromJsonDecoder): + class EnhancedAASDecoder(StrictAASFromJsonDecoder): @classmethod - def _construct_asset(cls, dct): - return super()._construct_asset(dct, object_class=EnhancedAsset) + def _construct_submodel(cls, dct, object_class=EnhancedSubmodel): + return super()._construct_submodel(dct, object_class=object_class) data = """ [ { - "modelType": {"name": "Asset"}, - "identification": {"id": "https://acplt.org/Test_Asset", "idType": "IRI"}, - "kind": "Instance" + "modelType": {"name": "Submodel"}, + "identification": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} } ]""" parsed_data = json.loads(data, cls=EnhancedAASDecoder) self.assertEqual(1, len(parsed_data)) - self.assertIsInstance(parsed_data[0], EnhancedAsset) + self.assertIsInstance(parsed_data[0], EnhancedSubmodel) self.assertEqual(parsed_data[0].enhanced_attribute, "fancy!") @@ -352,7 +341,7 @@ def test_stripped_entity(self) -> None: "globalAssetId": { "keys": [{ "idType": "IRI", - "type": "Asset", + "type": "GlobalReference", "value": "test_asset" }] }, @@ -405,14 +394,14 @@ def test_stripped_asset_administration_shell(self) -> None: "modelType": {"name": "AssetAdministrationShell"}, "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, "assetInformation": { - "assetKind": "Instance", - "globalAssetId": { - "keys": [{ - "idType": "IRI", - "type": "Asset", - "value": "test_asset" - }] - } + "assetKind": "Instance", + "globalAssetId": { + "keys": [{ + "idType": "IRI", + "type": "GlobalReference", + "value": "test_asset" + }] + } }, "submodels": [{ "keys": [{ diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index c022c65..bf2e4da 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -29,8 +29,8 @@ def test_serialize_object(self) -> None: json_data = json.dumps(test_object, cls=AASToJsonEncoder) def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) - asset_reference = model.AASReference(asset_key, model.Asset) + asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) + asset_reference = model.Reference(asset_key) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() @@ -52,8 +52,8 @@ def test_random_object_serialization(self) -> None: class JsonSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) - asset_reference = model.AASReference(asset_key, model.Asset) + asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) + asset_reference = model.Reference(asset_key) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() @@ -221,9 +221,8 @@ def test_stripped_submodel_element_collection(self) -> None: self._checkNormalAndStripped("value", sec) def test_stripped_asset_administration_shell(self) -> None: - asset_ref = model.AASReference( - (model.Key(model.KeyElements.ASSET, "http://acplt.org/test_ref", model.KeyType.IRI),), - model.Asset + asset_ref = model.Reference( + (model.Key(model.KeyElements.GLOBAL_REFERENCE, "http://acplt.org/test_ref", model.KeyType.IRI),), ) submodel_ref = model.AASReference( (model.Key(model.KeyElements.SUBMODEL, "http://acplt.org/test_ref", model.KeyType.IRI),), diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 4ee13e5..7a8ca10 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -23,8 +23,8 @@ class JsonSerializationDeserializationTest(unittest.TestCase): def test_random_object_serialization_deserialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) - asset_reference = model.AASReference(asset_key, model.Asset) + asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) + asset_reference = model.Reference(asset_key) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 55d76e2..7f78cc6 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -68,31 +68,31 @@ def test_invalid_list_name(self) -> None: def test_invalid_element_in_list(self) -> None: xml = _xml_wrap(""" - + - + """) - self._assertInExceptionAndLog(xml, ["aas:invalidElement", "aas:assets"], KeyError, logging.WARNING) + self._assertInExceptionAndLog(xml, ["aas:invalidElement", "aas:submodels"], KeyError, logging.WARNING) def test_missing_identification_attribute(self) -> None: xml = _xml_wrap(""" - - + + http://acplt.org/test_asset - Instance - - + + + """) self._assertInExceptionAndLog(xml, "idType", KeyError, logging.ERROR) def test_invalid_identification_attribute_value(self) -> None: xml = _xml_wrap(""" - - + + http://acplt.org/test_asset - Instance - - + + + """) self._assertInExceptionAndLog(xml, ["idType", "invalid"], ValueError, logging.ERROR) @@ -102,11 +102,6 @@ def test_missing_asset_kind(self) -> None: http://acplt.org/test_aas - - - http://acplt.org/asset_ref - - @@ -119,11 +114,6 @@ def test_missing_asset_kind_text(self) -> None: http://acplt.org/test_aas - - - http://acplt.org/asset_ref - - @@ -137,11 +127,6 @@ def test_invalid_asset_kind_text(self) -> None: http://acplt.org/test_aas - - - http://acplt.org/asset_ref - - invalidKind @@ -301,11 +286,6 @@ def test_duplicate_identifier(self) -> None: NotSet Instance - - - http://acplt.org/asset_ref - - @@ -485,11 +465,6 @@ def test_stripped_asset_administration_shell(self) -> None: http://acplt.org/test_aas Instance - - - http://acplt.org/test_ref - - diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 347dca7..232cdef 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -30,8 +30,8 @@ def test_serialize_object(self) -> None: # todo: is this a correct way to test it? def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) - asset_reference = model.AASReference(asset_key, model.Asset) + asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) + asset_reference = model.Reference(asset_key) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() @@ -51,8 +51,8 @@ def test_random_object_serialization(self) -> None: class XMLSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.ASSET, "asset", model.KeyType.CUSTOM),) - asset_reference = model.AASReference(asset_key, model.Asset) + asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) + asset_reference = model.Reference(asset_key) aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) submodel_identifier = submodel_key[0].get_identifier() From a5e59c9824977b48be6f1330b711a91c16a173e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Nov 2021 17:29:31 +0100 Subject: [PATCH 409/474] remove View and all references to it --- .../adapter/json/test_json_deserialization.py | 10 ++------- test/adapter/json/test_json_serialization.py | 22 ++++++++----------- test/adapter/xml/test_xml_deserialization.py | 11 ++-------- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index cb5a596..99c4774 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -409,22 +409,16 @@ def test_stripped_asset_administration_shell(self) -> None: "type": "Submodel", "value": "http://acplt.org/test_submodel" }] - }], - "views": [{ - "modelType": {"name": "View"}, - "idShort": "test_view" }] }""" - # check if JSON with submodels and views can be parsed successfully + # check if JSON with submodels can be parsed successfully aas = json.loads(data, cls=StrictAASFromJsonDecoder) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) self.assertEqual(len(aas.submodel), 1) - self.assertEqual(len(aas.view), 1) - # check if submodels and views are ignored in stripped mode + # check if submodels are ignored in stripped mode aas = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) self.assertEqual(len(aas.submodel), 0) - self.assertEqual(len(aas.view), 0) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index bf2e4da..132ee92 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,13 +1,10 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 PyI40AAS Contributors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available +# at https://www.apache.org/licenses/LICENSE-2.0. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 import io import unittest @@ -38,7 +35,7 @@ def test_random_object_serialization(self) -> None: submodel_reference = model.AASReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), - aas_identifier, submodel_={submodel_reference}) + aas_identifier, submodel={submodel_reference}) # serialize object to json json_data = json.dumps({ @@ -63,7 +60,7 @@ def test_random_object_serialization(self) -> None: # must be a Reference. (This seems to be a bug in the JSONSchema.) submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), - aas_identifier, submodel_={submodel_reference}) + aas_identifier, submodel={submodel_reference}) # serialize object to json json_data = json.dumps({ @@ -231,8 +228,7 @@ def test_stripped_asset_administration_shell(self) -> None: aas = model.AssetAdministrationShell( model.AssetInformation(global_asset_id=asset_ref), model.Identifier("http://acplt.org/test_aas", model.IdentifierType.IRI), - submodel_={submodel_ref}, - view=[model.View("test_view")] + submodel={submodel_ref} ) - self._checkNormalAndStripped({"submodels", "views"}, aas) + self._checkNormalAndStripped({"submodels"}, aas) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 7f78cc6..df39a41 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -473,29 +473,22 @@ def test_stripped_asset_administration_shell(self) -> None: - - - test_view - - """ bytes_io = io.BytesIO(xml.encode("utf-8")) - # check if XML with submodelRef and views can be parsed successfully + # check if XML with submodelRef can be parsed successfully aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) self.assertEqual(len(aas.submodel), 1) - self.assertEqual(len(aas.view), 1) - # check if submodelRef and views are ignored in stripped mode + # check if submodelRef is ignored in stripped mode aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, stripped=True) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) self.assertEqual(len(aas.submodel), 0) - self.assertEqual(len(aas.view), 0) class XmlDeserializationDerivingTest(unittest.TestCase): From 371b82bbe1a276359b7ce43697757ad417283ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 6 Apr 2022 23:29:49 +0200 Subject: [PATCH 410/474] remove Constraint --- test/adapter/json/test_json_deserialization.py | 4 ++-- test/adapter/xml/test_xml_deserialization.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 99c4774..4954ed5 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -278,7 +278,7 @@ def test_stripped_qualifiable(self) -> None: }] }""" - # check if JSON with constraints can be parsed successfully + # check if JSON with qualifiers can be parsed successfully submodel = json.loads(data, cls=StrictAASFromJsonDecoder) self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) @@ -286,7 +286,7 @@ def test_stripped_qualifiable(self) -> None: operation = submodel.submodel_element.pop() self.assertEqual(len(operation.qualifier), 1) - # check if constraints are ignored in stripped mode + # check if qualifiers are ignored in stripped mode submodel = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index df39a41..f68a326 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -207,19 +207,19 @@ def test_invalid_submodel_element(self) -> None: """) self._assertInExceptionAndLog(xml, "aas:invalidSubmodelElement", KeyError, logging.ERROR) - def test_invalid_constraint(self) -> None: + def test_empty_qualifier(self) -> None: xml = _xml_wrap(""" http://acplt.org/test_submodel - + """) - self._assertInExceptionAndLog(xml, "aas:invalidConstraint", KeyError, logging.ERROR) + self._assertInExceptionAndLog(xml, ["aas:qualifier", "has no child aas:type"], KeyError, logging.ERROR) def test_operation_variable_no_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted @@ -386,7 +386,7 @@ def test_stripped_qualifiable(self) -> None: """ bytes_io = io.BytesIO(xml.encode("utf-8")) - # check if XML with constraints can be parsed successfully + # check if XML with qualifiers can be parsed successfully submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False) self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) @@ -394,7 +394,7 @@ def test_stripped_qualifiable(self) -> None: operation = submodel.submodel_element.pop() self.assertEqual(len(operation.qualifier), 1) - # check if constraints are ignored in stripped mode + # check if qualifiers are ignored in stripped mode submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False, stripped=True) self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) From 4e41a20b820f5bb8785367f28bb73e5f247616ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 22 Jun 2022 14:07:51 +0200 Subject: [PATCH 411/474] rename Blob/mimeType File/mimeType to /contentType --- test/adapter/xml/test_xml_deserialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index f68a326..b9dac83 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -259,11 +259,11 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: Template test_file - application/problem+xml + application/problem+xml test_file2 - application/problem+xml + application/problem+xml From 51b9568e20a5a4d2fa08277ff3a7c14f2bd4c33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 25 Jun 2022 20:34:03 +0200 Subject: [PATCH 412/474] rename Identifiable/identification to Identifiable/id --- .../adapter/json/test_json_deserialization.py | 63 +++++++++---------- test/adapter/xml/test_xml_deserialization.py | 38 +++++------ 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 4954ed5..4ed6c06 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -1,10 +1,9 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT """ Additional tests for the adapter.json.json_deserialization module. @@ -16,9 +15,9 @@ import json import logging import unittest -from aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder, \ +from basyx.aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder, \ read_aas_json_file, read_aas_json_file_into -from aas import model +from basyx.aas import model class JsonDeserializationTest(unittest.TestCase): @@ -32,7 +31,7 @@ def test_file_format_missing_list(self) -> None: read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) + self.assertIn("submodels", cm.output[0]) # type: ignore def test_file_format_wrong_list(self) -> None: data = """ @@ -45,7 +44,7 @@ def test_file_format_wrong_list(self) -> None: "modelType": { "name": "AssetAdministrationShell" }, - "identification": { + "id": { "id": "https://acplt.org/Test_Asset", "idType": "IRI" }, @@ -59,8 +58,8 @@ def test_file_format_wrong_list(self) -> None: read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) - self.assertIn("AssetAdministrationShell", cm.output[0]) + self.assertIn("submodels", cm.output[0]) # type: ignore + self.assertIn("AssetAdministrationShell", cm.output[0]) # type: ignore def test_file_format_unknown_object(self) -> None: data = """ @@ -76,8 +75,8 @@ def test_file_format_unknown_object(self) -> None: read_aas_json_file(io.StringIO(data), failsafe=False) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) - self.assertIn("'foo'", cm.output[0]) + self.assertIn("submodels", cm.output[0]) # type: ignore + self.assertIn("'foo'", cm.output[0]) # type: ignore def test_broken_submodel(self) -> None: data = """ @@ -87,42 +86,42 @@ def test_broken_submodel(self) -> None: }, { "modelType": {"name": "Submodel"}, - "identification": ["https://acplt.org/Test_Submodel_broken_id", "IRI"] + "id": ["https://acplt.org/Test_Submodel_broken_id", "IRI"] }, { "modelType": {"name": "Submodel"}, - "identification": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} + "id": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} } ]""" # In strict mode, we should catch an exception - with self.assertRaisesRegex(KeyError, r"identification"): + with self.assertRaisesRegex(KeyError, r"id"): json.loads(data, cls=StrictAASFromJsonDecoder) # In failsafe mode, we should get a log entry and the first Submodel entry should be returned as untouched dict with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: parsed_data = json.loads(data, cls=AASFromJsonDecoder) - self.assertIn("identification", cm.output[0]) + self.assertIn("id", cm.output[0]) # type: ignore self.assertIsInstance(parsed_data, list) self.assertEqual(3, len(parsed_data)) self.assertIsInstance(parsed_data[0], dict) self.assertIsInstance(parsed_data[1], dict) self.assertIsInstance(parsed_data[2], model.Submodel) - self.assertEqual("https://acplt.org/Test_Submodel", parsed_data[2].identification.id) + self.assertEqual("https://acplt.org/Test_Submodel", parsed_data[2].id.id) def test_wrong_submodel_element_type(self) -> None: data = """ [ { "modelType": {"name": "Submodel"}, - "identification": { + "id": { "id": "http://acplt.org/Submodels/Assets/TestAsset/Identification", "idType": "IRI" }, "submodelElements": [ { "modelType": {"name": "Submodel"}, - "identification": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} + "id": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} }, { "modelType": "Broken modelType" @@ -139,15 +138,15 @@ def test_wrong_submodel_element_type(self) -> None: with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: with self.assertRaisesRegex(TypeError, r"SubmodelElement.*Submodel"): json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIn("modelType", cm.output[0]) + self.assertIn("modelType", cm.output[0]) # type: ignore # In failsafe mode, we should get a log entries for the broken object and the wrong type of the first two # submodelElements with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: parsed_data = json.loads(data, cls=AASFromJsonDecoder) - self.assertGreaterEqual(len(cm.output), 3) - self.assertIn("SubmodelElement", cm.output[1]) - self.assertIn("SubmodelElement", cm.output[2]) + self.assertGreaterEqual(len(cm.output), 3) # type: ignore + self.assertIn("SubmodelElement", cm.output[1]) # type: ignore + self.assertIn("SubmodelElement", cm.output[2]) # type: ignore self.assertIsInstance(parsed_data[0], model.Submodel) self.assertEqual(1, len(parsed_data[0].submodel_element)) @@ -160,21 +159,21 @@ def test_duplicate_identifier(self) -> None: { "assetAdministrationShells": [{ "modelType": {"name": "AssetAdministrationShell"}, - "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, + "id": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, "assetInformation": { "assetKind": "Instance" } }], "submodels": [{ "modelType": {"name": "Submodel"}, - "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"} + "id": {"idType": "IRI", "id": "http://acplt.org/test_aas"} }], "conceptDescriptions": [] }""" string_io = io.StringIO(data) with self.assertLogs(logging.getLogger(), level=logging.ERROR) as cm: read_aas_json_file(string_io, failsafe=True) - self.assertIn("duplicate identifier", cm.output[0]) + self.assertIn("duplicate identifier", cm.output[0]) # type: ignore string_io.seek(0) with self.assertRaisesRegex(KeyError, r"duplicate identifier"): read_aas_json_file(string_io, failsafe=False) @@ -192,7 +191,7 @@ def get_clean_store() -> model.DictObjectStore: { "submodels": [{ "modelType": {"name": "Submodel"}, - "identification": {"idType": "IRI", "id": "http://acplt.org/test_submodel"}, + "id": {"idType": "IRI", "id": "http://acplt.org/test_submodel"}, "idShort": "test456" }], "assetAdministrationShells": [], @@ -214,7 +213,7 @@ def get_clean_store() -> model.DictObjectStore: with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=False, ignore_existing=True) self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) + self.assertIn("already exists in the object store", log_ctx.output[0]) # type: ignore submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) self.assertEqual(submodel.id_short, "test123") @@ -247,7 +246,7 @@ def _construct_submodel(cls, dct, object_class=EnhancedSubmodel): [ { "modelType": {"name": "Submodel"}, - "identification": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} + "id": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} } ]""" parsed_data = json.loads(data, cls=EnhancedAASDecoder) @@ -261,7 +260,7 @@ def test_stripped_qualifiable(self) -> None: data = """ { "modelType": {"name": "Submodel"}, - "identification": {"idType": "IRI", "id": "http://acplt.org/test_stripped_submodel"}, + "id": {"idType": "IRI", "id": "http://acplt.org/test_stripped_submodel"}, "submodelElements": [{ "modelType": {"name": "Operation"}, "idShort": "test_operation", @@ -392,7 +391,7 @@ def test_stripped_asset_administration_shell(self) -> None: data = """ { "modelType": {"name": "AssetAdministrationShell"}, - "identification": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, + "id": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, "assetInformation": { "assetKind": "Instance", "globalAssetId": { diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index b9dac83..4c6b926 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -78,7 +78,7 @@ def test_missing_identification_attribute(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_asset + http://acplt.org/test_asset @@ -89,7 +89,7 @@ def test_invalid_identification_attribute_value(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_asset + http://acplt.org/test_asset @@ -100,7 +100,7 @@ def test_missing_asset_kind(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas @@ -112,7 +112,7 @@ def test_missing_asset_kind_text(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas @@ -125,7 +125,7 @@ def test_invalid_asset_kind_text(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas invalidKind @@ -138,7 +138,7 @@ def test_invalid_boolean(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -156,7 +156,7 @@ def test_no_modeling_kind(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -173,7 +173,7 @@ def test_reference_kind_mismatch(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas Instance @@ -196,7 +196,7 @@ def test_invalid_submodel_element(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -211,7 +211,7 @@ def test_empty_qualifier(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -227,7 +227,7 @@ def test_operation_variable_no_submodel_element(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -249,7 +249,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -282,7 +282,7 @@ def test_duplicate_identifier(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas NotSet Instance @@ -291,7 +291,7 @@ def test_duplicate_identifier(self) -> None: - http://acplt.org/test_aas + http://acplt.org/test_aas NotSet @@ -311,7 +311,7 @@ def get_clean_store() -> model.DictObjectStore: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel test456 @@ -348,7 +348,7 @@ def get_clean_store() -> model.DictObjectStore: def test_read_aas_xml_element(self) -> None: xml = """ - http://acplt.org/test_submodel + http://acplt.org/test_submodel """ @@ -362,7 +362,7 @@ class XmlDeserializationStrippedObjectsTest(unittest.TestCase): def test_stripped_qualifiable(self) -> None: xml = """ - http://acplt.org/test_stripped_submodel + http://acplt.org/test_stripped_submodel @@ -462,7 +462,7 @@ def test_stripped_submodel_element_collection(self) -> None: def test_stripped_asset_administration_shell(self) -> None: xml = """ - http://acplt.org/test_aas + http://acplt.org/test_aas Instance @@ -506,7 +506,7 @@ def construct_submodel(cls, element: etree.Element, object_class=EnhancedSubmode xml = """ - http://acplt.org/test_stripped_submodel + http://acplt.org/test_stripped_submodel """ From ffec555f4af28aae97f2c8b33287114842c84f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 18 Jul 2022 15:58:32 +0200 Subject: [PATCH 413/474] change type of Identifiable/id to str implement global and model references rename AASReference to ModelReference add Reference/referredSemanticId remove redundant definitions from XML schema (AAS and IEC61360) --- test/adapter/aasx/test_aasx.py | 7 +- .../adapter/json/test_json_deserialization.py | 68 +++++++------- test/adapter/json/test_json_serialization.py | 57 ++++++------ ...test_json_serialization_deserialization.py | 10 +-- test/adapter/xml/test_xml_deserialization.py | 89 +++++++------------ test/adapter/xml/test_xml_serialization.py | 24 ++--- 6 files changed, 121 insertions(+), 134 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 6b9709a..a3db000 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -25,9 +25,9 @@ class TestAASXUtils(unittest.TestCase): def test_name_friendlyfier(self) -> None: friendlyfier = aasx.NameFriendlyfier() - name1 = friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS-a", model.IdentifierType.IRI)) + name1 = friendlyfier.get_friendly_name("http://example.com/AAS-a") self.assertEqual("http___example_com_AAS_a", name1) - name2 = friendlyfier.get_friendly_name(model.Identifier("http://example.com/AAS+a", model.IdentifierType.IRI)) + name2 = friendlyfier.get_friendly_name("http://example.com/AAS+a") self.assertEqual("http___example_com_AAS_a_1", name2) def test_supplementary_file_container(self) -> None: @@ -85,8 +85,7 @@ def test_writing_reading_example_aas(self) -> None: with warnings.catch_warnings(record=True) as w: with aasx.AASXWriter(filename) as writer: # TODO test writing multiple AAS - writer.write_aas(model.Identifier(id_='https://acplt.org/Test_AssetAdministrationShell', - id_type=model.IdentifierType.IRI), + writer.write_aas('https://acplt.org/Test_AssetAdministrationShell', data, files, write_json=write_json) writer.write_core_properties(cp) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 4ed6c06..1aeb7aa 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -44,10 +44,7 @@ def test_file_format_wrong_list(self) -> None: "modelType": { "name": "AssetAdministrationShell" }, - "id": { - "id": "https://acplt.org/Test_Asset", - "idType": "IRI" - }, + "id": "https://acplt.org/Test_Asset", "assetInformation": { "assetKind": "Instance" } @@ -90,7 +87,7 @@ def test_broken_submodel(self) -> None: }, { "modelType": {"name": "Submodel"}, - "id": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} + "id": "https://acplt.org/Test_Submodel" } ]""" # In strict mode, we should catch an exception @@ -107,21 +104,18 @@ def test_broken_submodel(self) -> None: self.assertIsInstance(parsed_data[0], dict) self.assertIsInstance(parsed_data[1], dict) self.assertIsInstance(parsed_data[2], model.Submodel) - self.assertEqual("https://acplt.org/Test_Submodel", parsed_data[2].id.id) + self.assertEqual("https://acplt.org/Test_Submodel", parsed_data[2].id) def test_wrong_submodel_element_type(self) -> None: data = """ [ { "modelType": {"name": "Submodel"}, - "id": { - "id": "http://acplt.org/Submodels/Assets/TestAsset/Identification", - "idType": "IRI" - }, + "id": "http://acplt.org/Submodels/Assets/TestAsset/Identification", "submodelElements": [ { "modelType": {"name": "Submodel"}, - "id": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} + "id": "https://acplt.org/Test_Submodel" }, { "modelType": "Broken modelType" @@ -159,14 +153,14 @@ def test_duplicate_identifier(self) -> None: { "assetAdministrationShells": [{ "modelType": {"name": "AssetAdministrationShell"}, - "id": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, + "id": "http://acplt.org/test_aas", "assetInformation": { "assetKind": "Instance" } }], "submodels": [{ "modelType": {"name": "Submodel"}, - "id": {"idType": "IRI", "id": "http://acplt.org/test_aas"} + "id": "http://acplt.org/test_aas" }], "conceptDescriptions": [] }""" @@ -179,7 +173,7 @@ def test_duplicate_identifier(self) -> None: read_aas_json_file(string_io, failsafe=False) def test_duplicate_identifier_object_store(self) -> None: - sm_id = model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI) + sm_id = "http://acplt.org/test_submodel" def get_clean_store() -> model.DictObjectStore: store: model.DictObjectStore = model.DictObjectStore() @@ -191,7 +185,7 @@ def get_clean_store() -> model.DictObjectStore: { "submodels": [{ "modelType": {"name": "Submodel"}, - "id": {"idType": "IRI", "id": "http://acplt.org/test_submodel"}, + "id": "http://acplt.org/test_submodel", "idShort": "test456" }], "assetAdministrationShells": [], @@ -246,7 +240,7 @@ def _construct_submodel(cls, dct, object_class=EnhancedSubmodel): [ { "modelType": {"name": "Submodel"}, - "id": {"id": "https://acplt.org/Test_Submodel", "idType": "IRI"} + "id": "https://acplt.org/Test_Submodel" } ]""" parsed_data = json.loads(data, cls=EnhancedAASDecoder) @@ -260,7 +254,7 @@ def test_stripped_qualifiable(self) -> None: data = """ { "modelType": {"name": "Submodel"}, - "id": {"idType": "IRI", "id": "http://acplt.org/test_stripped_submodel"}, + "id": "http://acplt.org/test_stripped_submodel", "submodelElements": [{ "modelType": {"name": "Operation"}, "idShort": "test_operation", @@ -299,18 +293,30 @@ def test_stripped_annotated_relationship_element(self) -> None: "idShort": "test_annotated_relationship_element", "category": "PARAMETER", "first": { - "keys": [{ - "idType": "IdShort", - "type": "AnnotatedRelationshipElement", - "value": "test_ref" - }] + "type": "ModelReference", + "keys": [ + { + "type": "Submodel", + "value": "http://acplt.org/Test_Submodel" + }, + { + "type": "AnnotatedRelationshipElement", + "value": "test_ref" + } + ] }, "second": { - "keys": [{ - "idType": "IdShort", - "type": "AnnotatedRelationshipElement", - "value": "test_ref" - }] + "type": "ModelReference", + "keys": [ + { + "type": "Submodel", + "value": "http://acplt.org/Test_Submodel" + }, + { + "type": "AnnotatedRelationshipElement", + "value": "test_ref" + } + ] }, "annotation": [{ "modelType": {"name": "MultiLanguageProperty"}, @@ -338,8 +344,8 @@ def test_stripped_entity(self) -> None: "idShort": "test_entity", "entityType": "SelfManagedEntity", "globalAssetId": { + "type": "GlobalReference", "keys": [{ - "idType": "IRI", "type": "GlobalReference", "value": "test_asset" }] @@ -391,20 +397,20 @@ def test_stripped_asset_administration_shell(self) -> None: data = """ { "modelType": {"name": "AssetAdministrationShell"}, - "id": {"idType": "IRI", "id": "http://acplt.org/test_aas"}, + "id": "http://acplt.org/test_aas", "assetInformation": { "assetKind": "Instance", "globalAssetId": { + "type": "GlobalReference", "keys": [{ - "idType": "IRI", "type": "GlobalReference", "value": "test_asset" }] } }, "submodels": [{ + "type": "ModelReference", "keys": [{ - "idType": "IRI", "type": "Submodel", "value": "http://acplt.org/test_submodel" }] diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 132ee92..e2b8288 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,22 +1,21 @@ -# Copyright (c) 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. +# 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: EPL-2.0 OR Apache-2.0 +# SPDX-License-Identifier: MIT import io import unittest import json -from aas import model -from aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE +from basyx.aas import model +from basyx.aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE from jsonschema import validate # type: ignore from typing import Set, Union -from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, create_example, example_concept_description +from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ + example_aas_mandatory_attributes, example_submodel_template, create_example class JsonSerializationTest(unittest.TestCase): @@ -26,13 +25,13 @@ def test_serialize_object(self) -> None: json_data = json.dumps(test_object, cls=AASToJsonEncoder) def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) - asset_reference = model.Reference(asset_key) - aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) + asset_reference = model.GlobalReference(asset_key) + aas_identifier = "AAS1" + submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() assert(submodel_identifier is not None) - submodel_reference = model.AASReference(submodel_key, model.Submodel) + submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), aas_identifier, submodel={submodel_reference}) @@ -49,16 +48,18 @@ def test_random_object_serialization(self) -> None: class JsonSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) - asset_reference = model.Reference(asset_key) - aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) + asset_reference = model.GlobalReference(asset_key) + aas_identifier = "AAS1" + submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() assert(submodel_identifier is not None) - submodel_reference = model.AASReference(submodel_key, model.Submodel) + submodel_reference = model.ModelReference(submodel_key, model.Submodel) # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) + submodel = model.Submodel(submodel_identifier, + semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, + "http://acplt.org/TestSemanticId"),))) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), aas_identifier, submodel={submodel_reference}) @@ -182,7 +183,7 @@ def test_stripped_qualifiable(self) -> None: qualifier2 = model.Qualifier("test_qualifier2", str) operation = model.Operation("test_operation", qualifier={qualifier}) submodel = model.Submodel( - model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI), + "http://acplt.org/test_submodel", submodel_element=[operation], qualifier={qualifier2} ) @@ -192,8 +193,8 @@ def test_stripped_qualifiable(self) -> None: def test_stripped_annotated_relationship_element(self) -> None: mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") - ref = model.AASReference( - (model.Key(model.KeyElements.SUBMODEL, "http://acplt.org/test_ref", model.KeyType.IRI),), + ref = model.ModelReference( + (model.Key(model.KeyTypes.SUBMODEL, "http://acplt.org/test_ref"),), model.Submodel ) are = model.AnnotatedRelationshipElement( @@ -218,16 +219,16 @@ def test_stripped_submodel_element_collection(self) -> None: self._checkNormalAndStripped("value", sec) def test_stripped_asset_administration_shell(self) -> None: - asset_ref = model.Reference( - (model.Key(model.KeyElements.GLOBAL_REFERENCE, "http://acplt.org/test_ref", model.KeyType.IRI),), + asset_ref = model.GlobalReference( + (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/test_ref"),), ) - submodel_ref = model.AASReference( - (model.Key(model.KeyElements.SUBMODEL, "http://acplt.org/test_ref", model.KeyType.IRI),), + submodel_ref = model.ModelReference( + (model.Key(model.KeyTypes.SUBMODEL, "http://acplt.org/test_ref"),), model.Submodel ) aas = model.AssetAdministrationShell( model.AssetInformation(global_asset_id=asset_ref), - model.Identifier("http://acplt.org/test_aas", model.IdentifierType.IRI), + "http://acplt.org/test_aas", submodel={submodel_ref} ) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 7a8ca10..4218b77 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -23,13 +23,13 @@ class JsonSerializationDeserializationTest(unittest.TestCase): def test_random_object_serialization_deserialization(self) -> None: - asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) - asset_reference = model.Reference(asset_key) - aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) + asset_reference = model.GlobalReference(asset_key) + aas_identifier = "AAS1" + submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() assert(submodel_identifier is not None) - submodel_reference = model.AASReference(submodel_key, model.Submodel) + submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), aas_identifier, submodel_={submodel_reference}) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 4c6b926..61f3a39 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -50,7 +50,7 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], read_aas_xml_file(bytes_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: - self.assertIn(s, log_ctx.output[0]) + self.assertIn(s, log_ctx.output[0]) # type: ignore self.assertIn(s, str(cause)) def test_malformed_xml(self) -> None: @@ -74,33 +74,11 @@ def test_invalid_element_in_list(self) -> None: """) self._assertInExceptionAndLog(xml, ["aas:invalidElement", "aas:submodels"], KeyError, logging.WARNING) - def test_missing_identification_attribute(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_asset - - - - """) - self._assertInExceptionAndLog(xml, "idType", KeyError, logging.ERROR) - - def test_invalid_identification_attribute_value(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_asset - - - - """) - self._assertInExceptionAndLog(xml, ["idType", "invalid"], ValueError, logging.ERROR) - def test_missing_asset_kind(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas @@ -112,7 +90,7 @@ def test_missing_asset_kind_text(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas @@ -125,7 +103,7 @@ def test_invalid_asset_kind_text(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas invalidKind @@ -138,7 +116,7 @@ def test_invalid_boolean(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -156,7 +134,7 @@ def test_no_modeling_kind(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -173,13 +151,13 @@ def test_reference_kind_mismatch(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas Instance - + - http://acplt.org/test_ref + http://acplt.org/test_ref @@ -187,8 +165,8 @@ def test_reference_kind_mismatch(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - for s in ("GLOBAL_REFERENCE", "IRI=http://acplt.org/test_ref", "AssetAdministrationShell"): - self.assertIn(s, context.output[0]) + for s in ("SUBMODEL", "http://acplt.org/test_ref", "AssetAdministrationShell"): + self.assertIn(s, context.output[0]) # type: ignore def test_invalid_submodel_element(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted @@ -196,7 +174,7 @@ def test_invalid_submodel_element(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -211,7 +189,7 @@ def test_empty_qualifier(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -227,7 +205,7 @@ def test_operation_variable_no_submodel_element(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -241,7 +219,7 @@ def test_operation_variable_no_submodel_element(self) -> None: """) - self._assertInExceptionAndLog(xml, ["aas:value", "has no submodel element"], KeyError, logging.ERROR) + self._assertInExceptionAndLog(xml, "aas:value", KeyError, logging.ERROR) def test_operation_variable_too_many_submodel_elements(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted @@ -249,7 +227,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel @@ -275,14 +253,13 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - self.assertIn("aas:value", context.output[0]) - self.assertIn("more than one submodel element", context.output[0]) + self.assertIn("aas:value", context.output[0]) # type: ignore def test_duplicate_identifier(self) -> None: xml = _xml_wrap(""" - http://acplt.org/test_aas + http://acplt.org/test_aas NotSet Instance @@ -291,7 +268,7 @@ def test_duplicate_identifier(self) -> None: - http://acplt.org/test_aas + http://acplt.org/test_aas NotSet @@ -300,7 +277,7 @@ def test_duplicate_identifier(self) -> None: self._assertInExceptionAndLog(xml, "duplicate identifier", KeyError, logging.ERROR) def test_duplicate_identifier_object_store(self) -> None: - sm_id = model.Identifier("http://acplt.org/test_submodel", model.IdentifierType.IRI) + sm_id = "http://acplt.org/test_submodel" def get_clean_store() -> model.DictObjectStore: store: model.DictObjectStore = model.DictObjectStore() @@ -311,7 +288,7 @@ def get_clean_store() -> model.DictObjectStore: xml = _xml_wrap(""" - http://acplt.org/test_submodel + http://acplt.org/test_submodel test456 @@ -330,7 +307,7 @@ def get_clean_store() -> model.DictObjectStore: with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) + self.assertIn("already exists in the object store", log_ctx.output[0]) # type: ignore submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) self.assertEqual(submodel.id_short, "test123") @@ -348,7 +325,7 @@ def get_clean_store() -> model.DictObjectStore: def test_read_aas_xml_element(self) -> None: xml = """ - http://acplt.org/test_submodel + http://acplt.org/test_submodel """ @@ -362,7 +339,7 @@ class XmlDeserializationStrippedObjectsTest(unittest.TestCase): def test_stripped_qualifiable(self) -> None: xml = """ - http://acplt.org/test_stripped_submodel + http://acplt.org/test_stripped_submodel @@ -405,14 +382,16 @@ def test_stripped_annotated_relationship_element(self) -> None: xml = """ test_annotated_relationship_element - + - test_ref + http://acplt.org/Test_Submodel + test_ref - + - test_ref + http://acplt.org/Test_Submodel + test_ref @@ -462,14 +441,14 @@ def test_stripped_submodel_element_collection(self) -> None: def test_stripped_asset_administration_shell(self) -> None: xml = """ - http://acplt.org/test_aas + http://acplt.org/test_aas Instance - + - http://acplt.org/test_ref + http://acplt.org/test_ref @@ -506,7 +485,7 @@ def construct_submodel(cls, element: etree.Element, object_class=EnhancedSubmode xml = """ - http://acplt.org/test_stripped_submodel + http://acplt.org/test_stripped_submodel """ diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 232cdef..d194585 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -30,13 +30,13 @@ def test_serialize_object(self) -> None: # todo: is this a correct way to test it? def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) - asset_reference = model.Reference(asset_key) - aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) + asset_reference = model.GlobalReference(asset_key) + aas_identifier = "AAS1" + submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() assert (submodel_identifier is not None) - submodel_reference = model.AASReference(submodel_key, model.Submodel) + submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), aas_identifier, submodel_={submodel_reference}) @@ -51,14 +51,16 @@ def test_random_object_serialization(self) -> None: class XMLSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyElements.GLOBAL_REFERENCE, "test", model.KeyType.CUSTOM),) - asset_reference = model.Reference(asset_key) - aas_identifier = model.Identifier("AAS1", model.IdentifierType.CUSTOM) - submodel_key = (model.Key(model.KeyElements.SUBMODEL, "SM1", model.KeyType.CUSTOM),) + asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) + asset_reference = model.GlobalReference(asset_key) + aas_identifier = "AAS1" + submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() assert(submodel_identifier is not None) - submodel_reference = model.AASReference(submodel_key, model.Submodel) - submodel = model.Submodel(submodel_identifier, semantic_id=model.Reference((),)) + submodel_reference = model.ModelReference(submodel_key, model.Submodel) + submodel = model.Submodel(submodel_identifier, + semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, + "http://acplt.org/TestSemanticId"),))) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), aas_identifier, submodel_={submodel_reference}) From 36b9c43f3acf8671596edb8ba47955375b88e644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 16 Aug 2022 18:28:52 +0200 Subject: [PATCH 414/474] test.adapter.aasx: add whitespace after assert --- test/adapter/aasx/test_aasx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index a3db000..61b3230 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -105,9 +105,9 @@ def test_writing_reading_example_aas(self) -> None: example_aas.check_full_example(checker, new_data) # Check core properties - assert(isinstance(cp.created, datetime.datetime)) # to make mypy happy + assert isinstance(cp.created, datetime.datetime) # to make mypy happy self.assertIsInstance(new_cp.created, datetime.datetime) - assert(isinstance(new_cp.created, datetime.datetime)) # to make mypy happy + assert isinstance(new_cp.created, datetime.datetime) # to make mypy happy self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") self.assertIsNone(new_cp.lastModifiedBy) From 8e6eb97d2baa34679578ee7797bac73e8da895d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 17 Oct 2022 22:55:10 +0200 Subject: [PATCH 415/474] model: update SubmodelElementCollection In V3.0RC02 the attributes `allowDuplicates` and `ordered` were removed from `SubmodelElementCollection`. Additionally the `semanticId` of contained elements is no longer unique. Because of that, the extra classes `SubmodelElementCollectionOrdered`, `SubmodelElementCollectionUnordered`, `SubmodelElementCollectionOrderedUniqueSemanticId` and `SubmodelElementCollectionUnorderedUniqueSemanticId` aren't needed anymore. Also ignore two tests which will be modified and re-enabled later. --- test/adapter/json/test_json_deserialization.py | 10 ++++------ test/adapter/json/test_json_serialization.py | 2 +- test/adapter/xml/test_xml_deserialization.py | 1 + 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 1aeb7aa..0bff2b0 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -373,8 +373,6 @@ def test_stripped_submodel_element_collection(self) -> None: { "modelType": {"name": "SubmodelElementCollection"}, "idShort": "test_submodel_element_collection", - "ordered": false, - "allowDuplicates": true, "value": [{ "modelType": {"name": "MultiLanguageProperty"}, "idShort": "test_multi_language_property" @@ -383,14 +381,14 @@ def test_stripped_submodel_element_collection(self) -> None: # check if JSON with value can be parsed successfully sec = json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIsInstance(sec, model.SubmodelElementCollectionUnordered) - assert isinstance(sec, model.SubmodelElementCollectionUnordered) + self.assertIsInstance(sec, model.SubmodelElementCollection) + assert isinstance(sec, model.SubmodelElementCollection) self.assertEqual(len(sec.value), 1) # check if value is ignored in stripped mode sec = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) - self.assertIsInstance(sec, model.SubmodelElementCollectionUnordered) - assert isinstance(sec, model.SubmodelElementCollectionUnordered) + self.assertIsInstance(sec, model.SubmodelElementCollection) + assert isinstance(sec, model.SubmodelElementCollection) self.assertEqual(len(sec.value), 0) def test_stripped_asset_administration_shell(self) -> None: diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index e2b8288..77eecbd 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -214,7 +214,7 @@ def test_stripped_entity(self) -> None: def test_stripped_submodel_element_collection(self) -> None: mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") - sec = model.SubmodelElementCollectionOrdered("test_submodel_element_collection", value=[mlp]) + sec = model.SubmodelElementCollection("test_submodel_element_collection", value=[mlp]) self._checkNormalAndStripped("value", sec) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 61f3a39..3386174 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -112,6 +112,7 @@ def test_invalid_asset_kind_text(self) -> None: """) self._assertInExceptionAndLog(xml, ["aas:assetKind", "invalidKind"], ValueError, logging.ERROR) + @unittest.skip # type: ignore def test_invalid_boolean(self) -> None: xml = _xml_wrap(""" From ec32c850a39c2f24dfb39e703528b5c3a6d00f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 21 Oct 2022 01:11:04 +0200 Subject: [PATCH 416/474] add SubmodelElementList In turn re-enable the unittests that were disabled in dfba9ee1d0222d4f36b703be8a64b23807b37f2e. --- test/adapter/json/test_json_deserialization.py | 4 ++-- test/adapter/xml/test_xml_deserialization.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 0bff2b0..7f94089 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -261,13 +261,13 @@ def test_stripped_qualifiable(self) -> None: "qualifiers": [{ "modelType": {"name": "Qualifier"}, "type": "test_qualifier", - "valueType": "string" + "valueType": "xs:string" }] }], "qualifiers": [{ "modelType": {"name": "Qualifier"}, "type": "test_qualifier", - "valueType": "string" + "valueType": "xs:string" }] }""" diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 3386174..5af3c3f 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -112,7 +112,6 @@ def test_invalid_asset_kind_text(self) -> None: """) self._assertInExceptionAndLog(xml, ["aas:assetKind", "invalidKind"], ValueError, logging.ERROR) - @unittest.skip # type: ignore def test_invalid_boolean(self) -> None: xml = _xml_wrap(""" @@ -120,10 +119,11 @@ def test_invalid_boolean(self) -> None: http://acplt.org/test_submodel - - False + + False collection - + Capability + @@ -348,7 +348,7 @@ def test_stripped_qualifiable(self) -> None: test_qualifier - string + xs:string @@ -357,7 +357,7 @@ def test_stripped_qualifiable(self) -> None: test_qualifier - string + xs:string From 6bbacd4fd32ece5634a688197c37e356b8384831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 20 Mar 2023 15:51:30 +0100 Subject: [PATCH 417/474] begin integration of new XML and JSON schemata --- test/adapter/json/test_json_serialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 77eecbd..ec2ddf3 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -21,7 +21,7 @@ class JsonSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER", - description={"en-us": "Germany", "de": "Deutschland"}) + description=model.LangStringSet({"en-US": "Germany", "de": "Deutschland"})) json_data = json.dumps(test_object, cls=AASToJsonEncoder) def test_random_object_serialization(self) -> None: diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index d194585..11b6027 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -25,7 +25,7 @@ def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER", - description={"en-us": "Germany", "de": "Deutschland"}) + description=model.LangStringSet({"en-US": "Germany", "de": "Deutschland"})) xml_data = xml_serialization.property_to_xml(test_object, xml_serialization.NS_AAS+"test_object") # todo: is this a correct way to test it? @@ -84,6 +84,7 @@ def test_full_example_serialization(self) -> None: data = example_aas.create_full_example() file = io.BytesIO() write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file="/home/jkhsjdhjs/Desktop/aas.xml", data=data, pretty_print=True) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) From 94acf12aee4929e6519f6f0d1a7e00f78ff5cb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 1 Apr 2023 13:59:42 +0200 Subject: [PATCH 418/474] fix some mypy errors --- test/adapter/xml/test_xml_serialization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 11b6027..b822bca 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -84,7 +84,6 @@ def test_full_example_serialization(self) -> None: data = example_aas.create_full_example() file = io.BytesIO() write_aas_xml_file(file=file, data=data) - write_aas_xml_file(file="/home/jkhsjdhjs/Desktop/aas.xml", data=data, pretty_print=True) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) From 5ffc4ad51f29a8851e85304b3356906b09186467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 7 Apr 2023 16:01:14 +0200 Subject: [PATCH 419/474] update implementation and adapters for new DataSpecification classes and schema Changes: - fix bugs in XML/JSON schema by copying some sections from the V3.0 schema - remove `ABAC.xsd` and `IEC61360.xsd` (not needed anymore, now unified in a single schema) - adapter.json: fix `HasDataSpecification` (de-)serialization - adapter.xml: add `HasDataSpecification` (de-)serialization - move XML namespace definitions to `adapter._generic` - remove `ConceptDescriptionIEC61360` - remove `example_concept_description.py` (as it only contained `ConceptDescriptionIEC61360`) - fix some minor issues in `AASDataChecker` (type hints, error messages, etc.) - add `HasDataSpecification` support to `AASDataChecker` - add `EmbeddedDataSpecifications` to example data - add `__repr__()` to `EmbeddedDataSpecification`, `DataSpecificationIEC61360` and `DataSpecificationPhysicalUnit` - remove `value_id` attribute from `DataSpecificationIEC61360` - rename attribute accesses of `DataSpecificationPhysicalUnit` that were missed in 6cc19c67ec891af29a7d58a331b7256208a570a1 - update tests and compliance tool example files in accordance to the changes --- .../adapter/json/test_json_deserialization.py | 63 +++---- test/adapter/json/test_json_serialization.py | 10 +- ...test_json_serialization_deserialization.py | 39 ++-- test/adapter/xml/test_xml_deserialization.py | 177 ++++++------------ test/adapter/xml/test_xml_serialization.py | 31 ++- .../test_xml_serialization_deserialization.py | 9 +- 6 files changed, 109 insertions(+), 220 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 7f94089..a3f92e8 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -21,29 +21,14 @@ class JsonDeserializationTest(unittest.TestCase): - def test_file_format_missing_list(self) -> None: - data = """ - { - "assetAdministrationShells": [], - "conceptDescriptions": [] - }""" - with self.assertRaisesRegex(KeyError, r"submodels"): - read_aas_json_file(io.StringIO(data), failsafe=False) - with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) # type: ignore - def test_file_format_wrong_list(self) -> None: data = """ { "assetAdministrationShells": [], - "assets": [], "conceptDescriptions": [], "submodels": [ { - "modelType": { - "name": "AssetAdministrationShell" - }, + "modelType": "AssetAdministrationShell", "id": "https://acplt.org/Test_Asset", "assetInformation": { "assetKind": "Instance" @@ -79,14 +64,14 @@ def test_broken_submodel(self) -> None: data = """ [ { - "modelType": {"name": "Submodel"} + "modelType": "Submodel" }, { - "modelType": {"name": "Submodel"}, + "modelType": "Submodel", "id": ["https://acplt.org/Test_Submodel_broken_id", "IRI"] }, { - "modelType": {"name": "Submodel"}, + "modelType": "Submodel", "id": "https://acplt.org/Test_Submodel" } ]""" @@ -110,18 +95,20 @@ def test_wrong_submodel_element_type(self) -> None: data = """ [ { - "modelType": {"name": "Submodel"}, + "modelType": "Submodel", "id": "http://acplt.org/Submodels/Assets/TestAsset/Identification", "submodelElements": [ { - "modelType": {"name": "Submodel"}, + "modelType": "Submodel", "id": "https://acplt.org/Test_Submodel" }, { - "modelType": "Broken modelType" + "modelType": { + "name": "Broken modelType" + } }, { - "modelType": {"name": "Capability"}, + "modelType": "Capability", "idShort": "TestCapability" } ] @@ -152,14 +139,14 @@ def test_duplicate_identifier(self) -> None: data = """ { "assetAdministrationShells": [{ - "modelType": {"name": "AssetAdministrationShell"}, + "modelType": "AssetAdministrationShell", "id": "http://acplt.org/test_aas", "assetInformation": { "assetKind": "Instance" } }], "submodels": [{ - "modelType": {"name": "Submodel"}, + "modelType": "Submodel", "id": "http://acplt.org/test_aas" }], "conceptDescriptions": [] @@ -184,7 +171,7 @@ def get_clean_store() -> model.DictObjectStore: data = """ { "submodels": [{ - "modelType": {"name": "Submodel"}, + "modelType": "Submodel", "id": "http://acplt.org/test_submodel", "idShort": "test456" }], @@ -239,7 +226,7 @@ def _construct_submodel(cls, dct, object_class=EnhancedSubmodel): data = """ [ { - "modelType": {"name": "Submodel"}, + "modelType": "Submodel", "id": "https://acplt.org/Test_Submodel" } ]""" @@ -253,19 +240,17 @@ class JsonDeserializationStrippedObjectsTest(unittest.TestCase): def test_stripped_qualifiable(self) -> None: data = """ { - "modelType": {"name": "Submodel"}, + "modelType": "Submodel", "id": "http://acplt.org/test_stripped_submodel", "submodelElements": [{ - "modelType": {"name": "Operation"}, + "modelType": "Operation", "idShort": "test_operation", "qualifiers": [{ - "modelType": {"name": "Qualifier"}, "type": "test_qualifier", "valueType": "xs:string" }] }], "qualifiers": [{ - "modelType": {"name": "Qualifier"}, "type": "test_qualifier", "valueType": "xs:string" }] @@ -289,7 +274,7 @@ def test_stripped_qualifiable(self) -> None: def test_stripped_annotated_relationship_element(self) -> None: data = """ { - "modelType": {"name": "AnnotatedRelationshipElement"}, + "modelType": "AnnotatedRelationshipElement", "idShort": "test_annotated_relationship_element", "category": "PARAMETER", "first": { @@ -318,8 +303,8 @@ def test_stripped_annotated_relationship_element(self) -> None: } ] }, - "annotation": [{ - "modelType": {"name": "MultiLanguageProperty"}, + "annotations": [{ + "modelType": "MultiLanguageProperty", "idShort": "test_multi_language_property", "category": "CONSTANT" }] @@ -340,7 +325,7 @@ def test_stripped_annotated_relationship_element(self) -> None: def test_stripped_entity(self) -> None: data = """ { - "modelType": {"name": "Entity"}, + "modelType": "Entity", "idShort": "test_entity", "entityType": "SelfManagedEntity", "globalAssetId": { @@ -351,7 +336,7 @@ def test_stripped_entity(self) -> None: }] }, "statements": [{ - "modelType": {"name": "MultiLanguageProperty"}, + "modelType": "MultiLanguageProperty", "idShort": "test_multi_language_property" }] }""" @@ -371,10 +356,10 @@ def test_stripped_entity(self) -> None: def test_stripped_submodel_element_collection(self) -> None: data = """ { - "modelType": {"name": "SubmodelElementCollection"}, + "modelType": "SubmodelElementCollection", "idShort": "test_submodel_element_collection", "value": [{ - "modelType": {"name": "MultiLanguageProperty"}, + "modelType": "MultiLanguageProperty", "idShort": "test_multi_language_property" }] }""" @@ -394,7 +379,7 @@ def test_stripped_submodel_element_collection(self) -> None: def test_stripped_asset_administration_shell(self) -> None: data = """ { - "modelType": {"name": "AssetAdministrationShell"}, + "modelType": "AssetAdministrationShell", "id": "http://acplt.org/test_aas", "assetInformation": { "assetKind": "Instance", diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index ec2ddf3..efc9911 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -14,7 +14,7 @@ from jsonschema import validate # type: ignore from typing import Set, Union -from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ +from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ example_aas_mandatory_attributes, example_submodel_template, create_example @@ -66,9 +66,7 @@ def test_random_object_serialization(self) -> None: # serialize object to json json_data = json.dumps({ 'assetAdministrationShells': [test_aas], - 'submodels': [submodel], - 'assets': [], - 'conceptDescriptions': [], + 'submodels': [submodel] }, cls=AASToJsonEncoder) json_data_new = json.loads(json_data) @@ -138,7 +136,7 @@ def test_missing_serialization(self) -> None: def test_concept_description_serialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_concept_description.create_iec61360_concept_description()) + data.add(example_aas.create_example_concept_description()) file = io.StringIO() write_aas_json_file(file=file, data=data) @@ -204,7 +202,7 @@ def test_stripped_annotated_relationship_element(self) -> None: annotation=[mlp] ) - self._checkNormalAndStripped("annotation", are) + self._checkNormalAndStripped("annotations", are) def test_stripped_entity(self) -> None: mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 4218b77..d83b664 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -1,24 +1,20 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: MIT import io import json import unittest -from aas import model -from aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file +from basyx.aas import model +from basyx.aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file -from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, example_concept_description, create_example -from aas.examples.data._helper import AASDataChecker +from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ + example_aas_mandatory_attributes, example_submodel_template, create_example +from basyx.aas.examples.data._helper import AASDataChecker class JsonSerializationDeserializationTest(unittest.TestCase): @@ -28,11 +24,11 @@ def test_random_object_serialization_deserialization(self) -> None: aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() - assert(submodel_identifier is not None) + assert submodel_identifier is not None submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), - aas_identifier, submodel_={submodel_reference}) + aas_identifier, submodel={submodel_reference}) # serialize object to json json_data = json.dumps({ @@ -97,19 +93,6 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: class JsonSerializationDeserializationTest5(unittest.TestCase): - def test_example_iec61360_concept_description_serialization_deserialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_concept_description.create_iec61360_concept_description()) - file = io.StringIO() - write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module - file.seek(0) - json_object_store = read_aas_json_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - example_concept_description.check_full_example(checker, json_object_store) - - -class JsonSerializationDeserializationTest6(unittest.TestCase): def test_example_all_examples_serialization_deserialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = create_example() file = io.StringIO() diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 5af3c3f..995a525 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -12,6 +12,7 @@ from basyx.aas import model from basyx.aas.adapter.xml import StrictAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, \ read_aas_xml_file_into, read_aas_xml_element +from basyx.aas.adapter._generic import XML_NS_MAP from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -19,7 +20,7 @@ def _xml_wrap(xml: str) -> str: return \ """""" \ - """ """ \ + f""" """ \ + xml + """""" @@ -118,13 +119,11 @@ def test_invalid_boolean(self) -> None: http://acplt.org/test_submodel - - - False - collection - Capability - - + + False + collection + Capability + @@ -136,7 +135,6 @@ def test_no_modeling_kind(self) -> None: http://acplt.org/test_submodel - """) @@ -156,9 +154,13 @@ def test_reference_kind_mismatch(self) -> None: Instance - + + ModelReference - http://acplt.org/test_ref + + Submodel + http://acplt.org/test_ref + @@ -170,16 +172,12 @@ def test_reference_kind_mismatch(self) -> None: self.assertIn(s, context.output[0]) # type: ignore def test_invalid_submodel_element(self) -> None: - # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 xml = _xml_wrap(""" http://acplt.org/test_submodel - - - + @@ -191,7 +189,6 @@ def test_empty_qualifier(self) -> None: http://acplt.org/test_submodel - @@ -208,14 +205,14 @@ def test_operation_variable_no_submodel_element(self) -> None: http://acplt.org/test_submodel - - - test_operation - + + test_operation + + - - - + + + @@ -230,10 +227,10 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: http://acplt.org/test_submodel - - - test_operation - + + test_operation + + Template @@ -245,9 +242,9 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: application/problem+xml - - - + + + @@ -271,7 +268,6 @@ def test_duplicate_identifier(self) -> None: http://acplt.org/test_aas NotSet - """) @@ -291,7 +287,6 @@ def get_clean_store() -> model.DictObjectStore: http://acplt.org/test_submodel test456 - """) @@ -324,10 +319,9 @@ def get_clean_store() -> model.DictObjectStore: self.assertEqual(submodel.id_short, "test123") def test_read_aas_xml_element(self) -> None: - xml = """ - + xml = f""" + http://acplt.org/test_submodel - """ bytes_io = io.BytesIO(xml.encode("utf-8")) @@ -338,21 +332,19 @@ def test_read_aas_xml_element(self) -> None: class XmlDeserializationStrippedObjectsTest(unittest.TestCase): def test_stripped_qualifiable(self) -> None: - xml = """ - + xml = f""" + http://acplt.org/test_stripped_submodel - - - test_operation - - - test_qualifier - xs:string - - - - + + test_operation + + + test_qualifier + xs:string + + + @@ -379,91 +371,35 @@ def test_stripped_qualifiable(self) -> None: self.assertEqual(len(submodel.qualifier), 0) self.assertEqual(len(submodel.submodel_element), 0) - def test_stripped_annotated_relationship_element(self) -> None: - xml = """ - - test_annotated_relationship_element - - - http://acplt.org/Test_Submodel - test_ref - - - - - http://acplt.org/Test_Submodel - test_ref - - - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - # XML schema requires annotations to be present, so parsing should fail - with self.assertRaises(KeyError): - read_aas_xml_element(bytes_io, XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT, failsafe=False) - - # check if it can be parsed in stripped mode - read_aas_xml_element(bytes_io, XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT, failsafe=False, stripped=True) - - def test_stripped_entity(self) -> None: - xml = """ - - test_entity - CoManagedEntity - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - # XML schema requires statements to be present, so parsing should fail - with self.assertRaises(KeyError): - read_aas_xml_element(bytes_io, XMLConstructables.ENTITY, failsafe=False) - - # check if it can be parsed in stripped mode - read_aas_xml_element(bytes_io, XMLConstructables.ENTITY, failsafe=False, stripped=True) - - def test_stripped_submodel_element_collection(self) -> None: - xml = """ - - test_collection - true - false - - """ - bytes_io = io.BytesIO(xml.encode("utf-8")) - - # XML schema requires value to be present, so parsing should fail - with self.assertRaises(KeyError): - read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False) - - # check if it can be parsed in stripped mode - read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL_ELEMENT_COLLECTION, failsafe=False, stripped=True) - def test_stripped_asset_administration_shell(self) -> None: - xml = """ - + xml = f""" + http://acplt.org/test_aas Instance - - + + + ModelReference - http://acplt.org/test_ref + + Submodel + http://acplt.org/test_ref + - - + + """ bytes_io = io.BytesIO(xml.encode("utf-8")) - # check if XML with submodelRef can be parsed successfully + # check if XML with submodels can be parsed successfully aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) self.assertEqual(len(aas.submodel), 1) - # check if submodelRef is ignored in stripped mode + # check if submodels are ignored in stripped mode aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, stripped=True) self.assertIsInstance(aas, model.AssetAdministrationShell) @@ -484,10 +420,9 @@ def construct_submodel(cls, element: etree.Element, object_class=EnhancedSubmode -> model.Submodel: return super().construct_submodel(element, object_class=object_class, **kwargs) - xml = """ - + xml = f""" + http://acplt.org/test_stripped_submodel - """ bytes_io = io.BytesIO(xml.encode("utf-8")) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index b822bca..4b5f9ee 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -1,23 +1,19 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2020 the Eclipse BaSyx Authors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: MIT import io import unittest from lxml import etree # type: ignore -from aas import model -from aas.adapter.xml import write_aas_xml_file, xml_serialization, XML_SCHEMA_FILE +from basyx.aas import model +from basyx.aas.adapter.xml import write_aas_xml_file, xml_serialization, XML_SCHEMA_FILE -from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ - example_aas_mandatory_attributes, example_aas, example_concept_description +from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ + example_submodel_template, example_aas_mandatory_attributes class XMLSerializationTest(unittest.TestCase): @@ -39,7 +35,7 @@ def test_random_object_serialization(self) -> None: submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), - aas_identifier, submodel_={submodel_reference}) + aas_identifier, submodel={submodel_reference}) test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() test_data.add(test_aas) @@ -56,14 +52,13 @@ def test_random_object_serialization(self) -> None: aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() - assert(submodel_identifier is not None) + assert submodel_identifier is not None submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier, semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/TestSemanticId"),))) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), - aas_identifier, submodel_={submodel_reference}) - + aas_identifier, submodel={submodel_reference}) # serialize object to xml test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() test_data.add(test_aas) @@ -123,7 +118,7 @@ def test_full_empty_example_serialization(self) -> None: def test_missing_serialization(self) -> None: data = example_aas_missing_attributes.create_full_example() file = io.BytesIO() - write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file=file, data=data, pretty_print=True) # load schema aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) @@ -135,7 +130,7 @@ def test_missing_serialization(self) -> None: def test_concept_description(self) -> None: data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_concept_description.create_iec61360_concept_description()) + data.add(example_aas.create_example_concept_description()) file = io.BytesIO() write_aas_xml_file(file=file, data=data) diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index 66931e8..c326539 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -11,7 +11,7 @@ from basyx.aas import model from basyx.aas.adapter.xml import write_aas_xml_file, read_aas_xml_file -from basyx.aas.examples.data import example_concept_description, example_aas_missing_attributes, example_aas, \ +from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ example_aas_mandatory_attributes, example_submodel_template, create_example from basyx.aas.examples.data._helper import AASDataChecker @@ -48,13 +48,6 @@ def test_example_submodel_template_serialization_deserialization(self) -> None: checker = AASDataChecker(raise_immediately=True) example_submodel_template.check_full_example(checker, object_store) - def test_example_iec61360_concept_description_serialization_deserialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_concept_description.create_iec61360_concept_description()) - object_store = _serialize_and_deserialize(data) - checker = AASDataChecker(raise_immediately=True) - example_concept_description.check_full_example(checker, object_store) - def test_example_all_examples_serialization_deserialization(self) -> None: data: model.DictObjectStore[model.Identifiable] = create_example() object_store = _serialize_and_deserialize(data) From d53ec3de15817c1710476c90ae2f145cfdbd3fca Mon Sep 17 00:00:00 2001 From: David Niebert Date: Tue, 25 Apr 2023 10:41:41 +0200 Subject: [PATCH 420/474] Change type of all occurrences of `AssetInformation.global_asset_id` from `base.ModelReference` to `base.Identifier` --- test/adapter/json/test_json_serialization.py | 13 +++---------- .../json/test_json_serialization_deserialization.py | 4 +--- test/adapter/xml/test_xml_serialization.py | 8 ++------ 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index efc9911..0395523 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -25,15 +25,13 @@ def test_serialize_object(self) -> None: json_data = json.dumps(test_object, cls=AASToJsonEncoder) def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) - asset_reference = model.GlobalReference(asset_key) aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() assert(submodel_identifier is not None) submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("test")), aas_identifier, submodel={submodel_reference}) # serialize object to json @@ -48,8 +46,6 @@ def test_random_object_serialization(self) -> None: class JsonSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) - asset_reference = model.GlobalReference(asset_key) aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() @@ -60,7 +56,7 @@ def test_random_object_serialization(self) -> None: submodel = model.Submodel(submodel_identifier, semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/TestSemanticId"),))) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("test")), aas_identifier, submodel={submodel_reference}) # serialize object to json @@ -217,15 +213,12 @@ def test_stripped_submodel_element_collection(self) -> None: self._checkNormalAndStripped("value", sec) def test_stripped_asset_administration_shell(self) -> None: - asset_ref = model.GlobalReference( - (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/test_ref"),), - ) submodel_ref = model.ModelReference( (model.Key(model.KeyTypes.SUBMODEL, "http://acplt.org/test_ref"),), model.Submodel ) aas = model.AssetAdministrationShell( - model.AssetInformation(global_asset_id=asset_ref), + model.AssetInformation(global_asset_id=model.Identifier("http://acplt.org/test_ref")), "http://acplt.org/test_aas", submodel={submodel_ref} ) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index d83b664..28b3711 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -19,15 +19,13 @@ class JsonSerializationDeserializationTest(unittest.TestCase): def test_random_object_serialization_deserialization(self) -> None: - asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) - asset_reference = model.GlobalReference(asset_key) aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() assert submodel_identifier is not None submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("test")), aas_identifier, submodel={submodel_reference}) # serialize object to json diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 4b5f9ee..15ef7bc 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -26,15 +26,13 @@ def test_serialize_object(self) -> None: # todo: is this a correct way to test it? def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) - asset_reference = model.GlobalReference(asset_key) aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() assert (submodel_identifier is not None) submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("Test")), aas_identifier, submodel={submodel_reference}) test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() @@ -47,8 +45,6 @@ def test_random_object_serialization(self) -> None: class XMLSerializationSchemaTest(unittest.TestCase): def test_random_object_serialization(self) -> None: - asset_key = (model.Key(model.KeyTypes.GLOBAL_REFERENCE, "test"),) - asset_reference = model.GlobalReference(asset_key) aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) submodel_identifier = submodel_key[0].get_identifier() @@ -57,7 +53,7 @@ def test_random_object_serialization(self) -> None: submodel = model.Submodel(submodel_identifier, semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/TestSemanticId"),))) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=asset_reference), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("Test")), aas_identifier, submodel={submodel_reference}) # serialize object to xml test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() From beca5c05bf288381a3b1173a8fe0282b235fe230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 15 May 2023 16:05:10 +0200 Subject: [PATCH 421/474] update XML/JSON schemata, adapter and test files for updated `AssetInformation` attribute --- test/adapter/json/test_json_deserialization.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index a3f92e8..f1087ef 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -383,13 +383,7 @@ def test_stripped_asset_administration_shell(self) -> None: "id": "http://acplt.org/test_aas", "assetInformation": { "assetKind": "Instance", - "globalAssetId": { - "type": "GlobalReference", - "keys": [{ - "type": "GlobalReference", - "value": "test_asset" - }] - } + "globalAssetId": "test_asset" }, "submodels": [{ "type": "ModelReference", From a152c61ed5a9948b37d0f987867095de9f0fa381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 20 Jul 2023 01:48:53 +0200 Subject: [PATCH 422/474] remove wrapping `Identifier()` around strings These were introduced in past commits for the attributes `asset_type` and `global_asset_id` of `AssetInformation`, but aren't necessary since `Identifier` is implemented as an alias for `str`. --- test/adapter/json/test_json_serialization.py | 6 +++--- .../adapter/json/test_json_serialization_deserialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 0395523..bb42a03 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -31,7 +31,7 @@ def test_random_object_serialization(self) -> None: assert(submodel_identifier is not None) submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("test")), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"), aas_identifier, submodel={submodel_reference}) # serialize object to json @@ -56,7 +56,7 @@ def test_random_object_serialization(self) -> None: submodel = model.Submodel(submodel_identifier, semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/TestSemanticId"),))) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("test")), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"), aas_identifier, submodel={submodel_reference}) # serialize object to json @@ -218,7 +218,7 @@ def test_stripped_asset_administration_shell(self) -> None: model.Submodel ) aas = model.AssetAdministrationShell( - model.AssetInformation(global_asset_id=model.Identifier("http://acplt.org/test_ref")), + model.AssetInformation(global_asset_id="http://acplt.org/test_ref"), "http://acplt.org/test_aas", submodel={submodel_ref} ) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 28b3711..2d64af3 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -25,7 +25,7 @@ def test_random_object_serialization_deserialization(self) -> None: assert submodel_identifier is not None submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("test")), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"), aas_identifier, submodel={submodel_reference}) # serialize object to json diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 15ef7bc..3f70b0a 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -32,7 +32,7 @@ def test_random_object_serialization(self) -> None: assert (submodel_identifier is not None) submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("Test")), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="Test"), aas_identifier, submodel={submodel_reference}) test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() @@ -53,7 +53,7 @@ def test_random_object_serialization(self) -> None: submodel = model.Submodel(submodel_identifier, semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/TestSemanticId"),))) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id=model.Identifier("Test")), + test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="Test"), aas_identifier, submodel={submodel_reference}) # serialize object to xml test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() From a15bd3dd98549eb0319c135e52cbd9862c37e79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 20 Jul 2023 02:06:30 +0200 Subject: [PATCH 423/474] change type of `Entity.global_asset_id` to `Identifier` --- test/adapter/json/test_json_deserialization.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index f1087ef..3ee11cb 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -328,13 +328,7 @@ def test_stripped_entity(self) -> None: "modelType": "Entity", "idShort": "test_entity", "entityType": "SelfManagedEntity", - "globalAssetId": { - "type": "GlobalReference", - "keys": [{ - "type": "GlobalReference", - "value": "test_asset" - }] - }, + "globalAssetId": "test_asset", "statements": [{ "modelType": "MultiLanguageProperty", "idShort": "test_multi_language_property" From 99eca879c5d48df573e272cdd50f516fd1a586ad Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Fri, 11 Aug 2023 14:25:18 +0200 Subject: [PATCH 424/474] model.base: Rename GlobalReference to ExternalReference Version 3.0 of the spec renames `ReferenceTypes/GlobalReference` to `ReferenceTypes/ExternalReference` to avoid confusion with `KeyTypes/GlobalReference`. --- test/adapter/json/test_json_serialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index bb42a03..57d6174 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -54,7 +54,7 @@ def test_random_object_serialization(self) -> None: # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which # must be a Reference. (This seems to be a bug in the JSONSchema.) submodel = model.Submodel(submodel_identifier, - semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, + semantic_id=model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/TestSemanticId"),))) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"), aas_identifier, submodel={submodel_reference}) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 3f70b0a..4dbb561 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -51,7 +51,7 @@ def test_random_object_serialization(self) -> None: assert submodel_identifier is not None submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier, - semantic_id=model.GlobalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, + semantic_id=model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://acplt.org/TestSemanticId"),))) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="Test"), aas_identifier, submodel={submodel_reference}) From 247b437055de1ca415b877f8a40bbb20f49ae41b Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Fri, 11 Aug 2023 14:52:10 +0200 Subject: [PATCH 425/474] Fix PyCodeStyle --- test/adapter/json/test_json_serialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 57d6174..5b32f4d 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -55,7 +55,7 @@ def test_random_object_serialization(self) -> None: # must be a Reference. (This seems to be a bug in the JSONSchema.) submodel = model.Submodel(submodel_identifier, semantic_id=model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, - "http://acplt.org/TestSemanticId"),))) + "http://acplt.org/TestSemanticId"),))) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"), aas_identifier, submodel={submodel_reference}) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 4dbb561..094dcdb 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -52,7 +52,7 @@ def test_random_object_serialization(self) -> None: submodel_reference = model.ModelReference(submodel_key, model.Submodel) submodel = model.Submodel(submodel_identifier, semantic_id=model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, - "http://acplt.org/TestSemanticId"),))) + "http://acplt.org/TestSemanticId"),))) test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="Test"), aas_identifier, submodel={submodel_reference}) # serialize object to xml From 314126fdc9d43bde957d00e469afd6b6f9da88c4 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Thu, 24 Aug 2023 09:31:01 +0200 Subject: [PATCH 426/474] model.base: Rename ModelingKind to ModellingKind (#104) In version 3.0 the Enum ModelingKind has been renamed to ModellingKind. This renames the class definition and all its occurences in code and documentation. --- test/adapter/xml/test_xml_deserialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 995a525..2634132 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -130,7 +130,7 @@ def test_invalid_boolean(self) -> None: """) self._assertInExceptionAndLog(xml, "False", ValueError, logging.ERROR) - def test_no_modeling_kind(self) -> None: + def test_no_modelling_kind(self) -> None: xml = _xml_wrap(""" @@ -140,11 +140,11 @@ def test_no_modeling_kind(self) -> None: """) # should get parsed successfully object_store = read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - # modeling kind should default to INSTANCE + # modelling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) # to make mypy happy - self.assertEqual(submodel.kind, model.ModelingKind.INSTANCE) + self.assertEqual(submodel.kind, model.ModellingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: xml = _xml_wrap(""" From 4581f1df7c4873a93d4cf2e19db9a3a1a59b79ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 24 Aug 2023 16:52:58 +0200 Subject: [PATCH 427/474] replace all usages of `LangStringSet` with a `ConstrainedLangStringSet` `LangStringSet` still remains concrete but may be made abstract in the future. --- test/adapter/json/test_json_serialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 5b32f4d..cfef89d 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -21,7 +21,7 @@ class JsonSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER", - description=model.LangStringSet({"en-US": "Germany", "de": "Deutschland"})) + description=model.MultiLanguageTextType({"en-US": "Germany", "de": "Deutschland"})) json_data = json.dumps(test_object, cls=AASToJsonEncoder) def test_random_object_serialization(self) -> None: diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 094dcdb..c75bfea 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -21,7 +21,7 @@ def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER", - description=model.LangStringSet({"en-US": "Germany", "de": "Deutschland"})) + description=model.MultiLanguageTextType({"en-US": "Germany", "de": "Deutschland"})) xml_data = xml_serialization.property_to_xml(test_object, xml_serialization.NS_AAS+"test_object") # todo: is this a correct way to test it? From 10d3597ed8e8bbf0d994bb924a726c24e19f5d93 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 17 Oct 2023 13:30:44 +0200 Subject: [PATCH 428/474] Update compliance tool test files As default value of Submodel.id_short, ConceptDescription.id_short, AAS.id_short is set to None, test files had to be updated. --- test/adapter/xml/test_xml_deserialization.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 2634132..4a473d2 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -258,7 +258,6 @@ def test_duplicate_identifier(self) -> None: http://acplt.org/test_aas - NotSet Instance @@ -267,7 +266,6 @@ def test_duplicate_identifier(self) -> None: http://acplt.org/test_aas - NotSet """) From 3709e9172745af170b9b76f7213908f0b3cf2575 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 27 Oct 2023 22:57:39 +0200 Subject: [PATCH 429/474] Update test files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add globalAssetId for all `ÀssetInformation` - Fix Entity.specificAssetIds in test files --- test/adapter/json/test_json_deserialization.py | 6 ++++-- test/adapter/xml/test_xml_deserialization.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 3ee11cb..7f127be 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -31,7 +31,8 @@ def test_file_format_wrong_list(self) -> None: "modelType": "AssetAdministrationShell", "id": "https://acplt.org/Test_Asset", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Instance", + "globalAssetId": "https://acplt.org/Test_AssetId" } } ] @@ -142,7 +143,8 @@ def test_duplicate_identifier(self) -> None: "modelType": "AssetAdministrationShell", "id": "http://acplt.org/test_aas", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Instance", + "globalAssetId": "https://acplt.org/Test_AssetId" } }], "submodels": [{ diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 4a473d2..f562e02 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -81,6 +81,7 @@ def test_missing_asset_kind(self) -> None: http://acplt.org/test_aas + http://acplt.org/TestAsset/ @@ -94,6 +95,7 @@ def test_missing_asset_kind_text(self) -> None: http://acplt.org/test_aas + http://acplt.org/TestAsset/ @@ -107,6 +109,7 @@ def test_invalid_asset_kind_text(self) -> None: http://acplt.org/test_aas invalidKind + http://acplt.org/TestAsset/ @@ -153,6 +156,7 @@ def test_reference_kind_mismatch(self) -> None: http://acplt.org/test_aas Instance + http://acplt.org/TestAsset/ ModelReference @@ -260,6 +264,7 @@ def test_duplicate_identifier(self) -> None: http://acplt.org/test_aas Instance + http://acplt.org/TestAsset/ @@ -375,6 +380,7 @@ def test_stripped_asset_administration_shell(self) -> None: http://acplt.org/test_aas Instance + http://acplt.org/TestAsset/ From e9fca7ae0c5f59849b65560d1234fefab14ab4b9 Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Tue, 21 Nov 2023 14:07:00 +0100 Subject: [PATCH 430/474] test.adapter.xml: Reintroduce "# type: ignore" tags that got lost in merge --- test/adapter/xml/test_xml_deserialization.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index f562e02..9310e44 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -221,7 +221,7 @@ def test_operation_variable_no_submodel_element(self) -> None: """) - self._assertInExceptionAndLog(xml, "aas:value", KeyError, logging.ERROR) + self._assertInExceptionAndLog(xml, ["aas:value", "has no submodel element"], KeyError, logging.ERROR) def test_operation_variable_too_many_submodel_elements(self) -> None: # TODO: simplify this should our suggestion regarding the XML schema get accepted @@ -256,6 +256,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) self.assertIn("aas:value", context.output[0]) # type: ignore + self.assertIn("more than one submodel element", context.output[0]) def test_duplicate_identifier(self) -> None: xml = _xml_wrap(""" @@ -306,7 +307,7 @@ def get_clean_store() -> model.DictObjectStore: with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) # type: ignore + self.assertIn("already exists in the object store", log_ctx.output[0]) submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) self.assertEqual(submodel.id_short, "test123") From 4a073acbee7bd21c7fc980459548f30c594088f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 21 Nov 2023 20:08:54 +0100 Subject: [PATCH 431/474] test.adapter.xml: remove 'type: ignore' comments These comments were mistakenly re-added in a previous merge of main into improve/V30. --- test/adapter/xml/test_xml_deserialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 9310e44..4ff06aa 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -51,7 +51,7 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], read_aas_xml_file(bytes_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: - self.assertIn(s, log_ctx.output[0]) # type: ignore + self.assertIn(s, log_ctx.output[0]) self.assertIn(s, str(cause)) def test_malformed_xml(self) -> None: @@ -173,7 +173,7 @@ def test_reference_kind_mismatch(self) -> None: with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) for s in ("SUBMODEL", "http://acplt.org/test_ref", "AssetAdministrationShell"): - self.assertIn(s, context.output[0]) # type: ignore + self.assertIn(s, context.output[0]) def test_invalid_submodel_element(self) -> None: xml = _xml_wrap(""" @@ -255,7 +255,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) - self.assertIn("aas:value", context.output[0]) # type: ignore + self.assertIn("aas:value", context.output[0]) self.assertIn("more than one submodel element", context.output[0]) def test_duplicate_identifier(self) -> None: From 3465b013c9f830be01f79e28281fd9916e124ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 Mar 2024 23:59:37 +0100 Subject: [PATCH 432/474] test.adapter.xml: test deserialization without namespace prefixes --- test/adapter/xml/test_xml_deserialization.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 4ff06aa..cf0814d 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -333,6 +333,22 @@ def test_read_aas_xml_element(self) -> None: submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL) self.assertIsInstance(submodel, model.Submodel) + def test_no_namespace_prefix(self) -> None: + def xml(id_: str) -> str: + return f""" + + + + {id_} + + + + """ + + self._assertInExceptionAndLog(xml(""), f'{{{XML_NS_MAP["aas"]}}}id on line 5 has no text', KeyError, + logging.ERROR) + read_aas_xml_file(io.StringIO(xml("urn:x-test:test-submodel"))) + class XmlDeserializationStrippedObjectsTest(unittest.TestCase): def test_stripped_qualifiable(self) -> None: From a5aa5c1b6d4b0d9aa0e3c71766c908c9d013df54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 Mar 2024 22:40:17 +0100 Subject: [PATCH 433/474] test.adater.xml.test_xml_deserialization: simplify ... by using `StringIO` instead of `BytesIO`. --- test/adapter/xml/test_xml_deserialization.py | 45 +++++++++----------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index cf0814d..dd0e48b 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -18,10 +18,7 @@ def _xml_wrap(xml: str) -> str: - return \ - """""" \ - f""" """ \ - + xml + """""" + return f'{xml}' def _root_cause(exception: BaseException) -> BaseException: @@ -44,11 +41,11 @@ def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], """ if isinstance(strings, str): strings = [strings] - bytes_io = io.BytesIO(xml.encode("utf-8")) + string_io = io.StringIO(xml) with self.assertLogs(logging.getLogger(), level=log_level) as log_ctx: - read_aas_xml_file(bytes_io, failsafe=True) + read_aas_xml_file(string_io, failsafe=True) with self.assertRaises(error_type) as err_ctx: - read_aas_xml_file(bytes_io, failsafe=False) + read_aas_xml_file(string_io, failsafe=False) cause = _root_cause(err_ctx.exception) for s in strings: self.assertIn(s, log_ctx.output[0]) @@ -142,7 +139,7 @@ def test_no_modelling_kind(self) -> None: """) # should get parsed successfully - object_store = read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) + object_store = read_aas_xml_file(io.StringIO(xml), failsafe=False) # modelling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) @@ -171,7 +168,7 @@ def test_reference_kind_mismatch(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: - read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) + read_aas_xml_file(io.StringIO(xml), failsafe=False) for s in ("SUBMODEL", "http://acplt.org/test_ref", "AssetAdministrationShell"): self.assertIn(s, context.output[0]) @@ -254,7 +251,7 @@ def test_operation_variable_too_many_submodel_elements(self) -> None: """) with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: - read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), failsafe=False) + read_aas_xml_file(io.StringIO(xml), failsafe=False) self.assertIn("aas:value", context.output[0]) self.assertIn("more than one submodel element", context.output[0]) @@ -294,10 +291,10 @@ def get_clean_store() -> model.DictObjectStore: """) - bytes_io = io.BytesIO(xml.encode("utf-8")) + string_io = io.StringIO(xml) object_store = get_clean_store() - identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=True, ignore_existing=False) + identifiers = read_aas_xml_file_into(object_store, string_io, replace_existing=True, ignore_existing=False) self.assertEqual(identifiers.pop(), sm_id) submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) @@ -305,7 +302,7 @@ def get_clean_store() -> model.DictObjectStore: object_store = get_clean_store() with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: - identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=True) + identifiers = read_aas_xml_file_into(object_store, string_io, replace_existing=False, ignore_existing=True) self.assertEqual(len(identifiers), 0) self.assertIn("already exists in the object store", log_ctx.output[0]) submodel = object_store.pop() @@ -314,7 +311,7 @@ def get_clean_store() -> model.DictObjectStore: object_store = get_clean_store() with self.assertRaises(KeyError) as err_ctx: - identifiers = read_aas_xml_file_into(object_store, bytes_io, replace_existing=False, ignore_existing=False) + identifiers = read_aas_xml_file_into(object_store, string_io, replace_existing=False, ignore_existing=False) self.assertEqual(len(identifiers), 0) cause = _root_cause(err_ctx.exception) self.assertIn("already exists in the object store", str(cause)) @@ -328,9 +325,9 @@ def test_read_aas_xml_element(self) -> None: http://acplt.org/test_submodel """ - bytes_io = io.BytesIO(xml.encode("utf-8")) + string_io = io.StringIO(xml) - submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL) + submodel = read_aas_xml_element(string_io, XMLConstructables.SUBMODEL) self.assertIsInstance(submodel, model.Submodel) def test_no_namespace_prefix(self) -> None: @@ -374,10 +371,10 @@ def test_stripped_qualifiable(self) -> None: """ - bytes_io = io.BytesIO(xml.encode("utf-8")) + string_io = io.StringIO(xml) # check if XML with qualifiers can be parsed successfully - submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False) + submodel = read_aas_xml_element(string_io, XMLConstructables.SUBMODEL, failsafe=False) self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) self.assertEqual(len(submodel.qualifier), 1) @@ -385,7 +382,7 @@ def test_stripped_qualifiable(self) -> None: self.assertEqual(len(operation.qualifier), 1) # check if qualifiers are ignored in stripped mode - submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, failsafe=False, stripped=True) + submodel = read_aas_xml_element(string_io, XMLConstructables.SUBMODEL, failsafe=False, stripped=True) self.assertIsInstance(submodel, model.Submodel) assert isinstance(submodel, model.Submodel) self.assertEqual(len(submodel.qualifier), 0) @@ -412,16 +409,16 @@ def test_stripped_asset_administration_shell(self) -> None: """ - bytes_io = io.BytesIO(xml.encode("utf-8")) + string_io = io.StringIO(xml) # check if XML with submodels can be parsed successfully - aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) + aas = read_aas_xml_element(string_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) self.assertEqual(len(aas.submodel), 1) # check if submodels are ignored in stripped mode - aas = read_aas_xml_element(bytes_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, + aas = read_aas_xml_element(string_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, stripped=True) self.assertIsInstance(aas, model.AssetAdministrationShell) assert isinstance(aas, model.AssetAdministrationShell) @@ -446,9 +443,9 @@ def construct_submodel(cls, element: etree.Element, object_class=EnhancedSubmode http://acplt.org/test_stripped_submodel """ - bytes_io = io.BytesIO(xml.encode("utf-8")) + string_io = io.StringIO(xml) - submodel = read_aas_xml_element(bytes_io, XMLConstructables.SUBMODEL, decoder=EnhancedAASDecoder) + submodel = read_aas_xml_element(string_io, XMLConstructables.SUBMODEL, decoder=EnhancedAASDecoder) self.assertIsInstance(submodel, EnhancedSubmodel) assert isinstance(submodel, EnhancedSubmodel) self.assertEqual(submodel.enhanced_attribute, "fancy!") From 6ed3797efaefe27dc5a915e5950bebb4b25f5ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 Mar 2024 22:48:37 +0100 Subject: [PATCH 434/474] test.adapter.json: add `BytesIO` test --- ...test_json_serialization_deserialization.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 2d64af3..351b718 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -16,6 +16,8 @@ example_aas_mandatory_attributes, example_submodel_template, create_example from basyx.aas.examples.data._helper import AASDataChecker +from typing import Iterable, IO + class JsonSerializationDeserializationTest(unittest.TestCase): def test_random_object_serialization_deserialization(self) -> None: @@ -41,15 +43,17 @@ def test_random_object_serialization_deserialization(self) -> None: json_object_store = read_aas_json_file(io.StringIO(json_data), failsafe=False) def test_example_serialization_deserialization(self) -> None: - data = example_aas.create_full_example() - file = io.StringIO() - write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module - file.seek(0) - json_object_store = read_aas_json_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, json_object_store) + # test with TextIO and BinaryIO, which should both be supported + t: Iterable[IO] = (io.StringIO(), io.BytesIO()) + for file in t: + data = example_aas.create_full_example() + write_aas_json_file(file=file, data=data) + + # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module + file.seek(0) + json_object_store = read_aas_json_file(file, failsafe=False) + checker = AASDataChecker(raise_immediately=True) + example_aas.check_full_example(checker, json_object_store) class JsonSerializationDeserializationTest2(unittest.TestCase): From f59684733ef5f64d439f2221491a7386df589df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 13 Mar 2024 23:12:32 +0100 Subject: [PATCH 435/474] adapter.xml: add function for serializing single objects --- .../xml/test_xml_serialization_deserialization.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index c326539..919092b 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -9,7 +9,8 @@ import unittest from basyx.aas import model -from basyx.aas.adapter.xml import write_aas_xml_file, read_aas_xml_file +from basyx.aas.adapter.xml import write_aas_xml_file, read_aas_xml_file, write_aas_xml_element, read_aas_xml_element, \ + XMLConstructables from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ example_aas_mandatory_attributes, example_submodel_template, create_example @@ -53,3 +54,15 @@ def test_example_all_examples_serialization_deserialization(self) -> None: object_store = _serialize_and_deserialize(data) checker = AASDataChecker(raise_immediately=True) checker.check_object_store(object_store, data) + + +class XMLSerializationDeserializationSingleObjectTest(unittest.TestCase): + def test_submodel_serialization_deserialization(self) -> None: + submodel: model.Submodel = example_submodel_template.create_example_submodel_template() + bytes_io = io.BytesIO() + write_aas_xml_element(bytes_io, submodel) + bytes_io.seek(0) + submodel2: model.Submodel = read_aas_xml_element(bytes_io, # type: ignore[assignment] + XMLConstructables.SUBMODEL, failsafe=False) + checker = AASDataChecker(raise_immediately=True) + checker.check_submodel_equal(submodel2, submodel) From 2d10cc24f7d42476b2e8c82ed88b582936012683 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Mon, 4 Mar 2024 13:23:32 +0100 Subject: [PATCH 436/474] Reintroduce schema files for testing XML and JSON serialization When we removed the schema files, we overlooked that JSON and XML serialization need them. We probably need to find a way of fetching them during runtime of the test to avoid having to host them in our repository. --- test/adapter/json/aasJSONSchema.json | 1528 ++++++++++++++++++ test/adapter/json/test_json_serialization.py | 7 +- test/adapter/xml/aasXMLSchema.xsd | 1344 +++++++++++++++ test/adapter/xml/test_xml_serialization.py | 6 +- 4 files changed, 2882 insertions(+), 3 deletions(-) create mode 100644 test/adapter/json/aasJSONSchema.json create mode 100644 test/adapter/xml/aasXMLSchema.xsd diff --git a/test/adapter/json/aasJSONSchema.json b/test/adapter/json/aasJSONSchema.json new file mode 100644 index 0000000..f48db4d --- /dev/null +++ b/test/adapter/json/aasJSONSchema.json @@ -0,0 +1,1528 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "AssetAdministrationShellEnvironment", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/Environment" + } + ], + "$id": "https://admin-shell.io/aas/3/0", + "definitions": { + "AasSubmodelElements": { + "type": "string", + "enum": [ + "AnnotatedRelationshipElement", + "BasicEventElement", + "Blob", + "Capability", + "DataElement", + "Entity", + "EventElement", + "File", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "SubmodelElement", + "SubmodelElementCollection", + "SubmodelElementList" + ] + }, + "AbstractLangString": { + "type": "object", + "properties": { + "language": { + "type": "string", + "pattern": "^(([a-zA-Z]{2,3}(-[a-zA-Z]{3}(-[a-zA-Z]{3}){2})?|[a-zA-Z]{4}|[a-zA-Z]{5,8})(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-(([a-zA-Z0-9]){5,8}|[0-9]([a-zA-Z0-9]){3}))*(-[0-9A-WY-Za-wy-z](-([a-zA-Z0-9]){2,8})+)*(-[xX](-([a-zA-Z0-9]){1,8})+)?|[xX](-([a-zA-Z0-9]){1,8})+|((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)))$" + }, + "text": { + "type": "string", + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + } + }, + "required": [ + "language", + "text" + ] + }, + "AdministrativeInformation": { + "allOf": [ + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "version": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "maxLength": 4 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^(0|[1-9][0-9]*)$" + } + ] + }, + "revision": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "maxLength": 4 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^(0|[1-9][0-9]*)$" + } + ] + }, + "creator": { + "$ref": "#/definitions/Reference" + }, + "templateId": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + } + } + } + ] + }, + "AnnotatedRelationshipElement": { + "allOf": [ + { + "$ref": "#/definitions/RelationshipElement_abstract" + }, + { + "properties": { + "annotations": { + "type": "array", + "items": { + "$ref": "#/definitions/DataElement_choice" + }, + "minItems": 1 + }, + "modelType": { + "const": "AnnotatedRelationshipElement" + } + } + } + ] + }, + "AssetAdministrationShell": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "derivedFrom": { + "$ref": "#/definitions/Reference" + }, + "assetInformation": { + "$ref": "#/definitions/AssetInformation" + }, + "submodels": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + }, + "modelType": { + "const": "AssetAdministrationShell" + } + }, + "required": [ + "assetInformation" + ] + } + ] + }, + "AssetInformation": { + "type": "object", + "properties": { + "assetKind": { + "$ref": "#/definitions/AssetKind" + }, + "globalAssetId": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "specificAssetIds": { + "type": "array", + "items": { + "$ref": "#/definitions/SpecificAssetId" + }, + "minItems": 1 + }, + "assetType": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "defaultThumbnail": { + "$ref": "#/definitions/Resource" + } + }, + "required": [ + "assetKind" + ] + }, + "AssetKind": { + "type": "string", + "enum": [ + "Instance", + "NotApplicable", + "Type" + ] + }, + "BasicEventElement": { + "allOf": [ + { + "$ref": "#/definitions/EventElement" + }, + { + "properties": { + "observed": { + "$ref": "#/definitions/Reference" + }, + "direction": { + "$ref": "#/definitions/Direction" + }, + "state": { + "$ref": "#/definitions/StateOfEvent" + }, + "messageTopic": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "messageBroker": { + "$ref": "#/definitions/Reference" + }, + "lastUpdate": { + "type": "string", + "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" + }, + "minInterval": { + "type": "string", + "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" + }, + "maxInterval": { + "type": "string", + "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" + }, + "modelType": { + "const": "BasicEventElement" + } + }, + "required": [ + "observed", + "direction", + "state" + ] + } + ] + }, + "Blob": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "value": { + "type": "string", + "contentEncoding": "base64" + }, + "contentType": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "maxLength": 100 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" + } + ] + }, + "modelType": { + "const": "Blob" + } + }, + "required": [ + "contentType" + ] + } + ] + }, + "Capability": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "modelType": { + "const": "Capability" + } + } + } + ] + }, + "ConceptDescription": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "isCaseOf": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + }, + "modelType": { + "const": "ConceptDescription" + } + } + } + ] + }, + "DataElement": { + "$ref": "#/definitions/SubmodelElement" + }, + "DataElement_choice": { + "oneOf": [ + { + "$ref": "#/definitions/Blob" + }, + { + "$ref": "#/definitions/File" + }, + { + "$ref": "#/definitions/MultiLanguageProperty" + }, + { + "$ref": "#/definitions/Property" + }, + { + "$ref": "#/definitions/Range" + }, + { + "$ref": "#/definitions/ReferenceElement" + } + ] + }, + "DataSpecificationContent": { + "type": "object", + "properties": { + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": [ + "modelType" + ] + }, + "DataSpecificationContent_choice": { + "oneOf": [ + { + "$ref": "#/definitions/DataSpecificationIec61360" + } + ] + }, + "DataSpecificationIec61360": { + "allOf": [ + { + "$ref": "#/definitions/DataSpecificationContent" + }, + { + "properties": { + "preferredName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangStringPreferredNameTypeIec61360" + }, + "minItems": 1 + }, + "shortName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangStringShortNameTypeIec61360" + }, + "minItems": 1 + }, + "unit": { + "type": "string", + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "unitId": { + "$ref": "#/definitions/Reference" + }, + "sourceOfDefinition": { + "type": "string", + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "symbol": { + "type": "string", + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "dataType": { + "$ref": "#/definitions/DataTypeIec61360" + }, + "definition": { + "type": "array", + "items": { + "$ref": "#/definitions/LangStringDefinitionTypeIec61360" + }, + "minItems": 1 + }, + "valueFormat": { + "type": "string", + "minLength": 1, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "valueList": { + "$ref": "#/definitions/ValueList" + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "levelType": { + "$ref": "#/definitions/LevelType" + }, + "modelType": { + "const": "DataSpecificationIec61360" + } + }, + "required": [ + "preferredName" + ] + } + ] + }, + "DataTypeDefXsd": { + "type": "string", + "enum": [ + "xs:anyURI", + "xs:base64Binary", + "xs:boolean", + "xs:byte", + "xs:date", + "xs:dateTime", + "xs:decimal", + "xs:double", + "xs:duration", + "xs:float", + "xs:gDay", + "xs:gMonth", + "xs:gMonthDay", + "xs:gYear", + "xs:gYearMonth", + "xs:hexBinary", + "xs:int", + "xs:integer", + "xs:long", + "xs:negativeInteger", + "xs:nonNegativeInteger", + "xs:nonPositiveInteger", + "xs:positiveInteger", + "xs:short", + "xs:string", + "xs:time", + "xs:unsignedByte", + "xs:unsignedInt", + "xs:unsignedLong", + "xs:unsignedShort" + ] + }, + "DataTypeIec61360": { + "type": "string", + "enum": [ + "BLOB", + "BOOLEAN", + "DATE", + "FILE", + "HTML", + "INTEGER_COUNT", + "INTEGER_CURRENCY", + "INTEGER_MEASURE", + "IRDI", + "IRI", + "RATIONAL", + "RATIONAL_MEASURE", + "REAL_COUNT", + "REAL_CURRENCY", + "REAL_MEASURE", + "STRING", + "STRING_TRANSLATABLE", + "TIME", + "TIMESTAMP" + ] + }, + "Direction": { + "type": "string", + "enum": [ + "input", + "output" + ] + }, + "EmbeddedDataSpecification": { + "type": "object", + "properties": { + "dataSpecification": { + "$ref": "#/definitions/Reference" + }, + "dataSpecificationContent": { + "$ref": "#/definitions/DataSpecificationContent_choice" + } + }, + "required": [ + "dataSpecification", + "dataSpecificationContent" + ] + }, + "Entity": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "statements": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement_choice" + }, + "minItems": 1 + }, + "entityType": { + "$ref": "#/definitions/EntityType" + }, + "globalAssetId": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "specificAssetIds": { + "type": "array", + "items": { + "$ref": "#/definitions/SpecificAssetId" + }, + "minItems": 1 + }, + "modelType": { + "const": "Entity" + } + }, + "required": [ + "entityType" + ] + } + ] + }, + "EntityType": { + "type": "string", + "enum": [ + "CoManagedEntity", + "SelfManagedEntity" + ] + }, + "Environment": { + "type": "object", + "properties": { + "assetAdministrationShells": { + "type": "array", + "items": { + "$ref": "#/definitions/AssetAdministrationShell" + }, + "minItems": 1 + }, + "submodels": { + "type": "array", + "items": { + "$ref": "#/definitions/Submodel" + }, + "minItems": 1 + }, + "conceptDescriptions": { + "type": "array", + "items": { + "$ref": "#/definitions/ConceptDescription" + }, + "minItems": 1 + } + } + }, + "EventElement": { + "$ref": "#/definitions/SubmodelElement" + }, + "EventPayload": { + "type": "object", + "properties": { + "source": { + "$ref": "#/definitions/Reference" + }, + "sourceSemanticId": { + "$ref": "#/definitions/Reference" + }, + "observableReference": { + "$ref": "#/definitions/Reference" + }, + "observableSemanticId": { + "$ref": "#/definitions/Reference" + }, + "topic": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "subjectId": { + "$ref": "#/definitions/Reference" + }, + "timeStamp": { + "type": "string", + "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" + }, + "payload": { + "type": "string", + "contentEncoding": "base64" + } + }, + "required": [ + "source", + "observableReference", + "timeStamp" + ] + }, + "Extension": { + "allOf": [ + { + "$ref": "#/definitions/HasSemantics" + }, + { + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "valueType": { + "$ref": "#/definitions/DataTypeDefXsd" + }, + "value": { + "type": "string" + }, + "refersTo": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + } + }, + "required": [ + "name" + ] + } + ] + }, + "File": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "value": { + "type": "string" + }, + "contentType": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "maxLength": 100 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" + } + ] + }, + "modelType": { + "const": "File" + } + }, + "required": [ + "contentType" + ] + } + ] + }, + "HasDataSpecification": { + "type": "object", + "properties": { + "embeddedDataSpecifications": { + "type": "array", + "items": { + "$ref": "#/definitions/EmbeddedDataSpecification" + }, + "minItems": 1 + } + } + }, + "HasExtensions": { + "type": "object", + "properties": { + "extensions": { + "type": "array", + "items": { + "$ref": "#/definitions/Extension" + }, + "minItems": 1 + } + } + }, + "HasKind": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/definitions/ModellingKind" + } + } + }, + "HasSemantics": { + "type": "object", + "properties": { + "semanticId": { + "$ref": "#/definitions/Reference" + }, + "supplementalSemanticIds": { + "type": "array", + "items": { + "$ref": "#/definitions/Reference" + }, + "minItems": 1 + } + } + }, + "Identifiable": { + "allOf": [ + { + "$ref": "#/definitions/Referable" + }, + { + "properties": { + "administration": { + "$ref": "#/definitions/AdministrativeInformation" + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + } + }, + "required": [ + "id" + ] + } + ] + }, + "Key": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/KeyTypes" + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + } + }, + "required": [ + "type", + "value" + ] + }, + "KeyTypes": { + "type": "string", + "enum": [ + "AnnotatedRelationshipElement", + "AssetAdministrationShell", + "BasicEventElement", + "Blob", + "Capability", + "ConceptDescription", + "DataElement", + "Entity", + "EventElement", + "File", + "FragmentReference", + "GlobalReference", + "Identifiable", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "Referable", + "ReferenceElement", + "RelationshipElement", + "Submodel", + "SubmodelElement", + "SubmodelElementCollection", + "SubmodelElementList" + ] + }, + "LangStringDefinitionTypeIec61360": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 1023 + } + } + } + ] + }, + "LangStringNameType": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 128 + } + } + } + ] + }, + "LangStringPreferredNameTypeIec61360": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 255 + } + } + } + ] + }, + "LangStringShortNameTypeIec61360": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 18 + } + } + } + ] + }, + "LangStringTextType": { + "allOf": [ + { + "$ref": "#/definitions/AbstractLangString" + }, + { + "properties": { + "text": { + "maxLength": 1023 + } + } + } + ] + }, + "LevelType": { + "type": "object", + "properties": { + "min": { + "type": "boolean" + }, + "nom": { + "type": "boolean" + }, + "typ": { + "type": "boolean" + }, + "max": { + "type": "boolean" + } + }, + "required": [ + "min", + "nom", + "typ", + "max" + ] + }, + "ModelType": { + "type": "string", + "enum": [ + "AnnotatedRelationshipElement", + "AssetAdministrationShell", + "BasicEventElement", + "Blob", + "Capability", + "ConceptDescription", + "DataSpecificationIec61360", + "Entity", + "File", + "MultiLanguageProperty", + "Operation", + "Property", + "Range", + "ReferenceElement", + "RelationshipElement", + "Submodel", + "SubmodelElementCollection", + "SubmodelElementList" + ] + }, + "ModellingKind": { + "type": "string", + "enum": [ + "Instance", + "Template" + ] + }, + "MultiLanguageProperty": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/LangStringTextType" + }, + "minItems": 1 + }, + "valueId": { + "$ref": "#/definitions/Reference" + }, + "modelType": { + "const": "MultiLanguageProperty" + } + } + } + ] + }, + "Operation": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "inputVariables": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + }, + "minItems": 1 + }, + "outputVariables": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + }, + "minItems": 1 + }, + "inoutputVariables": { + "type": "array", + "items": { + "$ref": "#/definitions/OperationVariable" + }, + "minItems": 1 + }, + "modelType": { + "const": "Operation" + } + } + } + ] + }, + "OperationVariable": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/SubmodelElement_choice" + } + }, + "required": [ + "value" + ] + }, + "Property": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "valueType": { + "$ref": "#/definitions/DataTypeDefXsd" + }, + "value": { + "type": "string" + }, + "valueId": { + "$ref": "#/definitions/Reference" + }, + "modelType": { + "const": "Property" + } + }, + "required": [ + "valueType" + ] + } + ] + }, + "Qualifiable": { + "type": "object", + "properties": { + "qualifiers": { + "type": "array", + "items": { + "$ref": "#/definitions/Qualifier" + }, + "minItems": 1 + }, + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": [ + "modelType" + ] + }, + "Qualifier": { + "allOf": [ + { + "$ref": "#/definitions/HasSemantics" + }, + { + "properties": { + "kind": { + "$ref": "#/definitions/QualifierKind" + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "valueType": { + "$ref": "#/definitions/DataTypeDefXsd" + }, + "value": { + "type": "string" + }, + "valueId": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "type", + "valueType" + ] + } + ] + }, + "QualifierKind": { + "type": "string", + "enum": [ + "ConceptQualifier", + "TemplateQualifier", + "ValueQualifier" + ] + }, + "Range": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "valueType": { + "$ref": "#/definitions/DataTypeDefXsd" + }, + "min": { + "type": "string" + }, + "max": { + "type": "string" + }, + "modelType": { + "const": "Range" + } + }, + "required": [ + "valueType" + ] + } + ] + }, + "Referable": { + "allOf": [ + { + "$ref": "#/definitions/HasExtensions" + }, + { + "properties": { + "category": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "idShort": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "maxLength": 128 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$" + } + ] + }, + "displayName": { + "type": "array", + "items": { + "$ref": "#/definitions/LangStringNameType" + }, + "minItems": 1 + }, + "description": { + "type": "array", + "items": { + "$ref": "#/definitions/LangStringTextType" + }, + "minItems": 1 + }, + "modelType": { + "$ref": "#/definitions/ModelType" + } + }, + "required": [ + "modelType" + ] + } + ] + }, + "Reference": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/ReferenceTypes" + }, + "referredSemanticId": { + "$ref": "#/definitions/Reference" + }, + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/Key" + }, + "minItems": 1 + } + }, + "required": [ + "type", + "keys" + ] + }, + "ReferenceElement": { + "allOf": [ + { + "$ref": "#/definitions/DataElement" + }, + { + "properties": { + "value": { + "$ref": "#/definitions/Reference" + }, + "modelType": { + "const": "ReferenceElement" + } + } + } + ] + }, + "ReferenceTypes": { + "type": "string", + "enum": [ + "ExternalReference", + "ModelReference" + ] + }, + "RelationshipElement": { + "allOf": [ + { + "$ref": "#/definitions/RelationshipElement_abstract" + }, + { + "properties": { + "modelType": { + "const": "RelationshipElement" + } + } + } + ] + }, + "RelationshipElement_abstract": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "first": { + "$ref": "#/definitions/Reference" + }, + "second": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "first", + "second" + ] + } + ] + }, + "RelationshipElement_choice": { + "oneOf": [ + { + "$ref": "#/definitions/RelationshipElement" + }, + { + "$ref": "#/definitions/AnnotatedRelationshipElement" + } + ] + }, + "Resource": { + "type": "object", + "properties": { + "path": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "maxLength": 2000 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$" + } + ] + }, + "contentType": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "maxLength": 100 + }, + { + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + { + "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" + } + ] + } + }, + "required": [ + "path" + ] + }, + "SpecificAssetId": { + "allOf": [ + { + "$ref": "#/definitions/HasSemantics" + }, + { + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "externalSubjectId": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "name", + "value" + ] + } + ] + }, + "StateOfEvent": { + "type": "string", + "enum": [ + "off", + "on" + ] + }, + "Submodel": { + "allOf": [ + { + "$ref": "#/definitions/Identifiable" + }, + { + "$ref": "#/definitions/HasKind" + }, + { + "$ref": "#/definitions/HasSemantics" + }, + { + "$ref": "#/definitions/Qualifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + }, + { + "properties": { + "submodelElements": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement_choice" + }, + "minItems": 1 + }, + "modelType": { + "const": "Submodel" + } + } + } + ] + }, + "SubmodelElement": { + "allOf": [ + { + "$ref": "#/definitions/Referable" + }, + { + "$ref": "#/definitions/HasSemantics" + }, + { + "$ref": "#/definitions/Qualifiable" + }, + { + "$ref": "#/definitions/HasDataSpecification" + } + ] + }, + "SubmodelElementCollection": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement_choice" + }, + "minItems": 1 + }, + "modelType": { + "const": "SubmodelElementCollection" + } + } + } + ] + }, + "SubmodelElementList": { + "allOf": [ + { + "$ref": "#/definitions/SubmodelElement" + }, + { + "properties": { + "orderRelevant": { + "type": "boolean" + }, + "semanticIdListElement": { + "$ref": "#/definitions/Reference" + }, + "typeValueListElement": { + "$ref": "#/definitions/AasSubmodelElements" + }, + "valueTypeListElement": { + "$ref": "#/definitions/DataTypeDefXsd" + }, + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/SubmodelElement_choice" + }, + "minItems": 1 + }, + "modelType": { + "const": "SubmodelElementList" + } + }, + "required": [ + "typeValueListElement" + ] + } + ] + }, + "SubmodelElement_choice": { + "oneOf": [ + { + "$ref": "#/definitions/RelationshipElement" + }, + { + "$ref": "#/definitions/AnnotatedRelationshipElement" + }, + { + "$ref": "#/definitions/BasicEventElement" + }, + { + "$ref": "#/definitions/Blob" + }, + { + "$ref": "#/definitions/Capability" + }, + { + "$ref": "#/definitions/Entity" + }, + { + "$ref": "#/definitions/File" + }, + { + "$ref": "#/definitions/MultiLanguageProperty" + }, + { + "$ref": "#/definitions/Operation" + }, + { + "$ref": "#/definitions/Property" + }, + { + "$ref": "#/definitions/Range" + }, + { + "$ref": "#/definitions/ReferenceElement" + }, + { + "$ref": "#/definitions/SubmodelElementCollection" + }, + { + "$ref": "#/definitions/SubmodelElementList" + } + ] + }, + "ValueList": { + "type": "object", + "properties": { + "valueReferencePairs": { + "type": "array", + "items": { + "$ref": "#/definitions/ValueReferencePair" + }, + "minItems": 1 + } + }, + "required": [ + "valueReferencePairs" + ] + }, + "ValueReferencePair": { + "type": "object", + "properties": { + "value": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + }, + "valueId": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "value", + "valueId" + ] + } + } +} \ No newline at end of file diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index cfef89d..95a0dd4 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -4,13 +4,13 @@ # the LICENSE file of this project. # # SPDX-License-Identifier: MIT - +import os import io import unittest import json from basyx.aas import model -from basyx.aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, JSON_SCHEMA_FILE +from basyx.aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file from jsonschema import validate # type: ignore from typing import Set, Union @@ -18,6 +18,9 @@ example_aas_mandatory_attributes, example_submodel_template, create_example +JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') + + class JsonSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER", diff --git a/test/adapter/xml/aasXMLSchema.xsd b/test/adapter/xml/aasXMLSchema.xsd new file mode 100644 index 0000000..25d7a52 --- /dev/null +++ b/test/adapter/xml/aasXMLSchema.xsd @@ -0,0 +1,1344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index c75bfea..178c130 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -5,17 +5,21 @@ # # SPDX-License-Identifier: MIT import io +import os import unittest from lxml import etree # type: ignore from basyx.aas import model -from basyx.aas.adapter.xml import write_aas_xml_file, xml_serialization, XML_SCHEMA_FILE +from basyx.aas.adapter.xml import write_aas_xml_file, xml_serialization from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ example_submodel_template, example_aas_mandatory_attributes +XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasXMLSchema.xsd') + + class XMLSerializationTest(unittest.TestCase): def test_serialize_object(self) -> None: test_object = model.Property("test_id_short", From 2fde0781e0dca01d0f1c84b6de1bd25478dc428e Mon Sep 17 00:00:00 2001 From: Sebastian Heppner Date: Thu, 14 Mar 2024 05:20:28 +0100 Subject: [PATCH 437/474] test: Remove schema files and adapt CI to fetch them This removes the JSON Schema and XSD files that stem from the aas-specs repository. Instead, this adds the logic in the CI to fetch these files at runtime. Furthermore, we skip the unittests that make use of the schema files when they do not exist. --- test/adapter/json/aasJSONSchema.json | 1528 ------------------ test/adapter/json/test_json_serialization.py | 7 +- test/adapter/xml/aasXMLSchema.xsd | 1344 --------------- test/adapter/xml/test_xml_serialization.py | 7 +- 4 files changed, 12 insertions(+), 2874 deletions(-) delete mode 100644 test/adapter/json/aasJSONSchema.json delete mode 100644 test/adapter/xml/aasXMLSchema.xsd diff --git a/test/adapter/json/aasJSONSchema.json b/test/adapter/json/aasJSONSchema.json deleted file mode 100644 index f48db4d..0000000 --- a/test/adapter/json/aasJSONSchema.json +++ /dev/null @@ -1,1528 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "title": "AssetAdministrationShellEnvironment", - "type": "object", - "allOf": [ - { - "$ref": "#/definitions/Environment" - } - ], - "$id": "https://admin-shell.io/aas/3/0", - "definitions": { - "AasSubmodelElements": { - "type": "string", - "enum": [ - "AnnotatedRelationshipElement", - "BasicEventElement", - "Blob", - "Capability", - "DataElement", - "Entity", - "EventElement", - "File", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "SubmodelElement", - "SubmodelElementCollection", - "SubmodelElementList" - ] - }, - "AbstractLangString": { - "type": "object", - "properties": { - "language": { - "type": "string", - "pattern": "^(([a-zA-Z]{2,3}(-[a-zA-Z]{3}(-[a-zA-Z]{3}){2})?|[a-zA-Z]{4}|[a-zA-Z]{5,8})(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-(([a-zA-Z0-9]){5,8}|[0-9]([a-zA-Z0-9]){3}))*(-[0-9A-WY-Za-wy-z](-([a-zA-Z0-9]){2,8})+)*(-[xX](-([a-zA-Z0-9]){1,8})+)?|[xX](-([a-zA-Z0-9]){1,8})+|((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)))$" - }, - "text": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - } - }, - "required": [ - "language", - "text" - ] - }, - "AdministrativeInformation": { - "allOf": [ - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "version": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 4 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^(0|[1-9][0-9]*)$" - } - ] - }, - "revision": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 4 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^(0|[1-9][0-9]*)$" - } - ] - }, - "creator": { - "$ref": "#/definitions/Reference" - }, - "templateId": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - } - } - } - ] - }, - "AnnotatedRelationshipElement": { - "allOf": [ - { - "$ref": "#/definitions/RelationshipElement_abstract" - }, - { - "properties": { - "annotations": { - "type": "array", - "items": { - "$ref": "#/definitions/DataElement_choice" - }, - "minItems": 1 - }, - "modelType": { - "const": "AnnotatedRelationshipElement" - } - } - } - ] - }, - "AssetAdministrationShell": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "derivedFrom": { - "$ref": "#/definitions/Reference" - }, - "assetInformation": { - "$ref": "#/definitions/AssetInformation" - }, - "submodels": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - }, - "modelType": { - "const": "AssetAdministrationShell" - } - }, - "required": [ - "assetInformation" - ] - } - ] - }, - "AssetInformation": { - "type": "object", - "properties": { - "assetKind": { - "$ref": "#/definitions/AssetKind" - }, - "globalAssetId": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "specificAssetIds": { - "type": "array", - "items": { - "$ref": "#/definitions/SpecificAssetId" - }, - "minItems": 1 - }, - "assetType": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "defaultThumbnail": { - "$ref": "#/definitions/Resource" - } - }, - "required": [ - "assetKind" - ] - }, - "AssetKind": { - "type": "string", - "enum": [ - "Instance", - "NotApplicable", - "Type" - ] - }, - "BasicEventElement": { - "allOf": [ - { - "$ref": "#/definitions/EventElement" - }, - { - "properties": { - "observed": { - "$ref": "#/definitions/Reference" - }, - "direction": { - "$ref": "#/definitions/Direction" - }, - "state": { - "$ref": "#/definitions/StateOfEvent" - }, - "messageTopic": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "messageBroker": { - "$ref": "#/definitions/Reference" - }, - "lastUpdate": { - "type": "string", - "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" - }, - "minInterval": { - "type": "string", - "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" - }, - "maxInterval": { - "type": "string", - "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" - }, - "modelType": { - "const": "BasicEventElement" - } - }, - "required": [ - "observed", - "direction", - "state" - ] - } - ] - }, - "Blob": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "value": { - "type": "string", - "contentEncoding": "base64" - }, - "contentType": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 100 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" - } - ] - }, - "modelType": { - "const": "Blob" - } - }, - "required": [ - "contentType" - ] - } - ] - }, - "Capability": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "modelType": { - "const": "Capability" - } - } - } - ] - }, - "ConceptDescription": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "isCaseOf": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - }, - "modelType": { - "const": "ConceptDescription" - } - } - } - ] - }, - "DataElement": { - "$ref": "#/definitions/SubmodelElement" - }, - "DataElement_choice": { - "oneOf": [ - { - "$ref": "#/definitions/Blob" - }, - { - "$ref": "#/definitions/File" - }, - { - "$ref": "#/definitions/MultiLanguageProperty" - }, - { - "$ref": "#/definitions/Property" - }, - { - "$ref": "#/definitions/Range" - }, - { - "$ref": "#/definitions/ReferenceElement" - } - ] - }, - "DataSpecificationContent": { - "type": "object", - "properties": { - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ - "modelType" - ] - }, - "DataSpecificationContent_choice": { - "oneOf": [ - { - "$ref": "#/definitions/DataSpecificationIec61360" - } - ] - }, - "DataSpecificationIec61360": { - "allOf": [ - { - "$ref": "#/definitions/DataSpecificationContent" - }, - { - "properties": { - "preferredName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringPreferredNameTypeIec61360" - }, - "minItems": 1 - }, - "shortName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringShortNameTypeIec61360" - }, - "minItems": 1 - }, - "unit": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "unitId": { - "$ref": "#/definitions/Reference" - }, - "sourceOfDefinition": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "symbol": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "dataType": { - "$ref": "#/definitions/DataTypeIec61360" - }, - "definition": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringDefinitionTypeIec61360" - }, - "minItems": 1 - }, - "valueFormat": { - "type": "string", - "minLength": 1, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "valueList": { - "$ref": "#/definitions/ValueList" - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "levelType": { - "$ref": "#/definitions/LevelType" - }, - "modelType": { - "const": "DataSpecificationIec61360" - } - }, - "required": [ - "preferredName" - ] - } - ] - }, - "DataTypeDefXsd": { - "type": "string", - "enum": [ - "xs:anyURI", - "xs:base64Binary", - "xs:boolean", - "xs:byte", - "xs:date", - "xs:dateTime", - "xs:decimal", - "xs:double", - "xs:duration", - "xs:float", - "xs:gDay", - "xs:gMonth", - "xs:gMonthDay", - "xs:gYear", - "xs:gYearMonth", - "xs:hexBinary", - "xs:int", - "xs:integer", - "xs:long", - "xs:negativeInteger", - "xs:nonNegativeInteger", - "xs:nonPositiveInteger", - "xs:positiveInteger", - "xs:short", - "xs:string", - "xs:time", - "xs:unsignedByte", - "xs:unsignedInt", - "xs:unsignedLong", - "xs:unsignedShort" - ] - }, - "DataTypeIec61360": { - "type": "string", - "enum": [ - "BLOB", - "BOOLEAN", - "DATE", - "FILE", - "HTML", - "INTEGER_COUNT", - "INTEGER_CURRENCY", - "INTEGER_MEASURE", - "IRDI", - "IRI", - "RATIONAL", - "RATIONAL_MEASURE", - "REAL_COUNT", - "REAL_CURRENCY", - "REAL_MEASURE", - "STRING", - "STRING_TRANSLATABLE", - "TIME", - "TIMESTAMP" - ] - }, - "Direction": { - "type": "string", - "enum": [ - "input", - "output" - ] - }, - "EmbeddedDataSpecification": { - "type": "object", - "properties": { - "dataSpecification": { - "$ref": "#/definitions/Reference" - }, - "dataSpecificationContent": { - "$ref": "#/definitions/DataSpecificationContent_choice" - } - }, - "required": [ - "dataSpecification", - "dataSpecificationContent" - ] - }, - "Entity": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "statements": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement_choice" - }, - "minItems": 1 - }, - "entityType": { - "$ref": "#/definitions/EntityType" - }, - "globalAssetId": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "specificAssetIds": { - "type": "array", - "items": { - "$ref": "#/definitions/SpecificAssetId" - }, - "minItems": 1 - }, - "modelType": { - "const": "Entity" - } - }, - "required": [ - "entityType" - ] - } - ] - }, - "EntityType": { - "type": "string", - "enum": [ - "CoManagedEntity", - "SelfManagedEntity" - ] - }, - "Environment": { - "type": "object", - "properties": { - "assetAdministrationShells": { - "type": "array", - "items": { - "$ref": "#/definitions/AssetAdministrationShell" - }, - "minItems": 1 - }, - "submodels": { - "type": "array", - "items": { - "$ref": "#/definitions/Submodel" - }, - "minItems": 1 - }, - "conceptDescriptions": { - "type": "array", - "items": { - "$ref": "#/definitions/ConceptDescription" - }, - "minItems": 1 - } - } - }, - "EventElement": { - "$ref": "#/definitions/SubmodelElement" - }, - "EventPayload": { - "type": "object", - "properties": { - "source": { - "$ref": "#/definitions/Reference" - }, - "sourceSemanticId": { - "$ref": "#/definitions/Reference" - }, - "observableReference": { - "$ref": "#/definitions/Reference" - }, - "observableSemanticId": { - "$ref": "#/definitions/Reference" - }, - "topic": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "subjectId": { - "$ref": "#/definitions/Reference" - }, - "timeStamp": { - "type": "string", - "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" - }, - "payload": { - "type": "string", - "contentEncoding": "base64" - } - }, - "required": [ - "source", - "observableReference", - "timeStamp" - ] - }, - "Extension": { - "allOf": [ - { - "$ref": "#/definitions/HasSemantics" - }, - { - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "value": { - "type": "string" - }, - "refersTo": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - } - }, - "required": [ - "name" - ] - } - ] - }, - "File": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "value": { - "type": "string" - }, - "contentType": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 100 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" - } - ] - }, - "modelType": { - "const": "File" - } - }, - "required": [ - "contentType" - ] - } - ] - }, - "HasDataSpecification": { - "type": "object", - "properties": { - "embeddedDataSpecifications": { - "type": "array", - "items": { - "$ref": "#/definitions/EmbeddedDataSpecification" - }, - "minItems": 1 - } - } - }, - "HasExtensions": { - "type": "object", - "properties": { - "extensions": { - "type": "array", - "items": { - "$ref": "#/definitions/Extension" - }, - "minItems": 1 - } - } - }, - "HasKind": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/definitions/ModellingKind" - } - } - }, - "HasSemantics": { - "type": "object", - "properties": { - "semanticId": { - "$ref": "#/definitions/Reference" - }, - "supplementalSemanticIds": { - "type": "array", - "items": { - "$ref": "#/definitions/Reference" - }, - "minItems": 1 - } - } - }, - "Identifiable": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "properties": { - "administration": { - "$ref": "#/definitions/AdministrativeInformation" - }, - "id": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - } - }, - "required": [ - "id" - ] - } - ] - }, - "Key": { - "type": "object", - "properties": { - "type": { - "$ref": "#/definitions/KeyTypes" - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - } - }, - "required": [ - "type", - "value" - ] - }, - "KeyTypes": { - "type": "string", - "enum": [ - "AnnotatedRelationshipElement", - "AssetAdministrationShell", - "BasicEventElement", - "Blob", - "Capability", - "ConceptDescription", - "DataElement", - "Entity", - "EventElement", - "File", - "FragmentReference", - "GlobalReference", - "Identifiable", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "Referable", - "ReferenceElement", - "RelationshipElement", - "Submodel", - "SubmodelElement", - "SubmodelElementCollection", - "SubmodelElementList" - ] - }, - "LangStringDefinitionTypeIec61360": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 1023 - } - } - } - ] - }, - "LangStringNameType": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 128 - } - } - } - ] - }, - "LangStringPreferredNameTypeIec61360": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 255 - } - } - } - ] - }, - "LangStringShortNameTypeIec61360": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 18 - } - } - } - ] - }, - "LangStringTextType": { - "allOf": [ - { - "$ref": "#/definitions/AbstractLangString" - }, - { - "properties": { - "text": { - "maxLength": 1023 - } - } - } - ] - }, - "LevelType": { - "type": "object", - "properties": { - "min": { - "type": "boolean" - }, - "nom": { - "type": "boolean" - }, - "typ": { - "type": "boolean" - }, - "max": { - "type": "boolean" - } - }, - "required": [ - "min", - "nom", - "typ", - "max" - ] - }, - "ModelType": { - "type": "string", - "enum": [ - "AnnotatedRelationshipElement", - "AssetAdministrationShell", - "BasicEventElement", - "Blob", - "Capability", - "ConceptDescription", - "DataSpecificationIec61360", - "Entity", - "File", - "MultiLanguageProperty", - "Operation", - "Property", - "Range", - "ReferenceElement", - "RelationshipElement", - "Submodel", - "SubmodelElementCollection", - "SubmodelElementList" - ] - }, - "ModellingKind": { - "type": "string", - "enum": [ - "Instance", - "Template" - ] - }, - "MultiLanguageProperty": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringTextType" - }, - "minItems": 1 - }, - "valueId": { - "$ref": "#/definitions/Reference" - }, - "modelType": { - "const": "MultiLanguageProperty" - } - } - } - ] - }, - "Operation": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "inputVariables": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - }, - "minItems": 1 - }, - "outputVariables": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - }, - "minItems": 1 - }, - "inoutputVariables": { - "type": "array", - "items": { - "$ref": "#/definitions/OperationVariable" - }, - "minItems": 1 - }, - "modelType": { - "const": "Operation" - } - } - } - ] - }, - "OperationVariable": { - "type": "object", - "properties": { - "value": { - "$ref": "#/definitions/SubmodelElement_choice" - } - }, - "required": [ - "value" - ] - }, - "Property": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "value": { - "type": "string" - }, - "valueId": { - "$ref": "#/definitions/Reference" - }, - "modelType": { - "const": "Property" - } - }, - "required": [ - "valueType" - ] - } - ] - }, - "Qualifiable": { - "type": "object", - "properties": { - "qualifiers": { - "type": "array", - "items": { - "$ref": "#/definitions/Qualifier" - }, - "minItems": 1 - }, - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ - "modelType" - ] - }, - "Qualifier": { - "allOf": [ - { - "$ref": "#/definitions/HasSemantics" - }, - { - "properties": { - "kind": { - "$ref": "#/definitions/QualifierKind" - }, - "type": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "value": { - "type": "string" - }, - "valueId": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "type", - "valueType" - ] - } - ] - }, - "QualifierKind": { - "type": "string", - "enum": [ - "ConceptQualifier", - "TemplateQualifier", - "ValueQualifier" - ] - }, - "Range": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "valueType": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "min": { - "type": "string" - }, - "max": { - "type": "string" - }, - "modelType": { - "const": "Range" - } - }, - "required": [ - "valueType" - ] - } - ] - }, - "Referable": { - "allOf": [ - { - "$ref": "#/definitions/HasExtensions" - }, - { - "properties": { - "category": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "idShort": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 128 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$" - } - ] - }, - "displayName": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringNameType" - }, - "minItems": 1 - }, - "description": { - "type": "array", - "items": { - "$ref": "#/definitions/LangStringTextType" - }, - "minItems": 1 - }, - "modelType": { - "$ref": "#/definitions/ModelType" - } - }, - "required": [ - "modelType" - ] - } - ] - }, - "Reference": { - "type": "object", - "properties": { - "type": { - "$ref": "#/definitions/ReferenceTypes" - }, - "referredSemanticId": { - "$ref": "#/definitions/Reference" - }, - "keys": { - "type": "array", - "items": { - "$ref": "#/definitions/Key" - }, - "minItems": 1 - } - }, - "required": [ - "type", - "keys" - ] - }, - "ReferenceElement": { - "allOf": [ - { - "$ref": "#/definitions/DataElement" - }, - { - "properties": { - "value": { - "$ref": "#/definitions/Reference" - }, - "modelType": { - "const": "ReferenceElement" - } - } - } - ] - }, - "ReferenceTypes": { - "type": "string", - "enum": [ - "ExternalReference", - "ModelReference" - ] - }, - "RelationshipElement": { - "allOf": [ - { - "$ref": "#/definitions/RelationshipElement_abstract" - }, - { - "properties": { - "modelType": { - "const": "RelationshipElement" - } - } - } - ] - }, - "RelationshipElement_abstract": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "first": { - "$ref": "#/definitions/Reference" - }, - "second": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "first", - "second" - ] - } - ] - }, - "RelationshipElement_choice": { - "oneOf": [ - { - "$ref": "#/definitions/RelationshipElement" - }, - { - "$ref": "#/definitions/AnnotatedRelationshipElement" - } - ] - }, - "Resource": { - "type": "object", - "properties": { - "path": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 2000 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$" - } - ] - }, - "contentType": { - "type": "string", - "allOf": [ - { - "minLength": 1, - "maxLength": 100 - }, - { - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - { - "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$" - } - ] - } - }, - "required": [ - "path" - ] - }, - "SpecificAssetId": { - "allOf": [ - { - "$ref": "#/definitions/HasSemantics" - }, - { - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "externalSubjectId": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "name", - "value" - ] - } - ] - }, - "StateOfEvent": { - "type": "string", - "enum": [ - "off", - "on" - ] - }, - "Submodel": { - "allOf": [ - { - "$ref": "#/definitions/Identifiable" - }, - { - "$ref": "#/definitions/HasKind" - }, - { - "$ref": "#/definitions/HasSemantics" - }, - { - "$ref": "#/definitions/Qualifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - }, - { - "properties": { - "submodelElements": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement_choice" - }, - "minItems": 1 - }, - "modelType": { - "const": "Submodel" - } - } - } - ] - }, - "SubmodelElement": { - "allOf": [ - { - "$ref": "#/definitions/Referable" - }, - { - "$ref": "#/definitions/HasSemantics" - }, - { - "$ref": "#/definitions/Qualifiable" - }, - { - "$ref": "#/definitions/HasDataSpecification" - } - ] - }, - "SubmodelElementCollection": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement_choice" - }, - "minItems": 1 - }, - "modelType": { - "const": "SubmodelElementCollection" - } - } - } - ] - }, - "SubmodelElementList": { - "allOf": [ - { - "$ref": "#/definitions/SubmodelElement" - }, - { - "properties": { - "orderRelevant": { - "type": "boolean" - }, - "semanticIdListElement": { - "$ref": "#/definitions/Reference" - }, - "typeValueListElement": { - "$ref": "#/definitions/AasSubmodelElements" - }, - "valueTypeListElement": { - "$ref": "#/definitions/DataTypeDefXsd" - }, - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/SubmodelElement_choice" - }, - "minItems": 1 - }, - "modelType": { - "const": "SubmodelElementList" - } - }, - "required": [ - "typeValueListElement" - ] - } - ] - }, - "SubmodelElement_choice": { - "oneOf": [ - { - "$ref": "#/definitions/RelationshipElement" - }, - { - "$ref": "#/definitions/AnnotatedRelationshipElement" - }, - { - "$ref": "#/definitions/BasicEventElement" - }, - { - "$ref": "#/definitions/Blob" - }, - { - "$ref": "#/definitions/Capability" - }, - { - "$ref": "#/definitions/Entity" - }, - { - "$ref": "#/definitions/File" - }, - { - "$ref": "#/definitions/MultiLanguageProperty" - }, - { - "$ref": "#/definitions/Operation" - }, - { - "$ref": "#/definitions/Property" - }, - { - "$ref": "#/definitions/Range" - }, - { - "$ref": "#/definitions/ReferenceElement" - }, - { - "$ref": "#/definitions/SubmodelElementCollection" - }, - { - "$ref": "#/definitions/SubmodelElementList" - } - ] - }, - "ValueList": { - "type": "object", - "properties": { - "valueReferencePairs": { - "type": "array", - "items": { - "$ref": "#/definitions/ValueReferencePair" - }, - "minItems": 1 - } - }, - "required": [ - "valueReferencePairs" - ] - }, - "ValueReferencePair": { - "type": "object", - "properties": { - "value": { - "type": "string", - "minLength": 1, - "maxLength": 2000, - "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" - }, - "valueId": { - "$ref": "#/definitions/Reference" - } - }, - "required": [ - "value", - "valueId" - ] - } - } -} \ No newline at end of file diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 95a0dd4..874f8f0 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -18,7 +18,7 @@ example_aas_mandatory_attributes, example_submodel_template, create_example -JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasJSONSchema.json') +JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), '../schemas/aasJSONSchema.json') class JsonSerializationTest(unittest.TestCase): @@ -48,6 +48,11 @@ def test_random_object_serialization(self) -> None: class JsonSerializationSchemaTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not os.path.exists(JSON_SCHEMA_FILE): + raise unittest.SkipTest(f"JSON Schema does not exist at {JSON_SCHEMA_FILE}, skipping test") + def test_random_object_serialization(self) -> None: aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) diff --git a/test/adapter/xml/aasXMLSchema.xsd b/test/adapter/xml/aasXMLSchema.xsd deleted file mode 100644 index 25d7a52..0000000 --- a/test/adapter/xml/aasXMLSchema.xsd +++ /dev/null @@ -1,1344 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 178c130..0ca44a7 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -17,7 +17,7 @@ example_submodel_template, example_aas_mandatory_attributes -XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'aasXMLSchema.xsd') +XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), '../schemas/aasXMLSchema.xsd') class XMLSerializationTest(unittest.TestCase): @@ -48,6 +48,11 @@ def test_random_object_serialization(self) -> None: class XMLSerializationSchemaTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not os.path.exists(XML_SCHEMA_FILE): + raise unittest.SkipTest(f"XSD schema does not exist at {XML_SCHEMA_FILE}, skipping test") + def test_random_object_serialization(self) -> None: aas_identifier = "AAS1" submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) From e11c6cc14e93677a3bd2ffcc16b35e8d244cd945 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Wed, 10 Apr 2024 16:50:36 +0200 Subject: [PATCH 438/474] Update Copyright Notices (#224) This PR fixes the outdated `NOTICE`. While doing that, I `notice`d, that the years in the copyright strings were outdated as well, so I updated them (using the `/etc/scripts/set_copyright_year.sh`) In the future, we should create a recurring task that makes us update the years at least once a year. Maybe it should also become a task before publishing a new release? Fixes #196 Depends on #235 --- test/adapter/aasx/test_aasx.py | 22 ++++++++----------- .../adapter/json/test_json_deserialization.py | 2 +- test/adapter/json/test_json_serialization.py | 2 +- ...test_json_serialization_deserialization.py | 2 +- test/adapter/xml/test_xml_deserialization.py | 2 +- test/adapter/xml/test_xml_serialization.py | 2 +- .../test_xml_serialization_deserialization.py | 2 +- 7 files changed, 15 insertions(+), 19 deletions(-) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 61b3230..2fe4e0f 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -1,13 +1,9 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2022 the Eclipse BaSyx Authors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: MIT import datetime import hashlib import io @@ -17,9 +13,9 @@ import warnings import pyecma376_2 -from aas import model -from aas.adapter import aasx -from aas.examples.data import example_aas, _helper, example_aas_mandatory_attributes +from basyx.aas import model +from basyx.aas.adapter import aasx +from basyx.aas.examples.data import example_aas, example_aas_mandatory_attributes, _helper class TestAASXUtils(unittest.TestCase): @@ -71,7 +67,7 @@ def test_writing_reading_example_aas(self) -> None: # Create OPC/AASX core properties cp = pyecma376_2.OPCCoreProperties() cp.created = datetime.datetime.now() - cp.creator = "PyI40AAS Testing Framework" + cp.creator = "Eclipse BaSyx Python Testing Framework" # Write AASX file for write_json in (False, True): @@ -109,7 +105,7 @@ def test_writing_reading_example_aas(self) -> None: self.assertIsInstance(new_cp.created, datetime.datetime) assert isinstance(new_cp.created, datetime.datetime) # to make mypy happy self.assertAlmostEqual(new_cp.created, cp.created, delta=datetime.timedelta(milliseconds=20)) - self.assertEqual(new_cp.creator, "PyI40AAS Testing Framework") + self.assertEqual(new_cp.creator, "Eclipse BaSyx Python Testing Framework") self.assertIsNone(new_cp.lastModifiedBy) # Check files diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 7f127be..28da288 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. diff --git a/test/adapter/json/test_json_serialization.py b/test/adapter/json/test_json_serialization.py index 874f8f0..3e9240a 100644 --- a/test/adapter/json/test_json_serialization.py +++ b/test/adapter/json/test_json_serialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/test/adapter/json/test_json_serialization_deserialization.py index 351b718..9b016e1 100644 --- a/test/adapter/json/test_json_serialization_deserialization.py +++ b/test/adapter/json/test_json_serialization_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index dd0e48b..aebc3ad 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 0ca44a7..6d0e544 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index 919092b..2e06c44 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 the Eclipse BaSyx Authors +# Copyright (c) 2023 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. From a8f33b7a17dc16a65f19bf69c6ae21490fa00258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 14:56:16 +0200 Subject: [PATCH 439/474] adapter.aasx: allow deleting files from `SupplementaryFileContainer` `AbstractSupplementaryFileContainer` and `DictSupplementaryFileContainer` are extended by a `delete_file()` method, that allows deleting files from them. Since different files may have the same content, references to the files contents in `DictSupplementaryFileContainer._store` are tracked via `_store_refcount`. A files contents are only deleted from `_store`, if all filenames referring to these these contents are deleted, i.e. if the refcount reaches 0. --- test/adapter/aasx/test_aasx.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 2fe4e0f..132a935 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -54,6 +54,23 @@ def test_supplementary_file_container(self) -> None: container.write_file("/TestFile.pdf", file_content) self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), "78450a66f59d74c073bf6858db340090ea72a8b1") + # Add same file again with different content_type to test reference counting + with open(__file__, 'rb') as f: + duplicate_file = container.add_file("/TestFile.pdf", f, "image/jpeg") + self.assertIn(duplicate_file, container) + + # Delete files + container.delete_file(new_name) + self.assertNotIn(new_name, container) + # File should still be accessible + container.write_file(duplicate_file, file_content) + + container.delete_file(duplicate_file) + self.assertNotIn(duplicate_file, container) + # File should now not be accessible anymore + with self.assertRaises(KeyError): + container.write_file(duplicate_file, file_content) + class AASXWriterTest(unittest.TestCase): def test_writing_reading_example_aas(self) -> None: From e189f4b005b20631442e7fc6b2b6c85608fd31c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 20 Jun 2024 00:15:57 +0200 Subject: [PATCH 440/474] adapter.xml, test.adapter.xml: add lxml typechecking Now that we require `lxml-stubs`, we can remove the `type: ignore` coments from the `lxml` imports. To make mypy happy, many typehints are adjusted, mostly `etree.Element` -> `etree._Element`. --- test/adapter/xml/test_xml_deserialization.py | 4 ++-- test/adapter/xml/test_xml_serialization.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index aebc3ad..8275dc0 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -13,7 +13,7 @@ from basyx.aas.adapter.xml import StrictAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, \ read_aas_xml_file_into, read_aas_xml_element from basyx.aas.adapter._generic import XML_NS_MAP -from lxml import etree # type: ignore +from lxml import etree from typing import Iterable, Type, Union @@ -434,7 +434,7 @@ def __init__(self, *args, **kwargs): class EnhancedAASDecoder(StrictAASFromXmlDecoder): @classmethod - def construct_submodel(cls, element: etree.Element, object_class=EnhancedSubmodel, **kwargs) \ + def construct_submodel(cls, element: etree._Element, object_class=EnhancedSubmodel, **kwargs) \ -> model.Submodel: return super().construct_submodel(element, object_class=object_class, **kwargs) diff --git a/test/adapter/xml/test_xml_serialization.py b/test/adapter/xml/test_xml_serialization.py index 6d0e544..11328f6 100644 --- a/test/adapter/xml/test_xml_serialization.py +++ b/test/adapter/xml/test_xml_serialization.py @@ -8,7 +8,7 @@ import os import unittest -from lxml import etree # type: ignore +from lxml import etree from basyx.aas import model from basyx.aas.adapter.xml import write_aas_xml_file, xml_serialization From 910d9a06e95d1f861dbc01f92bff3e37019ea300 Mon Sep 17 00:00:00 2001 From: Joshua Benning <54218874+JAB1305@users.noreply.github.com> Date: Tue, 16 Jul 2024 21:07:46 +0200 Subject: [PATCH 441/474] test_xml_deserialization: Add tests for `_tag_replace_namespace` (#284) Previously, the function `_tag_replace_namespace` in `adapter.xml.xml_deserialization` did not have corresponding unittests yet. This adds the tests for the most common use cases. --- test/adapter/xml/test_xml_deserialization.py | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 8275dc0..5d32797 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -12,6 +12,7 @@ from basyx.aas import model from basyx.aas.adapter.xml import StrictAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, \ read_aas_xml_file_into, read_aas_xml_element +from basyx.aas.adapter.xml.xml_deserialization import _tag_replace_namespace from basyx.aas.adapter._generic import XML_NS_MAP from lxml import etree from typing import Iterable, Type, Union @@ -449,3 +450,29 @@ def construct_submodel(cls, element: etree._Element, object_class=EnhancedSubmod self.assertIsInstance(submodel, EnhancedSubmodel) assert isinstance(submodel, EnhancedSubmodel) self.assertEqual(submodel.enhanced_attribute, "fancy!") + + +class TestTagReplaceNamespace(unittest.TestCase): + def test_known_namespace(self): + tag = '{https://admin-shell.io/aas/3/0}tag' + expected = 'aas:tag' + self.assertEqual(_tag_replace_namespace(tag, XML_NS_MAP), expected) + + def test_empty_prefix(self): + # Empty prefix should not be replaced as otherwise it would apply everywhere + tag = '{https://admin-shell.io/aas/3/0}tag' + nsmap = {"": "https://admin-shell.io/aas/3/0"} + expected = '{https://admin-shell.io/aas/3/0}tag' + self.assertEqual(_tag_replace_namespace(tag, nsmap), expected) + + def test_empty_namespace(self): + # Empty namespaces should also have no effect + tag = '{https://admin-shell.io/aas/3/0}tag' + nsmap = {"aas": ""} + expected = '{https://admin-shell.io/aas/3/0}tag' + self.assertEqual(_tag_replace_namespace(tag, nsmap), expected) + + def test_unknown_namespace(self): + tag = '{http://unknownnamespace.com}unknown' + expected = '{http://unknownnamespace.com}unknown' # Unknown namespace should remain unchanged + self.assertEqual(_tag_replace_namespace(tag, XML_NS_MAP), expected) From 2b3ab76fd3d85b5f05087a57cf475708d629225d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 18:33:38 +0200 Subject: [PATCH 442/474] http-api-oas: move to test/adapter/ directory improve filename use .yaml instead of .yml file extension (see https://yaml.org/faq.html) --- test/adapter/http-api-oas.yaml | 2141 ++++++++++++++++++++++++++++++++ 1 file changed, 2141 insertions(+) create mode 100644 test/adapter/http-api-oas.yaml diff --git a/test/adapter/http-api-oas.yaml b/test/adapter/http-api-oas.yaml new file mode 100644 index 0000000..13c4199 --- /dev/null +++ b/test/adapter/http-api-oas.yaml @@ -0,0 +1,2141 @@ +openapi: 3.0.0 +info: + version: "1" + title: PyI40AAS REST API + description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). + + + The git repository of this document is available [here](https://git.rwth-aachen.de/leon.moeller/pyi40aas-oas). + + + --- + + **General:** + + + Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. + + + This specification contains one exemplary `PATCH` route. In the future there should be a `PATCH` route for every `PUT` route, where it makes sense. + + + In our implementation the AAS Interface is available at `/aas/{identifier}` and the Submodel Interface at `/submodel/{identifier}`." + contact: + name: "Michael Thies, Torben Miny, Leon Möller" + license: + name: Use under Eclipse Public License 2.0 + url: "https://www.eclipse.org/legal/epl-2.0/" +servers: + - url: http://{authority}/{basePath}/{api-version} + description: This is the Server to access the Asset Administration Shell + variables: + authority: + default: localhost:8080 + description: The authority is the server url (made of IP-Address or DNS-Name, user information, and/or port information) of the hosting environment for the Asset Administration Shell + basePath: + default: api + description: The basePath variable is additional path information for the hosting environment. It may contain the name of an aggregation point like 'shells' and/or API version information and/or tenant-id information, etc. + api-version: + default: v1 + description: The Version of the API-Specification +paths: + "/": + get: + summary: Retrieves the stripped AssetAdministrationShell, without Submodel-References and Views. + operationId: ReadAAS + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/AASResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/AASResult" + tags: + - Asset Administration Shell Interface + "/submodels": + get: + summary: Returns all Submodel-References of the AssetAdministrationShell + operationId: ReadAASSubmodelReferences + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceListResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceListResult" + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new Submodel-Reference to the AssetAdministrationShell + operationId: CreateAASSubmodelReference + requestBody: + description: The Submodel-Reference to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Reference" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + headers: + Location: + description: The URL of the created Submodel-Reference + schema: + type: string + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "409": + description: Submodel-Reference already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "422": + description: Request body is not a Reference or not resolvable + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + tags: + - Asset Administration Shell Interface + "/submodels/{submodel-identifier}": + parameters: + - name: submodel-identifier + in: path + description: The Identifier of the referenced Submodel + required: true + schema: + type: string + get: + summary: Returns the Reference specified by submodel-identifier + operationId: ReadAASSubmodelReference + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "404": + description: AssetAdministrationShell not found or the specified Submodel is not referenced + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes the Reference specified by submodel-identifier + operationId: DeleteAASSubmodelReference + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: AssetAdministrationShell not found or the specified Submodel is not referenced + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Asset Administration Shell Interface + "/views": + get: + summary: Returns all Views of the AssetAdministrationShell + operationId: ReadAASViews + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewListResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewListResult" + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new View to the AssetAdministrationShell + operationId: CreateAASView + requestBody: + description: The View to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The URL of the created View + schema: + type: string + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: View with same idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + "/views/{view-idShort}": + parameters: + - name: view-idShort + in: path + description: The idShort of the View + required: true + schema: + type: string + get: + summary: Returns a specific View of the AssetAdministrationShell + operationId: ReadAASView + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + put: + summary: Updates a specific View of the AssetAdministrationShell + operationId: UpdateAASView + requestBody: + description: The View used to overwrite the existing View + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "201": + description: Success (idShort changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The new URL of the View + schema: + type: string + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + patch: + summary: Updates a specific View of the AssetAdministrationShell by only providing properties that should be changed + operationId: PatchAASView + requestBody: + description: The (partial) View used to update the existing View + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "201": + description: Success (idShort changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The new URL of the View + schema: + type: string + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes a specific View from the Asset Administration Shell + operationId: DeleteAASView + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Asset Administration Shell Interface + "//": + get: + summary: "Returns the stripped Submodel (without SubmodelElements and Constraints (property: qualifiers))" + operationId: ReadSubmodel + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelResult" + "404": + description: No Submodel found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelResult" + tags: + - Submodel Interface + "/constraints": + get: + summary: Returns all Constraints of the current Submodel + operationId: ReadSubmodelConstraints + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintListResult" + "404": + description: Submodel not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintListResult" + tags: + - Submodel Interface + post: + summary: Adds a new Constraint to the Submodel + operationId: CreateSubmodelConstraint + requestBody: + description: The Constraint to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Constraint" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + headers: + Location: + description: The URL of the created Constraint + schema: + type: string + "404": + description: Submodel not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "409": + description: "When trying to add a qualifier: Qualifier with same type already exists" + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "422": + description: Request body is not a valid Qualifier + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + "/constraints/{qualifier-type}": + parameters: + - name: qualifier-type + in: path + description: The type of the Qualifier + required: true + schema: + type: string + get: + summary: Retrieves a specific Qualifier of the Submodel's constraints (Formulas cannot be referred to yet) + operationId: ReadSubmodelConstraint + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "404": + description: Submodel or Constraint not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + put: + summary: Updates an existing Qualifier in the Submodel (Formulas cannot be referred to yet) + operationId: UpdateSubmodelConstraint + requestBody: + description: The Qualifier used to overwrite the existing Qualifier + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Qualifier" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "201": + description: Success (type changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + headers: + Location: + description: The new URL of the Qualifier + schema: + type: string + "404": + description: Submodel or Constraint not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "409": + description: type changed and new type already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "422": + description: Request body is not a valid Qualifier + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + delete: + summary: Deletes an existing Qualifier from the Submodel (Formulas cannot be referred to yet) + operationId: DeleteSubmodelConstraint + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: Submodel or Constraint not found + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Submodel Interface + "/submodelElements": + get: + summary: Returns all SubmodelElements of the current Submodel + operationId: ReadSubmodelSubmodelElements + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "404": + description: Submodel not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + tags: + - Submodel Interface + post: + summary: Adds a new SubmodelElement to the Submodel + operationId: CreateSubmodelSubmodelElement + requestBody: + description: The SubmodelElement to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElement" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The URL of the created SubmodelElement + schema: + type: string + "404": + description: Submodel not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "409": + description: SubmodelElement with same idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + tags: + - Submodel Interface + "/{idShort-path}": + parameters: + - name: idShort-path + in: path + description: A /-separated concatenation of !-prefixed idShorts + required: true + schema: + type: string + get: + summary: Returns the (stripped) (nested) SubmodelElement + operationId: ReadSubmodelSubmodelElement + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + tags: + - Submodel Interface + put: + summary: Updates a nested SubmodelElement + operationId: UpdateSubmodelSubmodelElement + requestBody: + description: The SubmodelElement used to overwrite the existing SubmodelElement + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElement" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "201": + description: Success (idShort changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The new URL of the SubmodelElement + schema: + type: string + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement **or** the type of the new SubmodelElement differs from the existing one + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + tags: + - Submodel Interface + delete: + summary: Deletes a specific (nested) SubmodelElement from the Submodel + operationId: DeleteSubmodelSubmodelElement + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Submodel Interface + "/{idShort-path}/value": + parameters: + - name: idShort-path + in: path + description: A /-separated concatenation of !-prefixed idShorts + required: true + schema: + type: string + get: + summary: If the (nested) SubmodelElement is a SubmodelElementCollection, return contained (stripped) SubmodelElements + operationId: ReadSubmodelSubmodelElementValue + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "400": + description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + tags: + - Submodel Interface + post: + summary: If the (nested) SubmodelElement is a SubmodelElementCollection, add a SubmodelElement to its value + operationId: CreateSubmodelSubmodelElementValue + requestBody: + description: The SubmodelElement to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElement" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The URL of the created SubmodelElement + schema: + type: string + "400": + description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "409": + description: SubmodelElement with same idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + tags: + - Submodel Interface + "/{idShort-path}/annotation": + parameters: + - name: idShort-path + in: path + description: A /-separated concatenation of !-prefixed idShorts + required: true + schema: + type: string + get: + summary: If the (nested) SubmodelElement is an AnnotatedRelationshipElement, return contained (stripped) SubmodelElements + operationId: ReadSubmodelSubmodelElementAnnotation + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "400": + description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + tags: + - Submodel Interface + post: + summary: If the (nested) SubmodelElement is an AnnotatedRelationshipElement, add a SubmodelElement to its annotation + operationId: CreateSubmodelSubmodelElementAnnotation + requestBody: + description: The SubmodelElement to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElement" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The URL of the created SubmodelElement + schema: + type: string + "400": + description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "409": + description: SubmodelElement with given idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + tags: + - Submodel Interface + "/{idShort-path}/statement": + parameters: + - name: idShort-path + in: path + description: A /-separated concatenation of !-prefixed idShorts + required: true + schema: + type: string + get: + summary: If the (nested) SubmodelElement is an Entity, return contained (stripped) SubmodelElements + operationId: ReadSubmodelSubmodelElementStatement + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "400": + description: SubmodelElement exists, but is not an Entity, so /statement is not possible. + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + tags: + - Submodel Interface + post: + summary: If the (nested) SubmodelElement is an Entity, add a SubmodelElement to its statement + operationId: CreateSubmodelSubmodelElementStatement + requestBody: + description: The SubmodelElement to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElement" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The URL of the created SubmodelElement + schema: + type: string + "400": + description: SubmodelElement exists, but is not an Entity, so /statement is not possible + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "409": + description: SubmodelElement with same idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + tags: + - Submodel Interface + "/{idShort-path}/constraints": + parameters: + - name: idShort-path + in: path + description: A /-separated concatenation of !-prefixed idShorts + required: true + schema: + type: string + get: + summary: Returns all Constraints of the (nested) SubmodelElement + operationId: ReadSubmodelSubmodelElementConstraints + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintListResult" + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintListResult" + tags: + - Submodel Interface + post: + summary: Adds a new Constraint to the (nested) SubmodelElement + operationId: CreateSubmodelSubmodelElementConstraint + requestBody: + description: The Constraint to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Constraint" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + headers: + Location: + description: The URL of the created Constraint + schema: + type: string + "404": + description: Submodel or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "409": + description: "When trying to add a qualifier: Qualifier with specified type already exists" + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "422": + description: Request body is not a valid Qualifier + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + "/{idShort-path}/constraints/{qualifier-type}": + parameters: + - name: idShort-path + in: path + description: A /-separated concatenation of !-prefixed idShorts + required: true + schema: + type: string + - name: qualifier-type + in: path + description: "Type of the qualifier" + required: true + schema: + type: string + get: + summary: Retrieves a specific Qualifier of the (nested) SubmodelElements's Constraints (Formulas cannot be referred to yet) + operationId: ReadSubmodelSubmodelElementConstraint + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "404": + description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + put: + summary: Updates an existing Qualifier in the (nested) SubmodelElement (Formulas cannot be referred to yet) + operationId: UpdateSubmodelSubmodelElementConstraint + requestBody: + description: The Qualifier used to overwrite the existing Qualifier + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Qualifier" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "201": + description: Success (type changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + headers: + Location: + description: The new URL of the Qualifier + schema: + type: string + "404": + description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "409": + description: type changed and new type already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "422": + description: Request body is not a valid Qualifier + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + delete: + summary: Deletes an existing Qualifier from the (nested) SubmodelElement (Formulas cannot be referred to yet) + operationId: DeleteSubmodelSubmodelElementConstraint + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Submodel Interface +components: + schemas: + BaseResult: + type: object + properties: + success: + type: boolean + readOnly: true + error: + type: object + nullable: true + readOnly: true + properties: + type: + enum: + - Unspecified + - Debug + - Information + - Warning + - Error + - Fatal + - Exception + type: string + code: + type: string + text: + type: string + data: + nullable: true + readOnly: true + type: object + AASResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedAssetAdministrationShell" + ReferenceResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/Reference" + ReferenceListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/Reference" + ViewResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/View" + ViewListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/View" + SubmodelResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedSubmodel" + SubmodelElementResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedSubmodelElement" + SubmodelElementListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/StrippedSubmodelElement" + ConstraintResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/Constraint" + ConstraintListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/Constraint" + StrippedAssetAdministrationShell: + allOf: + - $ref: "#/components/schemas/AssetAdministrationShell" + - properties: + views: + not: {} + submodels: + not: {} + conceptDictionaries: + not: {} + StrippedSubmodel: + allOf: + - $ref: "#/components/schemas/Submodel" + - properties: + submodelElements: + not: {} + qualifiers: + not: {} + StrippedSubmodelElement: + allOf: + - $ref: "#/components/schemas/SubmodelElement" + - properties: + qualifiers: + not: {} + Referable: + allOf: + - $ref: '#/components/schemas/HasExtensions' + - properties: + idShort: + type: string + category: + type: string + displayName: + type: string + description: + type: array + items: + $ref: '#/components/schemas/LangString' + modelType: + $ref: '#/components/schemas/ModelType' + required: + - modelType + Identifiable: + allOf: + - $ref: '#/components/schemas/Referable' + - properties: + identification: + $ref: '#/components/schemas/Identifier' + administration: + $ref: '#/components/schemas/AdministrativeInformation' + required: + - identification + Qualifiable: + type: object + properties: + qualifiers: + type: array + items: + $ref: '#/components/schemas/Constraint' + HasSemantics: + type: object + properties: + semanticId: + $ref: '#/components/schemas/Reference' + HasDataSpecification: + type: object + properties: + embeddedDataSpecifications: + type: array + items: + $ref: '#/components/schemas/EmbeddedDataSpecification' + HasExtensions: + type: object + properties: + extensions: + type: array + items: + $ref: '#/components/schemas/Extension' + Extension: + allOf: + - $ref: '#/components/schemas/HasSemantics' + - properties: + name: + type: string + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + value: + type: string + refersTo: + $ref: '#/components/schemas/Reference' + required: + - name + AssetAdministrationShell: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - properties: + derivedFrom: + $ref: '#/components/schemas/Reference' + assetInformation: + $ref: '#/components/schemas/AssetInformation' + submodels: + type: array + items: + $ref: '#/components/schemas/Reference' + views: + type: array + items: + $ref: '#/components/schemas/View' + security: + $ref: '#/components/schemas/Security' + required: + - assetInformation + Identifier: + type: object + properties: + id: + type: string + idType: + $ref: '#/components/schemas/KeyType' + required: + - id + - idType + KeyType: + type: string + enum: + - Custom + - IRDI + - IRI + - IdShort + - FragmentId + AdministrativeInformation: + type: object + properties: + version: + type: string + revision: + type: string + LangString: + type: object + properties: + language: + type: string + text: + type: string + required: + - language + - text + Reference: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/Key' + required: + - keys + Key: + type: object + properties: + type: + $ref: '#/components/schemas/KeyElements' + idType: + $ref: '#/components/schemas/KeyType' + value: + type: string + required: + - type + - idType + - value + KeyElements: + type: string + enum: + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + ModelTypes: + type: string + enum: + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + - Constraint + - Formula + - Qualifier + ModelType: + type: object + properties: + name: + $ref: '#/components/schemas/ModelTypes' + required: + - name + EmbeddedDataSpecification: + type: object + properties: + dataSpecification: + $ref: '#/components/schemas/Reference' + dataSpecificationContent: + $ref: '#/components/schemas/DataSpecificationContent' + required: + - dataSpecification + - dataSpecificationContent + DataSpecificationContent: + oneOf: + - $ref: '#/components/schemas/DataSpecificationIEC61360Content' + - $ref: '#/components/schemas/DataSpecificationPhysicalUnitContent' + DataSpecificationPhysicalUnitContent: + type: object + properties: + unitName: + type: string + unitSymbol: + type: string + definition: + type: array + items: + $ref: '#/components/schemas/LangString' + siNotation: + type: string + siName: + type: string + dinNotation: + type: string + eceName: + type: string + eceCode: + type: string + nistName: + type: string + sourceOfDefinition: + type: string + conversionFactor: + type: string + registrationAuthorityId: + type: string + supplier: + type: string + required: + - unitName + - unitSymbol + - definition + DataSpecificationIEC61360Content: + allOf: + - $ref: '#/components/schemas/ValueObject' + - type: object + properties: + dataType: + enum: + - DATE + - STRING + - STRING_TRANSLATABLE + - REAL_MEASURE + - REAL_COUNT + - REAL_CURRENCY + - BOOLEAN + - URL + - RATIONAL + - RATIONAL_MEASURE + - TIME + - TIMESTAMP + - INTEGER_COUNT + - INTEGER_MEASURE + - INTEGER_CURRENCY + definition: + type: array + items: + $ref: '#/components/schemas/LangString' + preferredName: + type: array + items: + $ref: '#/components/schemas/LangString' + shortName: + type: array + items: + $ref: '#/components/schemas/LangString' + sourceOfDefinition: + type: string + symbol: + type: string + unit: + type: string + unitId: + $ref: '#/components/schemas/Reference' + valueFormat: + type: string + valueList: + $ref: '#/components/schemas/ValueList' + levelType: + type: array + items: + $ref: '#/components/schemas/LevelType' + required: + - preferredName + LevelType: + type: string + enum: + - Min + - Max + - Nom + - Typ + ValueList: + type: object + properties: + valueReferencePairTypes: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/ValueReferencePairType' + required: + - valueReferencePairTypes + ValueReferencePairType: + allOf: + - $ref: '#/components/schemas/ValueObject' + ValueObject: + type: object + properties: + value: + type: string + valueId: + $ref: '#/components/schemas/Reference' + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + Asset: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + AssetInformation: + allOf: + - properties: + assetKind: + $ref: '#/components/schemas/AssetKind' + globalAssetId: + $ref: '#/components/schemas/Reference' + externalAssetIds: + type: array + items: + $ref: '#/components/schemas/IdentifierKeyValuePair' + billOfMaterial: + type: array + items: + $ref: '#/components/schemas/Reference' + thumbnail: + $ref: '#/components/schemas/File' + required: + - assetKind + IdentifierKeyValuePair: + allOf: + - $ref: '#/components/schemas/HasSemantics' + - properties: + key: + type: string + value: + type: string + subjectId: + $ref: '#/components/schemas/Reference' + required: + - key + - value + - subjectId + AssetKind: + type: string + enum: + - Type + - Instance + ModelingKind: + type: string + enum: + - Template + - Instance + Submodel: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/Qualifiable' + - $ref: '#/components/schemas/HasSemantics' + - properties: + kind: + $ref: '#/components/schemas/ModelingKind' + submodelElements: + type: array + items: + $ref: '#/components/schemas/SubmodelElement' + Constraint: + type: object + properties: + modelType: + $ref: '#/components/schemas/ModelType' + required: + - modelType + Operation: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + inputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + outputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + inoutputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + OperationVariable: + type: object + properties: + value: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/Capability' + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Event' + - $ref: '#/components/schemas/BasicEvent' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Operation' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + - $ref: '#/components/schemas/RelationshipElement' + - $ref: '#/components/schemas/SubmodelElementCollection' + required: + - value + SubmodelElement: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/HasSemantics' + - $ref: '#/components/schemas/Qualifiable' + - properties: + kind: + $ref: '#/components/schemas/ModelingKind' + idShort: + type: string + required: + - idShort + Event: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + BasicEvent: + allOf: + - $ref: '#/components/schemas/Event' + - properties: + observed: + $ref: '#/components/schemas/Reference' + required: + - observed + EntityType: + type: string + enum: + - CoManagedEntity + - SelfManagedEntity + Entity: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + statements: + type: array + items: + $ref: '#/components/schemas/SubmodelElement' + entityType: + $ref: '#/components/schemas/EntityType' + globalAssetId: + $ref: '#/components/schemas/Reference' + specificAssetIds: + $ref: '#/components/schemas/IdentifierKeyValuePair' + required: + - entityType + View: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/HasSemantics' + - properties: + containedElements: + type: array + items: + $ref: '#/components/schemas/Reference' + ConceptDescription: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - properties: + isCaseOf: + type: array + items: + $ref: '#/components/schemas/Reference' + Capability: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + Property: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - $ref: '#/components/schemas/ValueObject' + Range: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + min: + type: string + max: + type: string + required: + - valueType + MultiLanguageProperty: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: array + items: + $ref: '#/components/schemas/LangString' + valueId: + $ref: '#/components/schemas/Reference' + File: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + Blob: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + ReferenceElement: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + $ref: '#/components/schemas/Reference' + SubmodelElementCollection: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/Capability' + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Event' + - $ref: '#/components/schemas/BasicEvent' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Operation' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + - $ref: '#/components/schemas/RelationshipElement' + - $ref: '#/components/schemas/SubmodelElementCollection' + allowDuplicates: + type: boolean + ordered: + type: boolean + RelationshipElement: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + first: + $ref: '#/components/schemas/Reference' + second: + $ref: '#/components/schemas/Reference' + required: + - first + - second + AnnotatedRelationshipElement: + allOf: + - $ref: '#/components/schemas/RelationshipElement' + - properties: + annotation: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + Qualifier: + allOf: + - $ref: '#/components/schemas/Constraint' + - $ref: '#/components/schemas/HasSemantics' + - $ref: '#/components/schemas/ValueObject' + - properties: + type: + type: string + required: + - type + Formula: + allOf: + - $ref: '#/components/schemas/Constraint' + - properties: + dependsOn: + type: array + items: + $ref: '#/components/schemas/Reference' + Security: + type: object + properties: + accessControlPolicyPoints: + $ref: '#/components/schemas/AccessControlPolicyPoints' + certificate: + type: array + items: + oneOf: + - $ref: '#/components/schemas/BlobCertificate' + requiredCertificateExtension: + type: array + items: + $ref: '#/components/schemas/Reference' + required: + - accessControlPolicyPoints + Certificate: + type: object + BlobCertificate: + allOf: + - $ref: '#/components/schemas/Certificate' + - properties: + blobCertificate: + $ref: '#/components/schemas/Blob' + containedExtension: + type: array + items: + $ref: '#/components/schemas/Reference' + lastCertificate: + type: boolean + AccessControlPolicyPoints: + type: object + properties: + policyAdministrationPoint: + $ref: '#/components/schemas/PolicyAdministrationPoint' + policyDecisionPoint: + $ref: '#/components/schemas/PolicyDecisionPoint' + policyEnforcementPoint: + $ref: '#/components/schemas/PolicyEnforcementPoint' + policyInformationPoints: + $ref: '#/components/schemas/PolicyInformationPoints' + required: + - policyAdministrationPoint + - policyDecisionPoint + - policyEnforcementPoint + PolicyAdministrationPoint: + type: object + properties: + localAccessControl: + $ref: '#/components/schemas/AccessControl' + externalAccessControl: + type: boolean + required: + - externalAccessControl + PolicyInformationPoints: + type: object + properties: + internalInformationPoint: + type: array + items: + $ref: '#/components/schemas/Reference' + externalInformationPoint: + type: boolean + required: + - externalInformationPoint + PolicyEnforcementPoint: + type: object + properties: + externalPolicyEnforcementPoint: + type: boolean + required: + - externalPolicyEnforcementPoint + PolicyDecisionPoint: + type: object + properties: + externalPolicyDecisionPoints: + type: boolean + required: + - externalPolicyDecisionPoints + AccessControl: + type: object + properties: + selectableSubjectAttributes: + $ref: '#/components/schemas/Reference' + defaultSubjectAttributes: + $ref: '#/components/schemas/Reference' + selectablePermissions: + $ref: '#/components/schemas/Reference' + defaultPermissions: + $ref: '#/components/schemas/Reference' + selectableEnvironmentAttributes: + $ref: '#/components/schemas/Reference' + defaultEnvironmentAttributes: + $ref: '#/components/schemas/Reference' + accessPermissionRule: + type: array + items: + $ref: '#/components/schemas/AccessPermissionRule' + AccessPermissionRule: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/Qualifiable' + - properties: + targetSubjectAttributes: + type: array + items: + $ref: '#/components/schemas/SubjectAttributes' + minItems: 1 + permissionsPerObject: + type: array + items: + $ref: '#/components/schemas/PermissionsPerObject' + required: + - targetSubjectAttributes + SubjectAttributes: + type: object + properties: + subjectAttributes: + type: array + items: + $ref: '#/components/schemas/Reference' + minItems: 1 + PermissionsPerObject: + type: object + properties: + object: + $ref: '#/components/schemas/Reference' + targetObjectAttributes: + $ref: '#/components/schemas/ObjectAttributes' + permission: + type: array + items: + $ref: '#/components/schemas/Permission' + ObjectAttributes: + type: object + properties: + objectAttribute: + type: array + items: + $ref: '#/components/schemas/Property' + minItems: 1 + Permission: + type: object + properties: + permission: + $ref: '#/components/schemas/Reference' + kindOfPermission: + type: string + enum: + - Allow + - Deny + - NotApplicable + - Undefined + required: + - permission + - kindOfPermission From 33f8c3eeef0fb31f383d6a37e9c5be19b9225cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 22:17:05 +0200 Subject: [PATCH 443/474] http-api-oas: split into aas and submodel interface --- test/adapter/http-api-oas-aas.yaml | 1399 +++++++++++++++++ ...pi-oas.yaml => http-api-oas-submodel.yaml} | 340 +--- 2 files changed, 1401 insertions(+), 338 deletions(-) create mode 100644 test/adapter/http-api-oas-aas.yaml rename test/adapter/{http-api-oas.yaml => http-api-oas-submodel.yaml} (83%) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml new file mode 100644 index 0000000..2d6b062 --- /dev/null +++ b/test/adapter/http-api-oas-aas.yaml @@ -0,0 +1,1399 @@ +openapi: 3.0.0 +info: + version: "1" + title: PyI40AAS REST API + description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). + + + **AAS Interface** + + + Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`." + contact: + name: "Michael Thies, Torben Miny, Leon Möller" + license: + name: Use under Eclipse Public License 2.0 + url: "https://www.eclipse.org/legal/epl-2.0/" +servers: + - url: http://{authority}/{basePath}/{api-version} + description: This is the Server to access the Asset Administration Shell + variables: + authority: + default: localhost:8080 + description: The authority is the server url (made of IP-Address or DNS-Name, user information, and/or port information) of the hosting environment for the Asset Administration Shell + basePath: + default: api + description: The basePath variable is additional path information for the hosting environment. It may contain the name of an aggregation point like 'shells' and/or API version information and/or tenant-id information, etc. + api-version: + default: v1 + description: The Version of the API-Specification +paths: + "/": + get: + summary: Retrieves the stripped AssetAdministrationShell, without Submodel-References and Views. + operationId: ReadAAS + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/AASResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/AASResult" + tags: + - Asset Administration Shell Interface + "/submodels": + get: + summary: Returns all Submodel-References of the AssetAdministrationShell + operationId: ReadAASSubmodelReferences + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceListResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceListResult" + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new Submodel-Reference to the AssetAdministrationShell + operationId: CreateAASSubmodelReference + requestBody: + description: The Submodel-Reference to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Reference" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + headers: + Location: + description: The URL of the created Submodel-Reference + schema: + type: string + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "409": + description: Submodel-Reference already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "422": + description: Request body is not a Reference or not resolvable + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + tags: + - Asset Administration Shell Interface + "/submodels/{submodel-identifier}": + parameters: + - name: submodel-identifier + in: path + description: The Identifier of the referenced Submodel + required: true + schema: + type: string + get: + summary: Returns the Reference specified by submodel-identifier + operationId: ReadAASSubmodelReference + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "404": + description: AssetAdministrationShell not found or the specified Submodel is not referenced + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes the Reference specified by submodel-identifier + operationId: DeleteAASSubmodelReference + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: AssetAdministrationShell not found or the specified Submodel is not referenced + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Asset Administration Shell Interface + "/views": + get: + summary: Returns all Views of the AssetAdministrationShell + operationId: ReadAASViews + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewListResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewListResult" + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new View to the AssetAdministrationShell + operationId: CreateAASView + requestBody: + description: The View to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The URL of the created View + schema: + type: string + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: View with same idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + "/views/{view-idShort}": + parameters: + - name: view-idShort + in: path + description: The idShort of the View + required: true + schema: + type: string + get: + summary: Returns a specific View of the AssetAdministrationShell + operationId: ReadAASView + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + put: + summary: Updates a specific View of the AssetAdministrationShell + operationId: UpdateAASView + requestBody: + description: The View used to overwrite the existing View + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "201": + description: Success (idShort changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The new URL of the View + schema: + type: string + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + patch: + summary: Updates a specific View of the AssetAdministrationShell by only providing properties that should be changed + operationId: PatchAASView + requestBody: + description: The (partial) View used to update the existing View + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "201": + description: Success (idShort changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The new URL of the View + schema: + type: string + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes a specific View from the Asset Administration Shell + operationId: DeleteAASView + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Asset Administration Shell Interface +components: + schemas: + BaseResult: + type: object + properties: + success: + type: boolean + readOnly: true + error: + type: object + nullable: true + readOnly: true + properties: + type: + enum: + - Unspecified + - Debug + - Information + - Warning + - Error + - Fatal + - Exception + type: string + code: + type: string + text: + type: string + data: + nullable: true + readOnly: true + type: object + AASResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedAssetAdministrationShell" + ReferenceResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/Reference" + ReferenceListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/Reference" + ViewResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/View" + ViewListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/View" + SubmodelResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedSubmodel" + SubmodelElementResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedSubmodelElement" + SubmodelElementListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/StrippedSubmodelElement" + ConstraintResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/Constraint" + ConstraintListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/Constraint" + StrippedAssetAdministrationShell: + allOf: + - $ref: "#/components/schemas/AssetAdministrationShell" + - properties: + views: + not: {} + submodels: + not: {} + conceptDictionaries: + not: {} + StrippedSubmodel: + allOf: + - $ref: "#/components/schemas/Submodel" + - properties: + submodelElements: + not: {} + qualifiers: + not: {} + StrippedSubmodelElement: + allOf: + - $ref: "#/components/schemas/SubmodelElement" + - properties: + qualifiers: + not: {} + Referable: + allOf: + - $ref: '#/components/schemas/HasExtensions' + - properties: + idShort: + type: string + category: + type: string + displayName: + type: string + description: + type: array + items: + $ref: '#/components/schemas/LangString' + modelType: + $ref: '#/components/schemas/ModelType' + required: + - modelType + Identifiable: + allOf: + - $ref: '#/components/schemas/Referable' + - properties: + identification: + $ref: '#/components/schemas/Identifier' + administration: + $ref: '#/components/schemas/AdministrativeInformation' + required: + - identification + Qualifiable: + type: object + properties: + qualifiers: + type: array + items: + $ref: '#/components/schemas/Constraint' + HasSemantics: + type: object + properties: + semanticId: + $ref: '#/components/schemas/Reference' + HasDataSpecification: + type: object + properties: + embeddedDataSpecifications: + type: array + items: + $ref: '#/components/schemas/EmbeddedDataSpecification' + HasExtensions: + type: object + properties: + extensions: + type: array + items: + $ref: '#/components/schemas/Extension' + Extension: + allOf: + - $ref: '#/components/schemas/HasSemantics' + - properties: + name: + type: string + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + value: + type: string + refersTo: + $ref: '#/components/schemas/Reference' + required: + - name + AssetAdministrationShell: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - properties: + derivedFrom: + $ref: '#/components/schemas/Reference' + assetInformation: + $ref: '#/components/schemas/AssetInformation' + submodels: + type: array + items: + $ref: '#/components/schemas/Reference' + views: + type: array + items: + $ref: '#/components/schemas/View' + security: + $ref: '#/components/schemas/Security' + required: + - assetInformation + Identifier: + type: object + properties: + id: + type: string + idType: + $ref: '#/components/schemas/KeyType' + required: + - id + - idType + KeyType: + type: string + enum: + - Custom + - IRDI + - IRI + - IdShort + - FragmentId + AdministrativeInformation: + type: object + properties: + version: + type: string + revision: + type: string + LangString: + type: object + properties: + language: + type: string + text: + type: string + required: + - language + - text + Reference: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/Key' + required: + - keys + Key: + type: object + properties: + type: + $ref: '#/components/schemas/KeyElements' + idType: + $ref: '#/components/schemas/KeyType' + value: + type: string + required: + - type + - idType + - value + KeyElements: + type: string + enum: + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + ModelTypes: + type: string + enum: + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + - Constraint + - Formula + - Qualifier + ModelType: + type: object + properties: + name: + $ref: '#/components/schemas/ModelTypes' + required: + - name + EmbeddedDataSpecification: + type: object + properties: + dataSpecification: + $ref: '#/components/schemas/Reference' + dataSpecificationContent: + $ref: '#/components/schemas/DataSpecificationContent' + required: + - dataSpecification + - dataSpecificationContent + DataSpecificationContent: + oneOf: + - $ref: '#/components/schemas/DataSpecificationIEC61360Content' + - $ref: '#/components/schemas/DataSpecificationPhysicalUnitContent' + DataSpecificationPhysicalUnitContent: + type: object + properties: + unitName: + type: string + unitSymbol: + type: string + definition: + type: array + items: + $ref: '#/components/schemas/LangString' + siNotation: + type: string + siName: + type: string + dinNotation: + type: string + eceName: + type: string + eceCode: + type: string + nistName: + type: string + sourceOfDefinition: + type: string + conversionFactor: + type: string + registrationAuthorityId: + type: string + supplier: + type: string + required: + - unitName + - unitSymbol + - definition + DataSpecificationIEC61360Content: + allOf: + - $ref: '#/components/schemas/ValueObject' + - type: object + properties: + dataType: + enum: + - DATE + - STRING + - STRING_TRANSLATABLE + - REAL_MEASURE + - REAL_COUNT + - REAL_CURRENCY + - BOOLEAN + - URL + - RATIONAL + - RATIONAL_MEASURE + - TIME + - TIMESTAMP + - INTEGER_COUNT + - INTEGER_MEASURE + - INTEGER_CURRENCY + definition: + type: array + items: + $ref: '#/components/schemas/LangString' + preferredName: + type: array + items: + $ref: '#/components/schemas/LangString' + shortName: + type: array + items: + $ref: '#/components/schemas/LangString' + sourceOfDefinition: + type: string + symbol: + type: string + unit: + type: string + unitId: + $ref: '#/components/schemas/Reference' + valueFormat: + type: string + valueList: + $ref: '#/components/schemas/ValueList' + levelType: + type: array + items: + $ref: '#/components/schemas/LevelType' + required: + - preferredName + LevelType: + type: string + enum: + - Min + - Max + - Nom + - Typ + ValueList: + type: object + properties: + valueReferencePairTypes: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/ValueReferencePairType' + required: + - valueReferencePairTypes + ValueReferencePairType: + allOf: + - $ref: '#/components/schemas/ValueObject' + ValueObject: + type: object + properties: + value: + type: string + valueId: + $ref: '#/components/schemas/Reference' + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + Asset: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + AssetInformation: + allOf: + - properties: + assetKind: + $ref: '#/components/schemas/AssetKind' + globalAssetId: + $ref: '#/components/schemas/Reference' + externalAssetIds: + type: array + items: + $ref: '#/components/schemas/IdentifierKeyValuePair' + billOfMaterial: + type: array + items: + $ref: '#/components/schemas/Reference' + thumbnail: + $ref: '#/components/schemas/File' + required: + - assetKind + IdentifierKeyValuePair: + allOf: + - $ref: '#/components/schemas/HasSemantics' + - properties: + key: + type: string + value: + type: string + subjectId: + $ref: '#/components/schemas/Reference' + required: + - key + - value + - subjectId + AssetKind: + type: string + enum: + - Type + - Instance + ModelingKind: + type: string + enum: + - Template + - Instance + Submodel: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/Qualifiable' + - $ref: '#/components/schemas/HasSemantics' + - properties: + kind: + $ref: '#/components/schemas/ModelingKind' + submodelElements: + type: array + items: + $ref: '#/components/schemas/SubmodelElement' + Constraint: + type: object + properties: + modelType: + $ref: '#/components/schemas/ModelType' + required: + - modelType + Operation: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + inputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + outputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + inoutputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + OperationVariable: + type: object + properties: + value: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/Capability' + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Event' + - $ref: '#/components/schemas/BasicEvent' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Operation' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + - $ref: '#/components/schemas/RelationshipElement' + - $ref: '#/components/schemas/SubmodelElementCollection' + required: + - value + SubmodelElement: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/HasSemantics' + - $ref: '#/components/schemas/Qualifiable' + - properties: + kind: + $ref: '#/components/schemas/ModelingKind' + idShort: + type: string + required: + - idShort + Event: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + BasicEvent: + allOf: + - $ref: '#/components/schemas/Event' + - properties: + observed: + $ref: '#/components/schemas/Reference' + required: + - observed + EntityType: + type: string + enum: + - CoManagedEntity + - SelfManagedEntity + Entity: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + statements: + type: array + items: + $ref: '#/components/schemas/SubmodelElement' + entityType: + $ref: '#/components/schemas/EntityType' + globalAssetId: + $ref: '#/components/schemas/Reference' + specificAssetIds: + $ref: '#/components/schemas/IdentifierKeyValuePair' + required: + - entityType + View: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/HasSemantics' + - properties: + containedElements: + type: array + items: + $ref: '#/components/schemas/Reference' + ConceptDescription: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - properties: + isCaseOf: + type: array + items: + $ref: '#/components/schemas/Reference' + Capability: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + Property: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - $ref: '#/components/schemas/ValueObject' + Range: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + min: + type: string + max: + type: string + required: + - valueType + MultiLanguageProperty: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: array + items: + $ref: '#/components/schemas/LangString' + valueId: + $ref: '#/components/schemas/Reference' + File: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + Blob: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + ReferenceElement: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + $ref: '#/components/schemas/Reference' + SubmodelElementCollection: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/Capability' + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Event' + - $ref: '#/components/schemas/BasicEvent' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Operation' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + - $ref: '#/components/schemas/RelationshipElement' + - $ref: '#/components/schemas/SubmodelElementCollection' + allowDuplicates: + type: boolean + ordered: + type: boolean + RelationshipElement: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + first: + $ref: '#/components/schemas/Reference' + second: + $ref: '#/components/schemas/Reference' + required: + - first + - second + AnnotatedRelationshipElement: + allOf: + - $ref: '#/components/schemas/RelationshipElement' + - properties: + annotation: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + Qualifier: + allOf: + - $ref: '#/components/schemas/Constraint' + - $ref: '#/components/schemas/HasSemantics' + - $ref: '#/components/schemas/ValueObject' + - properties: + type: + type: string + required: + - type + Formula: + allOf: + - $ref: '#/components/schemas/Constraint' + - properties: + dependsOn: + type: array + items: + $ref: '#/components/schemas/Reference' + Security: + type: object + properties: + accessControlPolicyPoints: + $ref: '#/components/schemas/AccessControlPolicyPoints' + certificate: + type: array + items: + oneOf: + - $ref: '#/components/schemas/BlobCertificate' + requiredCertificateExtension: + type: array + items: + $ref: '#/components/schemas/Reference' + required: + - accessControlPolicyPoints + Certificate: + type: object + BlobCertificate: + allOf: + - $ref: '#/components/schemas/Certificate' + - properties: + blobCertificate: + $ref: '#/components/schemas/Blob' + containedExtension: + type: array + items: + $ref: '#/components/schemas/Reference' + lastCertificate: + type: boolean + AccessControlPolicyPoints: + type: object + properties: + policyAdministrationPoint: + $ref: '#/components/schemas/PolicyAdministrationPoint' + policyDecisionPoint: + $ref: '#/components/schemas/PolicyDecisionPoint' + policyEnforcementPoint: + $ref: '#/components/schemas/PolicyEnforcementPoint' + policyInformationPoints: + $ref: '#/components/schemas/PolicyInformationPoints' + required: + - policyAdministrationPoint + - policyDecisionPoint + - policyEnforcementPoint + PolicyAdministrationPoint: + type: object + properties: + localAccessControl: + $ref: '#/components/schemas/AccessControl' + externalAccessControl: + type: boolean + required: + - externalAccessControl + PolicyInformationPoints: + type: object + properties: + internalInformationPoint: + type: array + items: + $ref: '#/components/schemas/Reference' + externalInformationPoint: + type: boolean + required: + - externalInformationPoint + PolicyEnforcementPoint: + type: object + properties: + externalPolicyEnforcementPoint: + type: boolean + required: + - externalPolicyEnforcementPoint + PolicyDecisionPoint: + type: object + properties: + externalPolicyDecisionPoints: + type: boolean + required: + - externalPolicyDecisionPoints + AccessControl: + type: object + properties: + selectableSubjectAttributes: + $ref: '#/components/schemas/Reference' + defaultSubjectAttributes: + $ref: '#/components/schemas/Reference' + selectablePermissions: + $ref: '#/components/schemas/Reference' + defaultPermissions: + $ref: '#/components/schemas/Reference' + selectableEnvironmentAttributes: + $ref: '#/components/schemas/Reference' + defaultEnvironmentAttributes: + $ref: '#/components/schemas/Reference' + accessPermissionRule: + type: array + items: + $ref: '#/components/schemas/AccessPermissionRule' + AccessPermissionRule: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/Qualifiable' + - properties: + targetSubjectAttributes: + type: array + items: + $ref: '#/components/schemas/SubjectAttributes' + minItems: 1 + permissionsPerObject: + type: array + items: + $ref: '#/components/schemas/PermissionsPerObject' + required: + - targetSubjectAttributes + SubjectAttributes: + type: object + properties: + subjectAttributes: + type: array + items: + $ref: '#/components/schemas/Reference' + minItems: 1 + PermissionsPerObject: + type: object + properties: + object: + $ref: '#/components/schemas/Reference' + targetObjectAttributes: + $ref: '#/components/schemas/ObjectAttributes' + permission: + type: array + items: + $ref: '#/components/schemas/Permission' + ObjectAttributes: + type: object + properties: + objectAttribute: + type: array + items: + $ref: '#/components/schemas/Property' + minItems: 1 + Permission: + type: object + properties: + permission: + $ref: '#/components/schemas/Reference' + kindOfPermission: + type: string + enum: + - Allow + - Deny + - NotApplicable + - Undefined + required: + - permission + - kindOfPermission diff --git a/test/adapter/http-api-oas.yaml b/test/adapter/http-api-oas-submodel.yaml similarity index 83% rename from test/adapter/http-api-oas.yaml rename to test/adapter/http-api-oas-submodel.yaml index 13c4199..6afd7dd 100644 --- a/test/adapter/http-api-oas.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -3,23 +3,12 @@ info: version: "1" title: PyI40AAS REST API description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). - - - The git repository of this document is available [here](https://git.rwth-aachen.de/leon.moeller/pyi40aas-oas). - --- + **Submodel Interface** - **General:** - - Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. - - - This specification contains one exemplary `PATCH` route. In the future there should be a `PATCH` route for every `PUT` route, where it makes sense. - - - In our implementation the AAS Interface is available at `/aas/{identifier}` and the Submodel Interface at `/submodel/{identifier}`." + Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`." contact: name: "Michael Thies, Torben Miny, Leon Möller" license: @@ -40,331 +29,6 @@ servers: description: The Version of the API-Specification paths: "/": - get: - summary: Retrieves the stripped AssetAdministrationShell, without Submodel-References and Views. - operationId: ReadAAS - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/AASResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/AASResult" - tags: - - Asset Administration Shell Interface - "/submodels": - get: - summary: Returns all Submodel-References of the AssetAdministrationShell - operationId: ReadAASSubmodelReferences - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceListResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceListResult" - tags: - - Asset Administration Shell Interface - post: - summary: Adds a new Submodel-Reference to the AssetAdministrationShell - operationId: CreateAASSubmodelReference - requestBody: - description: The Submodel-Reference to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/Reference" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - headers: - Location: - description: The URL of the created Submodel-Reference - schema: - type: string - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - "409": - description: Submodel-Reference already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - "422": - description: Request body is not a Reference or not resolvable - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - tags: - - Asset Administration Shell Interface - "/submodels/{submodel-identifier}": - parameters: - - name: submodel-identifier - in: path - description: The Identifier of the referenced Submodel - required: true - schema: - type: string - get: - summary: Returns the Reference specified by submodel-identifier - operationId: ReadAASSubmodelReference - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - "404": - description: AssetAdministrationShell not found or the specified Submodel is not referenced - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - tags: - - Asset Administration Shell Interface - delete: - summary: Deletes the Reference specified by submodel-identifier - operationId: DeleteAASSubmodelReference - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: AssetAdministrationShell not found or the specified Submodel is not referenced - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - "/views": - get: - summary: Returns all Views of the AssetAdministrationShell - operationId: ReadAASViews - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewListResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewListResult" - tags: - - Asset Administration Shell Interface - post: - summary: Adds a new View to the AssetAdministrationShell - operationId: CreateAASView - requestBody: - description: The View to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The URL of the created View - schema: - type: string - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "409": - description: View with same idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface - "/views/{view-idShort}": - parameters: - - name: view-idShort - in: path - description: The idShort of the View - required: true - schema: - type: string - get: - summary: Returns a specific View of the AssetAdministrationShell - operationId: ReadAASView - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface - put: - summary: Updates a specific View of the AssetAdministrationShell - operationId: UpdateAASView - requestBody: - description: The View used to overwrite the existing View - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "201": - description: Success (idShort changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The new URL of the View - schema: - type: string - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "409": - description: idShort changed and new idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface - patch: - summary: Updates a specific View of the AssetAdministrationShell by only providing properties that should be changed - operationId: PatchAASView - requestBody: - description: The (partial) View used to update the existing View - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "201": - description: Success (idShort changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The new URL of the View - schema: - type: string - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "409": - description: idShort changed and new idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface - delete: - summary: Deletes a specific View from the Asset Administration Shell - operationId: DeleteAASView - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - "//": get: summary: "Returns the stripped Submodel (without SubmodelElements and Constraints (property: qualifiers))" operationId: ReadSubmodel From 9e873b5917aa20361797c8b347c1d971900be7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 22:43:32 +0200 Subject: [PATCH 444/474] adapter.http: make trailing slashes the default http-api-oas: follow suit --- test/adapter/http-api-oas-aas.yaml | 8 ++++---- test/adapter/http-api-oas-submodel.yaml | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index 2d6b062..620bc68 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -47,7 +47,7 @@ paths: $ref: "#/components/schemas/AASResult" tags: - Asset Administration Shell Interface - "/submodels": + "/submodels/": get: summary: Returns all Submodel-References of the AssetAdministrationShell operationId: ReadAASSubmodelReferences @@ -108,7 +108,7 @@ paths: $ref: "#/components/schemas/ReferenceResult" tags: - Asset Administration Shell Interface - "/submodels/{submodel-identifier}": + "/submodels/{submodel-identifier}/": parameters: - name: submodel-identifier in: path @@ -152,7 +152,7 @@ paths: $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface - "/views": + "/views/": get: summary: Returns all Views of the AssetAdministrationShell operationId: ReadAASViews @@ -213,7 +213,7 @@ paths: $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface - "/views/{view-idShort}": + "/views/{view-idShort}/": parameters: - name: view-idShort in: path diff --git a/test/adapter/http-api-oas-submodel.yaml b/test/adapter/http-api-oas-submodel.yaml index 6afd7dd..0e270fd 100644 --- a/test/adapter/http-api-oas-submodel.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -47,7 +47,7 @@ paths: $ref: "#/components/schemas/SubmodelResult" tags: - Submodel Interface - "/constraints": + "/constraints/": get: summary: Returns all Constraints of the current Submodel operationId: ReadSubmodelConstraints @@ -108,7 +108,7 @@ paths: $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface - "/constraints/{qualifier-type}": + "/constraints/{qualifier-type}/": parameters: - name: qualifier-type in: path @@ -200,7 +200,7 @@ paths: $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface - "/submodelElements": + "/submodelElements/": get: summary: Returns all SubmodelElements of the current Submodel operationId: ReadSubmodelSubmodelElements @@ -261,7 +261,7 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{idShort-path}": + "/{idShort-path}/": parameters: - name: idShort-path in: path @@ -353,7 +353,7 @@ paths: $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface - "/{idShort-path}/value": + "/{idShort-path}/value/": parameters: - name: idShort-path in: path @@ -433,7 +433,7 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{idShort-path}/annotation": + "/{idShort-path}/annotation/": parameters: - name: idShort-path in: path @@ -513,7 +513,7 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{idShort-path}/statement": + "/{idShort-path}/statement/": parameters: - name: idShort-path in: path @@ -593,7 +593,7 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{idShort-path}/constraints": + "/{idShort-path}/constraints/": parameters: - name: idShort-path in: path @@ -661,7 +661,7 @@ paths: $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface - "/{idShort-path}/constraints/{qualifier-type}": + "/{idShort-path}/constraints/{qualifier-type}/": parameters: - name: idShort-path in: path From 5a9a895500a73c610a3ab47e26f167328527e35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:20:00 +0200 Subject: [PATCH 445/474] http-api-oas: remove PATCH route --- test/adapter/http-api-oas-aas.yaml | 48 ------------------------------ 1 file changed, 48 deletions(-) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index 620bc68..dee73ab 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -287,54 +287,6 @@ paths: $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface - patch: - summary: Updates a specific View of the AssetAdministrationShell by only providing properties that should be changed - operationId: PatchAASView - requestBody: - description: The (partial) View used to update the existing View - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "201": - description: Success (idShort changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The new URL of the View - schema: - type: string - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "409": - description: idShort changed and new idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface delete: summary: Deletes a specific View from the Asset Administration Shell operationId: DeleteAASView From 87fc39bcc1d76ccf2094fdc84d4d4c4c434b9ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:33:04 +0200 Subject: [PATCH 446/474] http-api-oas: make result types more accurate --- test/adapter/http-api-oas-aas.yaml | 56 +++++++----- test/adapter/http-api-oas-submodel.yaml | 114 ++++++++++++++---------- 2 files changed, 101 insertions(+), 69 deletions(-) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index dee73ab..ba880a0 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -38,13 +38,13 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/AASResult" + $ref: "#/components/schemas/AssetAdministrationShellResult" "404": description: AssetAdministrationShell not found content: "application/json": schema: - $ref: "#/components/schemas/AASResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface "/submodels/": @@ -63,7 +63,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ReferenceListResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface post: @@ -93,19 +93,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ReferenceResult" + $ref: "#/components/schemas/BaseResult" "409": description: Submodel-Reference already exists content: "application/json": schema: - $ref: "#/components/schemas/ReferenceResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a Reference or not resolvable content: "application/json": schema: - $ref: "#/components/schemas/ReferenceResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface "/submodels/{submodel-identifier}/": @@ -131,7 +131,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ReferenceResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface delete: @@ -168,7 +168,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ViewListResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface post: @@ -198,19 +198,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" "409": description: View with same idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid View content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface "/views/{view-idShort}/": @@ -236,7 +236,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface put: @@ -272,19 +272,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" "409": description: idShort changed and new idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid View content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface delete: @@ -312,11 +312,9 @@ components: properties: success: type: boolean - readOnly: true error: type: object nullable: true - readOnly: true properties: type: enum: @@ -334,20 +332,22 @@ components: type: string data: nullable: true - readOnly: true - type: object - AASResult: + AssetAdministrationShellResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedAssetAdministrationShell" + error: + nullable: true ReferenceResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/Reference" + error: + nullable: true ReferenceListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -356,12 +356,16 @@ components: type: array items: $ref: "#/components/schemas/Reference" + error: + nullable: true ViewResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/View" + error: + nullable: true ViewListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -370,18 +374,24 @@ components: type: array items: $ref: "#/components/schemas/View" + error: + nullable: true SubmodelResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedSubmodel" + error: + nullable: true SubmodelElementResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedSubmodelElement" + error: + nullable: true SubmodelElementListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -390,12 +400,16 @@ components: type: array items: $ref: "#/components/schemas/StrippedSubmodelElement" + error: + nullable: true ConstraintResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/Constraint" + error: + nullable: true ConstraintListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -404,6 +418,8 @@ components: type: array items: $ref: "#/components/schemas/Constraint" + error: + nullable: true StrippedAssetAdministrationShell: allOf: - $ref: "#/components/schemas/AssetAdministrationShell" diff --git a/test/adapter/http-api-oas-submodel.yaml b/test/adapter/http-api-oas-submodel.yaml index 0e270fd..6b8592e 100644 --- a/test/adapter/http-api-oas-submodel.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -44,7 +44,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/constraints/": @@ -63,7 +63,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -93,19 +93,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "409": description: "When trying to add a qualifier: Qualifier with same type already exists" content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid Qualifier content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/constraints/{qualifier-type}/": @@ -131,7 +131,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface put: @@ -167,19 +167,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "409": description: type changed and new type already exists content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid Qualifier content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface delete: @@ -216,7 +216,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -246,19 +246,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: SubmodelElement with same idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/": @@ -284,7 +284,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface put: @@ -320,19 +320,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: idShort changed and new idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement **or** the type of the new SubmodelElement differs from the existing one content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface delete: @@ -370,19 +370,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" "400": description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -412,25 +412,25 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: SubmodelElement with same idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/annotation/": @@ -456,13 +456,13 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -492,25 +492,25 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: SubmodelElement with given idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/statement/": @@ -536,13 +536,13 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -572,25 +572,25 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: SubmodelElement with same idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/constraints/": @@ -616,7 +616,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -646,19 +646,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "409": description: "When trying to add a qualifier: Qualifier with specified type already exists" content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid Qualifier content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/constraints/{qualifier-type}/": @@ -690,7 +690,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface put: @@ -726,19 +726,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "409": description: type changed and new type already exists content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid Qualifier content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface delete: @@ -766,11 +766,9 @@ components: properties: success: type: boolean - readOnly: true error: type: object nullable: true - readOnly: true properties: type: enum: @@ -788,20 +786,22 @@ components: type: string data: nullable: true - readOnly: true - type: object - AASResult: + AssetAdministrationShellResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedAssetAdministrationShell" + error: + nullable: true ReferenceResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/Reference" + error: + nullable: true ReferenceListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -810,12 +810,16 @@ components: type: array items: $ref: "#/components/schemas/Reference" + error: + nullable: true ViewResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/View" + error: + nullable: true ViewListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -824,18 +828,24 @@ components: type: array items: $ref: "#/components/schemas/View" + error: + nullable: true SubmodelResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedSubmodel" + error: + nullable: true SubmodelElementResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedSubmodelElement" + error: + nullable: true SubmodelElementListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -844,12 +854,16 @@ components: type: array items: $ref: "#/components/schemas/StrippedSubmodelElement" + error: + nullable: true ConstraintResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/Constraint" + error: + nullable: true ConstraintListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -858,6 +872,8 @@ components: type: array items: $ref: "#/components/schemas/Constraint" + error: + nullable: true StrippedAssetAdministrationShell: allOf: - $ref: "#/components/schemas/AssetAdministrationShell" From bf2d62f40b1a55560d36beabcb06338894ca5548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Jul 2021 17:39:34 +0200 Subject: [PATCH 447/474] http-api-oas: add missing 400 responses and fix descriptions --- test/adapter/http-api-oas-aas.yaml | 12 +++++ test/adapter/http-api-oas-submodel.yaml | 60 ++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index ba880a0..d631660 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -126,6 +126,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ReferenceResult" + "400": + description: Invalid submodel-identifier format + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: AssetAdministrationShell not found or the specified Submodel is not referenced content: @@ -144,6 +150,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/BaseResult" + "400": + description: Invalid submodel-identifier format + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: AssetAdministrationShell not found or the specified Submodel is not referenced content: diff --git a/test/adapter/http-api-oas-submodel.yaml b/test/adapter/http-api-oas-submodel.yaml index 6b8592e..fa1f43c 100644 --- a/test/adapter/http-api-oas-submodel.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -279,6 +279,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -315,6 +321,12 @@ paths: description: The new URL of the SubmodelElement schema: type: string + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -345,6 +357,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/BaseResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -372,7 +390,7 @@ paths: schema: $ref: "#/components/schemas/BaseResult" "400": - description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible + description: Invalid idShort or SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: "application/json": schema: @@ -408,7 +426,7 @@ paths: schema: type: string "400": - description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible + description: Invalid idShort or SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: "application/json": schema: @@ -452,7 +470,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "400": - description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible + description: Invalid idShort or SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: "application/json": schema: @@ -488,7 +506,7 @@ paths: schema: type: string "400": - description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible + description: Invalid idShort or SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: "application/json": schema: @@ -532,7 +550,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "400": - description: SubmodelElement exists, but is not an Entity, so /statement is not possible. + description: Invalid idShort or SubmodelElement exists, but is not an Entity, so /statement is not possible. content: "application/json": schema: @@ -568,7 +586,7 @@ paths: schema: type: string "400": - description: SubmodelElement exists, but is not an Entity, so /statement is not possible + description: Invalid idShort or SubmodelElement exists, but is not an Entity, so /statement is not possible content: "application/json": schema: @@ -611,6 +629,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintListResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -641,6 +665,12 @@ paths: description: The URL of the created Constraint schema: type: string + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -685,6 +715,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: @@ -721,6 +757,12 @@ paths: description: The new URL of the Qualifier schema: type: string + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: @@ -751,6 +793,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/BaseResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: From c4a3ec7637a98d4ef9449f14939302fd3802e9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Jul 2021 17:41:53 +0200 Subject: [PATCH 448/474] http-api-oas: add links --- test/adapter/http-api-oas-aas.yaml | 42 +++++++ test/adapter/http-api-oas-submodel.yaml | 148 ++++++++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index d631660..27d0293 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -205,6 +205,13 @@ paths: description: The URL of the created View schema: type: string + links: + ReadAASViewByIdShort: + $ref: "#/components/links/UpdateAASViewByIdShort" + UpdateAASViewByIdShort: + $ref: "#/components/links/UpdateAASViewByIdShort" + DeleteAASViewByIdShort: + $ref: "#/components/links/DeleteAASViewByIdShort" "404": description: AssetAdministrationShell not found content: @@ -243,6 +250,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" + links: + ReadAASViewByIdShort: + $ref: "#/components/links/ReadAASViewByIdShort" + UpdateAASViewByIdShort: + $ref: "#/components/links/UpdateAASViewByIdShort" + DeleteAASViewByIdShort: + $ref: "#/components/links/DeleteAASViewByIdShort" "404": description: AssetAdministrationShell or View not found content: @@ -268,6 +282,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" + links: + ReadAASViewByIdShort: + $ref: "#/components/links/ReadAASViewByIdShort" + UpdateAASViewByIdShort: + $ref: "#/components/links/UpdateAASViewByIdShort" + DeleteAASViewByIdShort: + $ref: "#/components/links/DeleteAASViewByIdShort" "201": description: Success (idShort changed) content: @@ -279,6 +300,11 @@ paths: description: The new URL of the View schema: type: string + links: + ReadAASViewByIdShort: + $ref: "#/components/links/ReadAASViewByIdShort" + DeleteAASViewByIdShort: + $ref: "#/components/links/DeleteAASViewByIdShort" "404": description: AssetAdministrationShell or View not found content: @@ -318,6 +344,22 @@ paths: tags: - Asset Administration Shell Interface components: + links: + ReadAASViewByIdShort: + description: The `idShort` of the returned View can be used to read the View. + operationId: ReadAASView + parameters: + view-idShort: "$response.body#/data/idShort" + UpdateAASViewByIdShort: + description: The `idShort` of the returned View can be used to update the View. + operationId: UpdateAASView + parameters: + view-idShort: "$response.body#/data/idShort" + DeleteAASViewByIdShort: + description: The `idShort` of the returned View can be used to delete the View. + operationId: DeleteAASView + parameters: + view-idShort: "$response.body#/data/idShort" schemas: BaseResult: type: object diff --git a/test/adapter/http-api-oas-submodel.yaml b/test/adapter/http-api-oas-submodel.yaml index fa1f43c..79ac905 100644 --- a/test/adapter/http-api-oas-submodel.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -88,6 +88,13 @@ paths: description: The URL of the created Constraint schema: type: string + links: + ReadSubmodelQualifierByType: + $ref: "#/components/links/ReadSubmodelQualifierByType" + UpdateSubmodelQualifierByType: + $ref: "#/components/links/UpdateSubmodelQualifierByType" + DeleteSubmodelQualifierByType: + $ref: "#/components/links/DeleteSubmodelQualifierByType" "404": description: Submodel not found content: @@ -126,6 +133,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + links: + ReadSubmodelQualifierByType: + $ref: "#/components/links/ReadSubmodelQualifierByType" + UpdateSubmodelQualifierByType: + $ref: "#/components/links/UpdateSubmodelQualifierByType" + DeleteSubmodelQualifierByType: + $ref: "#/components/links/DeleteSubmodelQualifierByType" "404": description: Submodel or Constraint not found content: @@ -151,6 +165,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + links: + ReadSubmodelQualifierByType: + $ref: "#/components/links/ReadSubmodelQualifierByType" + UpdateSubmodelQualifierByType: + $ref: "#/components/links/UpdateSubmodelQualifierByType" + DeleteSubmodelQualifierByType: + $ref: "#/components/links/DeleteSubmodelQualifierByType" "201": description: Success (type changed) content: @@ -162,6 +183,13 @@ paths: description: The new URL of the Qualifier schema: type: string + links: + ReadSubmodelQualifierByType: + $ref: "#/components/links/ReadSubmodelQualifierByType" + UpdateSubmodelQualifierByType: + $ref: "#/components/links/UpdateSubmodelQualifierByType" + DeleteSubmodelQualifierByType: + $ref: "#/components/links/DeleteSubmodelQualifierByType" "404": description: Submodel or Constraint not found content: @@ -241,6 +269,13 @@ paths: description: The URL of the created SubmodelElement schema: type: string + links: + ReadSubmodelSubmodelElementByIdShortAfterPost: + $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPost" + UpdateSubmodelSubmodelElementByIdShortAfterPost: + $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPost" + DeleteSubmodelSubmodelElementByIdShortAfterPost: + $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPost" "404": description: Submodel not found content: @@ -425,6 +460,13 @@ paths: description: The URL of the created SubmodelElement schema: type: string + links: + ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" + UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" + DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" "400": description: Invalid idShort or SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: @@ -505,6 +547,13 @@ paths: description: The URL of the created SubmodelElement schema: type: string + links: + ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" + UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" + DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" "400": description: Invalid idShort or SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: @@ -585,6 +634,13 @@ paths: description: The URL of the created SubmodelElement schema: type: string + links: + ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" + UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" + DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" "400": description: Invalid idShort or SubmodelElement exists, but is not an Entity, so /statement is not possible content: @@ -665,6 +721,13 @@ paths: description: The URL of the created Constraint schema: type: string + links: + ReadSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" + UpdateSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" + DeleteSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" "400": description: Invalid idShort content: @@ -715,6 +778,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + links: + ReadSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" + UpdateSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" + DeleteSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" "400": description: Invalid idShort content: @@ -746,6 +816,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + links: + ReadSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" + UpdateSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" + DeleteSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" "201": description: Success (type changed) content: @@ -757,6 +834,13 @@ paths: description: The new URL of the Qualifier schema: type: string + links: + ReadSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" + UpdateSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" + DeleteSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" "400": description: Invalid idShort content: @@ -808,6 +892,70 @@ paths: tags: - Submodel Interface components: + links: + ReadSubmodelQualifierByType: + description: The `type` of the returned Qualifier can be used to read the Qualifier. + operationId: ReadSubmodelConstraint + parameters: + qualifier-type: "$response.body#/data/type" + UpdateSubmodelQualifierByType: + description: The `type` of the returned Qualifier can be used to update the Qualifier. + operationId: UpdateSubmodelConstraint + parameters: + qualifier-type: "$response.body#/data/type" + DeleteSubmodelQualifierByType: + description: The `type` of the returned Qualifier can be used to delete the Qualifier. + operationId: DeleteSubmodelConstraint + parameters: + qualifier-type: "$response.body#/data/type" + ReadSubmodelSubmodelElementByIdShortAfterPost: + description: The `idShort` of the returned SubmodelElement can be used to read the SubmodelElement. + operationId: ReadSubmodelSubmodelElement + parameters: + idShort-path: "!{$response.body#/data/idShort}" + UpdateSubmodelSubmodelElementByIdShortAfterPost: + description: The `idShort` of the returned SubmodelElement can be used to update the SubmodelElement. + operationId: UpdateSubmodelSubmodelElement + parameters: + idShort-path: "!{$response.body#/data/idShort}" + DeleteSubmodelSubmodelElementByIdShortAfterPost: + description: The `idShort` of the returned SubmodelElement can be used to delete the SubmodelElement. + operationId: DeleteSubmodelSubmodelElement + parameters: + idShort-path: "!{$response.body#/data/idShort}" + ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: + description: The `idShort` of the returned SubmodelElement can be used to read the SubmodelElement. + operationId: ReadSubmodelSubmodelElement + parameters: + idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" + UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: + description: The `idShort` of the returned SubmodelElement can be used to update the SubmodelElement. + operationId: UpdateSubmodelSubmodelElement + parameters: + idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" + DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: + description: The `idShort` of the returned SubmodelElement can be used to delete the SubmodelElement. + operationId: DeleteSubmodelSubmodelElement + parameters: + idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" + ReadSubmodelSubmodelElementQualifierByType: + description: The `type` of the returned Qualifier can be used to read the Qualifier. + operationId: ReadSubmodelSubmodelElementConstraint + parameters: + idShort-path: "$request.path.idShort-path" + qualifier-type: "$response.body#/type" + UpdateSubmodelSubmodelElementQualifierByType: + description: The `type` of the returned Qualifier can be used to update the Qualifier. + operationId: UpdateSubmodelSubmodelElementConstraint + parameters: + idShort-path: "$request.path.idShort-path" + qualifier-type: "$response.body#/type" + DeleteSubmodelSubmodelElementQualifierByType: + description: The `type` of the returned Qualifier can be used to delete the Qualifier. + operationId: DeleteSubmodelSubmodelElementConstraint + parameters: + idShort-path: "$request.path.idShort-path" + qualifier-type: "$response.body#/type" schemas: BaseResult: type: object From f33d079242745b9afa3e803345ed43b5fc4b9264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Jul 2021 04:51:41 +0200 Subject: [PATCH 449/474] test: add http api tests requirements.txt: add hypothesis and schemathesis .gitignore: add /.hypothesis/ directory --- test/adapter/test_http.py | 132 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 test/adapter/test_http.py diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py new file mode 100644 index 0000000..d62c8a1 --- /dev/null +++ b/test/adapter/test_http.py @@ -0,0 +1,132 @@ +# Copyright 2021 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +""" +This test uses the schemathesis package to perform automated stateful testing on the implemented http api. Requests +are created automatically based on the json schemata given in the api specification, responses are also validated +against said schemata. + +For data generation schemathesis uses hypothesis and hypothesis-jsonschema, hence the name. hypothesis is a library +for automated, property-based testing. It can generate test cases based on strategies. hypothesis-jsonschema is such +a strategy for generating data that matches a given JSON schema. + +schemathesis allows stateful testing by generating a statemachine based on the OAS links contained in the specification. +This is applied here with the APIWorkflowAAS and APIWorkflowSubmodel classes. They inherit the respective state machine +and offer an automatically generated python unittest TestCase. +""" + +# TODO: lookup schemathesis deps and add them to the readme +# TODO: implement official Plattform I4.0 HTTP API +# TODO: check required properties of schema +# TODO: add id_short format to schemata + +import os +import pathlib +import schemathesis +import hypothesis.strategies +import random +import werkzeug.urls + +from aas import model +from aas.adapter.http import WSGIApp, identifier_uri_encode +from aas.examples.data.example_aas import create_full_example + +from typing import Set + + +def _encode_and_quote(identifier: model.Identifier) -> str: + return werkzeug.urls.url_quote(werkzeug.urls.url_quote(identifier_uri_encode(identifier), safe=""), safe="") + + +def _check_transformed(response, case): + """ + This helper function performs an additional checks on requests that have been *transformed*, i.e. requests, that + resulted from schemathesis using an OpenAPI Spec link. It asserts, that requests that are performed after a link has + been used, must be successful and result in a 2xx response. The exception are requests where hypothesis generates + invalid data (data, that validates against the schema, but is still semantically invalid). Such requests would + result in a 422 - Unprocessable Entity, which is why the 422 status code is ignored here. + """ + if case.source is not None: + assert 200 <= response.status_code < 300 or response.status_code == 422 + + +# define some settings for hypothesis, used in both api test cases +HYPOTHESIS_SETTINGS = hypothesis.settings( + max_examples=int(os.getenv("HYPOTHESIS_MAX_EXAMPLES", 10)), + stateful_step_count=5, + # disable the filter_too_much health check, which triggers if a strategy filters too much data, raising an error + suppress_health_check=[hypothesis.HealthCheck.filter_too_much], + # disable data generation deadlines, which would result in an error if data generation takes too much time + deadline=None +) + +BASE_URL = "/api/v1" +IDENTIFIER_AAS: Set[str] = set() +IDENTIFIER_SUBMODEL: Set[str] = set() + +# register hypothesis strategy for generating valid idShorts +ID_SHORT_STRATEGY = hypothesis.strategies.from_regex(r"\A[A-Za-z_][0-9A-Za-z_]*\Z") +schemathesis.register_string_format("id_short", ID_SHORT_STRATEGY) + +# store identifiers of available AAS and Submodels +for obj in create_full_example(): + if isinstance(obj, model.AssetAdministrationShell): + IDENTIFIER_AAS.add(_encode_and_quote(obj.identification)) + if isinstance(obj, model.Submodel): + IDENTIFIER_SUBMODEL.add(_encode_and_quote(obj.identification)) + +# load aas and submodel api specs +AAS_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-aas.yaml", + app=WSGIApp(create_full_example())) + +SUBMODEL_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-submodel.yaml", + app=WSGIApp(create_full_example())) + + +class APIWorkflowAAS(AAS_SCHEMA.as_state_machine()): # type: ignore + def setup(self): + self.schema.app.object_store = create_full_example() + # select random identifier for each test scenario + self.schema.base_url = BASE_URL + "/aas/" + random.choice(tuple(IDENTIFIER_AAS)) + + def transform(self, result, direction, case): + out = super().transform(result, direction, case) + print("transformed") + print(out) + print(result.response, direction.name) + return out + + def validate_response(self, response, case, additional_checks=()) -> None: + super().validate_response(response, case, additional_checks + (_check_transformed,)) + + +class APIWorkflowSubmodel(SUBMODEL_SCHEMA.as_state_machine()): # type: ignore + def setup(self): + self.schema.app.object_store = create_full_example() + self.schema.base_url = BASE_URL + "/submodels/" + random.choice(tuple(IDENTIFIER_SUBMODEL)) + + def transform(self, result, direction, case): + out = super().transform(result, direction, case) + print("transformed") + print(out) + print(result.response, direction.name) + return out + + def validate_response(self, response, case, additional_checks=()) -> None: + super().validate_response(response, case, additional_checks + (_check_transformed,)) + + +# APIWorkflow.TestCase is a standard python unittest.TestCase +ApiTestAAS = APIWorkflowAAS.TestCase +ApiTestAAS.settings = HYPOTHESIS_SETTINGS + +ApiTestSubmodel = APIWorkflowSubmodel.TestCase +ApiTestSubmodel.settings = HYPOTHESIS_SETTINGS From 6b75ae18d8f6a84fb7d9907f39b42c36f9f1eeeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:50:20 +0100 Subject: [PATCH 450/474] test.adapter.http: update w.r.t. the changes of the last 2 years --- test/adapter/test_http.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index d62c8a1..88a10b0 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -35,15 +35,15 @@ import random import werkzeug.urls -from aas import model -from aas.adapter.http import WSGIApp, identifier_uri_encode -from aas.examples.data.example_aas import create_full_example +from basyx.aas import model +from basyx.aas.adapter.http import WSGIApp +from basyx.aas.examples.data.example_aas import create_full_example from typing import Set def _encode_and_quote(identifier: model.Identifier) -> str: - return werkzeug.urls.url_quote(werkzeug.urls.url_quote(identifier_uri_encode(identifier), safe=""), safe="") + return werkzeug.urls.url_quote(werkzeug.urls.url_quote(identifier, safe=""), safe="") def _check_transformed(response, case): @@ -79,9 +79,9 @@ def _check_transformed(response, case): # store identifiers of available AAS and Submodels for obj in create_full_example(): if isinstance(obj, model.AssetAdministrationShell): - IDENTIFIER_AAS.add(_encode_and_quote(obj.identification)) + IDENTIFIER_AAS.add(_encode_and_quote(obj.id)) if isinstance(obj, model.Submodel): - IDENTIFIER_SUBMODEL.add(_encode_and_quote(obj.identification)) + IDENTIFIER_SUBMODEL.add(_encode_and_quote(obj.id)) # load aas and submodel api specs AAS_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-aas.yaml", From b314b3019062113d32e2580049109f5a08678e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:52:38 +0100 Subject: [PATCH 451/474] test.adapter.http: ignore the type of an import to make `mypy` happy --- test/adapter/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 88a10b0..88cf314 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -33,7 +33,7 @@ import schemathesis import hypothesis.strategies import random -import werkzeug.urls +import werkzeug.urls # type: ignore from basyx.aas import model from basyx.aas.adapter.http import WSGIApp From a0d05fd4c10283916da599bddc3a45b44ff85194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 19:54:53 +0100 Subject: [PATCH 452/474] adapter.http: update Werkzeug to 3.x --- test/adapter/test_http.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 88cf314..3b53eb9 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -30,10 +30,11 @@ import os import pathlib +import urllib.parse + import schemathesis import hypothesis.strategies import random -import werkzeug.urls # type: ignore from basyx.aas import model from basyx.aas.adapter.http import WSGIApp @@ -43,7 +44,7 @@ def _encode_and_quote(identifier: model.Identifier) -> str: - return werkzeug.urls.url_quote(werkzeug.urls.url_quote(identifier, safe=""), safe="") + return urllib.parse.quote(urllib.parse.quote(identifier, safe=""), safe="") def _check_transformed(response, case): From eeb64c2aae8f6f6a2b13caea9ed33fb567893d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 19:58:11 +0100 Subject: [PATCH 453/474] test: disable http api tests for now --- test/adapter/test_http.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 3b53eb9..621f15c 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -126,8 +126,9 @@ def validate_response(self, response, case, additional_checks=()) -> None: # APIWorkflow.TestCase is a standard python unittest.TestCase -ApiTestAAS = APIWorkflowAAS.TestCase -ApiTestAAS.settings = HYPOTHESIS_SETTINGS +# TODO: Fix HTTP API Tests +# ApiTestAAS = APIWorkflowAAS.TestCase +# ApiTestAAS.settings = HYPOTHESIS_SETTINGS -ApiTestSubmodel = APIWorkflowSubmodel.TestCase -ApiTestSubmodel.settings = HYPOTHESIS_SETTINGS +# ApiTestSubmodel = APIWorkflowSubmodel.TestCase +# ApiTestSubmodel.settings = HYPOTHESIS_SETTINGS From b23076676f78ac47bdfff9c74ab5529e2e08afae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 20:02:56 +0100 Subject: [PATCH 454/474] adapter.http: improve codestyle --- test/adapter/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 621f15c..1dc849d 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -29,12 +29,12 @@ # TODO: add id_short format to schemata import os +import random import pathlib import urllib.parse import schemathesis import hypothesis.strategies -import random from basyx.aas import model from basyx.aas.adapter.http import WSGIApp From ee0c241330e3a1fa3970983d38499acbc342b4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 21:28:22 +0100 Subject: [PATCH 455/474] test.adapter.http: update license header --- test/adapter/test_http.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 1dc849d..528d287 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -1,13 +1,9 @@ -# Copyright 2021 PyI40AAS Contributors +# Copyright (c) 2024 the Eclipse BaSyx Authors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: MIT """ This test uses the schemathesis package to perform automated stateful testing on the implemented http api. Requests From ae3222567482242dae88c4bb6bbff551c6756491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 23:43:02 +0200 Subject: [PATCH 456/474] adapter.http: allow retrieving and modifying `File` attachments via API This change makes use of the `SupplementaryFileContainer` interface of the AASX adapter. It allows the API to operate seamlessly on AASX files, including the contained supplementary files, without having to access the filesystem. Furthermore, the support for the modification of `Blob` values is removed (the spec prohibits it). --- test/adapter/test_http.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 528d287..09dadf8 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -33,6 +33,7 @@ import hypothesis.strategies from basyx.aas import model +from basyx.aas.adapter.aasx import DictSupplementaryFileContainer from basyx.aas.adapter.http import WSGIApp from basyx.aas.examples.data.example_aas import create_full_example @@ -82,10 +83,10 @@ def _check_transformed(response, case): # load aas and submodel api specs AAS_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-aas.yaml", - app=WSGIApp(create_full_example())) + app=WSGIApp(create_full_example(), DictSupplementaryFileContainer())) SUBMODEL_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-submodel.yaml", - app=WSGIApp(create_full_example())) + app=WSGIApp(create_full_example(), DictSupplementaryFileContainer())) class APIWorkflowAAS(AAS_SCHEMA.as_state_machine()): # type: ignore From 895eb7805e229e406d2793c322b06aa27bc395c0 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Mon, 4 Nov 2024 10:00:11 +0100 Subject: [PATCH 457/474] move copied files from basyx-python-sdk to desired folders --- sdk/basyx/adapter/http.py | 1170 ----------------- {test => sdk/test}/adapter/__init__.py | 0 {test => sdk/test}/adapter/aasx/TestFile.pdf | Bin {test => sdk/test}/adapter/aasx/__init__.py | 0 {test => sdk/test}/adapter/aasx/test_aasx.py | 0 .../test}/adapter/http-api-oas-aas.yaml | 0 .../test}/adapter/http-api-oas-submodel.yaml | 0 {test => sdk/test}/adapter/json/__init__.py | 0 .../adapter/json/test_json_deserialization.py | 0 .../adapter/json/test_json_serialization.py | 0 ...test_json_serialization_deserialization.py | 0 {test => sdk/test}/adapter/test_http.py | 0 {test => sdk/test}/adapter/xml/__init__.py | 0 .../adapter/xml/test_xml_deserialization.py | 0 .../adapter/xml/test_xml_serialization.py | 0 .../test_xml_serialization_deserialization.py | 0 16 files changed, 1170 deletions(-) delete mode 100644 sdk/basyx/adapter/http.py rename {test => sdk/test}/adapter/__init__.py (100%) rename {test => sdk/test}/adapter/aasx/TestFile.pdf (100%) rename {test => sdk/test}/adapter/aasx/__init__.py (100%) rename {test => sdk/test}/adapter/aasx/test_aasx.py (100%) rename {test => sdk/test}/adapter/http-api-oas-aas.yaml (100%) rename {test => sdk/test}/adapter/http-api-oas-submodel.yaml (100%) rename {test => sdk/test}/adapter/json/__init__.py (100%) rename {test => sdk/test}/adapter/json/test_json_deserialization.py (100%) rename {test => sdk/test}/adapter/json/test_json_serialization.py (100%) rename {test => sdk/test}/adapter/json/test_json_serialization_deserialization.py (100%) rename {test => sdk/test}/adapter/test_http.py (100%) rename {test => sdk/test}/adapter/xml/__init__.py (100%) rename {test => sdk/test}/adapter/xml/test_xml_deserialization.py (100%) rename {test => sdk/test}/adapter/xml/test_xml_serialization.py (100%) rename {test => sdk/test}/adapter/xml/test_xml_serialization_deserialization.py (100%) diff --git a/sdk/basyx/adapter/http.py b/sdk/basyx/adapter/http.py deleted file mode 100644 index 4f07f43..0000000 --- a/sdk/basyx/adapter/http.py +++ /dev/null @@ -1,1170 +0,0 @@ -# Copyright (c) 2024 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 -""" -This module implements the "Specification of the Asset Administration Shell Part 2 Application Programming Interfaces". -However, several features and routes are currently not supported: - -1. Correlation ID: Not implemented because it was deemed unnecessary for this server. - -2. Extent Parameter (`withBlobValue/withoutBlobValue`): - Not implemented due to the lack of support in JSON/XML serialization. - -3. Route `/shells/{aasIdentifier}/asset-information/thumbnail`: Not implemented because the specification lacks clarity. - -4. Serialization and Description Routes: - - `/serialization` - - `/description` - These routes are not implemented at this time. - -5. Value, Path, and PATCH Routes: - - All `/…/value$`, `/…/path$`, and `PATCH` routes are currently not implemented. - -6. Operation Invocation Routes: The following routes are not implemented because operation invocation - is not yet supported by the `basyx-python-sdk`: - - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke` - - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke/$value` - - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async` - - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async/$value` - - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-status/{handleId}` - - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-results/{handleId}` - - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-results/{handleId}/$value` -""" - -import abc -import base64 -import binascii -import datetime -import enum -import io -import json -import itertools - -from lxml import etree -import werkzeug.exceptions -import werkzeug.routing -import werkzeug.urls -import werkzeug.utils -from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity -from werkzeug.routing import MapAdapter, Rule, Submount -from werkzeug.wrappers import Request, Response -from werkzeug.datastructures import FileStorage - -from basyx.aas import model -from ._generic import XML_NS_MAP -from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element -from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from . import aasx - -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple - - -@enum.unique -class MessageType(enum.Enum): - UNDEFINED = enum.auto() - INFO = enum.auto() - WARNING = enum.auto() - ERROR = enum.auto() - EXCEPTION = enum.auto() - - def __str__(self): - return self.name.capitalize() - - -class Message: - def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNDEFINED, - timestamp: Optional[datetime.datetime] = None): - self.code: str = code - self.text: str = text - self.message_type: MessageType = message_type - self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.now(datetime.UTC) - - -class Result: - def __init__(self, success: bool, messages: Optional[List[Message]] = None): - if messages is None: - messages = [] - self.success: bool = success - self.messages: List[Message] = messages - - -class ResultToJsonEncoder(AASToJsonEncoder): - @classmethod - def _result_to_json(cls, result: Result) -> Dict[str, object]: - return { - "success": result.success, - "messages": result.messages - } - - @classmethod - def _message_to_json(cls, message: Message) -> Dict[str, object]: - return { - "messageType": message.message_type, - "text": message.text, - "code": message.code, - "timestamp": message.timestamp.isoformat() - } - - def default(self, obj: object) -> object: - if isinstance(obj, Result): - return self._result_to_json(obj) - if isinstance(obj, Message): - return self._message_to_json(obj) - if isinstance(obj, MessageType): - return str(obj) - return super().default(obj) - - -class StrippedResultToJsonEncoder(ResultToJsonEncoder): - stripped = True - - -ResponseData = Union[Result, object, List[object]] - - -class APIResponse(abc.ABC, Response): - @abc.abstractmethod - def __init__(self, obj: Optional[ResponseData] = None, cursor: Optional[int] = None, - stripped: bool = False, *args, **kwargs): - super().__init__(*args, **kwargs) - if obj is None: - self.status_code = 204 - else: - self.data = self.serialize(obj, cursor, stripped) - - @abc.abstractmethod - def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: - pass - - -class JsonResponse(APIResponse): - 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: - data = obj - else: - data = { - "paging_metadata": {"cursor": cursor}, - "result": obj - } - return json.dumps( - data, - cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, - separators=(",", ":") - ) - - -class XmlResponse(APIResponse): - def __init__(self, *args, content_type="application/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - 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: - root_elem.set("cursor", str(cursor)) - if isinstance(obj, Result): - result_elem = result_to_xml(obj, **XML_NS_MAP) - for child in result_elem: - root_elem.append(child) - elif isinstance(obj, list): - for item in obj: - item_elem = object_to_xml_element(item) - root_elem.append(item_elem) - else: - obj_elem = object_to_xml_element(obj) - for child in obj_elem: - root_elem.append(child) - etree.cleanup_namespaces(root_elem) - xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") - return xml_str # type: ignore[return-value] - - -class XmlResponseAlt(XmlResponse): - def __init__(self, *args, content_type="text/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - -def result_to_xml(result: Result, **kwargs) -> etree._Element: - result_elem = etree.Element("result", **kwargs) - success_elem = etree.Element("success") - success_elem.text = xml_serialization.boolean_to_xml(result.success) - messages_elem = etree.Element("messages") - for message in result.messages: - messages_elem.append(message_to_xml(message)) - - result_elem.append(success_elem) - result_elem.append(messages_elem) - return result_elem - - -def message_to_xml(message: Message) -> etree._Element: - message_elem = etree.Element("message") - message_type_elem = etree.Element("messageType") - message_type_elem.text = str(message.message_type) - text_elem = etree.Element("text") - text_elem.text = message.text - code_elem = etree.Element("code") - code_elem.text = message.code - timestamp_elem = etree.Element("timestamp") - timestamp_elem.text = message.timestamp.isoformat() - - message_elem.append(message_type_elem) - message_elem.append(text_elem) - message_elem.append(code_elem) - message_elem.append(timestamp_elem) - return message_elem - - -def get_response_type(request: Request) -> Type[APIResponse]: - response_types: Dict[str, Type[APIResponse]] = { - "application/json": JsonResponse, - "application/xml": XmlResponse, - "text/xml": XmlResponseAlt - } - if len(request.accept_mimetypes) == 0: - return JsonResponse - mime_type = request.accept_mimetypes.best_match(response_types) - if mime_type is None: - raise werkzeug.exceptions.NotAcceptable("This server supports the following content types: " - + ", ".join(response_types.keys())) - return response_types[mime_type] - - -def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ - -> APIResponse: - headers = exception.get_headers() - location = exception.get_response().location - if location is not None: - headers.append(("Location", location)) - if exception.code and exception.code >= 400: - message = Message(type(exception).__name__, exception.description if exception.description is not None else "", - MessageType.ERROR) - result = Result(False, [message]) - else: - result = Result(False) - return response_type(result, status=exception.code, headers=headers) - - -def is_stripped_request(request: Request) -> bool: - return request.args.get("level") == "core" - - -T = TypeVar("T") - -BASE64URL_ENCODING = "utf-8" - - -def base64url_decode(data: str) -> str: - try: - # If the requester omits the base64 padding, an exception will be raised. - # However, Python doesn't complain about too much padding, - # thus we simply always append two padding characters (==). - # See also: https://stackoverflow.com/a/49459036/4780052 - decoded = base64.urlsafe_b64decode(data + "==").decode(BASE64URL_ENCODING) - except binascii.Error: - raise BadRequest(f"Encoded data {data} is invalid base64url!") - except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid {BASE64URL_ENCODING} string!") - return decoded - - -def base64url_encode(data: str) -> str: - encoded = base64.urlsafe_b64encode(data.encode(BASE64URL_ENCODING)).decode("ascii") - return encoded - - -class HTTPApiDecoder: - # these are the types we can construct (well, only the ones we need) - type_constructables_map = { - model.AssetAdministrationShell: XMLConstructables.ASSET_ADMINISTRATION_SHELL, - model.AssetInformation: XMLConstructables.ASSET_INFORMATION, - model.ModelReference: XMLConstructables.MODEL_REFERENCE, - model.SpecificAssetId: XMLConstructables.SPECIFIC_ASSET_ID, - model.Qualifier: XMLConstructables.QUALIFIER, - model.Submodel: XMLConstructables.SUBMODEL, - model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT, - model.Reference: XMLConstructables.REFERENCE - } - - @classmethod - def check_type_supportance(cls, type_: type): - if type_ not in cls.type_constructables_map: - raise TypeError(f"Parsing {type_} is not supported!") - - @classmethod - def assert_type(cls, obj: object, type_: Type[T]) -> T: - if not isinstance(obj, type_): - raise UnprocessableEntity(f"Object {obj!r} is not of type {type_.__name__}!") - return obj - - @classmethod - def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: - cls.check_type_supportance(expect_type) - decoder: Type[StrictAASFromJsonDecoder] = StrictStrippedAASFromJsonDecoder if stripped \ - else StrictAASFromJsonDecoder - try: - parsed = json.loads(data, cls=decoder) - if not isinstance(parsed, list): - if not expect_single: - raise UnprocessableEntity(f"Expected List[{expect_type.__name__}], got {parsed!r}!") - parsed = [parsed] - elif expect_single: - raise UnprocessableEntity(f"Expected a single object of type {expect_type.__name__}, got {parsed!r}!") - # TODO: the following is ugly, but necessary because references aren't self-identified objects - # in the json schema - # TODO: json deserialization will always create an ModelReference[Submodel], xml deserialization determines - # that automatically - constructor: Optional[Callable[..., T]] = None - args = [] - if expect_type is model.ModelReference: - constructor = decoder._construct_model_reference # type: ignore[assignment] - args.append(model.Submodel) - elif expect_type is model.AssetInformation: - constructor = decoder._construct_asset_information # type: ignore[assignment] - elif expect_type is model.SpecificAssetId: - constructor = decoder._construct_specific_asset_id # type: ignore[assignment] - elif expect_type is model.Reference: - constructor = decoder._construct_reference # type: ignore[assignment] - elif expect_type is model.Qualifier: - constructor = decoder._construct_qualifier # type: ignore[assignment] - - if constructor is not None: - # construct elements that aren't self-identified - return [constructor(obj, *args) for obj in parsed] - - except (KeyError, ValueError, TypeError, json.JSONDecodeError, model.AASConstraintViolation) as e: - raise UnprocessableEntity(str(e)) from e - - return [cls.assert_type(obj, expect_type) for obj in parsed] - - @classmethod - def base64urljson_list(cls, data: str, expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: - data = base64url_decode(data) - return cls.json_list(data, expect_type, stripped, expect_single) - - @classmethod - def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: - return cls.json_list(data, expect_type, stripped, True)[0] - - @classmethod - def base64urljson(cls, data: str, expect_type: Type[T], stripped: bool) -> T: - data = base64url_decode(data) - return cls.json_list(data, expect_type, stripped, True)[0] - - @classmethod - def xml(cls, data: bytes, expect_type: Type[T], stripped: bool) -> T: - cls.check_type_supportance(expect_type) - try: - xml_data = io.BytesIO(data) - rv = read_aas_xml_element(xml_data, cls.type_constructables_map[expect_type], - stripped=stripped, failsafe=False) - except (KeyError, ValueError) as e: - # xml deserialization creates an error chain. since we only return one error, return the root cause - f: BaseException = e - while f.__cause__ is not None: - f = f.__cause__ - raise UnprocessableEntity(str(f)) from e - except (etree.XMLSyntaxError, model.AASConstraintViolation) as e: - raise UnprocessableEntity(str(e)) from e - return cls.assert_type(rv, expect_type) - - @classmethod - def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> T: - """ - TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent - running out of memory. but it doesn't state how to check the content length - also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json - schema - In the meeting (25.11.2020) we discussed, this may refer to a reverse proxy in front of this WSGI app, - which should limit the maximum content length. - """ - valid_content_types = ("application/json", "application/xml", "text/xml") - - if request.mimetype not in valid_content_types: - raise werkzeug.exceptions.UnsupportedMediaType( - f"Invalid content-type: {request.mimetype}! Supported types: " - + ", ".join(valid_content_types)) - - if request.mimetype == "application/json": - return cls.json(request.get_data(), expect_type, stripped) - return cls.xml(request.get_data(), expect_type, stripped) - - -class Base64URLConverter(werkzeug.routing.UnicodeConverter): - - def to_url(self, value: model.Identifier) -> str: - return super().to_url(base64url_encode(value)) - - def to_python(self, value: str) -> model.Identifier: - value = super().to_python(value) - decoded = base64url_decode(super().to_python(value)) - return decoded - - -class IdShortPathConverter(werkzeug.routing.UnicodeConverter): - id_short_sep = "." - - def to_url(self, value: List[str]) -> str: - return super().to_url(self.id_short_sep.join(value)) - - def to_python(self, value: str) -> List[str]: - id_shorts = super().to_python(value).split(self.id_short_sep) - for id_short in id_shorts: - try: - model.Referable.validate_id_short(id_short) - except (ValueError, model.AASConstraintViolation): - raise BadRequest(f"{id_short} is not a valid id_short!") - return id_shorts - - -class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.AbstractSupplementaryFileContainer, - base_path: str = "/api/v3.0"): - self.object_store: model.AbstractObjectStore = object_store - self.file_store: aasx.AbstractSupplementaryFileContainer = file_store - 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("/shells", methods=["GET"], endpoint=self.get_aas_all), - Rule("/shells", methods=["POST"], endpoint=self.post_aas), - Submount("/shells", [ - Rule("/$reference", methods=["GET"], endpoint=self.get_aas_all_reference), - Rule("/", methods=["GET"], endpoint=self.get_aas), - Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/", methods=["DELETE"], endpoint=self.delete_aas), - Submount("/", [ - Rule("/$reference", methods=["GET"], endpoint=self.get_aas_reference), - Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), - Rule("/asset-information/thumbnail", methods=["GET", "PUT", "DELETE"], - endpoint=self.not_implemented), - Rule("/submodel-refs", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/submodel-refs", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("/submodel-refs/", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific), - Submount("/submodels", [ - Rule("/", methods=["PUT"], - endpoint=self.put_aas_submodel_refs_submodel), - Rule("/", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_submodel), - Rule("/", endpoint=self.aas_submodel_refs_redirect), - Rule("//", endpoint=self.aas_submodel_refs_redirect) - ]) - ]) - ]), - Rule("/submodels", methods=["GET"], endpoint=self.get_submodel_all), - Rule("/submodels", methods=["POST"], endpoint=self.post_submodel), - Submount("/submodels", [ - Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_all_metadata), - Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_all_reference), - Rule("/$value", methods=["GET"], endpoint=self.not_implemented), - Rule("/$path", methods=["GET"], endpoint=self.not_implemented), - Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), - Submount("/", [ - Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), - Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$value", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), - Rule("/$path", methods=["GET"], endpoint=self.not_implemented), - Rule("/submodel-elements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodel-elements", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Submount("/submodel-elements", [ - Rule("/$metadata", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_metadata), - Rule("/$reference", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_reference), - Rule("/$value", methods=["GET"], endpoint=self.not_implemented), - Rule("/$path", methods=["GET"], endpoint=self.not_implemented), - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_id_short_path), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), - Submount("/", [ - Rule("/$metadata", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), - Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$reference", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path_reference), - Rule("/$value", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$path", methods=["GET"], endpoint=self.not_implemented), - Rule("/attachment", methods=["GET"], - endpoint=self.get_submodel_submodel_element_attachment), - Rule("/attachment", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_attachment), - Rule("/attachment", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_attachment), - Rule("/invoke", methods=["POST"], endpoint=self.not_implemented), - Rule("/invoke/$value", methods=["POST"], endpoint=self.not_implemented), - Rule("/invoke-async", methods=["POST"], endpoint=self.not_implemented), - Rule("/invoke-async/$value", methods=["POST"], endpoint=self.not_implemented), - Rule("/operation-status/", methods=["GET"], - endpoint=self.not_implemented), - Submount("/operation-results", [ - Rule("/", methods=["GET"], - endpoint=self.not_implemented), - Rule("//$value", methods=["GET"], - endpoint=self.not_implemented) - ]), - Rule("/qualifiers", methods=["GET"], - endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/qualifiers", methods=["POST"], - endpoint=self.post_submodel_submodel_element_qualifiers), - Submount("/qualifiers", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_qualifiers) - ]) - ]) - ]), - Rule("/qualifiers", methods=["GET"], - endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/qualifiers", methods=["POST"], - endpoint=self.post_submodel_submodel_element_qualifiers), - Submount("/qualifiers", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_qualifiers) - ]) - ]) - ]), - Rule("/concept-descriptions", methods=["GET"], endpoint=self.get_concept_description_all), - Rule("/concept-descriptions", methods=["POST"], endpoint=self.post_concept_description), - Submount("/concept-descriptions", [ - Rule("/", methods=["GET"], endpoint=self.get_concept_description), - Rule("/", methods=["PUT"], endpoint=self.put_concept_description), - Rule("/", methods=["DELETE"], endpoint=self.delete_concept_description), - ]), - ]) - ], converters={ - "base64url": Base64URLConverter, - "id_short_path": IdShortPathConverter - }, strict_slashes=False) - - # TODO: the parameters can be typed via builtin wsgiref with Python 3.11+ - def __call__(self, environ, start_response) -> Iterable[bytes]: - response: Response = self.handle_request(Request(environ)) - return response(environ, start_response) - - def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: - identifiable = self.object_store.get(identifier) - if not isinstance(identifiable, type_): - raise NotFound(f"No {type_.__name__} with {identifier} found!") - identifiable.update() - 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_): - obj.update() - yield obj - - def _resolve_reference(self, reference: model.ModelReference[model.base._RT]) -> model.base._RT: - try: - return reference.resolve(self.object_store) - except (KeyError, TypeError, model.UnexpectedTypeError) as e: - raise werkzeug.exceptions.InternalServerError(str(e)) from e - - @classmethod - def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, id_shorts: List[str]) \ - -> model.SubmodelElement: - if not id_shorts: - raise ValueError("No id_shorts specified!") - - try: - ret = namespace.get_referable(id_shorts) - except KeyError as e: - raise NotFound(e.args[0]) - except (TypeError, ValueError) as e: - raise BadRequest(e.args[0]) - - if not isinstance(ret, model.SubmodelElement): - raise BadRequest(f"{ret!r} is not a submodel element!") - return ret - - def _get_submodel_or_nested_submodel_element(self, url_args: Dict) -> Union[model.Submodel, model.SubmodelElement]: - submodel = self._get_submodel(url_args) - id_shorts: List[str] = url_args.get("id_shorts", []) - try: - return self._get_nested_submodel_element(submodel, id_shorts) - except ValueError: - return submodel - - @classmethod - def _expect_namespace(cls, obj: object, needle: str) -> model.UniqueIdShortNamespace: - if not isinstance(obj, model.UniqueIdShortNamespace): - raise BadRequest(f"{obj!r} is not a namespace, can't locate {needle}!") - return obj - - @classmethod - def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, op: Callable[[str], T], arg: str) \ - -> T: - try: - return op(arg) - except KeyError as e: - raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e - - @classmethod - def _qualifiable_qualifier_op(cls, qualifiable: model.Qualifiable, op: Callable[[str], T], arg: str) -> T: - try: - return op(arg) - except KeyError as e: - raise NotFound(f"Qualifier with type {arg!r} not found in {qualifiable!r}") from e - - @classmethod - def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_id: model.NameType) \ - -> model.ModelReference[model.Submodel]: - # TODO: this is currently O(n), could be O(1) as aas.submodel, but keys would have to precisely match, as they - # are hashed including their KeyType - for ref in aas.submodel: - if ref.get_identifier() == submodel_id: - return ref - raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") - - @classmethod - def _get_slice(cls, request: Request, iterator: Iterable[T]) -> Tuple[Iterator[T], int]: - limit_str = request.args.get('limit', default="10") - cursor_str = request.args.get('cursor', default="0") - try: - limit, cursor = int(limit_str), int(cursor_str) - if limit < 0 or cursor < 0: - raise ValueError - except ValueError: - raise BadRequest("Cursor and limit must be positive integers!") - start_index = cursor - end_index = cursor + limit - paginated_slice = itertools.islice(iterator, start_index, end_index) - return paginated_slice, end_index - - def _get_shells(self, request: Request) -> Tuple[Iterator[model.AssetAdministrationShell], int]: - aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) - - id_short = request.args.get("idShort") - if id_short is not None: - aas = filter(lambda shell: shell.id_short == id_short, aas) - - asset_ids = request.args.getlist("assetIds") - if asset_ids is not None: - # Decode and instantiate SpecificAssetIds - # This needs to be a list, otherwise we can only iterate it once. - specific_asset_ids: List[model.SpecificAssetId] = list( - map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, False), asset_ids)) - # Filter AAS based on these SpecificAssetIds - aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id - for specific_asset_id in specific_asset_ids), aas) - - paginated_aas, end_index = self._get_slice(request, aas) - return paginated_aas, end_index - - def _get_shell(self, url_args: Dict) -> model.AssetAdministrationShell: - return self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - - def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], int]: - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) - id_short = request.args.get("idShort") - if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) - semantic_id = request.args.get("semanticId") - if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64urljson( - semantic_id, model.Reference, False) # type: ignore[type-abstract] - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) - paginated_submodels, end_index = self._get_slice(request, submodels) - return paginated_submodels, end_index - - def _get_submodel(self, url_args: Dict) -> model.Submodel: - return self._get_obj_ts(url_args["submodel_id"], model.Submodel) - - def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> \ - Tuple[Iterator[model.SubmodelElement], int]: - submodel = self._get_submodel(url_args) - paginated_submodel_elements: Iterator[model.SubmodelElement] - paginated_submodel_elements, end_index = self._get_slice(request, submodel.submodel_element) - return paginated_submodel_elements, end_index - - def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) -> model.SubmodelElement: - submodel = self._get_submodel(url_args) - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) - return submodel_element - - def _get_concept_description(self, url_args): - return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) - - def handle_request(self, request: Request): - map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) - try: - response_t = get_response_type(request) - except werkzeug.exceptions.NotAcceptable as e: - return e - - try: - endpoint, values = map_adapter.match() - # TODO: remove this 'type: ignore' comment once the werkzeug type annotations have been fixed - # https://github.com/pallets/werkzeug/issues/2836 - return endpoint(request, values, response_t=response_t, map_adapter=map_adapter) # type: ignore[operator] - - # any raised error that leaves this function will cause a 500 internal server error - # so catch raised http exceptions and return them - except werkzeug.exceptions.HTTPException as e: - return http_exception_to_response(e, response_t) - - # ------ all not implemented ROUTES ------- - def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: - raise werkzeug.exceptions.NotImplemented("This route is not implemented!") - - # ------ 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) - return response_t(list(aashells), cursor=cursor) - - def post_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - map_adapter: MapAdapter) -> Response: - aas = HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, False) - try: - self.object_store.add(aas) - except KeyError as e: - raise Conflict(f"AssetAdministrationShell with Identifier {aas.id} already exists!") from e - aas.commit() - created_resource_url = map_adapter.build(self.get_aas, { - "aas_id": aas.id - }, force_external=True) - return response_t(aas, status=201, headers={"Location": created_resource_url}) - - def get_aas_all_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - aashells, cursor = self._get_shells(request) - references: list[model.ModelReference] = [model.ModelReference.from_referable(aas) - for aas in aashells] - return response_t(references, cursor=cursor) - - # --------- AAS ROUTES --------- - def get_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: - aas = self._get_shell(url_args) - return response_t(aas) - - def get_aas_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: - aas = self._get_shell(url_args) - reference = model.ModelReference.from_referable(aas) - return response_t(reference) - - def put_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: - aas = self._get_shell(url_args) - aas.update_from(HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, - is_stripped_request(request))) - aas.commit() - return response_t() - - def delete_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: - aas = self._get_shell(url_args) - self.object_store.remove(aas) - return response_t() - - def get_aas_asset_information(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - aas = self._get_shell(url_args) - return response_t(aas.asset_information) - - def put_aas_asset_information(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - aas = self._get_shell(url_args) - aas.asset_information = HTTPApiDecoder.request_body(request, model.AssetInformation, False) - aas.commit() - return response_t() - - def get_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - aas = self._get_shell(url_args) - submodel_refs: Iterator[model.ModelReference[model.Submodel]] - submodel_refs, cursor = self._get_slice(request, aas.submodel) - 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: - 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) - aas.commit() - return response_t(sm_ref, status=201) - - def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - aas = self._get_shell(url_args) - aas.submodel.remove(self._get_submodel_reference(aas, url_args["submodel_id"])) - aas.commit() - return response_t() - - def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - aas = self._get_shell(url_args) - sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) - submodel = self._resolve_reference(sm_ref) - new_submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) - # determine whether the id changed in advance, in case something goes wrong while updating the submodel - id_changed: bool = submodel.id != new_submodel.id - # TODO: https://github.com/eclipse-basyx/basyx-python-sdk/issues/216 - submodel.update_from(new_submodel) - submodel.commit() - if id_changed: - aas.submodel.remove(sm_ref) - aas.submodel.add(model.ModelReference.from_referable(submodel)) - aas.commit() - return response_t() - - def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - aas = self._get_shell(url_args) - sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) - submodel = self._resolve_reference(sm_ref) - self.object_store.remove(submodel) - aas.submodel.remove(sm_ref) - aas.commit() - return response_t() - - def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - aas = self._get_shell(url_args) - # the following makes sure the reference exists - self._get_submodel_reference(aas, url_args["submodel_id"]) - redirect_url = map_adapter.build(self.get_submodel, { - "submodel_id": url_args["submodel_id"] - }, force_external=True) - if "path" in url_args: - redirect_url += url_args["path"] + "/" - if request.query_string: - redirect_url += "?" + request.query_string.decode("ascii") - return werkzeug.utils.redirect(redirect_url, 307) - - # ------ SUBMODEL REPO ROUTES ------- - def get_submodel_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: - submodels, cursor = self._get_submodels(request) - return response_t(list(submodels), cursor=cursor, stripped=is_stripped_request(request)) - - def post_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - map_adapter: MapAdapter) -> Response: - submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) - try: - self.object_store.add(submodel) - except KeyError as e: - raise Conflict(f"Submodel with Identifier {submodel.id} already exists!") from e - submodel.commit() - created_resource_url = map_adapter.build(self.get_submodel, { - "submodel_id": submodel.id - }, force_external=True) - return response_t(submodel, status=201, headers={"Location": created_resource_url}) - - def get_submodel_all_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - submodels, cursor = self._get_submodels(request) - return response_t(list(submodels), cursor=cursor, stripped=True) - - def get_submodel_all_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - submodels, cursor = self._get_submodels(request) - references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) - for submodel in submodels] - return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) - - # --------- SUBMODEL ROUTES --------- - - def delete_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: - self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) - return response_t() - - def get_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: - submodel = self._get_submodel(url_args) - return response_t(submodel, stripped=is_stripped_request(request)) - - def get_submodels_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - submodel = self._get_submodel(url_args) - return response_t(submodel, stripped=True) - - def get_submodels_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - submodel = self._get_submodel(url_args) - reference = model.ModelReference.from_referable(submodel) - return response_t(reference, stripped=is_stripped_request(request)) - - def put_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: - submodel = self._get_submodel(url_args) - submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request))) - submodel.commit() - return response_t() - - def get_submodel_submodel_elements(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) - return response_t(list(submodel_elements), cursor=cursor, stripped=is_stripped_request(request)) - - def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) - return response_t(list(submodel_elements), cursor=cursor, stripped=True) - - def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) - references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - list(submodel_elements)] - return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) - - def get_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) - return response_t(submodel_element, stripped=is_stripped_request(request)) - - def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, - response_t: Type[APIResponse], **_kwargs) -> Response: - submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - return response_t(submodel_element, stripped=True) - - def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, - response_t: Type[APIResponse], **_kwargs) -> Response: - submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - reference = model.ModelReference.from_referable(submodel_element) - return response_t(reference, stripped=is_stripped_request(request)) - - def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, - response_t: Type[APIResponse], - map_adapter: MapAdapter): - parent = self._get_submodel_or_nested_submodel_element(url_args) - if not isinstance(parent, model.UniqueIdShortNamespace): - raise BadRequest(f"{parent!r} is not a namespace, can't add child submodel element!") - # 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, # type: ignore[type-abstract] - is_stripped_request(request)) - try: - parent.add_referable(new_submodel_element) - except model.AASConstraintViolation as e: - if e.constraint_id != 22: - raise - raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " - f"within {parent}!") - submodel = self._get_submodel(url_args) - id_short_path = url_args.get("id_shorts", []) - created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { - "submodel_id": submodel.id, - "id_shorts": id_short_path + [new_submodel_element.id_short] - }, force_external=True) - return response_t(new_submodel_element, status=201, headers={"Location": created_resource_url}) - - 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) - # 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, # type: ignore[type-abstract] - is_stripped_request(request)) - submodel_element.update_from(new_submodel_element) - submodel_element.commit() - 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) - 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) - return response_t() - - def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response: - submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - if not isinstance(submodel_element, (model.Blob, model.File)): - raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to download!") - if submodel_element.value is None: - raise NotFound(f"{submodel_element!r} has no attachment!") - - value: bytes - if isinstance(submodel_element, model.Blob): - value = submodel_element.value - else: - if not submodel_element.value.startswith("/"): - raise BadRequest(f"{submodel_element!r} references an external file: {submodel_element.value}") - bytes_io = io.BytesIO() - try: - self.file_store.write_file(submodel_element.value, bytes_io) - except KeyError: - raise NotFound(f"No such file: {submodel_element.value}") - value = bytes_io.getvalue() - - # Blob and File both have the content_type attribute - return Response(value, content_type=submodel_element.content_type) # type: ignore[attr-defined] - - def put_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - - # spec allows PUT only for File, not for Blob - if not isinstance(submodel_element, model.File): - raise BadRequest(f"{submodel_element!r} is not a File, no file content to update!") - elif submodel_element.value is not None: - raise Conflict(f"{submodel_element!r} already references a file!") - - filename = request.form.get('fileName') - if filename is None: - raise BadRequest("No 'fileName' specified!") - elif not filename.startswith("/"): - raise BadRequest(f"Given 'fileName' doesn't start with a slash (/): {filename}") - - file_storage: Optional[FileStorage] = request.files.get('file') - if file_storage is None: - raise BadRequest("Missing file to upload") - elif file_storage.mimetype != submodel_element.content_type: - raise werkzeug.exceptions.UnsupportedMediaType( - f"Request body is of type {file_storage.mimetype!r}, " - f"while {submodel_element!r} has content_type {submodel_element.content_type!r}!") - - submodel_element.value = self.file_store.add_file(filename, file_storage.stream, submodel_element.content_type) - submodel_element.commit() - return response_t() - - def delete_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, - response_t: Type[APIResponse], - **_kwargs) -> Response: - submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - if not isinstance(submodel_element, (model.Blob, model.File)): - raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to delete!") - elif submodel_element.value is None: - raise NotFound(f"{submodel_element!r} has no attachment!") - - if isinstance(submodel_element, model.Blob): - submodel_element.value = None - else: - if not submodel_element.value.startswith("/"): - raise BadRequest(f"{submodel_element!r} references an external file: {submodel_element.value}") - try: - self.file_store.delete_file(submodel_element.value) - except KeyError: - pass - submodel_element.value = None - - submodel_element.commit() - return response_t() - - def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) - qualifier_type = url_args.get("qualifier_type") - if qualifier_type is None: - return response_t(list(sm_or_se.qualifier)) - return response_t(self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type)) - - def post_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - map_adapter: MapAdapter) -> Response: - sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) - qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) - if sm_or_se.qualifier.contains_id("type", qualifier.type): - raise Conflict(f"Qualifier with type {qualifier.type} already exists!") - sm_or_se.qualifier.add(qualifier) - sm_or_se.commit() - created_resource_url = map_adapter.build(self.get_submodel_submodel_element_qualifiers, { - "submodel_id": url_args["submodel_id"], - "id_shorts": url_args.get("id_shorts") or None, - "qualifier_type": qualifier.type - }, force_external=True) - return response_t(qualifier, status=201, headers={"Location": created_resource_url}) - - def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - map_adapter: MapAdapter) -> Response: - sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) - new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) - qualifier_type = url_args["qualifier_type"] - qualifier = self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type) - qualifier_type_changed = qualifier_type != new_qualifier.type - if qualifier_type_changed and sm_or_se.qualifier.contains_id("type", new_qualifier.type): - raise Conflict(f"A qualifier of type {new_qualifier.type!r} already exists for {sm_or_se!r}") - sm_or_se.remove_qualifier_by_type(qualifier.type) - sm_or_se.qualifier.add(new_qualifier) - sm_or_se.commit() - if qualifier_type_changed: - created_resource_url = map_adapter.build(self.get_submodel_submodel_element_qualifiers, { - "submodel_id": url_args["submodel_id"], - "id_shorts": url_args.get("id_shorts") or None, - "qualifier_type": new_qualifier.type - }, force_external=True) - return response_t(new_qualifier, status=201, headers={"Location": created_resource_url}) - return response_t(new_qualifier) - - def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, - response_t: Type[APIResponse], - **_kwargs) -> Response: - sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) - qualifier_type = url_args["qualifier_type"] - self._qualifiable_qualifier_op(sm_or_se, sm_or_se.remove_qualifier_by_type, qualifier_type) - sm_or_se.commit() - return response_t() - - # --------- CONCEPT DESCRIPTION ROUTES --------- - def get_concept_description_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> 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)) - - def post_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - map_adapter: MapAdapter) -> Response: - concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, - is_stripped_request(request)) - try: - self.object_store.add(concept_description) - except KeyError as e: - raise Conflict(f"ConceptDescription with Identifier {concept_description.id} already exists!") from e - concept_description.commit() - created_resource_url = map_adapter.build(self.get_concept_description, { - "concept_id": concept_description.id - }, force_external=True) - return response_t(concept_description, status=201, headers={"Location": created_resource_url}) - - def get_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - concept_description = self._get_concept_description(url_args) - return response_t(concept_description, stripped=is_stripped_request(request)) - - def put_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - concept_description = self._get_concept_description(url_args) - concept_description.update_from(HTTPApiDecoder.request_body(request, model.ConceptDescription, - is_stripped_request(request))) - concept_description.commit() - return response_t() - - def delete_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: - self.object_store.remove(self._get_concept_description(url_args)) - return response_t() - - -if __name__ == "__main__": - from werkzeug.serving import run_simple - from basyx.aas.examples.data.example_aas import create_full_example - - run_simple("localhost", 8080, WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()), - use_debugger=True, use_reloader=True) diff --git a/test/adapter/__init__.py b/sdk/test/adapter/__init__.py similarity index 100% rename from test/adapter/__init__.py rename to sdk/test/adapter/__init__.py diff --git a/test/adapter/aasx/TestFile.pdf b/sdk/test/adapter/aasx/TestFile.pdf similarity index 100% rename from test/adapter/aasx/TestFile.pdf rename to sdk/test/adapter/aasx/TestFile.pdf diff --git a/test/adapter/aasx/__init__.py b/sdk/test/adapter/aasx/__init__.py similarity index 100% rename from test/adapter/aasx/__init__.py rename to sdk/test/adapter/aasx/__init__.py diff --git a/test/adapter/aasx/test_aasx.py b/sdk/test/adapter/aasx/test_aasx.py similarity index 100% rename from test/adapter/aasx/test_aasx.py rename to sdk/test/adapter/aasx/test_aasx.py diff --git a/test/adapter/http-api-oas-aas.yaml b/sdk/test/adapter/http-api-oas-aas.yaml similarity index 100% rename from test/adapter/http-api-oas-aas.yaml rename to sdk/test/adapter/http-api-oas-aas.yaml diff --git a/test/adapter/http-api-oas-submodel.yaml b/sdk/test/adapter/http-api-oas-submodel.yaml similarity index 100% rename from test/adapter/http-api-oas-submodel.yaml rename to sdk/test/adapter/http-api-oas-submodel.yaml diff --git a/test/adapter/json/__init__.py b/sdk/test/adapter/json/__init__.py similarity index 100% rename from test/adapter/json/__init__.py rename to sdk/test/adapter/json/__init__.py diff --git a/test/adapter/json/test_json_deserialization.py b/sdk/test/adapter/json/test_json_deserialization.py similarity index 100% rename from test/adapter/json/test_json_deserialization.py rename to sdk/test/adapter/json/test_json_deserialization.py diff --git a/test/adapter/json/test_json_serialization.py b/sdk/test/adapter/json/test_json_serialization.py similarity index 100% rename from test/adapter/json/test_json_serialization.py rename to sdk/test/adapter/json/test_json_serialization.py diff --git a/test/adapter/json/test_json_serialization_deserialization.py b/sdk/test/adapter/json/test_json_serialization_deserialization.py similarity index 100% rename from test/adapter/json/test_json_serialization_deserialization.py rename to sdk/test/adapter/json/test_json_serialization_deserialization.py diff --git a/test/adapter/test_http.py b/sdk/test/adapter/test_http.py similarity index 100% rename from test/adapter/test_http.py rename to sdk/test/adapter/test_http.py diff --git a/test/adapter/xml/__init__.py b/sdk/test/adapter/xml/__init__.py similarity index 100% rename from test/adapter/xml/__init__.py rename to sdk/test/adapter/xml/__init__.py diff --git a/test/adapter/xml/test_xml_deserialization.py b/sdk/test/adapter/xml/test_xml_deserialization.py similarity index 100% rename from test/adapter/xml/test_xml_deserialization.py rename to sdk/test/adapter/xml/test_xml_deserialization.py diff --git a/test/adapter/xml/test_xml_serialization.py b/sdk/test/adapter/xml/test_xml_serialization.py similarity index 100% rename from test/adapter/xml/test_xml_serialization.py rename to sdk/test/adapter/xml/test_xml_serialization.py diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/sdk/test/adapter/xml/test_xml_serialization_deserialization.py similarity index 100% rename from test/adapter/xml/test_xml_serialization_deserialization.py rename to sdk/test/adapter/xml/test_xml_serialization_deserialization.py From b1b9895c190c92439989e580a0197558e9fcfd76 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Mon, 4 Nov 2024 10:02:19 +0100 Subject: [PATCH 458/474] sdk/basyx/adapter: modified the adapter from the basy-python-sdk repository to be compatible with aas-core3.0 --- sdk/basyx/adapter/aasx.py | 158 ++++++++---------- .../adapter/json/json_deserialization.py | 3 +- sdk/basyx/adapter/json/json_serialization.py | 22 +-- sdk/basyx/adapter/xml/xml_deserialization.py | 22 +-- 4 files changed, 92 insertions(+), 113 deletions(-) diff --git a/sdk/basyx/adapter/aasx.py b/sdk/basyx/adapter/aasx.py index 9f76bca..ee56a1e 100644 --- a/sdk/basyx/adapter/aasx.py +++ b/sdk/basyx/adapter/aasx.py @@ -39,6 +39,7 @@ from .xml.xml_deserialization import read_aas_xml_file import pyecma376_2 from .xml.xml_serialization import write_aas_xml_file + logger = logging.getLogger(__name__) RELATIONSHIP_TYPE_AASX_ORIGIN = "http://admin-shell.io/aasx/relationships/aasx-origin" @@ -47,6 +48,9 @@ RELATIONSHIP_TYPE_AAS_SUPL = "http://admin-shell.io/aasx/relationships/aas-suppl" +#id_type = model.Identifiable.__annotations__["id"] +id_type = str + class AASXReader: """ An AASXReader wraps an existing AASX package file to allow reading its contents and metadata. @@ -62,6 +66,7 @@ class AASXReader: reader.read_into(objects, files) """ + def __init__(self, file: Union[os.PathLike, str, IO]): """ Open an AASX reader for the given filename or file handle @@ -117,28 +122,28 @@ def get_thumbnail(self) -> Optional[bytes]: def read_into(self, object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", - override_existing: bool = False, **kwargs) -> Set[str]: + override_existing: bool = False, **kwargs) -> Set[id_type]: """ Read the contents of the AASX package and add them into a given - :class:`ObjectStore ` + :class:`ObjectStore ` This function does the main job of reading the AASX file's contents. It traverses the relationships within the package to find AAS JSON or XML parts, parses them and adds the contained AAS objects into the provided - ``object_store``. While doing so, it searches all parsed :class:`Submodels ` - for :class:`~basyx.aas.model.submodel.File` objects to extract the supplementary files. The referenced - supplementary files are added to the given ``file_store`` and the :class:`~basyx.aas.model.submodel.File` + ``object_store``. While doing so, it searches all parsed :class:`Submodels ` + for :class:`~aas_core3.types.File` objects to extract the supplementary files. The referenced + supplementary files are added to the given ``file_store`` and the :class:`~aas_core3.types.File` objects' values are updated with the absolute name of the supplementary file to allow for robust resolution the file within the ``file_store`` later. - :param object_store: An :class:`ObjectStore ` to add the AAS + :param object_store: An :class:`ObjectStore ` to add the AAS objects from the AASX file to :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the embedded supplementary files to :param override_existing: If ``True``, existing objects in the object store are overridden with objects from the - AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects + AASX that have the same Identifier. Default behavior is to skip those objects from the AASX. - :return: A set of the :class:`Identifiers ` of all - :class:`~basyx.aas.model.base.Identifiable` objects parsed from the AASX file + :return: A set of the Identifiers of all + :class:`~aas_core3.types.Identifiable` objects parsed from the AASX file """ # Find AASX-Origin part core_rels = self.reader.get_related_parts_by_type() @@ -147,17 +152,17 @@ def read_into(self, object_store: ObjectStore, except IndexError as e: raise ValueError("Not a valid AASX file: aasx-origin Relationship is missing.") from e - read_identifiables: Set[str] = set() + read_identifiables: Set[id_type] = set() # Iterate AAS files for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ - RELATIONSHIP_TYPE_AAS_SPEC]: + RELATIONSHIP_TYPE_AAS_SPEC]: self._read_aas_part_into(aas_part, object_store, file_store, read_identifiables, override_existing, **kwargs) # Iterate split parts of AAS file for split_part in self.reader.get_related_parts_by_type(aas_part)[ - RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: + RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: self._read_aas_part_into(split_part, object_store, file_store, read_identifiables, override_existing, **kwargs) @@ -178,7 +183,7 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> None: def _read_aas_part_into(self, part_name: str, object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", - read_identifiables: Set[str], + read_identifiables: Set[id_type], override_existing: bool, **kwargs) -> None: """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. @@ -195,7 +200,6 @@ def _read_aas_part_into(self, part_name: str, :param override_existing: If True, existing objects in the object store are overridden with objects from the AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. """ - #print("asd123123123") for obj in self._parse_aas_part(part_name, **kwargs): if obj.id in read_identifiables: @@ -220,21 +224,18 @@ def _parse_aas_part(self, part_name: str, **kwargs) -> ObjectStore: This method chooses and calls the correct parser. :param part_name: The OPC part name of the part to be parsed - :return: A DictObjectStore containing the parsed AAS objects + :return: An ObjectStore containing the parsed AAS objects """ - #print("asd123123123") content_type = self.reader.get_content_type(part_name) extension = part_name.split("/")[-1].split(".")[-1] if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - #print(part_name) return read_aas_xml_file(p, **kwargs) elif content_type.split(";")[0] in ("text/json", "application/json") \ or content_type == "" and extension == "json": logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) - #print("asd123123123") with self.reader.open_part(part_name) as p: return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs) @@ -272,7 +273,6 @@ def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel, element.value = final_name - class AASXWriter: """ An AASXWriter wraps a new AASX package file to write its contents to it piece by piece. @@ -331,24 +331,23 @@ def __init__(self, file: Union[os.PathLike, str, IO]): p.close() def write_aas(self, - aas_ids: Union[str], + aas_ids: Union[id_type], object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", write_json: bool = False) -> None: """ Convenience method to write one or more - :class:`AssetAdministrationShells ` with all included + :class:`AssetAdministrationShells` with all included and referenced objects to the AASX package according to the part name conventions from DotAAS. - This method takes the AASs' :class:`Identifiers ` (as ``aas_ids``) to retrieve + This method takes the AASs' Identifiers (as ``aas_ids``) to retrieve the AASs from the given ``object_store``. - :class:`References ` to :class:`Submodels ` - and :class:`ConceptDescriptions ` (via semanticId attributes) are + :class:`References` to :class:`Submodels` and :class:`ConceptDescriptions` (via semanticId attributes) are also resolved using the ``object_store``. All of these objects are written to an aas-spec part ``/aasx/data.xml`` or ``/aasx/data.json`` in the AASX package, compliant to the convention presented in "Details of the Asset Administration Shell". Supplementary files which are referenced by a - :class:`~basyx.aas.model.submodel.File` object in any of the - :class:`Submodels ` are also added to the AASX package. + :class:`aas_core3.type.File` object in any of the + :class:`Submodels` are also added to the AASX package. This method uses :meth:`write_all_aas_objects` to write the AASX part. @@ -361,56 +360,53 @@ def write_aas(self, To write multiple Asset Administration Shells to a single AASX package file, call this method once, passing a list of AAS Identifiers to the ``aas_ids`` parameter. - :param aas_ids: :class:`~basyx.aas.model.base.Identifier` or Iterable of - :class:`Identifiers ` of the AAS(s) to be written to the AASX file - :param object_store: :class:`ObjectStore ` to retrieve the - :class:`~basyx.aas.model.base.Identifiable` AAS objects - (:class:`~basyx.aas.model.aas.AssetAdministrationShell`, - :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.submodel.Submodel`) from + :param aas_ids: :class:`~aas_core3.types.Identifiable` or Iterable of + :class:`Identifiers ` of the AAS(s) to be written to the AASX file + :param object_store: :class:`ObjectStore ` to retrieve the + :class:`~aas_core3.types.Identifiable` AAS objects + (:class:`~aas_core3.types.Assetadministrationshell`, + :class:`~~aas_core3.types.ConceptDescription` and :class:`~aas_core3.types.Submodel`) from :param file_store: :class:`SupplementaryFileContainer ` to retrieve - supplementary files from, which are referenced by :class:`~basyx.aas.model.submodel.File` objects + supplementary files from, which are referenced by :class:`~aas_core3.types.File` objects :param write_json: If ``True``, JSON parts are created for the AAS and each - :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts. + :class:`~aas_core3.types.Submodel` in the AASX package file instead of XML parts. Defaults to ``False``. - :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable - :class:`Submodels ` and - :class:`ConceptDescriptions ` are skipped, logging a - warning/info message) + :raises KeyError: If one of the AAS could not be retrieved from the object store :raises TypeError: If one of the given AAS ids does not resolve to an AAS (but another - :class:`~basyx.aas.model.base.Identifiable` object) + Identifiable object) """ - #if isinstance(aas_ids, model.Identifiable.id): - # aas_ids = (aas_ids,) + objects_to_be_written: ObjectStore[model.Identifiable] = ObjectStore() for aas_id in aas_ids: try: - aas = object_store.get_identifiable(aas_id) + aas: model.AssetAdministrationShell = object_store.get_identifiable(aas_id) # TODO add failsafe mode except KeyError: raise if not isinstance(aas, model.AssetAdministrationShell): raise TypeError(f"Identifier {aas_id} does not belong to an AssetAdminstrationShell object but to " f"{aas!r}") - assert isinstance(aas,model.AssetAdministrationShell) # Add the AssetAdministrationShell object to the data part objects_to_be_written.add(aas) # Add referenced Submodels to the data part - for submodel_ref in aas.submodels: - try: - submodel_keys = submodel_ref.keys - for key in submodel_keys: - submodel_id = key.value - try: - submodel = object_store.get_identifiable(submodel_id) - except Exception: - continue - objects_to_be_written.add(submodel) - - except KeyError: - logger.warning("Could not find submodel %s. Skipping it.", str(submodel_ref)) - continue + if aas.submodels is not None: + + for submodel_ref in aas.submodels: + try: + submodel_keys = submodel_ref.keys + for key in submodel_keys: + submodel_id = key.value + try: + submodel = object_store.get_identifiable(submodel_id) + except Exception: + continue + objects_to_be_written.add(submodel) + + except KeyError: + logger.warning("Could not find submodel %s. Skipping it.", str(submodel_ref)) + continue # Traverse object tree and check if semanticIds are referencing to existing ConceptDescriptions in the # ObjectStore @@ -422,7 +418,7 @@ def write_aas(self, cd = object_store.get_identifiable(semantic_id) concept_descriptions.append(cd) except Exception: - continue + continue for element in concept_descriptions: objects_to_be_written.add(element) @@ -435,7 +431,7 @@ def write_aas(self, # Not actually required since you can always create a local dict def write_aas_objects(self, part_name: str, - object_ids: Iterable[str], + object_ids: Iterable[id_type], object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", write_json: bool = False, @@ -444,9 +440,9 @@ def write_aas_objects(self, """ A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as ``aas_id``) to retrieve it - from the given object_store. If the list of written objects includes :class:`~basyx.aas.model.submodel.Submodel` - objects, Supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within + This method takes the AAS's :class:`~aas_core3.types.Identifiable.id` (as ``aas_id``) to retrieve it + from the given object_store. If the list of written objects includes :class:`~aas_core3.types.Submodel` + objects, Supplementary files which are referenced by :class:`~aas_core3.types.File` objects within those submodels, are also added to the AASX package. .. attention:: @@ -457,13 +453,13 @@ def write_aas_objects(self, :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 part name and unique within the package. The extension of the part should match the data format (i.e. '.json' if ``write_json`` else '.xml'). - :param object_ids: A list of :class:`Identifiers ` of the objects to be written - to the AASX package. Only these :class:`~basyx.aas.model.base.Identifiable` objects (and included - :class:`~basyx.aas.model.base.Referable` objects) are written to the package. - :param object_store: The objects store to retrieve the :class:`~basyx.aas.model.base.Identifiable` objects from + :param object_ids: A list of :class:`Identifiers ` of the objects to be written + to the AASX package. Only these :class:`~aas_core3.types.Identifiable.id` objects (and included + :class:`~aas_core3.types.Referable` objects) are written to the package. + :param object_store: The objects store to retrieve the :class:`~aas_core3.types.Identifiable` objects from :param file_store: The - :class:`SupplementaryFileContainer ` - to retrieve supplementary files from (if there are any :class:`~basyx.aas.model.submodel.File` + :class:`SupplementaryFileContainer ` + to retrieve supplementary files from (if there are any :class:`~aas_core3.types.File` objects within the written objects. :param write_json: If ``True``, the part is written as a JSON file instead of an XML file. Defaults to ``False``. @@ -497,13 +493,13 @@ def write_all_aas_objects(self, split_part: bool = False, additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: """ - Write all AAS objects in a given :class:`ObjectStore ` to an XML + Write all AAS objects in a given :class:`ObjectStore ` to an XML or JSON part in the AASX package and add the referenced supplementary files to the package. - This method takes an :class:`ObjectStore ` and writes all + This method takes a :class:`ObjectStore ` and writes all contained objects into an ``aas_env`` part in the AASX package. If the ObjectStore includes - :class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by - :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store`` + :class:`~aas_core3.types.Submodel` objects, supplementary files which are referenced by + :class:`~aas_core3.types.submodel.File` objects within those Submodels, are fetched from the ``file_store`` and added to the AASX package. .. attention:: @@ -526,7 +522,6 @@ def write_all_aas_objects(self, logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) supplementary_files: List[str] = [] - # Retrieve objects and scan for referenced supplementary files for the_object in objects: if isinstance(the_object, model.Submodel): @@ -539,28 +534,18 @@ def write_all_aas_objects(self, continue supplementary_files.append(file_name) - - - # Add aas-spec relationship if not split_part: self._aas_part_names.append(part_name) - - # Write part # TODO allow writing xml *and* JSON part - #print(part_name) with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p: if write_json: write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects) else: write_aas_xml_file(p, objects) - - - - # Write submodel's supplementary files to AASX file supplementary_file_names = [] for file_name in supplementary_files: @@ -594,9 +579,6 @@ def write_all_aas_objects(self, additional_relationships), part_name) - - - def write_core_properties(self, core_properties: pyecma376_2.OPCCoreProperties): """ Write OPC Core Properties (meta data) to the AASX package file. @@ -743,13 +725,14 @@ class AbstractSupplementaryFileContainer(metaclass=abc.ABCMeta): Supplementary files may be PDF files or other binary or textual files, referenced in a File object of an AAS by their name. They are used to provide associated documents without embedding their contents (as - :class:`~basyx.aas.model.submodel.Blob` object) in the AAS. + :class:`~aas_core3.types.Blob` object) in the AAS. A SupplementaryFileContainer keeps track of the name and content_type (MIME type) for each file. Additionally it allows to resolve name conflicts by comparing the files' contents and providing an alternative name for a dissimilar new file. It also provides each files sha256 hash sum to allow name conflict checking in other classes (e.g. when writing AASX files). """ + @abc.abstractmethod def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: """ @@ -829,6 +812,7 @@ class DictSupplementaryFileContainer(AbstractSupplementaryFileContainer): """ SupplementaryFileContainer implementation using a dict to store the file contents in-memory. """ + def __init__(self): # Stores the files' contents, identified by their sha256 hash self._store: Dict[bytes, bytes] = {} diff --git a/sdk/basyx/adapter/json/json_deserialization.py b/sdk/basyx/adapter/json/json_deserialization.py index 8df4e01..a320fe4 100644 --- a/sdk/basyx/adapter/json/json_deserialization.py +++ b/sdk/basyx/adapter/json/json_deserialization.py @@ -74,7 +74,8 @@ def read_aas_json_file_into(object_store: ObjectStore, file: PathOrIO, replace_e for item in lst: identifiable = aas_jsonization.identifiable_from_jsonable(item) - + print(type(identifiable)) + print(expected_type) if identifiable.id in ret: error_message = f"{item} has a duplicate identifier already parsed in the document!" raise KeyError(error_message) diff --git a/sdk/basyx/adapter/json/json_serialization.py b/sdk/basyx/adapter/json/json_serialization.py index 7d8b924..3541353 100644 --- a/sdk/basyx/adapter/json/json_serialization.py +++ b/sdk/basyx/adapter/json/json_serialization.py @@ -10,22 +10,7 @@ Module for serializing Asset Administration Shell objects to the official JSON format -The module provides an custom JSONEncoder classes :class:`AASToJsonEncoder` and :class:`StrippedAASToJsonEncoder` -to be used with the Python standard :mod:`json` module. While the former serializes objects as defined in the -specification, the latter serializes stripped objects, excluding some attributes -(see https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91). -Each class contains a custom :meth:`~.AASToJsonEncoder.default` function which converts BaSyx Python SDK objects to -simple python types for an automatic JSON serialization. -To simplify the usage of this module, the :meth:`write_aas_json_file` and :meth:`object_store_to_json` are provided. -The former is used to serialize a given :class:`~basyx.AbstractObjectStore` to a file, while the -latter serializes the object store to a string and returns it. -The serialization is performed in an iterative approach: The :meth:`~.AASToJsonEncoder.default` function gets called for -every object and checks if an object is an BaSyx Python SDK object. In this case, it calls a special function for the -respective BaSyx Python SDK class which converts the object (but not the contained objects) into a simple Python dict, -which is serializable. Any contained BaSyx Python SDK objects are included into the dict as they are to be converted -later on. The special helper function ``_abstract_classes_to_json`` is called by most of the -conversion functions to handle all the attributes of abstract base classes. """ import base64 import contextlib @@ -69,6 +54,13 @@ def _create_dict(data: ObjectStore) -> dict: dict_['conceptDescriptions'] = concept_descriptions return dict_ +class _DetachingTextIOWrapper(io.TextIOWrapper): + """ + Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer. + """ + def __exit__(self, exc_type, exc_val, exc_tb): + self.detach() + def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: """ Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset diff --git a/sdk/basyx/adapter/xml/xml_deserialization.py b/sdk/basyx/adapter/xml/xml_deserialization.py index 193a0aa..f18e9b3 100644 --- a/sdk/basyx/adapter/xml/xml_deserialization.py +++ b/sdk/basyx/adapter/xml/xml_deserialization.py @@ -103,7 +103,8 @@ def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, - replace_existing: bool = False, ignore_existing: bool = False) -> Set[str]: + replace_existing: bool = False, ignore_existing: bool = False, + **parser_kwargs: Any) -> Set[str]: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 into a given :class:`ObjectStore `. @@ -134,8 +135,9 @@ def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, } element_constructors = {NS_AAS + k: v for k, v in element_constructors.items()} + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, **parser_kwargs) - root = etree.parse(file).getroot() + root = etree.parse(file, parser).getroot() if root is None: @@ -152,23 +154,23 @@ def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, for element in list_: str = etree.tostring(element).decode("utf-8-sig") - constructor = element_constructors[element_tag](str) + identifiable = element_constructors[element_tag](str) - if constructor.id in ret: + if identifiable.id in ret: error_message = f"{element} has a duplicate identifier already parsed in the document!" raise KeyError(error_message) - existing_element = object_store.get(constructor.id) + existing_element = object_store.get(identifiable.id) if existing_element is not None: if not replace_existing: - error_message = f"object with identifier {constructor.id} already exists " \ + error_message = f"object with identifier {identifiable.id} already exists " \ f"in the object store: {existing_element}!" if not ignore_existing: - raise KeyError(error_message + f" failed to insert {constructor}!") - logger.info(error_message + f" skipping insertion of {constructor}...") + raise KeyError(error_message + f" failed to insert {identifiable}!") + logger.info(error_message + f" skipping insertion of {identifiable}...") continue object_store.discard(existing_element) - object_store.add(constructor) - ret.add(constructor.id) + object_store.add(identifiable) + ret.add(identifiable.id) return ret From a5955b20d2577ecca7d924a64aa63de8b5202a03 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Tue, 5 Nov 2024 10:20:40 +0100 Subject: [PATCH 459/474] sdk/: Add unittests for aasx.py including various testfiles. Fix codestyle/mypy errors --- sdk/basyx/adapter/_generic.py | 2 +- sdk/basyx/adapter/aasx.py | 26 +- sdk/basyx/adapter/json/__init__.py | 4 - .../adapter/json/json_deserialization.py | 2 - sdk/basyx/adapter/json/json_serialization.py | 6 +- sdk/basyx/adapter/xml/__init__.py | 5 - sdk/basyx/adapter/xml/xml_deserialization.py | 1 - .../aasx => basyx/tutorial/data}/TestFile.pdf | Bin .../json => basyx/tutorial/data}/__init__.py | 0 .../tutorial/tutorial_create_simple_aas.py | 170 +- sdk/basyx/tutorial/tutorial_objectstore.py | 3 + sdk/pyproject.toml | 4 + sdk/test/adapter/aasx/example_aas.py | 52 + sdk/test/adapter/aasx/test_aasx.py | 28 +- sdk/test/adapter/http-api-oas-aas.yaml | 1421 ------------ sdk/test/adapter/http-api-oas-submodel.yaml | 2017 ----------------- .../adapter/json/test_json_deserialization.py | 402 ---- .../adapter/json/test_json_serialization.py | 234 -- ...test_json_serialization_deserialization.py | 106 - sdk/test/adapter/test_http.py | 131 -- sdk/test/adapter/xml/__init__.py | 0 .../adapter/xml/test_xml_deserialization.py | 478 ---- .../adapter/xml/test_xml_serialization.py | 148 -- .../test_xml_serialization_deserialization.py | 68 - 24 files changed, 146 insertions(+), 5162 deletions(-) rename sdk/{test/adapter/aasx => basyx/tutorial/data}/TestFile.pdf (100%) rename sdk/{test/adapter/json => basyx/tutorial/data}/__init__.py (100%) create mode 100644 sdk/test/adapter/aasx/example_aas.py delete mode 100644 sdk/test/adapter/http-api-oas-aas.yaml delete mode 100644 sdk/test/adapter/http-api-oas-submodel.yaml delete mode 100644 sdk/test/adapter/json/test_json_deserialization.py delete mode 100644 sdk/test/adapter/json/test_json_serialization.py delete mode 100644 sdk/test/adapter/json/test_json_serialization_deserialization.py delete mode 100644 sdk/test/adapter/test_http.py delete mode 100644 sdk/test/adapter/xml/__init__.py delete mode 100644 sdk/test/adapter/xml/test_xml_deserialization.py delete mode 100644 sdk/test/adapter/xml/test_xml_serialization.py delete mode 100644 sdk/test/adapter/xml/test_xml_serialization_deserialization.py diff --git a/sdk/basyx/adapter/_generic.py b/sdk/basyx/adapter/_generic.py index 02a6bed..d2b7801 100644 --- a/sdk/basyx/adapter/_generic.py +++ b/sdk/basyx/adapter/_generic.py @@ -20,4 +20,4 @@ # XML Namespace definition XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} -XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" \ No newline at end of file +XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" diff --git a/sdk/basyx/adapter/aasx.py b/sdk/basyx/adapter/aasx.py index ee56a1e..6fa332e 100644 --- a/sdk/basyx/adapter/aasx.py +++ b/sdk/basyx/adapter/aasx.py @@ -31,6 +31,7 @@ import re from typing import Dict, Tuple, IO, Union, List, Set, Optional, Iterable, Iterator +from aas_core3.types import HasSemantics from basyx.object_store import ObjectStore from aas_core3 import types as model from .json.json_serialization import write_aas_json_file @@ -47,10 +48,11 @@ RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://admin-shell.io/aasx/relationships/aas-spec-split" RELATIONSHIP_TYPE_AAS_SUPL = "http://admin-shell.io/aasx/relationships/aas-suppl" - -#id_type = model.Identifiable.__annotations__["id"] +# id_type = model.Identifiable.__annotations__["id"] using this we can refer to the type_hint of "id" of the class +# Identifiable. Doing this leads to problems with mypy... id_type = str + class AASXReader: """ An AASXReader wraps an existing AASX package file to allow reading its contents and metadata. @@ -155,14 +157,12 @@ def read_into(self, object_store: ObjectStore, read_identifiables: Set[id_type] = set() # Iterate AAS files - for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[ - RELATIONSHIP_TYPE_AAS_SPEC]: + for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[RELATIONSHIP_TYPE_AAS_SPEC]: self._read_aas_part_into(aas_part, object_store, file_store, read_identifiables, override_existing, **kwargs) # Iterate split parts of AAS file - for split_part in self.reader.get_related_parts_by_type(aas_part)[ - RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: + for split_part in self.reader.get_related_parts_by_type(aas_part)[RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: self._read_aas_part_into(split_part, object_store, file_store, read_identifiables, override_existing, **kwargs) @@ -331,7 +331,7 @@ def __init__(self, file: Union[os.PathLike, str, IO]): p.close() def write_aas(self, - aas_ids: Union[id_type], + aas_ids: list[id_type], object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", write_json: bool = False) -> None: @@ -376,7 +376,6 @@ def write_aas(self, Identifiable object) """ - objects_to_be_written: ObjectStore[model.Identifiable] = ObjectStore() for aas_id in aas_ids: try: @@ -413,12 +412,13 @@ def write_aas(self, concept_descriptions: List[model.ConceptDescription] = [] for identifiable in objects_to_be_written: for element in identifiable.descend(): - try: + if isinstance(element, HasSemantics): + semantic_id = element.semantic_id - cd = object_store.get_identifiable(semantic_id) - concept_descriptions.append(cd) - except Exception: - continue + if semantic_id is not None: + for key in semantic_id.keys: + cd = object_store.get_identifiable(key.value) + concept_descriptions.append(cd) for element in concept_descriptions: objects_to_be_written.add(element) diff --git a/sdk/basyx/adapter/json/__init__.py b/sdk/basyx/adapter/json/__init__.py index 427833c..a2b0fde 100644 --- a/sdk/basyx/adapter/json/__init__.py +++ b/sdk/basyx/adapter/json/__init__.py @@ -16,7 +16,3 @@ AAS objects within a JSON file and return them as BaSyx Python SDK :class:`ObjectStore `. """ - -#from .json_serialization import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file, object_store_to_json -#from .json_deserialization import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrippedAASFromJsonDecoder, \ - # StrictStrippedAASFromJsonDecoder, read_aas_json_file, read_aas_json_file_into diff --git a/sdk/basyx/adapter/json/json_deserialization.py b/sdk/basyx/adapter/json/json_deserialization.py index a320fe4..499a66c 100644 --- a/sdk/basyx/adapter/json/json_deserialization.py +++ b/sdk/basyx/adapter/json/json_deserialization.py @@ -74,8 +74,6 @@ def read_aas_json_file_into(object_store: ObjectStore, file: PathOrIO, replace_e for item in lst: identifiable = aas_jsonization.identifiable_from_jsonable(item) - print(type(identifiable)) - print(expected_type) if identifiable.id in ret: error_message = f"{item} has a duplicate identifier already parsed in the document!" raise KeyError(error_message) diff --git a/sdk/basyx/adapter/json/json_serialization.py b/sdk/basyx/adapter/json/json_serialization.py index 3541353..4d7b699 100644 --- a/sdk/basyx/adapter/json/json_serialization.py +++ b/sdk/basyx/adapter/json/json_serialization.py @@ -27,7 +27,6 @@ import os from typing import BinaryIO, Dict, IO, Type, Union - Path = Union[str, bytes, os.PathLike] PathOrBinaryIO = Union[Path, BinaryIO] PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO @@ -54,13 +53,16 @@ def _create_dict(data: ObjectStore) -> dict: dict_['conceptDescriptions'] = concept_descriptions return dict_ + class _DetachingTextIOWrapper(io.TextIOWrapper): """ Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer. """ + def __exit__(self, exc_type, exc_val, exc_tb): self.detach() + def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: """ Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset @@ -89,5 +91,3 @@ def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: with cm as fp: json.dump(_create_dict(data), fp, **kwargs) - - diff --git a/sdk/basyx/adapter/xml/__init__.py b/sdk/basyx/adapter/xml/__init__.py index e986321..6d2bc75 100644 --- a/sdk/basyx/adapter/xml/__init__.py +++ b/sdk/basyx/adapter/xml/__init__.py @@ -9,8 +9,3 @@ :ref:`xml_deserialization `: The module offers a function to create an :class:`ObjectStore ` from a given xml document. """ - -#from .xml_serialization import object_store_to_xml_element, write_aas_xml_file, object_to_xml_element, \ -# write_aas_xml_element -#from .xml_deserialization import AASFromXmlDecoder, StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder, \ -# StrictStrippedAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element diff --git a/sdk/basyx/adapter/xml/xml_deserialization.py b/sdk/basyx/adapter/xml/xml_deserialization.py index f18e9b3..a597ea8 100644 --- a/sdk/basyx/adapter/xml/xml_deserialization.py +++ b/sdk/basyx/adapter/xml/xml_deserialization.py @@ -139,7 +139,6 @@ def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, root = etree.parse(file, parser).getroot() - if root is None: return ret # Add AAS objects to ObjectStore diff --git a/sdk/test/adapter/aasx/TestFile.pdf b/sdk/basyx/tutorial/data/TestFile.pdf similarity index 100% rename from sdk/test/adapter/aasx/TestFile.pdf rename to sdk/basyx/tutorial/data/TestFile.pdf diff --git a/sdk/test/adapter/json/__init__.py b/sdk/basyx/tutorial/data/__init__.py similarity index 100% rename from sdk/test/adapter/json/__init__.py rename to sdk/basyx/tutorial/data/__init__.py diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_create_simple_aas.py index 43b8995..11f5fd8 100644 --- a/sdk/basyx/tutorial/tutorial_create_simple_aas.py +++ b/sdk/basyx/tutorial/tutorial_create_simple_aas.py @@ -1,134 +1,78 @@ -#!/usr/bin/env python3 -# This work is licensed under a Creative Commons CCZero 1.0 Universal License. -# See http://creativecommons.org/publicdomain/zero/1.0/ for more information. -""" -Tutorial for the creation of a simple Asset Administration Shell, containing an AssetInformation object and a Submodel -reference using aas-core3.0-python -""" - -# Import all type classes from the aas-core3.0-python SDK +import json import aas_core3.types as aas_types +import aas_core3.jsonization as aas_jsonization +from basyx.object_store import ObjectStore +from basyx.adapter.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer +import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class. +import datetime +from pathlib import Path # Used for easier handling of auxiliary file's local path -# In this tutorial, you'll get a step-by-step guide on how to create an Asset Administration Shell (AAS) and all -# required objects within. First, you need an AssetInformation object for which you want to create an AAS. After that, -# an Asset Administration Shell can be created. Then, it's possible to add Submodels to the AAS. The Submodels can -# contain SubmodelElements. +Referencetype = aas_types.ReferenceTypes("ModelReference") -# Step-by-Step Guide: -# Step 1: create a simple Asset Administration Shell, containing AssetInformation object -# Step 2: create a simple Submodel -# Step 3: create a simple Property and add it to the Submodel +key_types = aas_types.KeyTypes("Submodel") +key = aas_types.Key(value="some-unique-global-identifier", type=key_types) -############################################################################################ -# Step 1: Create a Simple Asset Administration Shell Containing an AssetInformation object # -############################################################################################ -# Step 1.1: create the AssetInformation object -asset_information = aas_types.AssetInformation( - asset_kind=aas_types.AssetKind.INSTANCE, - global_asset_id='http://acplt.org/Simple_Asset' -) +reference = aas_types.Reference(type=Referencetype, keys=[key]) -# Step 1.2: create the Asset Administration Shell -identifier = 'https://acplt.org/Simple_AAS' -aas = aas_types.AssetAdministrationShell( - id=identifier, # set identifier - asset_information=asset_information, - submodels=[] +submodel = aas_types.Submodel( + id="some-unique-global-identifier", + submodel_elements=[ + aas_types.Property( + id_short="some_property", + value_type=aas_types.DataTypeDefXSD.INT, + value="1984", + semantic_id=reference + ) + ] ) +file_store = DictSupplementaryFileContainer() -############################################################# -# Step 2: Create a Simple Submodel Without SubmodelElements # -############################################################# +with open(Path(__file__).parent / 'data' / 'TestFile.pdf', 'rb') as f: + actual_file_name = file_store.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") -# Step 2.1: create the Submodel object -identifier = 'https://acplt.org/Simple_Submodel' -submodel = aas_types.Submodel( - id=identifier, - submodel_elements=[] -) +if submodel.submodel_elements is not None: + submodel.submodel_elements.append(aas_types.File(id_short="documentationFile", + content_type="application/pdf", + value=actual_file_name)) -# Step 2.2: create a reference to that Submodel and add it to the Asset Administration Shell's `submodel` set -submodel_reference = aas_types.Reference( - type=aas_types.ReferenceTypes.MODEL_REFERENCE, - keys=[aas_types.Key( - type=aas_types.KeyTypes.SUBMODEL, - value=identifier - )] -) +aas = aas_types.AssetAdministrationShell(id="urn:x-test:aas1", + asset_information=aas_types.AssetInformation( + asset_kind=aas_types.AssetKind.TYPE), + submodels=[reference]) -# Warning, this overwrites whatever is in the `aas.submodels` list. -# In your production code, it might make sense to check for already existing content. -aas.submodels = [submodel_reference] +obj_store: ObjectStore = ObjectStore() +obj_store.add(aas) +obj_store.add(submodel) -# =============================================================== -# ALTERNATIVE: step 1 and 2 can alternatively be done in one step -# In this version, the Submodel reference is passed to the Asset Administration Shell's constructor. -submodel = aas_types.Submodel( - id='https://acplt.org/Simple_Submodel', - submodel_elements=[] -) -aas = aas_types.AssetAdministrationShell( - id='https://acplt.org/Simple_AAS', - asset_information=asset_information, - submodels=[aas_types.Reference( - type=aas_types.ReferenceTypes.MODEL_REFERENCE, - keys=[aas_types.Key( - type=aas_types.KeyTypes.SUBMODEL, - value='https://acplt.org/Simple_Submodel' - )] - )] -) +# Serialize to a JSON-able mapping +jsonable = aas_jsonization.to_jsonable(submodel) -############################################################### -# Step 3: Create a Simple Property and Add it to the Submodel # -############################################################### +meta_data = pyecma376_2.OPCCoreProperties() +meta_data.creator = "Chair of Process Control Engineering" +meta_data.created = datetime.datetime.now() -# Step 3.1: create a global reference to a semantic description of the Property -# A global reference consists of one key which points to the address where the semantic description is stored -semantic_reference = aas_types.Reference( - type=aas_types.ReferenceTypes.MODEL_REFERENCE, - keys=[aas_types.Key( - type=aas_types.KeyTypes.GLOBAL_REFERENCE, - value='http://acplt.org/Properties/SimpleProperty' - )] -) +with AASXWriter("./MyAASXPackage.aasx") as writer: + writer.write_aas(aas_ids=["urn:x-test:aas1"], + object_store=obj_store, + file_store=file_store, + write_json=False) + writer.write_core_properties(meta_data) -# Step 3.2: create the simple Property -property_ = aas_types.Property( - id_short='ExampleProperty', # Identifying string of the element within the Submodel namespace - value_type=aas_types.DataTypeDefXSD.STRING, # Data type of the value - value='exampleValue', # Value of the Property - semantic_id=semantic_reference # set the semantic reference -) +new_object_store: ObjectStore = ObjectStore() +new_file_store = DictSupplementaryFileContainer() -# Step 3.3: add the Property to the Submodel +with AASXReader("./MyAASXPackage.aasx") as reader: + # Read all contained AAS objects and all referenced auxiliary files + reader.read_into(object_store=new_object_store, + file_store=new_file_store) -# Warning, this overwrites whatever is in the `submodel_elements` list. -# In your production code, it might make sense to check for already existing content. -submodel.submodel_elements = [property_] +print(new_object_store.__len__()) +for item in file_store.__iter__(): + print(item) - -# ===================================================================== -# ALTERNATIVE: step 2 and 3 can also be combined in a single statement: -# Again, we pass the Property to the Submodel's constructor instead of adding it afterward. -submodel = aas_types.Submodel( - id='https://acplt.org/Simple_Submodel', - submodel_elements=[ - aas_types.Property( - id_short='ExampleProperty', - value_type=aas_types.DataTypeDefXSD.STRING, - value='exampleValue', - semantic_id=aas_types.Reference( - type=aas_types.ReferenceTypes.MODEL_REFERENCE, - keys=[aas_types.Key( - type=aas_types.KeyTypes.GLOBAL_REFERENCE, - value='http://acplt.org/Properties/SimpleProperty' - )] - ) - ) - ] -) +for item in new_file_store.__iter__(): + print(item) diff --git a/sdk/basyx/tutorial/tutorial_objectstore.py b/sdk/basyx/tutorial/tutorial_objectstore.py index f98a093..ac75b31 100644 --- a/sdk/basyx/tutorial/tutorial_objectstore.py +++ b/sdk/basyx/tutorial/tutorial_objectstore.py @@ -78,3 +78,6 @@ # Retrieve parent of list_element by id_short print(element_list == obj_store.get_parent_referable("list_1")) + +print(aas.__dict__) +print(type(submodel1), type(aas)) diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 7b47198..7d46186 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -14,3 +14,7 @@ authors = [ description="The Eclipse BaSyx Python SDK, an implementation of the Asset Administration Shell for Industry 4.0 systems" readme = "README.md" license = {file = "./LICENSE"} + +[tool.setuptools] +packages = ["basyx"] +py-modules = ['basyx'] diff --git a/sdk/test/adapter/aasx/example_aas.py b/sdk/test/adapter/aasx/example_aas.py new file mode 100644 index 0000000..27601bd --- /dev/null +++ b/sdk/test/adapter/aasx/example_aas.py @@ -0,0 +1,52 @@ +from aas_core3 import types as model +from basyx.object_store import ObjectStore +import aas_core3.types as aas_types +from basyx.adapter.aasx import DictSupplementaryFileContainer +from pathlib import Path + + +def create_full_example() -> ObjectStore: + """ + Creates an object store which is filled with an example :class:`~basyx.aas.model.submodel.Submodel`, + :class:`~basyx.aas.model.concept.ConceptDescription` and :class:`~basyx.aas.model.aas.AssetAdministrationShell` + using the functions of this module + + :return: :class:`~basyx.aas.model.provider.DictObjectStore` + """ + Referencetype = aas_types.ReferenceTypes("ModelReference") + + key_types = aas_types.KeyTypes("Submodel") + + key = aas_types.Key(value="some-unique-global-identifier", type=key_types) + + reference = aas_types.Reference(type=Referencetype, keys=[key]) + + submodel = aas_types.Submodel( + id="some-unique-global-identifier", + submodel_elements=[ + aas_types.Property( + id_short="some_property", + value_type=aas_types.DataTypeDefXSD.INT, + value="1984", + semantic_id=reference + ) + ] + ) + file_store = DictSupplementaryFileContainer() + + with open(Path(__file__).parent.parent.parent.parent/ 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: + actual_file_name = file_store.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") + + if submodel.submodel_elements is not None: + submodel.submodel_elements.append(aas_types.File(id_short="documentationFile", + content_type="application/pdf", + value=actual_file_name)) + + aas = aas_types.AssetAdministrationShell(id="https://acplt.org/Test_AssetAdministrationShell", + asset_information=aas_types.AssetInformation( + asset_kind=aas_types.AssetKind.TYPE), + submodels=[reference]) + obj_store: ObjectStore[model.Identifiable] = ObjectStore() + obj_store.add(aas) + obj_store.add(submodel) + return obj_store diff --git a/sdk/test/adapter/aasx/test_aasx.py b/sdk/test/adapter/aasx/test_aasx.py index 132a935..340c523 100644 --- a/sdk/test/adapter/aasx/test_aasx.py +++ b/sdk/test/adapter/aasx/test_aasx.py @@ -11,11 +11,13 @@ import tempfile import unittest import warnings +from pathlib import Path # Used for easier handling of auxiliary file's local path import pyecma376_2 -from basyx.aas import model -from basyx.aas.adapter import aasx -from basyx.aas.examples.data import example_aas, example_aas_mandatory_attributes, _helper +from aas_core3 import types as model +from basyx.adapter import aasx +from . import example_aas +from basyx.object_store import ObjectStore class TestAASXUtils(unittest.TestCase): @@ -28,7 +30,7 @@ def test_name_friendlyfier(self) -> None: def test_supplementary_file_container(self) -> None: container = aasx.DictSupplementaryFileContainer() - with open(os.path.join(os.path.dirname(__file__), 'TestFile.pdf'), 'rb') as f: + with open(Path(__file__).parent.parent.parent.parent/ 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: new_name = container.add_file("/TestFile.pdf", f, "application/pdf") # Name should not be modified, since there is no conflict self.assertEqual("/TestFile.pdf", new_name) @@ -77,8 +79,8 @@ def test_writing_reading_example_aas(self) -> None: # Create example data and file_store data = example_aas.create_full_example() files = aasx.DictSupplementaryFileContainer() - with open(os.path.join(os.path.dirname(__file__), 'TestFile.pdf'), 'rb') as f: - files.add_file("/TestFile.pdf", f, "application/pdf") + with open(Path(__file__).parent.parent.parent.parent/ 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: + files.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") f.seek(0) # Create OPC/AASX core properties @@ -89,7 +91,7 @@ def test_writing_reading_example_aas(self) -> None: # Write AASX file for write_json in (False, True): with self.subTest(write_json=write_json): - fd, filename = tempfile.mkstemp(suffix=".aasx") + fd, filename = tempfile.mkstemp(suffix="test.aasx") os.close(fd) # Write AASX file @@ -98,7 +100,7 @@ def test_writing_reading_example_aas(self) -> None: with warnings.catch_warnings(record=True) as w: with aasx.AASXWriter(filename) as writer: # TODO test writing multiple AAS - writer.write_aas('https://acplt.org/Test_AssetAdministrationShell', + writer.write_aas(['https://acplt.org/Test_AssetAdministrationShell'], data, files, write_json=write_json) writer.write_core_properties(cp) @@ -107,16 +109,12 @@ def test_writing_reading_example_aas(self) -> None: f"{[warning.message for warning in w]}") # Read AASX file - new_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + new_data: ObjectStore[model.Identifiable] = ObjectStore() new_files = aasx.DictSupplementaryFileContainer() with aasx.AASXReader(filename) as reader: reader.read_into(new_data, new_files) new_cp = reader.get_core_properties() - # Check AAS objects - checker = _helper.AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, new_data) - # Check core properties assert isinstance(cp.created, datetime.datetime) # to make mypy happy self.assertIsInstance(new_cp.created, datetime.datetime) @@ -126,9 +124,9 @@ def test_writing_reading_example_aas(self) -> None: self.assertIsNone(new_cp.lastModifiedBy) # Check files - self.assertEqual(new_files.get_content_type("/TestFile.pdf"), "application/pdf") + self.assertEqual(new_files.get_content_type("/aasx/suppl/MyExampleFile.pdf"), "application/pdf") file_content = io.BytesIO() - new_files.write_file("/TestFile.pdf", file_content) + new_files.write_file("/aasx/suppl/MyExampleFile.pdf", file_content) self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), "78450a66f59d74c073bf6858db340090ea72a8b1") diff --git a/sdk/test/adapter/http-api-oas-aas.yaml b/sdk/test/adapter/http-api-oas-aas.yaml deleted file mode 100644 index 27d0293..0000000 --- a/sdk/test/adapter/http-api-oas-aas.yaml +++ /dev/null @@ -1,1421 +0,0 @@ -openapi: 3.0.0 -info: - version: "1" - title: PyI40AAS REST API - description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). - - - **AAS Interface** - - - Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`." - contact: - name: "Michael Thies, Torben Miny, Leon Möller" - license: - name: Use under Eclipse Public License 2.0 - url: "https://www.eclipse.org/legal/epl-2.0/" -servers: - - url: http://{authority}/{basePath}/{api-version} - description: This is the Server to access the Asset Administration Shell - variables: - authority: - default: localhost:8080 - description: The authority is the server url (made of IP-Address or DNS-Name, user information, and/or port information) of the hosting environment for the Asset Administration Shell - basePath: - default: api - description: The basePath variable is additional path information for the hosting environment. It may contain the name of an aggregation point like 'shells' and/or API version information and/or tenant-id information, etc. - api-version: - default: v1 - description: The Version of the API-Specification -paths: - "/": - get: - summary: Retrieves the stripped AssetAdministrationShell, without Submodel-References and Views. - operationId: ReadAAS - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/AssetAdministrationShellResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - "/submodels/": - get: - summary: Returns all Submodel-References of the AssetAdministrationShell - operationId: ReadAASSubmodelReferences - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceListResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - post: - summary: Adds a new Submodel-Reference to the AssetAdministrationShell - operationId: CreateAASSubmodelReference - requestBody: - description: The Submodel-Reference to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/Reference" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - headers: - Location: - description: The URL of the created Submodel-Reference - schema: - type: string - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: Submodel-Reference already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a Reference or not resolvable - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - "/submodels/{submodel-identifier}/": - parameters: - - name: submodel-identifier - in: path - description: The Identifier of the referenced Submodel - required: true - schema: - type: string - get: - summary: Returns the Reference specified by submodel-identifier - operationId: ReadAASSubmodelReference - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - "400": - description: Invalid submodel-identifier format - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: AssetAdministrationShell not found or the specified Submodel is not referenced - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - delete: - summary: Deletes the Reference specified by submodel-identifier - operationId: DeleteAASSubmodelReference - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "400": - description: Invalid submodel-identifier format - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: AssetAdministrationShell not found or the specified Submodel is not referenced - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - "/views/": - get: - summary: Returns all Views of the AssetAdministrationShell - operationId: ReadAASViews - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewListResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - post: - summary: Adds a new View to the AssetAdministrationShell - operationId: CreateAASView - requestBody: - description: The View to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The URL of the created View - schema: - type: string - links: - ReadAASViewByIdShort: - $ref: "#/components/links/UpdateAASViewByIdShort" - UpdateAASViewByIdShort: - $ref: "#/components/links/UpdateAASViewByIdShort" - DeleteAASViewByIdShort: - $ref: "#/components/links/DeleteAASViewByIdShort" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: View with same idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - "/views/{view-idShort}/": - parameters: - - name: view-idShort - in: path - description: The idShort of the View - required: true - schema: - type: string - get: - summary: Returns a specific View of the AssetAdministrationShell - operationId: ReadAASView - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - links: - ReadAASViewByIdShort: - $ref: "#/components/links/ReadAASViewByIdShort" - UpdateAASViewByIdShort: - $ref: "#/components/links/UpdateAASViewByIdShort" - DeleteAASViewByIdShort: - $ref: "#/components/links/DeleteAASViewByIdShort" - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - put: - summary: Updates a specific View of the AssetAdministrationShell - operationId: UpdateAASView - requestBody: - description: The View used to overwrite the existing View - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - links: - ReadAASViewByIdShort: - $ref: "#/components/links/ReadAASViewByIdShort" - UpdateAASViewByIdShort: - $ref: "#/components/links/UpdateAASViewByIdShort" - DeleteAASViewByIdShort: - $ref: "#/components/links/DeleteAASViewByIdShort" - "201": - description: Success (idShort changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The new URL of the View - schema: - type: string - links: - ReadAASViewByIdShort: - $ref: "#/components/links/ReadAASViewByIdShort" - DeleteAASViewByIdShort: - $ref: "#/components/links/DeleteAASViewByIdShort" - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: idShort changed and new idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - delete: - summary: Deletes a specific View from the Asset Administration Shell - operationId: DeleteAASView - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface -components: - links: - ReadAASViewByIdShort: - description: The `idShort` of the returned View can be used to read the View. - operationId: ReadAASView - parameters: - view-idShort: "$response.body#/data/idShort" - UpdateAASViewByIdShort: - description: The `idShort` of the returned View can be used to update the View. - operationId: UpdateAASView - parameters: - view-idShort: "$response.body#/data/idShort" - DeleteAASViewByIdShort: - description: The `idShort` of the returned View can be used to delete the View. - operationId: DeleteAASView - parameters: - view-idShort: "$response.body#/data/idShort" - schemas: - BaseResult: - type: object - properties: - success: - type: boolean - error: - type: object - nullable: true - properties: - type: - enum: - - Unspecified - - Debug - - Information - - Warning - - Error - - Fatal - - Exception - type: string - code: - type: string - text: - type: string - data: - nullable: true - AssetAdministrationShellResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/StrippedAssetAdministrationShell" - error: - nullable: true - ReferenceResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/Reference" - error: - nullable: true - ReferenceListResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - type: array - items: - $ref: "#/components/schemas/Reference" - error: - nullable: true - ViewResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/View" - error: - nullable: true - ViewListResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - type: array - items: - $ref: "#/components/schemas/View" - error: - nullable: true - SubmodelResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/StrippedSubmodel" - error: - nullable: true - SubmodelElementResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/StrippedSubmodelElement" - error: - nullable: true - SubmodelElementListResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - type: array - items: - $ref: "#/components/schemas/StrippedSubmodelElement" - error: - nullable: true - ConstraintResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/Constraint" - error: - nullable: true - ConstraintListResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - type: array - items: - $ref: "#/components/schemas/Constraint" - error: - nullable: true - StrippedAssetAdministrationShell: - allOf: - - $ref: "#/components/schemas/AssetAdministrationShell" - - properties: - views: - not: {} - submodels: - not: {} - conceptDictionaries: - not: {} - StrippedSubmodel: - allOf: - - $ref: "#/components/schemas/Submodel" - - properties: - submodelElements: - not: {} - qualifiers: - not: {} - StrippedSubmodelElement: - allOf: - - $ref: "#/components/schemas/SubmodelElement" - - properties: - qualifiers: - not: {} - Referable: - allOf: - - $ref: '#/components/schemas/HasExtensions' - - properties: - idShort: - type: string - category: - type: string - displayName: - type: string - description: - type: array - items: - $ref: '#/components/schemas/LangString' - modelType: - $ref: '#/components/schemas/ModelType' - required: - - modelType - Identifiable: - allOf: - - $ref: '#/components/schemas/Referable' - - properties: - identification: - $ref: '#/components/schemas/Identifier' - administration: - $ref: '#/components/schemas/AdministrativeInformation' - required: - - identification - Qualifiable: - type: object - properties: - qualifiers: - type: array - items: - $ref: '#/components/schemas/Constraint' - HasSemantics: - type: object - properties: - semanticId: - $ref: '#/components/schemas/Reference' - HasDataSpecification: - type: object - properties: - embeddedDataSpecifications: - type: array - items: - $ref: '#/components/schemas/EmbeddedDataSpecification' - HasExtensions: - type: object - properties: - extensions: - type: array - items: - $ref: '#/components/schemas/Extension' - Extension: - allOf: - - $ref: '#/components/schemas/HasSemantics' - - properties: - name: - type: string - valueType: - type: string - enum: - - anyUri - - base64Binary - - boolean - - date - - dateTime - - dateTimeStamp - - decimal - - integer - - long - - int - - short - - byte - - nonNegativeInteger - - positiveInteger - - unsignedLong - - unsignedInt - - unsignedShort - - unsignedByte - - nonPositiveInteger - - negativeInteger - - double - - duration - - dayTimeDuration - - yearMonthDuration - - float - - gDay - - gMonth - - gMonthDay - - gYear - - gYearMonth - - hexBinary - - NOTATION - - QName - - string - - normalizedString - - token - - language - - Name - - NCName - - ENTITY - - ID - - IDREF - - NMTOKEN - - time - value: - type: string - refersTo: - $ref: '#/components/schemas/Reference' - required: - - name - AssetAdministrationShell: - allOf: - - $ref: '#/components/schemas/Identifiable' - - $ref: '#/components/schemas/HasDataSpecification' - - properties: - derivedFrom: - $ref: '#/components/schemas/Reference' - assetInformation: - $ref: '#/components/schemas/AssetInformation' - submodels: - type: array - items: - $ref: '#/components/schemas/Reference' - views: - type: array - items: - $ref: '#/components/schemas/View' - security: - $ref: '#/components/schemas/Security' - required: - - assetInformation - Identifier: - type: object - properties: - id: - type: string - idType: - $ref: '#/components/schemas/KeyType' - required: - - id - - idType - KeyType: - type: string - enum: - - Custom - - IRDI - - IRI - - IdShort - - FragmentId - AdministrativeInformation: - type: object - properties: - version: - type: string - revision: - type: string - LangString: - type: object - properties: - language: - type: string - text: - type: string - required: - - language - - text - Reference: - type: object - properties: - keys: - type: array - items: - $ref: '#/components/schemas/Key' - required: - - keys - Key: - type: object - properties: - type: - $ref: '#/components/schemas/KeyElements' - idType: - $ref: '#/components/schemas/KeyType' - value: - type: string - required: - - type - - idType - - value - KeyElements: - type: string - enum: - - Asset - - AssetAdministrationShell - - ConceptDescription - - Submodel - - AccessPermissionRule - - AnnotatedRelationshipElement - - BasicEvent - - Blob - - Capability - - DataElement - - File - - Entity - - Event - - MultiLanguageProperty - - Operation - - Property - - Range - - ReferenceElement - - RelationshipElement - - SubmodelElement - - SubmodelElementCollection - - View - - GlobalReference - - FragmentReference - ModelTypes: - type: string - enum: - - Asset - - AssetAdministrationShell - - ConceptDescription - - Submodel - - AccessPermissionRule - - AnnotatedRelationshipElement - - BasicEvent - - Blob - - Capability - - DataElement - - File - - Entity - - Event - - MultiLanguageProperty - - Operation - - Property - - Range - - ReferenceElement - - RelationshipElement - - SubmodelElement - - SubmodelElementCollection - - View - - GlobalReference - - FragmentReference - - Constraint - - Formula - - Qualifier - ModelType: - type: object - properties: - name: - $ref: '#/components/schemas/ModelTypes' - required: - - name - EmbeddedDataSpecification: - type: object - properties: - dataSpecification: - $ref: '#/components/schemas/Reference' - dataSpecificationContent: - $ref: '#/components/schemas/DataSpecificationContent' - required: - - dataSpecification - - dataSpecificationContent - DataSpecificationContent: - oneOf: - - $ref: '#/components/schemas/DataSpecificationIEC61360Content' - - $ref: '#/components/schemas/DataSpecificationPhysicalUnitContent' - DataSpecificationPhysicalUnitContent: - type: object - properties: - unitName: - type: string - unitSymbol: - type: string - definition: - type: array - items: - $ref: '#/components/schemas/LangString' - siNotation: - type: string - siName: - type: string - dinNotation: - type: string - eceName: - type: string - eceCode: - type: string - nistName: - type: string - sourceOfDefinition: - type: string - conversionFactor: - type: string - registrationAuthorityId: - type: string - supplier: - type: string - required: - - unitName - - unitSymbol - - definition - DataSpecificationIEC61360Content: - allOf: - - $ref: '#/components/schemas/ValueObject' - - type: object - properties: - dataType: - enum: - - DATE - - STRING - - STRING_TRANSLATABLE - - REAL_MEASURE - - REAL_COUNT - - REAL_CURRENCY - - BOOLEAN - - URL - - RATIONAL - - RATIONAL_MEASURE - - TIME - - TIMESTAMP - - INTEGER_COUNT - - INTEGER_MEASURE - - INTEGER_CURRENCY - definition: - type: array - items: - $ref: '#/components/schemas/LangString' - preferredName: - type: array - items: - $ref: '#/components/schemas/LangString' - shortName: - type: array - items: - $ref: '#/components/schemas/LangString' - sourceOfDefinition: - type: string - symbol: - type: string - unit: - type: string - unitId: - $ref: '#/components/schemas/Reference' - valueFormat: - type: string - valueList: - $ref: '#/components/schemas/ValueList' - levelType: - type: array - items: - $ref: '#/components/schemas/LevelType' - required: - - preferredName - LevelType: - type: string - enum: - - Min - - Max - - Nom - - Typ - ValueList: - type: object - properties: - valueReferencePairTypes: - type: array - minItems: 1 - items: - $ref: '#/components/schemas/ValueReferencePairType' - required: - - valueReferencePairTypes - ValueReferencePairType: - allOf: - - $ref: '#/components/schemas/ValueObject' - ValueObject: - type: object - properties: - value: - type: string - valueId: - $ref: '#/components/schemas/Reference' - valueType: - type: string - enum: - - anyUri - - base64Binary - - boolean - - date - - dateTime - - dateTimeStamp - - decimal - - integer - - long - - int - - short - - byte - - nonNegativeInteger - - positiveInteger - - unsignedLong - - unsignedInt - - unsignedShort - - unsignedByte - - nonPositiveInteger - - negativeInteger - - double - - duration - - dayTimeDuration - - yearMonthDuration - - float - - gDay - - gMonth - - gMonthDay - - gYear - - gYearMonth - - hexBinary - - NOTATION - - QName - - string - - normalizedString - - token - - language - - Name - - NCName - - ENTITY - - ID - - IDREF - - NMTOKEN - - time - Asset: - allOf: - - $ref: '#/components/schemas/Identifiable' - - $ref: '#/components/schemas/HasDataSpecification' - AssetInformation: - allOf: - - properties: - assetKind: - $ref: '#/components/schemas/AssetKind' - globalAssetId: - $ref: '#/components/schemas/Reference' - externalAssetIds: - type: array - items: - $ref: '#/components/schemas/IdentifierKeyValuePair' - billOfMaterial: - type: array - items: - $ref: '#/components/schemas/Reference' - thumbnail: - $ref: '#/components/schemas/File' - required: - - assetKind - IdentifierKeyValuePair: - allOf: - - $ref: '#/components/schemas/HasSemantics' - - properties: - key: - type: string - value: - type: string - subjectId: - $ref: '#/components/schemas/Reference' - required: - - key - - value - - subjectId - AssetKind: - type: string - enum: - - Type - - Instance - ModelingKind: - type: string - enum: - - Template - - Instance - Submodel: - allOf: - - $ref: '#/components/schemas/Identifiable' - - $ref: '#/components/schemas/HasDataSpecification' - - $ref: '#/components/schemas/Qualifiable' - - $ref: '#/components/schemas/HasSemantics' - - properties: - kind: - $ref: '#/components/schemas/ModelingKind' - submodelElements: - type: array - items: - $ref: '#/components/schemas/SubmodelElement' - Constraint: - type: object - properties: - modelType: - $ref: '#/components/schemas/ModelType' - required: - - modelType - Operation: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - inputVariable: - type: array - items: - $ref: '#/components/schemas/OperationVariable' - outputVariable: - type: array - items: - $ref: '#/components/schemas/OperationVariable' - inoutputVariable: - type: array - items: - $ref: '#/components/schemas/OperationVariable' - OperationVariable: - type: object - properties: - value: - oneOf: - - $ref: '#/components/schemas/Blob' - - $ref: '#/components/schemas/File' - - $ref: '#/components/schemas/Capability' - - $ref: '#/components/schemas/Entity' - - $ref: '#/components/schemas/Event' - - $ref: '#/components/schemas/BasicEvent' - - $ref: '#/components/schemas/MultiLanguageProperty' - - $ref: '#/components/schemas/Operation' - - $ref: '#/components/schemas/Property' - - $ref: '#/components/schemas/Range' - - $ref: '#/components/schemas/ReferenceElement' - - $ref: '#/components/schemas/RelationshipElement' - - $ref: '#/components/schemas/SubmodelElementCollection' - required: - - value - SubmodelElement: - allOf: - - $ref: '#/components/schemas/Referable' - - $ref: '#/components/schemas/HasDataSpecification' - - $ref: '#/components/schemas/HasSemantics' - - $ref: '#/components/schemas/Qualifiable' - - properties: - kind: - $ref: '#/components/schemas/ModelingKind' - idShort: - type: string - required: - - idShort - Event: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - BasicEvent: - allOf: - - $ref: '#/components/schemas/Event' - - properties: - observed: - $ref: '#/components/schemas/Reference' - required: - - observed - EntityType: - type: string - enum: - - CoManagedEntity - - SelfManagedEntity - Entity: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - statements: - type: array - items: - $ref: '#/components/schemas/SubmodelElement' - entityType: - $ref: '#/components/schemas/EntityType' - globalAssetId: - $ref: '#/components/schemas/Reference' - specificAssetIds: - $ref: '#/components/schemas/IdentifierKeyValuePair' - required: - - entityType - View: - allOf: - - $ref: '#/components/schemas/Referable' - - $ref: '#/components/schemas/HasDataSpecification' - - $ref: '#/components/schemas/HasSemantics' - - properties: - containedElements: - type: array - items: - $ref: '#/components/schemas/Reference' - ConceptDescription: - allOf: - - $ref: '#/components/schemas/Identifiable' - - $ref: '#/components/schemas/HasDataSpecification' - - properties: - isCaseOf: - type: array - items: - $ref: '#/components/schemas/Reference' - Capability: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - Property: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - $ref: '#/components/schemas/ValueObject' - Range: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - valueType: - type: string - enum: - - anyUri - - base64Binary - - boolean - - date - - dateTime - - dateTimeStamp - - decimal - - integer - - long - - int - - short - - byte - - nonNegativeInteger - - positiveInteger - - unsignedLong - - unsignedInt - - unsignedShort - - unsignedByte - - nonPositiveInteger - - negativeInteger - - double - - duration - - dayTimeDuration - - yearMonthDuration - - float - - gDay - - gMonth - - gMonthDay - - gYear - - gYearMonth - - hexBinary - - NOTATION - - QName - - string - - normalizedString - - token - - language - - Name - - NCName - - ENTITY - - ID - - IDREF - - NMTOKEN - - time - min: - type: string - max: - type: string - required: - - valueType - MultiLanguageProperty: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - type: array - items: - $ref: '#/components/schemas/LangString' - valueId: - $ref: '#/components/schemas/Reference' - File: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - type: string - mimeType: - type: string - required: - - mimeType - Blob: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - type: string - mimeType: - type: string - required: - - mimeType - ReferenceElement: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - $ref: '#/components/schemas/Reference' - SubmodelElementCollection: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - type: array - items: - oneOf: - - $ref: '#/components/schemas/Blob' - - $ref: '#/components/schemas/File' - - $ref: '#/components/schemas/Capability' - - $ref: '#/components/schemas/Entity' - - $ref: '#/components/schemas/Event' - - $ref: '#/components/schemas/BasicEvent' - - $ref: '#/components/schemas/MultiLanguageProperty' - - $ref: '#/components/schemas/Operation' - - $ref: '#/components/schemas/Property' - - $ref: '#/components/schemas/Range' - - $ref: '#/components/schemas/ReferenceElement' - - $ref: '#/components/schemas/RelationshipElement' - - $ref: '#/components/schemas/SubmodelElementCollection' - allowDuplicates: - type: boolean - ordered: - type: boolean - RelationshipElement: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - first: - $ref: '#/components/schemas/Reference' - second: - $ref: '#/components/schemas/Reference' - required: - - first - - second - AnnotatedRelationshipElement: - allOf: - - $ref: '#/components/schemas/RelationshipElement' - - properties: - annotation: - type: array - items: - oneOf: - - $ref: '#/components/schemas/Blob' - - $ref: '#/components/schemas/File' - - $ref: '#/components/schemas/MultiLanguageProperty' - - $ref: '#/components/schemas/Property' - - $ref: '#/components/schemas/Range' - - $ref: '#/components/schemas/ReferenceElement' - Qualifier: - allOf: - - $ref: '#/components/schemas/Constraint' - - $ref: '#/components/schemas/HasSemantics' - - $ref: '#/components/schemas/ValueObject' - - properties: - type: - type: string - required: - - type - Formula: - allOf: - - $ref: '#/components/schemas/Constraint' - - properties: - dependsOn: - type: array - items: - $ref: '#/components/schemas/Reference' - Security: - type: object - properties: - accessControlPolicyPoints: - $ref: '#/components/schemas/AccessControlPolicyPoints' - certificate: - type: array - items: - oneOf: - - $ref: '#/components/schemas/BlobCertificate' - requiredCertificateExtension: - type: array - items: - $ref: '#/components/schemas/Reference' - required: - - accessControlPolicyPoints - Certificate: - type: object - BlobCertificate: - allOf: - - $ref: '#/components/schemas/Certificate' - - properties: - blobCertificate: - $ref: '#/components/schemas/Blob' - containedExtension: - type: array - items: - $ref: '#/components/schemas/Reference' - lastCertificate: - type: boolean - AccessControlPolicyPoints: - type: object - properties: - policyAdministrationPoint: - $ref: '#/components/schemas/PolicyAdministrationPoint' - policyDecisionPoint: - $ref: '#/components/schemas/PolicyDecisionPoint' - policyEnforcementPoint: - $ref: '#/components/schemas/PolicyEnforcementPoint' - policyInformationPoints: - $ref: '#/components/schemas/PolicyInformationPoints' - required: - - policyAdministrationPoint - - policyDecisionPoint - - policyEnforcementPoint - PolicyAdministrationPoint: - type: object - properties: - localAccessControl: - $ref: '#/components/schemas/AccessControl' - externalAccessControl: - type: boolean - required: - - externalAccessControl - PolicyInformationPoints: - type: object - properties: - internalInformationPoint: - type: array - items: - $ref: '#/components/schemas/Reference' - externalInformationPoint: - type: boolean - required: - - externalInformationPoint - PolicyEnforcementPoint: - type: object - properties: - externalPolicyEnforcementPoint: - type: boolean - required: - - externalPolicyEnforcementPoint - PolicyDecisionPoint: - type: object - properties: - externalPolicyDecisionPoints: - type: boolean - required: - - externalPolicyDecisionPoints - AccessControl: - type: object - properties: - selectableSubjectAttributes: - $ref: '#/components/schemas/Reference' - defaultSubjectAttributes: - $ref: '#/components/schemas/Reference' - selectablePermissions: - $ref: '#/components/schemas/Reference' - defaultPermissions: - $ref: '#/components/schemas/Reference' - selectableEnvironmentAttributes: - $ref: '#/components/schemas/Reference' - defaultEnvironmentAttributes: - $ref: '#/components/schemas/Reference' - accessPermissionRule: - type: array - items: - $ref: '#/components/schemas/AccessPermissionRule' - AccessPermissionRule: - allOf: - - $ref: '#/components/schemas/Referable' - - $ref: '#/components/schemas/Qualifiable' - - properties: - targetSubjectAttributes: - type: array - items: - $ref: '#/components/schemas/SubjectAttributes' - minItems: 1 - permissionsPerObject: - type: array - items: - $ref: '#/components/schemas/PermissionsPerObject' - required: - - targetSubjectAttributes - SubjectAttributes: - type: object - properties: - subjectAttributes: - type: array - items: - $ref: '#/components/schemas/Reference' - minItems: 1 - PermissionsPerObject: - type: object - properties: - object: - $ref: '#/components/schemas/Reference' - targetObjectAttributes: - $ref: '#/components/schemas/ObjectAttributes' - permission: - type: array - items: - $ref: '#/components/schemas/Permission' - ObjectAttributes: - type: object - properties: - objectAttribute: - type: array - items: - $ref: '#/components/schemas/Property' - minItems: 1 - Permission: - type: object - properties: - permission: - $ref: '#/components/schemas/Reference' - kindOfPermission: - type: string - enum: - - Allow - - Deny - - NotApplicable - - Undefined - required: - - permission - - kindOfPermission diff --git a/sdk/test/adapter/http-api-oas-submodel.yaml b/sdk/test/adapter/http-api-oas-submodel.yaml deleted file mode 100644 index 79ac905..0000000 --- a/sdk/test/adapter/http-api-oas-submodel.yaml +++ /dev/null @@ -1,2017 +0,0 @@ -openapi: 3.0.0 -info: - version: "1" - title: PyI40AAS REST API - description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). - - - **Submodel Interface** - - - Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`." - contact: - name: "Michael Thies, Torben Miny, Leon Möller" - license: - name: Use under Eclipse Public License 2.0 - url: "https://www.eclipse.org/legal/epl-2.0/" -servers: - - url: http://{authority}/{basePath}/{api-version} - description: This is the Server to access the Asset Administration Shell - variables: - authority: - default: localhost:8080 - description: The authority is the server url (made of IP-Address or DNS-Name, user information, and/or port information) of the hosting environment for the Asset Administration Shell - basePath: - default: api - description: The basePath variable is additional path information for the hosting environment. It may contain the name of an aggregation point like 'shells' and/or API version information and/or tenant-id information, etc. - api-version: - default: v1 - description: The Version of the API-Specification -paths: - "/": - get: - summary: "Returns the stripped Submodel (without SubmodelElements and Constraints (property: qualifiers))" - operationId: ReadSubmodel - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelResult" - "404": - description: No Submodel found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/constraints/": - get: - summary: Returns all Constraints of the current Submodel - operationId: ReadSubmodelConstraints - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintListResult" - "404": - description: Submodel not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - post: - summary: Adds a new Constraint to the Submodel - operationId: CreateSubmodelConstraint - requestBody: - description: The Constraint to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/Constraint" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - headers: - Location: - description: The URL of the created Constraint - schema: - type: string - links: - ReadSubmodelQualifierByType: - $ref: "#/components/links/ReadSubmodelQualifierByType" - UpdateSubmodelQualifierByType: - $ref: "#/components/links/UpdateSubmodelQualifierByType" - DeleteSubmodelQualifierByType: - $ref: "#/components/links/DeleteSubmodelQualifierByType" - "404": - description: Submodel not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: "When trying to add a qualifier: Qualifier with same type already exists" - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid Qualifier - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/constraints/{qualifier-type}/": - parameters: - - name: qualifier-type - in: path - description: The type of the Qualifier - required: true - schema: - type: string - get: - summary: Retrieves a specific Qualifier of the Submodel's constraints (Formulas cannot be referred to yet) - operationId: ReadSubmodelConstraint - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - links: - ReadSubmodelQualifierByType: - $ref: "#/components/links/ReadSubmodelQualifierByType" - UpdateSubmodelQualifierByType: - $ref: "#/components/links/UpdateSubmodelQualifierByType" - DeleteSubmodelQualifierByType: - $ref: "#/components/links/DeleteSubmodelQualifierByType" - "404": - description: Submodel or Constraint not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - put: - summary: Updates an existing Qualifier in the Submodel (Formulas cannot be referred to yet) - operationId: UpdateSubmodelConstraint - requestBody: - description: The Qualifier used to overwrite the existing Qualifier - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/Qualifier" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - links: - ReadSubmodelQualifierByType: - $ref: "#/components/links/ReadSubmodelQualifierByType" - UpdateSubmodelQualifierByType: - $ref: "#/components/links/UpdateSubmodelQualifierByType" - DeleteSubmodelQualifierByType: - $ref: "#/components/links/DeleteSubmodelQualifierByType" - "201": - description: Success (type changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - headers: - Location: - description: The new URL of the Qualifier - schema: - type: string - links: - ReadSubmodelQualifierByType: - $ref: "#/components/links/ReadSubmodelQualifierByType" - UpdateSubmodelQualifierByType: - $ref: "#/components/links/UpdateSubmodelQualifierByType" - DeleteSubmodelQualifierByType: - $ref: "#/components/links/DeleteSubmodelQualifierByType" - "404": - description: Submodel or Constraint not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: type changed and new type already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid Qualifier - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - delete: - summary: Deletes an existing Qualifier from the Submodel (Formulas cannot be referred to yet) - operationId: DeleteSubmodelConstraint - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or Constraint not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/submodelElements/": - get: - summary: Returns all SubmodelElements of the current Submodel - operationId: ReadSubmodelSubmodelElements - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementListResult" - "404": - description: Submodel not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - post: - summary: Adds a new SubmodelElement to the Submodel - operationId: CreateSubmodelSubmodelElement - requestBody: - description: The SubmodelElement to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElement" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - headers: - Location: - description: The URL of the created SubmodelElement - schema: - type: string - links: - ReadSubmodelSubmodelElementByIdShortAfterPost: - $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPost" - UpdateSubmodelSubmodelElementByIdShortAfterPost: - $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPost" - DeleteSubmodelSubmodelElementByIdShortAfterPost: - $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPost" - "404": - description: Submodel not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: SubmodelElement with same idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid SubmodelElement - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/{idShort-path}/": - parameters: - - name: idShort-path - in: path - description: A /-separated concatenation of !-prefixed idShorts - required: true - schema: - type: string - get: - summary: Returns the (stripped) (nested) SubmodelElement - operationId: ReadSubmodelSubmodelElement - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementListResult" - "400": - description: Invalid idShort - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - put: - summary: Updates a nested SubmodelElement - operationId: UpdateSubmodelSubmodelElement - requestBody: - description: The SubmodelElement used to overwrite the existing SubmodelElement - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElement" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - "201": - description: Success (idShort changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - headers: - Location: - description: The new URL of the SubmodelElement - schema: - type: string - "400": - description: Invalid idShort - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: idShort changed and new idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid SubmodelElement **or** the type of the new SubmodelElement differs from the existing one - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - delete: - summary: Deletes a specific (nested) SubmodelElement from the Submodel - operationId: DeleteSubmodelSubmodelElement - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "400": - description: Invalid idShort - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/{idShort-path}/value/": - parameters: - - name: idShort-path - in: path - description: A /-separated concatenation of !-prefixed idShorts - required: true - schema: - type: string - get: - summary: If the (nested) SubmodelElement is a SubmodelElementCollection, return contained (stripped) SubmodelElements - operationId: ReadSubmodelSubmodelElementValue - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "400": - description: Invalid idShort or SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - post: - summary: If the (nested) SubmodelElement is a SubmodelElementCollection, add a SubmodelElement to its value - operationId: CreateSubmodelSubmodelElementValue - requestBody: - description: The SubmodelElement to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElement" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - headers: - Location: - description: The URL of the created SubmodelElement - schema: - type: string - links: - ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" - UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" - DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" - "400": - description: Invalid idShort or SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: SubmodelElement with same idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid SubmodelElement - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/{idShort-path}/annotation/": - parameters: - - name: idShort-path - in: path - description: A /-separated concatenation of !-prefixed idShorts - required: true - schema: - type: string - get: - summary: If the (nested) SubmodelElement is an AnnotatedRelationshipElement, return contained (stripped) SubmodelElements - operationId: ReadSubmodelSubmodelElementAnnotation - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementListResult" - "400": - description: Invalid idShort or SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - post: - summary: If the (nested) SubmodelElement is an AnnotatedRelationshipElement, add a SubmodelElement to its annotation - operationId: CreateSubmodelSubmodelElementAnnotation - requestBody: - description: The SubmodelElement to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElement" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - headers: - Location: - description: The URL of the created SubmodelElement - schema: - type: string - links: - ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" - UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" - DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" - "400": - description: Invalid idShort or SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: SubmodelElement with given idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid SubmodelElement - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/{idShort-path}/statement/": - parameters: - - name: idShort-path - in: path - description: A /-separated concatenation of !-prefixed idShorts - required: true - schema: - type: string - get: - summary: If the (nested) SubmodelElement is an Entity, return contained (stripped) SubmodelElements - operationId: ReadSubmodelSubmodelElementStatement - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementListResult" - "400": - description: Invalid idShort or SubmodelElement exists, but is not an Entity, so /statement is not possible. - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - post: - summary: If the (nested) SubmodelElement is an Entity, add a SubmodelElement to its statement - operationId: CreateSubmodelSubmodelElementStatement - requestBody: - description: The SubmodelElement to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElement" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - headers: - Location: - description: The URL of the created SubmodelElement - schema: - type: string - links: - ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" - UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" - DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: - $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" - "400": - description: Invalid idShort or SubmodelElement exists, but is not an Entity, so /statement is not possible - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: SubmodelElement with same idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid SubmodelElement - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/{idShort-path}/constraints/": - parameters: - - name: idShort-path - in: path - description: A /-separated concatenation of !-prefixed idShorts - required: true - schema: - type: string - get: - summary: Returns all Constraints of the (nested) SubmodelElement - operationId: ReadSubmodelSubmodelElementConstraints - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintListResult" - "400": - description: Invalid idShort - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - post: - summary: Adds a new Constraint to the (nested) SubmodelElement - operationId: CreateSubmodelSubmodelElementConstraint - requestBody: - description: The Constraint to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/Constraint" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - headers: - Location: - description: The URL of the created Constraint - schema: - type: string - links: - ReadSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" - UpdateSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" - DeleteSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" - "400": - description: Invalid idShort - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: "When trying to add a qualifier: Qualifier with specified type already exists" - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid Qualifier - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/{idShort-path}/constraints/{qualifier-type}/": - parameters: - - name: idShort-path - in: path - description: A /-separated concatenation of !-prefixed idShorts - required: true - schema: - type: string - - name: qualifier-type - in: path - description: "Type of the qualifier" - required: true - schema: - type: string - get: - summary: Retrieves a specific Qualifier of the (nested) SubmodelElements's Constraints (Formulas cannot be referred to yet) - operationId: ReadSubmodelSubmodelElementConstraint - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - links: - ReadSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" - UpdateSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" - DeleteSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" - "400": - description: Invalid idShort - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - put: - summary: Updates an existing Qualifier in the (nested) SubmodelElement (Formulas cannot be referred to yet) - operationId: UpdateSubmodelSubmodelElementConstraint - requestBody: - description: The Qualifier used to overwrite the existing Qualifier - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/Qualifier" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - links: - ReadSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" - UpdateSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" - DeleteSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" - "201": - description: Success (type changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - headers: - Location: - description: The new URL of the Qualifier - schema: - type: string - links: - ReadSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" - UpdateSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" - DeleteSubmodelSubmodelElementQualifierByType: - $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" - "400": - description: Invalid idShort - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "409": - description: type changed and new type already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "422": - description: Request body is not a valid Qualifier - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - delete: - summary: Deletes an existing Qualifier from the (nested) SubmodelElement (Formulas cannot be referred to yet) - operationId: DeleteSubmodelSubmodelElementConstraint - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "400": - description: Invalid idShort - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface -components: - links: - ReadSubmodelQualifierByType: - description: The `type` of the returned Qualifier can be used to read the Qualifier. - operationId: ReadSubmodelConstraint - parameters: - qualifier-type: "$response.body#/data/type" - UpdateSubmodelQualifierByType: - description: The `type` of the returned Qualifier can be used to update the Qualifier. - operationId: UpdateSubmodelConstraint - parameters: - qualifier-type: "$response.body#/data/type" - DeleteSubmodelQualifierByType: - description: The `type` of the returned Qualifier can be used to delete the Qualifier. - operationId: DeleteSubmodelConstraint - parameters: - qualifier-type: "$response.body#/data/type" - ReadSubmodelSubmodelElementByIdShortAfterPost: - description: The `idShort` of the returned SubmodelElement can be used to read the SubmodelElement. - operationId: ReadSubmodelSubmodelElement - parameters: - idShort-path: "!{$response.body#/data/idShort}" - UpdateSubmodelSubmodelElementByIdShortAfterPost: - description: The `idShort` of the returned SubmodelElement can be used to update the SubmodelElement. - operationId: UpdateSubmodelSubmodelElement - parameters: - idShort-path: "!{$response.body#/data/idShort}" - DeleteSubmodelSubmodelElementByIdShortAfterPost: - description: The `idShort` of the returned SubmodelElement can be used to delete the SubmodelElement. - operationId: DeleteSubmodelSubmodelElement - parameters: - idShort-path: "!{$response.body#/data/idShort}" - ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: - description: The `idShort` of the returned SubmodelElement can be used to read the SubmodelElement. - operationId: ReadSubmodelSubmodelElement - parameters: - idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" - UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: - description: The `idShort` of the returned SubmodelElement can be used to update the SubmodelElement. - operationId: UpdateSubmodelSubmodelElement - parameters: - idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" - DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: - description: The `idShort` of the returned SubmodelElement can be used to delete the SubmodelElement. - operationId: DeleteSubmodelSubmodelElement - parameters: - idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" - ReadSubmodelSubmodelElementQualifierByType: - description: The `type` of the returned Qualifier can be used to read the Qualifier. - operationId: ReadSubmodelSubmodelElementConstraint - parameters: - idShort-path: "$request.path.idShort-path" - qualifier-type: "$response.body#/type" - UpdateSubmodelSubmodelElementQualifierByType: - description: The `type` of the returned Qualifier can be used to update the Qualifier. - operationId: UpdateSubmodelSubmodelElementConstraint - parameters: - idShort-path: "$request.path.idShort-path" - qualifier-type: "$response.body#/type" - DeleteSubmodelSubmodelElementQualifierByType: - description: The `type` of the returned Qualifier can be used to delete the Qualifier. - operationId: DeleteSubmodelSubmodelElementConstraint - parameters: - idShort-path: "$request.path.idShort-path" - qualifier-type: "$response.body#/type" - schemas: - BaseResult: - type: object - properties: - success: - type: boolean - error: - type: object - nullable: true - properties: - type: - enum: - - Unspecified - - Debug - - Information - - Warning - - Error - - Fatal - - Exception - type: string - code: - type: string - text: - type: string - data: - nullable: true - AssetAdministrationShellResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/StrippedAssetAdministrationShell" - error: - nullable: true - ReferenceResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/Reference" - error: - nullable: true - ReferenceListResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - type: array - items: - $ref: "#/components/schemas/Reference" - error: - nullable: true - ViewResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/View" - error: - nullable: true - ViewListResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - type: array - items: - $ref: "#/components/schemas/View" - error: - nullable: true - SubmodelResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/StrippedSubmodel" - error: - nullable: true - SubmodelElementResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/StrippedSubmodelElement" - error: - nullable: true - SubmodelElementListResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - type: array - items: - $ref: "#/components/schemas/StrippedSubmodelElement" - error: - nullable: true - ConstraintResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - $ref: "#/components/schemas/Constraint" - error: - nullable: true - ConstraintListResult: - allOf: - - $ref: "#/components/schemas/BaseResult" - - properties: - data: - type: array - items: - $ref: "#/components/schemas/Constraint" - error: - nullable: true - StrippedAssetAdministrationShell: - allOf: - - $ref: "#/components/schemas/AssetAdministrationShell" - - properties: - views: - not: {} - submodels: - not: {} - conceptDictionaries: - not: {} - StrippedSubmodel: - allOf: - - $ref: "#/components/schemas/Submodel" - - properties: - submodelElements: - not: {} - qualifiers: - not: {} - StrippedSubmodelElement: - allOf: - - $ref: "#/components/schemas/SubmodelElement" - - properties: - qualifiers: - not: {} - Referable: - allOf: - - $ref: '#/components/schemas/HasExtensions' - - properties: - idShort: - type: string - category: - type: string - displayName: - type: string - description: - type: array - items: - $ref: '#/components/schemas/LangString' - modelType: - $ref: '#/components/schemas/ModelType' - required: - - modelType - Identifiable: - allOf: - - $ref: '#/components/schemas/Referable' - - properties: - identification: - $ref: '#/components/schemas/Identifier' - administration: - $ref: '#/components/schemas/AdministrativeInformation' - required: - - identification - Qualifiable: - type: object - properties: - qualifiers: - type: array - items: - $ref: '#/components/schemas/Constraint' - HasSemantics: - type: object - properties: - semanticId: - $ref: '#/components/schemas/Reference' - HasDataSpecification: - type: object - properties: - embeddedDataSpecifications: - type: array - items: - $ref: '#/components/schemas/EmbeddedDataSpecification' - HasExtensions: - type: object - properties: - extensions: - type: array - items: - $ref: '#/components/schemas/Extension' - Extension: - allOf: - - $ref: '#/components/schemas/HasSemantics' - - properties: - name: - type: string - valueType: - type: string - enum: - - anyUri - - base64Binary - - boolean - - date - - dateTime - - dateTimeStamp - - decimal - - integer - - long - - int - - short - - byte - - nonNegativeInteger - - positiveInteger - - unsignedLong - - unsignedInt - - unsignedShort - - unsignedByte - - nonPositiveInteger - - negativeInteger - - double - - duration - - dayTimeDuration - - yearMonthDuration - - float - - gDay - - gMonth - - gMonthDay - - gYear - - gYearMonth - - hexBinary - - NOTATION - - QName - - string - - normalizedString - - token - - language - - Name - - NCName - - ENTITY - - ID - - IDREF - - NMTOKEN - - time - value: - type: string - refersTo: - $ref: '#/components/schemas/Reference' - required: - - name - AssetAdministrationShell: - allOf: - - $ref: '#/components/schemas/Identifiable' - - $ref: '#/components/schemas/HasDataSpecification' - - properties: - derivedFrom: - $ref: '#/components/schemas/Reference' - assetInformation: - $ref: '#/components/schemas/AssetInformation' - submodels: - type: array - items: - $ref: '#/components/schemas/Reference' - views: - type: array - items: - $ref: '#/components/schemas/View' - security: - $ref: '#/components/schemas/Security' - required: - - assetInformation - Identifier: - type: object - properties: - id: - type: string - idType: - $ref: '#/components/schemas/KeyType' - required: - - id - - idType - KeyType: - type: string - enum: - - Custom - - IRDI - - IRI - - IdShort - - FragmentId - AdministrativeInformation: - type: object - properties: - version: - type: string - revision: - type: string - LangString: - type: object - properties: - language: - type: string - text: - type: string - required: - - language - - text - Reference: - type: object - properties: - keys: - type: array - items: - $ref: '#/components/schemas/Key' - required: - - keys - Key: - type: object - properties: - type: - $ref: '#/components/schemas/KeyElements' - idType: - $ref: '#/components/schemas/KeyType' - value: - type: string - required: - - type - - idType - - value - KeyElements: - type: string - enum: - - Asset - - AssetAdministrationShell - - ConceptDescription - - Submodel - - AccessPermissionRule - - AnnotatedRelationshipElement - - BasicEvent - - Blob - - Capability - - DataElement - - File - - Entity - - Event - - MultiLanguageProperty - - Operation - - Property - - Range - - ReferenceElement - - RelationshipElement - - SubmodelElement - - SubmodelElementCollection - - View - - GlobalReference - - FragmentReference - ModelTypes: - type: string - enum: - - Asset - - AssetAdministrationShell - - ConceptDescription - - Submodel - - AccessPermissionRule - - AnnotatedRelationshipElement - - BasicEvent - - Blob - - Capability - - DataElement - - File - - Entity - - Event - - MultiLanguageProperty - - Operation - - Property - - Range - - ReferenceElement - - RelationshipElement - - SubmodelElement - - SubmodelElementCollection - - View - - GlobalReference - - FragmentReference - - Constraint - - Formula - - Qualifier - ModelType: - type: object - properties: - name: - $ref: '#/components/schemas/ModelTypes' - required: - - name - EmbeddedDataSpecification: - type: object - properties: - dataSpecification: - $ref: '#/components/schemas/Reference' - dataSpecificationContent: - $ref: '#/components/schemas/DataSpecificationContent' - required: - - dataSpecification - - dataSpecificationContent - DataSpecificationContent: - oneOf: - - $ref: '#/components/schemas/DataSpecificationIEC61360Content' - - $ref: '#/components/schemas/DataSpecificationPhysicalUnitContent' - DataSpecificationPhysicalUnitContent: - type: object - properties: - unitName: - type: string - unitSymbol: - type: string - definition: - type: array - items: - $ref: '#/components/schemas/LangString' - siNotation: - type: string - siName: - type: string - dinNotation: - type: string - eceName: - type: string - eceCode: - type: string - nistName: - type: string - sourceOfDefinition: - type: string - conversionFactor: - type: string - registrationAuthorityId: - type: string - supplier: - type: string - required: - - unitName - - unitSymbol - - definition - DataSpecificationIEC61360Content: - allOf: - - $ref: '#/components/schemas/ValueObject' - - type: object - properties: - dataType: - enum: - - DATE - - STRING - - STRING_TRANSLATABLE - - REAL_MEASURE - - REAL_COUNT - - REAL_CURRENCY - - BOOLEAN - - URL - - RATIONAL - - RATIONAL_MEASURE - - TIME - - TIMESTAMP - - INTEGER_COUNT - - INTEGER_MEASURE - - INTEGER_CURRENCY - definition: - type: array - items: - $ref: '#/components/schemas/LangString' - preferredName: - type: array - items: - $ref: '#/components/schemas/LangString' - shortName: - type: array - items: - $ref: '#/components/schemas/LangString' - sourceOfDefinition: - type: string - symbol: - type: string - unit: - type: string - unitId: - $ref: '#/components/schemas/Reference' - valueFormat: - type: string - valueList: - $ref: '#/components/schemas/ValueList' - levelType: - type: array - items: - $ref: '#/components/schemas/LevelType' - required: - - preferredName - LevelType: - type: string - enum: - - Min - - Max - - Nom - - Typ - ValueList: - type: object - properties: - valueReferencePairTypes: - type: array - minItems: 1 - items: - $ref: '#/components/schemas/ValueReferencePairType' - required: - - valueReferencePairTypes - ValueReferencePairType: - allOf: - - $ref: '#/components/schemas/ValueObject' - ValueObject: - type: object - properties: - value: - type: string - valueId: - $ref: '#/components/schemas/Reference' - valueType: - type: string - enum: - - anyUri - - base64Binary - - boolean - - date - - dateTime - - dateTimeStamp - - decimal - - integer - - long - - int - - short - - byte - - nonNegativeInteger - - positiveInteger - - unsignedLong - - unsignedInt - - unsignedShort - - unsignedByte - - nonPositiveInteger - - negativeInteger - - double - - duration - - dayTimeDuration - - yearMonthDuration - - float - - gDay - - gMonth - - gMonthDay - - gYear - - gYearMonth - - hexBinary - - NOTATION - - QName - - string - - normalizedString - - token - - language - - Name - - NCName - - ENTITY - - ID - - IDREF - - NMTOKEN - - time - Asset: - allOf: - - $ref: '#/components/schemas/Identifiable' - - $ref: '#/components/schemas/HasDataSpecification' - AssetInformation: - allOf: - - properties: - assetKind: - $ref: '#/components/schemas/AssetKind' - globalAssetId: - $ref: '#/components/schemas/Reference' - externalAssetIds: - type: array - items: - $ref: '#/components/schemas/IdentifierKeyValuePair' - billOfMaterial: - type: array - items: - $ref: '#/components/schemas/Reference' - thumbnail: - $ref: '#/components/schemas/File' - required: - - assetKind - IdentifierKeyValuePair: - allOf: - - $ref: '#/components/schemas/HasSemantics' - - properties: - key: - type: string - value: - type: string - subjectId: - $ref: '#/components/schemas/Reference' - required: - - key - - value - - subjectId - AssetKind: - type: string - enum: - - Type - - Instance - ModelingKind: - type: string - enum: - - Template - - Instance - Submodel: - allOf: - - $ref: '#/components/schemas/Identifiable' - - $ref: '#/components/schemas/HasDataSpecification' - - $ref: '#/components/schemas/Qualifiable' - - $ref: '#/components/schemas/HasSemantics' - - properties: - kind: - $ref: '#/components/schemas/ModelingKind' - submodelElements: - type: array - items: - $ref: '#/components/schemas/SubmodelElement' - Constraint: - type: object - properties: - modelType: - $ref: '#/components/schemas/ModelType' - required: - - modelType - Operation: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - inputVariable: - type: array - items: - $ref: '#/components/schemas/OperationVariable' - outputVariable: - type: array - items: - $ref: '#/components/schemas/OperationVariable' - inoutputVariable: - type: array - items: - $ref: '#/components/schemas/OperationVariable' - OperationVariable: - type: object - properties: - value: - oneOf: - - $ref: '#/components/schemas/Blob' - - $ref: '#/components/schemas/File' - - $ref: '#/components/schemas/Capability' - - $ref: '#/components/schemas/Entity' - - $ref: '#/components/schemas/Event' - - $ref: '#/components/schemas/BasicEvent' - - $ref: '#/components/schemas/MultiLanguageProperty' - - $ref: '#/components/schemas/Operation' - - $ref: '#/components/schemas/Property' - - $ref: '#/components/schemas/Range' - - $ref: '#/components/schemas/ReferenceElement' - - $ref: '#/components/schemas/RelationshipElement' - - $ref: '#/components/schemas/SubmodelElementCollection' - required: - - value - SubmodelElement: - allOf: - - $ref: '#/components/schemas/Referable' - - $ref: '#/components/schemas/HasDataSpecification' - - $ref: '#/components/schemas/HasSemantics' - - $ref: '#/components/schemas/Qualifiable' - - properties: - kind: - $ref: '#/components/schemas/ModelingKind' - idShort: - type: string - required: - - idShort - Event: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - BasicEvent: - allOf: - - $ref: '#/components/schemas/Event' - - properties: - observed: - $ref: '#/components/schemas/Reference' - required: - - observed - EntityType: - type: string - enum: - - CoManagedEntity - - SelfManagedEntity - Entity: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - statements: - type: array - items: - $ref: '#/components/schemas/SubmodelElement' - entityType: - $ref: '#/components/schemas/EntityType' - globalAssetId: - $ref: '#/components/schemas/Reference' - specificAssetIds: - $ref: '#/components/schemas/IdentifierKeyValuePair' - required: - - entityType - View: - allOf: - - $ref: '#/components/schemas/Referable' - - $ref: '#/components/schemas/HasDataSpecification' - - $ref: '#/components/schemas/HasSemantics' - - properties: - containedElements: - type: array - items: - $ref: '#/components/schemas/Reference' - ConceptDescription: - allOf: - - $ref: '#/components/schemas/Identifiable' - - $ref: '#/components/schemas/HasDataSpecification' - - properties: - isCaseOf: - type: array - items: - $ref: '#/components/schemas/Reference' - Capability: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - Property: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - $ref: '#/components/schemas/ValueObject' - Range: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - valueType: - type: string - enum: - - anyUri - - base64Binary - - boolean - - date - - dateTime - - dateTimeStamp - - decimal - - integer - - long - - int - - short - - byte - - nonNegativeInteger - - positiveInteger - - unsignedLong - - unsignedInt - - unsignedShort - - unsignedByte - - nonPositiveInteger - - negativeInteger - - double - - duration - - dayTimeDuration - - yearMonthDuration - - float - - gDay - - gMonth - - gMonthDay - - gYear - - gYearMonth - - hexBinary - - NOTATION - - QName - - string - - normalizedString - - token - - language - - Name - - NCName - - ENTITY - - ID - - IDREF - - NMTOKEN - - time - min: - type: string - max: - type: string - required: - - valueType - MultiLanguageProperty: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - type: array - items: - $ref: '#/components/schemas/LangString' - valueId: - $ref: '#/components/schemas/Reference' - File: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - type: string - mimeType: - type: string - required: - - mimeType - Blob: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - type: string - mimeType: - type: string - required: - - mimeType - ReferenceElement: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - $ref: '#/components/schemas/Reference' - SubmodelElementCollection: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - value: - type: array - items: - oneOf: - - $ref: '#/components/schemas/Blob' - - $ref: '#/components/schemas/File' - - $ref: '#/components/schemas/Capability' - - $ref: '#/components/schemas/Entity' - - $ref: '#/components/schemas/Event' - - $ref: '#/components/schemas/BasicEvent' - - $ref: '#/components/schemas/MultiLanguageProperty' - - $ref: '#/components/schemas/Operation' - - $ref: '#/components/schemas/Property' - - $ref: '#/components/schemas/Range' - - $ref: '#/components/schemas/ReferenceElement' - - $ref: '#/components/schemas/RelationshipElement' - - $ref: '#/components/schemas/SubmodelElementCollection' - allowDuplicates: - type: boolean - ordered: - type: boolean - RelationshipElement: - allOf: - - $ref: '#/components/schemas/SubmodelElement' - - properties: - first: - $ref: '#/components/schemas/Reference' - second: - $ref: '#/components/schemas/Reference' - required: - - first - - second - AnnotatedRelationshipElement: - allOf: - - $ref: '#/components/schemas/RelationshipElement' - - properties: - annotation: - type: array - items: - oneOf: - - $ref: '#/components/schemas/Blob' - - $ref: '#/components/schemas/File' - - $ref: '#/components/schemas/MultiLanguageProperty' - - $ref: '#/components/schemas/Property' - - $ref: '#/components/schemas/Range' - - $ref: '#/components/schemas/ReferenceElement' - Qualifier: - allOf: - - $ref: '#/components/schemas/Constraint' - - $ref: '#/components/schemas/HasSemantics' - - $ref: '#/components/schemas/ValueObject' - - properties: - type: - type: string - required: - - type - Formula: - allOf: - - $ref: '#/components/schemas/Constraint' - - properties: - dependsOn: - type: array - items: - $ref: '#/components/schemas/Reference' - Security: - type: object - properties: - accessControlPolicyPoints: - $ref: '#/components/schemas/AccessControlPolicyPoints' - certificate: - type: array - items: - oneOf: - - $ref: '#/components/schemas/BlobCertificate' - requiredCertificateExtension: - type: array - items: - $ref: '#/components/schemas/Reference' - required: - - accessControlPolicyPoints - Certificate: - type: object - BlobCertificate: - allOf: - - $ref: '#/components/schemas/Certificate' - - properties: - blobCertificate: - $ref: '#/components/schemas/Blob' - containedExtension: - type: array - items: - $ref: '#/components/schemas/Reference' - lastCertificate: - type: boolean - AccessControlPolicyPoints: - type: object - properties: - policyAdministrationPoint: - $ref: '#/components/schemas/PolicyAdministrationPoint' - policyDecisionPoint: - $ref: '#/components/schemas/PolicyDecisionPoint' - policyEnforcementPoint: - $ref: '#/components/schemas/PolicyEnforcementPoint' - policyInformationPoints: - $ref: '#/components/schemas/PolicyInformationPoints' - required: - - policyAdministrationPoint - - policyDecisionPoint - - policyEnforcementPoint - PolicyAdministrationPoint: - type: object - properties: - localAccessControl: - $ref: '#/components/schemas/AccessControl' - externalAccessControl: - type: boolean - required: - - externalAccessControl - PolicyInformationPoints: - type: object - properties: - internalInformationPoint: - type: array - items: - $ref: '#/components/schemas/Reference' - externalInformationPoint: - type: boolean - required: - - externalInformationPoint - PolicyEnforcementPoint: - type: object - properties: - externalPolicyEnforcementPoint: - type: boolean - required: - - externalPolicyEnforcementPoint - PolicyDecisionPoint: - type: object - properties: - externalPolicyDecisionPoints: - type: boolean - required: - - externalPolicyDecisionPoints - AccessControl: - type: object - properties: - selectableSubjectAttributes: - $ref: '#/components/schemas/Reference' - defaultSubjectAttributes: - $ref: '#/components/schemas/Reference' - selectablePermissions: - $ref: '#/components/schemas/Reference' - defaultPermissions: - $ref: '#/components/schemas/Reference' - selectableEnvironmentAttributes: - $ref: '#/components/schemas/Reference' - defaultEnvironmentAttributes: - $ref: '#/components/schemas/Reference' - accessPermissionRule: - type: array - items: - $ref: '#/components/schemas/AccessPermissionRule' - AccessPermissionRule: - allOf: - - $ref: '#/components/schemas/Referable' - - $ref: '#/components/schemas/Qualifiable' - - properties: - targetSubjectAttributes: - type: array - items: - $ref: '#/components/schemas/SubjectAttributes' - minItems: 1 - permissionsPerObject: - type: array - items: - $ref: '#/components/schemas/PermissionsPerObject' - required: - - targetSubjectAttributes - SubjectAttributes: - type: object - properties: - subjectAttributes: - type: array - items: - $ref: '#/components/schemas/Reference' - minItems: 1 - PermissionsPerObject: - type: object - properties: - object: - $ref: '#/components/schemas/Reference' - targetObjectAttributes: - $ref: '#/components/schemas/ObjectAttributes' - permission: - type: array - items: - $ref: '#/components/schemas/Permission' - ObjectAttributes: - type: object - properties: - objectAttribute: - type: array - items: - $ref: '#/components/schemas/Property' - minItems: 1 - Permission: - type: object - properties: - permission: - $ref: '#/components/schemas/Reference' - kindOfPermission: - type: string - enum: - - Allow - - Deny - - NotApplicable - - Undefined - required: - - permission - - kindOfPermission diff --git a/sdk/test/adapter/json/test_json_deserialization.py b/sdk/test/adapter/json/test_json_deserialization.py deleted file mode 100644 index 28da288..0000000 --- a/sdk/test/adapter/json/test_json_deserialization.py +++ /dev/null @@ -1,402 +0,0 @@ -# Copyright (c) 2023 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 -""" -Additional tests for the adapter.json.json_deserialization module. - -Deserialization is also somehow tested in the serialization tests -- at least, we get to know if exceptions are raised -when trying to reconstruct the serialized data structure. This module additionally tests error behaviour and verifies -deserialization results. -""" -import io -import json -import logging -import unittest -from basyx.aas.adapter.json import AASFromJsonDecoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder, \ - read_aas_json_file, read_aas_json_file_into -from basyx.aas import model - - -class JsonDeserializationTest(unittest.TestCase): - def test_file_format_wrong_list(self) -> None: - data = """ - { - "assetAdministrationShells": [], - "conceptDescriptions": [], - "submodels": [ - { - "modelType": "AssetAdministrationShell", - "id": "https://acplt.org/Test_Asset", - "assetInformation": { - "assetKind": "Instance", - "globalAssetId": "https://acplt.org/Test_AssetId" - } - } - ] - }""" - with self.assertRaisesRegex(TypeError, r"submodels.*AssetAdministrationShell"): - read_aas_json_file(io.StringIO(data), failsafe=False) - with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) # type: ignore - self.assertIn("AssetAdministrationShell", cm.output[0]) # type: ignore - - def test_file_format_unknown_object(self) -> None: - data = """ - { - "assetAdministrationShells": [], - "assets": [], - "conceptDescriptions": [], - "submodels": [ - { "x": "foo" } - ] - }""" - with self.assertRaisesRegex(TypeError, r"submodels.*'foo'"): - read_aas_json_file(io.StringIO(data), failsafe=False) - with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - read_aas_json_file(io.StringIO(data), failsafe=True) - self.assertIn("submodels", cm.output[0]) # type: ignore - self.assertIn("'foo'", cm.output[0]) # type: ignore - - def test_broken_submodel(self) -> None: - data = """ - [ - { - "modelType": "Submodel" - }, - { - "modelType": "Submodel", - "id": ["https://acplt.org/Test_Submodel_broken_id", "IRI"] - }, - { - "modelType": "Submodel", - "id": "https://acplt.org/Test_Submodel" - } - ]""" - # In strict mode, we should catch an exception - with self.assertRaisesRegex(KeyError, r"id"): - json.loads(data, cls=StrictAASFromJsonDecoder) - - # In failsafe mode, we should get a log entry and the first Submodel entry should be returned as untouched dict - with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - parsed_data = json.loads(data, cls=AASFromJsonDecoder) - self.assertIn("id", cm.output[0]) # type: ignore - self.assertIsInstance(parsed_data, list) - self.assertEqual(3, len(parsed_data)) - - self.assertIsInstance(parsed_data[0], dict) - self.assertIsInstance(parsed_data[1], dict) - self.assertIsInstance(parsed_data[2], model.Submodel) - self.assertEqual("https://acplt.org/Test_Submodel", parsed_data[2].id) - - def test_wrong_submodel_element_type(self) -> None: - data = """ - [ - { - "modelType": "Submodel", - "id": "http://acplt.org/Submodels/Assets/TestAsset/Identification", - "submodelElements": [ - { - "modelType": "Submodel", - "id": "https://acplt.org/Test_Submodel" - }, - { - "modelType": { - "name": "Broken modelType" - } - }, - { - "modelType": "Capability", - "idShort": "TestCapability" - } - ] - } - ]""" - # In strict mode, we should catch an exception for the unexpected Submodel within the Submodel - # The broken object should not raise an exception, but log a warning, even in strict mode. - with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - with self.assertRaisesRegex(TypeError, r"SubmodelElement.*Submodel"): - json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIn("modelType", cm.output[0]) # type: ignore - - # In failsafe mode, we should get a log entries for the broken object and the wrong type of the first two - # submodelElements - with self.assertLogs(logging.getLogger(), level=logging.WARNING) as cm: - parsed_data = json.loads(data, cls=AASFromJsonDecoder) - self.assertGreaterEqual(len(cm.output), 3) # type: ignore - self.assertIn("SubmodelElement", cm.output[1]) # type: ignore - self.assertIn("SubmodelElement", cm.output[2]) # type: ignore - - self.assertIsInstance(parsed_data[0], model.Submodel) - self.assertEqual(1, len(parsed_data[0].submodel_element)) - cap = parsed_data[0].submodel_element.pop() - self.assertIsInstance(cap, model.Capability) - self.assertEqual("TestCapability", cap.id_short) - - def test_duplicate_identifier(self) -> None: - data = """ - { - "assetAdministrationShells": [{ - "modelType": "AssetAdministrationShell", - "id": "http://acplt.org/test_aas", - "assetInformation": { - "assetKind": "Instance", - "globalAssetId": "https://acplt.org/Test_AssetId" - } - }], - "submodels": [{ - "modelType": "Submodel", - "id": "http://acplt.org/test_aas" - }], - "conceptDescriptions": [] - }""" - string_io = io.StringIO(data) - with self.assertLogs(logging.getLogger(), level=logging.ERROR) as cm: - read_aas_json_file(string_io, failsafe=True) - self.assertIn("duplicate identifier", cm.output[0]) # type: ignore - string_io.seek(0) - with self.assertRaisesRegex(KeyError, r"duplicate identifier"): - read_aas_json_file(string_io, failsafe=False) - - def test_duplicate_identifier_object_store(self) -> None: - sm_id = "http://acplt.org/test_submodel" - - def get_clean_store() -> model.DictObjectStore: - store: model.DictObjectStore = model.DictObjectStore() - submodel_ = model.Submodel(sm_id, id_short="test123") - store.add(submodel_) - return store - - data = """ - { - "submodels": [{ - "modelType": "Submodel", - "id": "http://acplt.org/test_submodel", - "idShort": "test456" - }], - "assetAdministrationShells": [], - "conceptDescriptions": [] - }""" - - string_io = io.StringIO(data) - - object_store = get_clean_store() - identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=True, ignore_existing=False) - self.assertEqual(identifiers.pop(), sm_id) - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test456") - - string_io.seek(0) - - object_store = get_clean_store() - with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: - identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=False, ignore_existing=True) - self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) # type: ignore - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test123") - - string_io.seek(0) - - object_store = get_clean_store() - with self.assertRaisesRegex(KeyError, r"already exists in the object store"): - identifiers = read_aas_json_file_into(object_store, string_io, replace_existing=False, - ignore_existing=False) - self.assertEqual(len(identifiers), 0) - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test123") - - -class JsonDeserializationDerivingTest(unittest.TestCase): - def test_asset_constructor_overriding(self) -> None: - class EnhancedSubmodel(model.Submodel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.enhanced_attribute = "fancy!" - - class EnhancedAASDecoder(StrictAASFromJsonDecoder): - @classmethod - def _construct_submodel(cls, dct, object_class=EnhancedSubmodel): - return super()._construct_submodel(dct, object_class=object_class) - - data = """ - [ - { - "modelType": "Submodel", - "id": "https://acplt.org/Test_Submodel" - } - ]""" - parsed_data = json.loads(data, cls=EnhancedAASDecoder) - self.assertEqual(1, len(parsed_data)) - self.assertIsInstance(parsed_data[0], EnhancedSubmodel) - self.assertEqual(parsed_data[0].enhanced_attribute, "fancy!") - - -class JsonDeserializationStrippedObjectsTest(unittest.TestCase): - def test_stripped_qualifiable(self) -> None: - data = """ - { - "modelType": "Submodel", - "id": "http://acplt.org/test_stripped_submodel", - "submodelElements": [{ - "modelType": "Operation", - "idShort": "test_operation", - "qualifiers": [{ - "type": "test_qualifier", - "valueType": "xs:string" - }] - }], - "qualifiers": [{ - "type": "test_qualifier", - "valueType": "xs:string" - }] - }""" - - # check if JSON with qualifiers can be parsed successfully - submodel = json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIsInstance(submodel, model.Submodel) - assert isinstance(submodel, model.Submodel) - self.assertEqual(len(submodel.qualifier), 1) - operation = submodel.submodel_element.pop() - self.assertEqual(len(operation.qualifier), 1) - - # check if qualifiers are ignored in stripped mode - submodel = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) - self.assertIsInstance(submodel, model.Submodel) - assert isinstance(submodel, model.Submodel) - self.assertEqual(len(submodel.qualifier), 0) - self.assertEqual(len(submodel.submodel_element), 0) - - def test_stripped_annotated_relationship_element(self) -> None: - data = """ - { - "modelType": "AnnotatedRelationshipElement", - "idShort": "test_annotated_relationship_element", - "category": "PARAMETER", - "first": { - "type": "ModelReference", - "keys": [ - { - "type": "Submodel", - "value": "http://acplt.org/Test_Submodel" - }, - { - "type": "AnnotatedRelationshipElement", - "value": "test_ref" - } - ] - }, - "second": { - "type": "ModelReference", - "keys": [ - { - "type": "Submodel", - "value": "http://acplt.org/Test_Submodel" - }, - { - "type": "AnnotatedRelationshipElement", - "value": "test_ref" - } - ] - }, - "annotations": [{ - "modelType": "MultiLanguageProperty", - "idShort": "test_multi_language_property", - "category": "CONSTANT" - }] - }""" - - # check if JSON with annotation can be parsed successfully - are = json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIsInstance(are, model.AnnotatedRelationshipElement) - assert isinstance(are, model.AnnotatedRelationshipElement) - self.assertEqual(len(are.annotation), 1) - - # check if annotation is ignored in stripped mode - are = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) - self.assertIsInstance(are, model.AnnotatedRelationshipElement) - assert isinstance(are, model.AnnotatedRelationshipElement) - self.assertEqual(len(are.annotation), 0) - - def test_stripped_entity(self) -> None: - data = """ - { - "modelType": "Entity", - "idShort": "test_entity", - "entityType": "SelfManagedEntity", - "globalAssetId": "test_asset", - "statements": [{ - "modelType": "MultiLanguageProperty", - "idShort": "test_multi_language_property" - }] - }""" - - # check if JSON with statements can be parsed successfully - entity = json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIsInstance(entity, model.Entity) - assert isinstance(entity, model.Entity) - self.assertEqual(len(entity.statement), 1) - - # check if statements is ignored in stripped mode - entity = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) - self.assertIsInstance(entity, model.Entity) - assert isinstance(entity, model.Entity) - self.assertEqual(len(entity.statement), 0) - - def test_stripped_submodel_element_collection(self) -> None: - data = """ - { - "modelType": "SubmodelElementCollection", - "idShort": "test_submodel_element_collection", - "value": [{ - "modelType": "MultiLanguageProperty", - "idShort": "test_multi_language_property" - }] - }""" - - # check if JSON with value can be parsed successfully - sec = json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIsInstance(sec, model.SubmodelElementCollection) - assert isinstance(sec, model.SubmodelElementCollection) - self.assertEqual(len(sec.value), 1) - - # check if value is ignored in stripped mode - sec = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) - self.assertIsInstance(sec, model.SubmodelElementCollection) - assert isinstance(sec, model.SubmodelElementCollection) - self.assertEqual(len(sec.value), 0) - - def test_stripped_asset_administration_shell(self) -> None: - data = """ - { - "modelType": "AssetAdministrationShell", - "id": "http://acplt.org/test_aas", - "assetInformation": { - "assetKind": "Instance", - "globalAssetId": "test_asset" - }, - "submodels": [{ - "type": "ModelReference", - "keys": [{ - "type": "Submodel", - "value": "http://acplt.org/test_submodel" - }] - }] - }""" - # check if JSON with submodels can be parsed successfully - aas = json.loads(data, cls=StrictAASFromJsonDecoder) - self.assertIsInstance(aas, model.AssetAdministrationShell) - assert isinstance(aas, model.AssetAdministrationShell) - self.assertEqual(len(aas.submodel), 1) - - # check if submodels are ignored in stripped mode - aas = json.loads(data, cls=StrictStrippedAASFromJsonDecoder) - self.assertIsInstance(aas, model.AssetAdministrationShell) - assert isinstance(aas, model.AssetAdministrationShell) - self.assertEqual(len(aas.submodel), 0) diff --git a/sdk/test/adapter/json/test_json_serialization.py b/sdk/test/adapter/json/test_json_serialization.py deleted file mode 100644 index 3e9240a..0000000 --- a/sdk/test/adapter/json/test_json_serialization.py +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright (c) 2023 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 -import os -import io -import unittest -import json - -from basyx.aas import model -from basyx.aas.adapter.json import AASToJsonEncoder, StrippedAASToJsonEncoder, write_aas_json_file -from jsonschema import validate # type: ignore -from typing import Set, Union - -from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ - example_aas_mandatory_attributes, example_submodel_template, create_example - - -JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), '../schemas/aasJSONSchema.json') - - -class JsonSerializationTest(unittest.TestCase): - def test_serialize_object(self) -> None: - test_object = model.Property("test_id_short", model.datatypes.String, category="PARAMETER", - description=model.MultiLanguageTextType({"en-US": "Germany", "de": "Deutschland"})) - json_data = json.dumps(test_object, cls=AASToJsonEncoder) - - def test_random_object_serialization(self) -> None: - aas_identifier = "AAS1" - submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) - submodel_identifier = submodel_key[0].get_identifier() - assert(submodel_identifier is not None) - submodel_reference = model.ModelReference(submodel_key, model.Submodel) - submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"), - aas_identifier, submodel={submodel_reference}) - - # serialize object to json - json_data = json.dumps({ - 'assetAdministrationShells': [test_aas], - 'submodels': [submodel], - 'assets': [], - 'conceptDescriptions': [], - }, cls=AASToJsonEncoder) - json_data_new = json.loads(json_data) - - -class JsonSerializationSchemaTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - if not os.path.exists(JSON_SCHEMA_FILE): - raise unittest.SkipTest(f"JSON Schema does not exist at {JSON_SCHEMA_FILE}, skipping test") - - def test_random_object_serialization(self) -> None: - aas_identifier = "AAS1" - submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) - submodel_identifier = submodel_key[0].get_identifier() - assert(submodel_identifier is not None) - submodel_reference = model.ModelReference(submodel_key, model.Submodel) - # The JSONSchema expects every object with HasSemnatics (like Submodels) to have a `semanticId` Reference, which - # must be a Reference. (This seems to be a bug in the JSONSchema.) - submodel = model.Submodel(submodel_identifier, - semantic_id=model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, - "http://acplt.org/TestSemanticId"),))) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"), - aas_identifier, submodel={submodel_reference}) - - # serialize object to json - json_data = json.dumps({ - 'assetAdministrationShells': [test_aas], - 'submodels': [submodel] - }, cls=AASToJsonEncoder) - json_data_new = json.loads(json_data) - - # load schema - with open(JSON_SCHEMA_FILE, 'r') as json_file: - aas_schema = json.load(json_file) - - # validate serialization against schema - validate(instance=json_data_new, schema=aas_schema) - - def test_aas_example_serialization(self) -> None: - data = example_aas.create_full_example() - file = io.StringIO() - write_aas_json_file(file=file, data=data) - - with open(JSON_SCHEMA_FILE, 'r') as json_file: - aas_json_schema = json.load(json_file) - - file.seek(0) - json_data = json.load(file) - - # validate serialization against schema - validate(instance=json_data, schema=aas_json_schema) - - def test_submodel_template_serialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_submodel_template.create_example_submodel_template()) - file = io.StringIO() - write_aas_json_file(file=file, data=data) - - with open(JSON_SCHEMA_FILE, 'r') as json_file: - aas_json_schema = json.load(json_file) - - file.seek(0) - json_data = json.load(file) - - # validate serialization against schema - validate(instance=json_data, schema=aas_json_schema) - - def test_full_empty_example_serialization(self) -> None: - data = example_aas_mandatory_attributes.create_full_example() - file = io.StringIO() - write_aas_json_file(file=file, data=data) - - with open(JSON_SCHEMA_FILE, 'r') as json_file: - aas_json_schema = json.load(json_file) - - file.seek(0) - json_data = json.load(file) - - # validate serialization against schema - validate(instance=json_data, schema=aas_json_schema) - - def test_missing_serialization(self) -> None: - data = example_aas_missing_attributes.create_full_example() - file = io.StringIO() - write_aas_json_file(file=file, data=data) - - with open(JSON_SCHEMA_FILE, 'r') as json_file: - aas_json_schema = json.load(json_file) - - file.seek(0) - json_data = json.load(file) - - # validate serialization against schema - validate(instance=json_data, schema=aas_json_schema) - - def test_concept_description_serialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_aas.create_example_concept_description()) - file = io.StringIO() - write_aas_json_file(file=file, data=data) - - with open(JSON_SCHEMA_FILE, 'r') as json_file: - aas_json_schema = json.load(json_file) - - file.seek(0) - json_data = json.load(file) - - # validate serialization against schema - validate(instance=json_data, schema=aas_json_schema) - - def test_full_example_serialization(self) -> None: - data = create_example() - file = io.StringIO() - write_aas_json_file(file=file, data=data) - - with open(JSON_SCHEMA_FILE, 'r') as json_file: - aas_json_schema = json.load(json_file) - - file.seek(0) - json_data = json.load(file) - - # validate serialization against schema - validate(instance=json_data, schema=aas_json_schema) - - -class JsonSerializationStrippedObjectsTest(unittest.TestCase): - def _checkNormalAndStripped(self, attributes: Union[Set[str], str], obj: object) -> None: - if isinstance(attributes, str): - attributes = {attributes} - - # attributes should be present when using the normal encoder, - # but must not be present when using the stripped encoder - for cls, assert_fn in ((AASToJsonEncoder, self.assertIn), (StrippedAASToJsonEncoder, self.assertNotIn)): - data = json.loads(json.dumps(obj, cls=cls)) - for attr in attributes: - assert_fn(attr, data) - - def test_stripped_qualifiable(self) -> None: - qualifier = model.Qualifier("test_qualifier", str) - qualifier2 = model.Qualifier("test_qualifier2", str) - operation = model.Operation("test_operation", qualifier={qualifier}) - submodel = model.Submodel( - "http://acplt.org/test_submodel", - submodel_element=[operation], - qualifier={qualifier2} - ) - - self._checkNormalAndStripped({"submodelElements", "qualifiers"}, submodel) - self._checkNormalAndStripped("qualifiers", operation) - - def test_stripped_annotated_relationship_element(self) -> None: - mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") - ref = model.ModelReference( - (model.Key(model.KeyTypes.SUBMODEL, "http://acplt.org/test_ref"),), - model.Submodel - ) - are = model.AnnotatedRelationshipElement( - "test_annotated_relationship_element", - ref, - ref, - annotation=[mlp] - ) - - self._checkNormalAndStripped("annotations", are) - - def test_stripped_entity(self) -> None: - mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") - entity = model.Entity("test_entity", model.EntityType.CO_MANAGED_ENTITY, statement=[mlp]) - - self._checkNormalAndStripped("statements", entity) - - def test_stripped_submodel_element_collection(self) -> None: - mlp = model.MultiLanguageProperty("test_multi_language_property", category="PARAMETER") - sec = model.SubmodelElementCollection("test_submodel_element_collection", value=[mlp]) - - self._checkNormalAndStripped("value", sec) - - def test_stripped_asset_administration_shell(self) -> None: - submodel_ref = model.ModelReference( - (model.Key(model.KeyTypes.SUBMODEL, "http://acplt.org/test_ref"),), - model.Submodel - ) - aas = model.AssetAdministrationShell( - model.AssetInformation(global_asset_id="http://acplt.org/test_ref"), - "http://acplt.org/test_aas", - submodel={submodel_ref} - ) - - self._checkNormalAndStripped({"submodels"}, aas) diff --git a/sdk/test/adapter/json/test_json_serialization_deserialization.py b/sdk/test/adapter/json/test_json_serialization_deserialization.py deleted file mode 100644 index 9b016e1..0000000 --- a/sdk/test/adapter/json/test_json_serialization_deserialization.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2023 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 - -import io -import json -import unittest - -from basyx.aas import model -from basyx.aas.adapter.json import AASToJsonEncoder, write_aas_json_file, read_aas_json_file - -from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ - example_aas_mandatory_attributes, example_submodel_template, create_example -from basyx.aas.examples.data._helper import AASDataChecker - -from typing import Iterable, IO - - -class JsonSerializationDeserializationTest(unittest.TestCase): - def test_random_object_serialization_deserialization(self) -> None: - aas_identifier = "AAS1" - submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) - submodel_identifier = submodel_key[0].get_identifier() - assert submodel_identifier is not None - submodel_reference = model.ModelReference(submodel_key, model.Submodel) - submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="test"), - aas_identifier, submodel={submodel_reference}) - - # serialize object to json - json_data = json.dumps({ - 'assetAdministrationShells': [test_aas], - 'submodels': [submodel], - 'assets': [], - 'conceptDescriptions': [], - }, cls=AASToJsonEncoder) - json_data_new = json.loads(json_data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module - json_object_store = read_aas_json_file(io.StringIO(json_data), failsafe=False) - - def test_example_serialization_deserialization(self) -> None: - # test with TextIO and BinaryIO, which should both be supported - t: Iterable[IO] = (io.StringIO(), io.BytesIO()) - for file in t: - data = example_aas.create_full_example() - write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module - file.seek(0) - json_object_store = read_aas_json_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, json_object_store) - - -class JsonSerializationDeserializationTest2(unittest.TestCase): - def test_example_mandatory_attributes_serialization_deserialization(self) -> None: - data = example_aas_mandatory_attributes.create_full_example() - file = io.StringIO() - write_aas_json_file(file=file, data=data) - - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module - file.seek(0) - json_object_store = read_aas_json_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - example_aas_mandatory_attributes.check_full_example(checker, json_object_store) - - -class JsonSerializationDeserializationTest3(unittest.TestCase): - def test_example_missing_attributes_serialization_deserialization(self) -> None: - data = example_aas_missing_attributes.create_full_example() - file = io.StringIO() - write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module - file.seek(0) - json_object_store = read_aas_json_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - example_aas_missing_attributes.check_full_example(checker, json_object_store) - - -class JsonSerializationDeserializationTest4(unittest.TestCase): - def test_example_submodel_template_serialization_deserialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_submodel_template.create_example_submodel_template()) - file = io.StringIO() - write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module - file.seek(0) - json_object_store = read_aas_json_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - example_submodel_template.check_full_example(checker, json_object_store) - - -class JsonSerializationDeserializationTest5(unittest.TestCase): - def test_example_all_examples_serialization_deserialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = create_example() - file = io.StringIO() - write_aas_json_file(file=file, data=data) - # try deserializing the json string into a DictObjectStore of AAS objects with help of the json module - file.seek(0) - json_object_store = read_aas_json_file(file, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - checker.check_object_store(json_object_store, data) diff --git a/sdk/test/adapter/test_http.py b/sdk/test/adapter/test_http.py deleted file mode 100644 index 09dadf8..0000000 --- a/sdk/test/adapter/test_http.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright (c) 2024 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 - -""" -This test uses the schemathesis package to perform automated stateful testing on the implemented http api. Requests -are created automatically based on the json schemata given in the api specification, responses are also validated -against said schemata. - -For data generation schemathesis uses hypothesis and hypothesis-jsonschema, hence the name. hypothesis is a library -for automated, property-based testing. It can generate test cases based on strategies. hypothesis-jsonschema is such -a strategy for generating data that matches a given JSON schema. - -schemathesis allows stateful testing by generating a statemachine based on the OAS links contained in the specification. -This is applied here with the APIWorkflowAAS and APIWorkflowSubmodel classes. They inherit the respective state machine -and offer an automatically generated python unittest TestCase. -""" - -# TODO: lookup schemathesis deps and add them to the readme -# TODO: implement official Plattform I4.0 HTTP API -# TODO: check required properties of schema -# TODO: add id_short format to schemata - -import os -import random -import pathlib -import urllib.parse - -import schemathesis -import hypothesis.strategies - -from basyx.aas import model -from basyx.aas.adapter.aasx import DictSupplementaryFileContainer -from basyx.aas.adapter.http import WSGIApp -from basyx.aas.examples.data.example_aas import create_full_example - -from typing import Set - - -def _encode_and_quote(identifier: model.Identifier) -> str: - return urllib.parse.quote(urllib.parse.quote(identifier, safe=""), safe="") - - -def _check_transformed(response, case): - """ - This helper function performs an additional checks on requests that have been *transformed*, i.e. requests, that - resulted from schemathesis using an OpenAPI Spec link. It asserts, that requests that are performed after a link has - been used, must be successful and result in a 2xx response. The exception are requests where hypothesis generates - invalid data (data, that validates against the schema, but is still semantically invalid). Such requests would - result in a 422 - Unprocessable Entity, which is why the 422 status code is ignored here. - """ - if case.source is not None: - assert 200 <= response.status_code < 300 or response.status_code == 422 - - -# define some settings for hypothesis, used in both api test cases -HYPOTHESIS_SETTINGS = hypothesis.settings( - max_examples=int(os.getenv("HYPOTHESIS_MAX_EXAMPLES", 10)), - stateful_step_count=5, - # disable the filter_too_much health check, which triggers if a strategy filters too much data, raising an error - suppress_health_check=[hypothesis.HealthCheck.filter_too_much], - # disable data generation deadlines, which would result in an error if data generation takes too much time - deadline=None -) - -BASE_URL = "/api/v1" -IDENTIFIER_AAS: Set[str] = set() -IDENTIFIER_SUBMODEL: Set[str] = set() - -# register hypothesis strategy for generating valid idShorts -ID_SHORT_STRATEGY = hypothesis.strategies.from_regex(r"\A[A-Za-z_][0-9A-Za-z_]*\Z") -schemathesis.register_string_format("id_short", ID_SHORT_STRATEGY) - -# store identifiers of available AAS and Submodels -for obj in create_full_example(): - if isinstance(obj, model.AssetAdministrationShell): - IDENTIFIER_AAS.add(_encode_and_quote(obj.id)) - if isinstance(obj, model.Submodel): - IDENTIFIER_SUBMODEL.add(_encode_and_quote(obj.id)) - -# load aas and submodel api specs -AAS_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-aas.yaml", - app=WSGIApp(create_full_example(), DictSupplementaryFileContainer())) - -SUBMODEL_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-submodel.yaml", - app=WSGIApp(create_full_example(), DictSupplementaryFileContainer())) - - -class APIWorkflowAAS(AAS_SCHEMA.as_state_machine()): # type: ignore - def setup(self): - self.schema.app.object_store = create_full_example() - # select random identifier for each test scenario - self.schema.base_url = BASE_URL + "/aas/" + random.choice(tuple(IDENTIFIER_AAS)) - - def transform(self, result, direction, case): - out = super().transform(result, direction, case) - print("transformed") - print(out) - print(result.response, direction.name) - return out - - def validate_response(self, response, case, additional_checks=()) -> None: - super().validate_response(response, case, additional_checks + (_check_transformed,)) - - -class APIWorkflowSubmodel(SUBMODEL_SCHEMA.as_state_machine()): # type: ignore - def setup(self): - self.schema.app.object_store = create_full_example() - self.schema.base_url = BASE_URL + "/submodels/" + random.choice(tuple(IDENTIFIER_SUBMODEL)) - - def transform(self, result, direction, case): - out = super().transform(result, direction, case) - print("transformed") - print(out) - print(result.response, direction.name) - return out - - def validate_response(self, response, case, additional_checks=()) -> None: - super().validate_response(response, case, additional_checks + (_check_transformed,)) - - -# APIWorkflow.TestCase is a standard python unittest.TestCase -# TODO: Fix HTTP API Tests -# ApiTestAAS = APIWorkflowAAS.TestCase -# ApiTestAAS.settings = HYPOTHESIS_SETTINGS - -# ApiTestSubmodel = APIWorkflowSubmodel.TestCase -# ApiTestSubmodel.settings = HYPOTHESIS_SETTINGS diff --git a/sdk/test/adapter/xml/__init__.py b/sdk/test/adapter/xml/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/sdk/test/adapter/xml/test_xml_deserialization.py b/sdk/test/adapter/xml/test_xml_deserialization.py deleted file mode 100644 index 5d32797..0000000 --- a/sdk/test/adapter/xml/test_xml_deserialization.py +++ /dev/null @@ -1,478 +0,0 @@ -# Copyright (c) 2023 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 - -import io -import logging -import unittest - -from basyx.aas import model -from basyx.aas.adapter.xml import StrictAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, \ - read_aas_xml_file_into, read_aas_xml_element -from basyx.aas.adapter.xml.xml_deserialization import _tag_replace_namespace -from basyx.aas.adapter._generic import XML_NS_MAP -from lxml import etree -from typing import Iterable, Type, Union - - -def _xml_wrap(xml: str) -> str: - return f'{xml}' - - -def _root_cause(exception: BaseException) -> BaseException: - while exception.__cause__ is not None: - exception = exception.__cause__ - return exception - - -class XmlDeserializationTest(unittest.TestCase): - def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], error_type: Type[BaseException], - log_level: int) -> None: - """ - Runs read_xml_aas_file in failsafe mode and checks if each string is contained in the first message logged. - Then runs it in non-failsafe mode and checks if each string is contained in the first error raised. - - :param xml: The xml document to parse. - :param strings: One or more strings to match. - :param error_type: The expected error type. - :param log_level: The log level on which the string is expected. - """ - if isinstance(strings, str): - strings = [strings] - string_io = io.StringIO(xml) - with self.assertLogs(logging.getLogger(), level=log_level) as log_ctx: - read_aas_xml_file(string_io, failsafe=True) - with self.assertRaises(error_type) as err_ctx: - read_aas_xml_file(string_io, failsafe=False) - cause = _root_cause(err_ctx.exception) - for s in strings: - self.assertIn(s, log_ctx.output[0]) - self.assertIn(s, str(cause)) - - def test_malformed_xml(self) -> None: - xml = ( - "invalid xml", - _xml_wrap("<<>>><<<<<"), - _xml_wrap("") - ) - for s in xml: - self._assertInExceptionAndLog(s, [], etree.XMLSyntaxError, logging.ERROR) - - def test_invalid_list_name(self) -> None: - xml = _xml_wrap("") - self._assertInExceptionAndLog(xml, "aas:invalidList", TypeError, logging.WARNING) - - def test_invalid_element_in_list(self) -> None: - xml = _xml_wrap(""" - - - - """) - self._assertInExceptionAndLog(xml, ["aas:invalidElement", "aas:submodels"], KeyError, logging.WARNING) - - def test_missing_asset_kind(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_aas - - http://acplt.org/TestAsset/ - - - - """) - self._assertInExceptionAndLog(xml, "aas:assetKind", KeyError, logging.ERROR) - - def test_missing_asset_kind_text(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_aas - - - http://acplt.org/TestAsset/ - - - - """) - self._assertInExceptionAndLog(xml, "aas:assetKind", KeyError, logging.ERROR) - - def test_invalid_asset_kind_text(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_aas - - invalidKind - http://acplt.org/TestAsset/ - - - - """) - self._assertInExceptionAndLog(xml, ["aas:assetKind", "invalidKind"], ValueError, logging.ERROR) - - def test_invalid_boolean(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_submodel - - - False - collection - Capability - - - - - """) - self._assertInExceptionAndLog(xml, "False", ValueError, logging.ERROR) - - def test_no_modelling_kind(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_submodel - - - """) - # should get parsed successfully - object_store = read_aas_xml_file(io.StringIO(xml), failsafe=False) - # modelling kind should default to INSTANCE - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - assert isinstance(submodel, model.Submodel) # to make mypy happy - self.assertEqual(submodel.kind, model.ModellingKind.INSTANCE) - - def test_reference_kind_mismatch(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_aas - - Instance - http://acplt.org/TestAsset/ - - - ModelReference - - - Submodel - http://acplt.org/test_ref - - - - - - """) - with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: - read_aas_xml_file(io.StringIO(xml), failsafe=False) - for s in ("SUBMODEL", "http://acplt.org/test_ref", "AssetAdministrationShell"): - self.assertIn(s, context.output[0]) - - def test_invalid_submodel_element(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_submodel - - - - - - """) - self._assertInExceptionAndLog(xml, "aas:invalidSubmodelElement", KeyError, logging.ERROR) - - def test_empty_qualifier(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_submodel - - - - - - """) - self._assertInExceptionAndLog(xml, ["aas:qualifier", "has no child aas:type"], KeyError, logging.ERROR) - - def test_operation_variable_no_submodel_element(self) -> None: - # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 - xml = _xml_wrap(""" - - - http://acplt.org/test_submodel - - - test_operation - - - - - - - - - - """) - self._assertInExceptionAndLog(xml, ["aas:value", "has no submodel element"], KeyError, logging.ERROR) - - def test_operation_variable_too_many_submodel_elements(self) -> None: - # TODO: simplify this should our suggestion regarding the XML schema get accepted - # https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/57 - xml = _xml_wrap(""" - - - http://acplt.org/test_submodel - - - test_operation - - - - - Template - test_file - application/problem+xml - - - test_file2 - application/problem+xml - - - - - - - - - """) - with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: - read_aas_xml_file(io.StringIO(xml), failsafe=False) - self.assertIn("aas:value", context.output[0]) - self.assertIn("more than one submodel element", context.output[0]) - - def test_duplicate_identifier(self) -> None: - xml = _xml_wrap(""" - - - http://acplt.org/test_aas - - Instance - http://acplt.org/TestAsset/ - - - - - - http://acplt.org/test_aas - - - """) - self._assertInExceptionAndLog(xml, "duplicate identifier", KeyError, logging.ERROR) - - def test_duplicate_identifier_object_store(self) -> None: - sm_id = "http://acplt.org/test_submodel" - - def get_clean_store() -> model.DictObjectStore: - store: model.DictObjectStore = model.DictObjectStore() - submodel_ = model.Submodel(sm_id, id_short="test123") - store.add(submodel_) - return store - - xml = _xml_wrap(""" - - - http://acplt.org/test_submodel - test456 - - - """) - string_io = io.StringIO(xml) - - object_store = get_clean_store() - identifiers = read_aas_xml_file_into(object_store, string_io, replace_existing=True, ignore_existing=False) - self.assertEqual(identifiers.pop(), sm_id) - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test456") - - object_store = get_clean_store() - with self.assertLogs(logging.getLogger(), level=logging.INFO) as log_ctx: - identifiers = read_aas_xml_file_into(object_store, string_io, replace_existing=False, ignore_existing=True) - self.assertEqual(len(identifiers), 0) - self.assertIn("already exists in the object store", log_ctx.output[0]) - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test123") - - object_store = get_clean_store() - with self.assertRaises(KeyError) as err_ctx: - identifiers = read_aas_xml_file_into(object_store, string_io, replace_existing=False, ignore_existing=False) - self.assertEqual(len(identifiers), 0) - cause = _root_cause(err_ctx.exception) - self.assertIn("already exists in the object store", str(cause)) - submodel = object_store.pop() - self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel.id_short, "test123") - - def test_read_aas_xml_element(self) -> None: - xml = f""" - - http://acplt.org/test_submodel - - """ - string_io = io.StringIO(xml) - - submodel = read_aas_xml_element(string_io, XMLConstructables.SUBMODEL) - self.assertIsInstance(submodel, model.Submodel) - - def test_no_namespace_prefix(self) -> None: - def xml(id_: str) -> str: - return f""" - - - - {id_} - - - - """ - - self._assertInExceptionAndLog(xml(""), f'{{{XML_NS_MAP["aas"]}}}id on line 5 has no text', KeyError, - logging.ERROR) - read_aas_xml_file(io.StringIO(xml("urn:x-test:test-submodel"))) - - -class XmlDeserializationStrippedObjectsTest(unittest.TestCase): - def test_stripped_qualifiable(self) -> None: - xml = f""" - - http://acplt.org/test_stripped_submodel - - - test_operation - - - test_qualifier - xs:string - - - - - - - test_qualifier - xs:string - - - - """ - string_io = io.StringIO(xml) - - # check if XML with qualifiers can be parsed successfully - submodel = read_aas_xml_element(string_io, XMLConstructables.SUBMODEL, failsafe=False) - self.assertIsInstance(submodel, model.Submodel) - assert isinstance(submodel, model.Submodel) - self.assertEqual(len(submodel.qualifier), 1) - operation = submodel.submodel_element.pop() - self.assertEqual(len(operation.qualifier), 1) - - # check if qualifiers are ignored in stripped mode - submodel = read_aas_xml_element(string_io, XMLConstructables.SUBMODEL, failsafe=False, stripped=True) - self.assertIsInstance(submodel, model.Submodel) - assert isinstance(submodel, model.Submodel) - self.assertEqual(len(submodel.qualifier), 0) - self.assertEqual(len(submodel.submodel_element), 0) - - def test_stripped_asset_administration_shell(self) -> None: - xml = f""" - - http://acplt.org/test_aas - - Instance - http://acplt.org/TestAsset/ - - - - ModelReference - - - Submodel - http://acplt.org/test_ref - - - - - - """ - string_io = io.StringIO(xml) - - # check if XML with submodels can be parsed successfully - aas = read_aas_xml_element(string_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False) - self.assertIsInstance(aas, model.AssetAdministrationShell) - assert isinstance(aas, model.AssetAdministrationShell) - self.assertEqual(len(aas.submodel), 1) - - # check if submodels are ignored in stripped mode - aas = read_aas_xml_element(string_io, XMLConstructables.ASSET_ADMINISTRATION_SHELL, failsafe=False, - stripped=True) - self.assertIsInstance(aas, model.AssetAdministrationShell) - assert isinstance(aas, model.AssetAdministrationShell) - self.assertEqual(len(aas.submodel), 0) - - -class XmlDeserializationDerivingTest(unittest.TestCase): - def test_submodel_constructor_overriding(self) -> None: - class EnhancedSubmodel(model.Submodel): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.enhanced_attribute = "fancy!" - - class EnhancedAASDecoder(StrictAASFromXmlDecoder): - @classmethod - def construct_submodel(cls, element: etree._Element, object_class=EnhancedSubmodel, **kwargs) \ - -> model.Submodel: - return super().construct_submodel(element, object_class=object_class, **kwargs) - - xml = f""" - - http://acplt.org/test_stripped_submodel - - """ - string_io = io.StringIO(xml) - - submodel = read_aas_xml_element(string_io, XMLConstructables.SUBMODEL, decoder=EnhancedAASDecoder) - self.assertIsInstance(submodel, EnhancedSubmodel) - assert isinstance(submodel, EnhancedSubmodel) - self.assertEqual(submodel.enhanced_attribute, "fancy!") - - -class TestTagReplaceNamespace(unittest.TestCase): - def test_known_namespace(self): - tag = '{https://admin-shell.io/aas/3/0}tag' - expected = 'aas:tag' - self.assertEqual(_tag_replace_namespace(tag, XML_NS_MAP), expected) - - def test_empty_prefix(self): - # Empty prefix should not be replaced as otherwise it would apply everywhere - tag = '{https://admin-shell.io/aas/3/0}tag' - nsmap = {"": "https://admin-shell.io/aas/3/0"} - expected = '{https://admin-shell.io/aas/3/0}tag' - self.assertEqual(_tag_replace_namespace(tag, nsmap), expected) - - def test_empty_namespace(self): - # Empty namespaces should also have no effect - tag = '{https://admin-shell.io/aas/3/0}tag' - nsmap = {"aas": ""} - expected = '{https://admin-shell.io/aas/3/0}tag' - self.assertEqual(_tag_replace_namespace(tag, nsmap), expected) - - def test_unknown_namespace(self): - tag = '{http://unknownnamespace.com}unknown' - expected = '{http://unknownnamespace.com}unknown' # Unknown namespace should remain unchanged - self.assertEqual(_tag_replace_namespace(tag, XML_NS_MAP), expected) diff --git a/sdk/test/adapter/xml/test_xml_serialization.py b/sdk/test/adapter/xml/test_xml_serialization.py deleted file mode 100644 index 11328f6..0000000 --- a/sdk/test/adapter/xml/test_xml_serialization.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (c) 2023 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 -import io -import os -import unittest - -from lxml import etree - -from basyx.aas import model -from basyx.aas.adapter.xml import write_aas_xml_file, xml_serialization - -from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ - example_submodel_template, example_aas_mandatory_attributes - - -XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), '../schemas/aasXMLSchema.xsd') - - -class XMLSerializationTest(unittest.TestCase): - def test_serialize_object(self) -> None: - test_object = model.Property("test_id_short", - model.datatypes.String, - category="PARAMETER", - description=model.MultiLanguageTextType({"en-US": "Germany", "de": "Deutschland"})) - xml_data = xml_serialization.property_to_xml(test_object, xml_serialization.NS_AAS+"test_object") - # todo: is this a correct way to test it? - - def test_random_object_serialization(self) -> None: - aas_identifier = "AAS1" - submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) - submodel_identifier = submodel_key[0].get_identifier() - assert (submodel_identifier is not None) - submodel_reference = model.ModelReference(submodel_key, model.Submodel) - submodel = model.Submodel(submodel_identifier) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="Test"), - aas_identifier, submodel={submodel_reference}) - - test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - test_data.add(test_aas) - test_data.add(submodel) - - test_file = io.BytesIO() - write_aas_xml_file(file=test_file, data=test_data) - - -class XMLSerializationSchemaTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - if not os.path.exists(XML_SCHEMA_FILE): - raise unittest.SkipTest(f"XSD schema does not exist at {XML_SCHEMA_FILE}, skipping test") - - def test_random_object_serialization(self) -> None: - aas_identifier = "AAS1" - submodel_key = (model.Key(model.KeyTypes.SUBMODEL, "SM1"),) - submodel_identifier = submodel_key[0].get_identifier() - assert submodel_identifier is not None - submodel_reference = model.ModelReference(submodel_key, model.Submodel) - submodel = model.Submodel(submodel_identifier, - semantic_id=model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, - "http://acplt.org/TestSemanticId"),))) - test_aas = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="Test"), - aas_identifier, submodel={submodel_reference}) - # serialize object to xml - test_data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - test_data.add(test_aas) - test_data.add(submodel) - - test_file = io.BytesIO() - write_aas_xml_file(file=test_file, data=test_data) - - # load schema - aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) - - # validate serialization against schema - parser = etree.XMLParser(schema=aas_schema) - test_file.seek(0) - root = etree.parse(test_file, parser=parser) - - def test_full_example_serialization(self) -> None: - data = example_aas.create_full_example() - file = io.BytesIO() - write_aas_xml_file(file=file, data=data) - - # load schema - aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) - - # validate serialization against schema - parser = etree.XMLParser(schema=aas_schema) - file.seek(0) - root = etree.parse(file, parser=parser) - - def test_submodel_template_serialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_submodel_template.create_example_submodel_template()) - file = io.BytesIO() - write_aas_xml_file(file=file, data=data) - - # load schema - aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) - - # validate serialization against schema - parser = etree.XMLParser(schema=aas_schema) - file.seek(0) - root = etree.parse(file, parser=parser) - - def test_full_empty_example_serialization(self) -> None: - data = example_aas_mandatory_attributes.create_full_example() - file = io.BytesIO() - write_aas_xml_file(file=file, data=data) - - # load schema - aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) - - # validate serialization against schema - parser = etree.XMLParser(schema=aas_schema) - file.seek(0) - root = etree.parse(file, parser=parser) - - def test_missing_serialization(self) -> None: - data = example_aas_missing_attributes.create_full_example() - file = io.BytesIO() - write_aas_xml_file(file=file, data=data, pretty_print=True) - - # load schema - aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) - - # validate serialization against schema - parser = etree.XMLParser(schema=aas_schema) - file.seek(0) - root = etree.parse(file, parser=parser) - - def test_concept_description(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_aas.create_example_concept_description()) - file = io.BytesIO() - write_aas_xml_file(file=file, data=data) - - # load schema - aas_schema = etree.XMLSchema(file=XML_SCHEMA_FILE) - - # validate serialization against schema - parser = etree.XMLParser(schema=aas_schema) - file.seek(0) - root = etree.parse(file, parser=parser) diff --git a/sdk/test/adapter/xml/test_xml_serialization_deserialization.py b/sdk/test/adapter/xml/test_xml_serialization_deserialization.py deleted file mode 100644 index 2e06c44..0000000 --- a/sdk/test/adapter/xml/test_xml_serialization_deserialization.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2023 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 - -import io -import unittest - -from basyx.aas import model -from basyx.aas.adapter.xml import write_aas_xml_file, read_aas_xml_file, write_aas_xml_element, read_aas_xml_element, \ - XMLConstructables - -from basyx.aas.examples.data import example_aas_missing_attributes, example_aas, \ - example_aas_mandatory_attributes, example_submodel_template, create_example -from basyx.aas.examples.data._helper import AASDataChecker - - -def _serialize_and_deserialize(data: model.DictObjectStore) -> model.DictObjectStore: - file = io.BytesIO() - write_aas_xml_file(file=file, data=data) - - # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml module - file.seek(0) - return read_aas_xml_file(file, failsafe=False) - - -class XMLSerializationDeserializationTest(unittest.TestCase): - def test_example_serialization_deserialization(self) -> None: - object_store = _serialize_and_deserialize(example_aas.create_full_example()) - checker = AASDataChecker(raise_immediately=True) - example_aas.check_full_example(checker, object_store) - - def test_example_mandatory_attributes_serialization_deserialization(self) -> None: - object_store = _serialize_and_deserialize(example_aas_mandatory_attributes.create_full_example()) - checker = AASDataChecker(raise_immediately=True) - example_aas_mandatory_attributes.check_full_example(checker, object_store) - - def test_example_missing_attributes_serialization_deserialization(self) -> None: - object_store = _serialize_and_deserialize(example_aas_missing_attributes.create_full_example()) - checker = AASDataChecker(raise_immediately=True) - example_aas_missing_attributes.check_full_example(checker, object_store) - - def test_example_submodel_template_serialization_deserialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() - data.add(example_submodel_template.create_example_submodel_template()) - object_store = _serialize_and_deserialize(data) - checker = AASDataChecker(raise_immediately=True) - example_submodel_template.check_full_example(checker, object_store) - - def test_example_all_examples_serialization_deserialization(self) -> None: - data: model.DictObjectStore[model.Identifiable] = create_example() - object_store = _serialize_and_deserialize(data) - checker = AASDataChecker(raise_immediately=True) - checker.check_object_store(object_store, data) - - -class XMLSerializationDeserializationSingleObjectTest(unittest.TestCase): - def test_submodel_serialization_deserialization(self) -> None: - submodel: model.Submodel = example_submodel_template.create_example_submodel_template() - bytes_io = io.BytesIO() - write_aas_xml_element(bytes_io, submodel) - bytes_io.seek(0) - submodel2: model.Submodel = read_aas_xml_element(bytes_io, # type: ignore[assignment] - XMLConstructables.SUBMODEL, failsafe=False) - checker = AASDataChecker(raise_immediately=True) - checker.check_submodel_equal(submodel2, submodel) From 09068267e3d92558158cb14cc04f43287225deb4 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Tue, 5 Nov 2024 10:27:03 +0100 Subject: [PATCH 460/474] .github/workflows/ci.yml: import lxml-stubs for CI tests sdk/: fix various typing/codestyle errors --- .github/workflows/ci.yml | 1 + sdk/basyx/adapter/aasx.py | 2 +- sdk/test/adapter/aasx/example_aas.py | 2 +- sdk/test/adapter/aasx/test_aasx.py | 6 ++++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6445a32..5a3af66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,7 @@ jobs: pip install pycodestyle mypy cd ./sdk pip install . + python -m pip install lxml-stubs - name: Check typing with MyPy run: | mypy sdk --exclude sdk/build diff --git a/sdk/basyx/adapter/aasx.py b/sdk/basyx/adapter/aasx.py index 6fa332e..4b71511 100644 --- a/sdk/basyx/adapter/aasx.py +++ b/sdk/basyx/adapter/aasx.py @@ -331,7 +331,7 @@ def __init__(self, file: Union[os.PathLike, str, IO]): p.close() def write_aas(self, - aas_ids: list[id_type], + aas_ids: List[id_type], object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", write_json: bool = False) -> None: diff --git a/sdk/test/adapter/aasx/example_aas.py b/sdk/test/adapter/aasx/example_aas.py index 27601bd..26ee28c 100644 --- a/sdk/test/adapter/aasx/example_aas.py +++ b/sdk/test/adapter/aasx/example_aas.py @@ -34,7 +34,7 @@ def create_full_example() -> ObjectStore: ) file_store = DictSupplementaryFileContainer() - with open(Path(__file__).parent.parent.parent.parent/ 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: + with open(Path(__file__).parent.parent.parent.parent / 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: actual_file_name = file_store.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") if submodel.submodel_elements is not None: diff --git a/sdk/test/adapter/aasx/test_aasx.py b/sdk/test/adapter/aasx/test_aasx.py index 340c523..1821754 100644 --- a/sdk/test/adapter/aasx/test_aasx.py +++ b/sdk/test/adapter/aasx/test_aasx.py @@ -30,7 +30,8 @@ def test_name_friendlyfier(self) -> None: def test_supplementary_file_container(self) -> None: container = aasx.DictSupplementaryFileContainer() - with open(Path(__file__).parent.parent.parent.parent/ 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: + with open(Path(__file__).parent.parent.parent.parent / 'basyx' / 'tutorial' / + 'data' / 'TestFile.pdf', 'rb') as f: new_name = container.add_file("/TestFile.pdf", f, "application/pdf") # Name should not be modified, since there is no conflict self.assertEqual("/TestFile.pdf", new_name) @@ -79,7 +80,8 @@ def test_writing_reading_example_aas(self) -> None: # Create example data and file_store data = example_aas.create_full_example() files = aasx.DictSupplementaryFileContainer() - with open(Path(__file__).parent.parent.parent.parent/ 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: + with open(Path(__file__).parent.parent.parent.parent / 'basyx' / + 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: files.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") f.seek(0) From 4d24ad818690cdec59ce0fac7dd39965ece4f384 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Tue, 5 Nov 2024 16:23:09 +0100 Subject: [PATCH 461/474] sdk/basyx/adapter/: redesigned adapter structure to match its use --- sdk/basyx/adapter/aasx.py | 406 +++++++++++++++++- sdk/basyx/adapter/json/__init__.py | 18 - .../adapter/json/json_deserialization.py | 107 ----- sdk/basyx/adapter/json/json_serialization.py | 93 ---- sdk/basyx/adapter/xml/__init__.py | 11 - sdk/basyx/adapter/xml/xml_deserialization.py | 195 --------- sdk/basyx/adapter/xml/xml_serialization.py | 71 --- 7 files changed, 400 insertions(+), 501 deletions(-) delete mode 100644 sdk/basyx/adapter/json/__init__.py delete mode 100644 sdk/basyx/adapter/json/json_deserialization.py delete mode 100644 sdk/basyx/adapter/json/json_serialization.py delete mode 100644 sdk/basyx/adapter/xml/__init__.py delete mode 100644 sdk/basyx/adapter/xml/xml_deserialization.py delete mode 100644 sdk/basyx/adapter/xml/xml_serialization.py diff --git a/sdk/basyx/adapter/aasx.py b/sdk/basyx/adapter/aasx.py index 4b71511..e155875 100644 --- a/sdk/basyx/adapter/aasx.py +++ b/sdk/basyx/adapter/aasx.py @@ -29,17 +29,20 @@ import logging import os import re -from typing import Dict, Tuple, IO, Union, List, Set, Optional, Iterable, Iterator +import contextlib +import json + +from typing import (Dict, Tuple, IO, Union, List, Set, Optional, Iterable, BinaryIO, TextIO, Any, Callable, + Iterator, ContextManager, TypeVar, get_args, Type) from aas_core3.types import HasSemantics from basyx.object_store import ObjectStore from aas_core3 import types as model -from .json.json_serialization import write_aas_json_file -from .json.json_deserialization import read_aas_json_file -from .xml.xml_serialization import write_aas_xml_file -from .xml.xml_deserialization import read_aas_xml_file import pyecma376_2 -from .xml.xml_serialization import write_aas_xml_file +import aas_core3.jsonization as aas_jsonization +from lxml import etree +import aas_core3.xmlization as aas_xmlization + logger = logging.getLogger(__name__) @@ -52,6 +55,16 @@ # Identifiable. Doing this leads to problems with mypy... id_type = str +# type aliases for path-like objects and IO +# used by write_aas_xml_file, read_aas_xml_file, write_aas_json_file, read_aas_json_file +Path = Union[str, bytes, os.PathLike] +PathOrBinaryIO = Union[Path, BinaryIO] +PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO + +# XML Namespace definition +XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} +XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" + class AASXReader: """ @@ -875,3 +888,384 @@ def __contains__(self, item: object) -> bool: def __iter__(self) -> Iterator[str]: return iter(self._name_map) + + +T = TypeVar('T') + + +def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: + """ + Helper function for getting an item from a (str→object) dict in a typesafe way. + + The type of the object is checked at runtime and a TypeError is raised, if the object has not the expected type. + + :param dct: The dict + :param key: The key of the item to retrieve + :param type_: The expected type of the item + :return: The item + :raises TypeError: If the item has an unexpected type + :raises KeyError: If the key is not found in the dict (just as usual) + """ + val = dct[key] + if not isinstance(val, type_): + raise TypeError("Dict entry '{}' has unexpected type {}".format(key, type(val).__name__)) + return val + + +def read_aas_json_file_into(object_store: ObjectStore, file: PathOrIO, replace_existing: bool = False, + ignore_existing: bool = False) -> Set[str]: + """ + Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 + into a given object store. + + :param object_store: The :class:`ObjectStore ` in which the + identifiable objects should be stored + :param file: A filename or file-like object to read the JSON-serialized data from + :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not + :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. + This parameter is ignored if replace_existing is ``True``. + :raises KeyError: 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`` + :raises TypeError: **Non-failsafe**: Encountered an element in the wrong list + (e.g. an AssetAdministrationShell in ``submodels``) + :return: A set of :class:`Identifiers ` that were added to object_store + """ + ret: Set[str] = set() + + # json.load() accepts TextIO and BinaryIO + cm: ContextManager[IO] + if isinstance(file, get_args(Path)): + # 'file' is a path, needs to be opened first + cm = open(file, "r", encoding="utf-8-sig") + else: + # 'file' is not a path, thus it must already be IO + # mypy seems to have issues narrowing the type due to get_args() + cm = contextlib.nullcontext(file) # type: ignore[arg-type] + + # read, parse and convert JSON file + with cm as fp: + data = json.load(fp) + + for name, expected_type in (('assetAdministrationShells', model.AssetAdministrationShell), + ('submodels', model.Submodel), + ('conceptDescriptions', model.ConceptDescription)): + try: + lst = _get_ts(data, name, list) + except (KeyError, TypeError): + continue + + for item in lst: + identifiable = aas_jsonization.identifiable_from_jsonable(item) + if identifiable.id in ret: + error_message = f"{item} has a duplicate identifier already parsed in the document!" + raise KeyError(error_message) + existing_element = object_store.get(identifiable.id) + if existing_element is not None: + if not replace_existing: + error_message = f"object with identifier {identifiable.id} already exists " \ + f"in the object store: {existing_element}!" + if not ignore_existing: + raise KeyError(error_message + f" failed to insert {identifiable}!") + object_store.discard(existing_element) + object_store.add(identifiable) + ret.add(identifiable.id) + + return ret + + +def read_aas_json_file(file, **kwargs) -> ObjectStore[model.Identifiable]: + """ + A wrapper of :meth:`~basyx.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects + in an empty :class:`~basyx.model.provider.DictObjectStore`. This function supports the same keyword arguments as + :meth:`~basyx.adapter.json.json_deserialization.read_aas_json_file_into`. + + :param file: A filename or file-like object to read the JSON-serialized data from + :param kwargs: Keyword arguments passed to :meth:`read_aas_json_file_into` + :raises KeyError: Encountered a duplicate identifier + :return: A :class:`~basyx.ObjectStore` containing all AAS objects from the JSON file + """ + obj_store: ObjectStore[model.Identifiable] = ObjectStore() + read_aas_json_file_into(obj_store, file, **kwargs) + return obj_store + + +def _create_dict(data: ObjectStore) -> dict: + # separate different kind of objects + asset_administration_shells: List = [] + submodels: List = [] + concept_descriptions: List = [] + for obj in data: + if isinstance(obj, model.AssetAdministrationShell): + asset_administration_shells.append(aas_jsonization.to_jsonable(obj)) + elif isinstance(obj, model.Submodel): + submodels.append(aas_jsonization.to_jsonable(obj)) + elif isinstance(obj, model.ConceptDescription): + concept_descriptions.append(aas_jsonization.to_jsonable(obj)) + dict_: Dict[str, List] = {} + if asset_administration_shells: + dict_['assetAdministrationShells'] = asset_administration_shells + if submodels: + dict_['submodels'] = submodels + if concept_descriptions: + dict_['conceptDescriptions'] = concept_descriptions + return dict_ + + +class _DetachingTextIOWrapper(io.TextIOWrapper): + """ + Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer. + """ + + def __exit__(self, exc_type, exc_val, exc_tb): + self.detach() + + +def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: + """ + Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset + Administration Shell', chapter 5.5 + + :param file: A filename or file-like object to write the JSON-serialized data to + :param data: :class:`ObjectStore ` which contains different objects of + the AAS meta model which should be serialized to a JSON file + :param kwargs: Additional keyword arguments to be passed to `json.dump()` + """ + + # json.dump() only accepts TextIO + cm: ContextManager[TextIO] + if isinstance(file, get_args(Path)): + # 'file' is a path, needs to be opened first + cm = open(file, "w", encoding="utf-8") + elif not hasattr(file, "encoding"): + # only TextIO has this attribute, so this must be BinaryIO, which needs to be wrapped + # mypy seems to have issues narrowing the type due to get_args() + cm = _DetachingTextIOWrapper(file, "utf-8", write_through=True) # type: ignore[arg-type] + else: + # we already got TextIO, nothing needs to be done + # mypy seems to have issues narrowing the type due to get_args() + cm = contextlib.nullcontext(file) # type: ignore[arg-type] + # serialize object to json# + + with cm as fp: + json.dump(_create_dict(data), fp, **kwargs) + + +NS_AAS = XML_NS_AAS + + +def _write_element(file: PathOrBinaryIO, element: etree._Element, **kwargs) -> None: + etree.ElementTree(element).write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) + + +def object_store_to_xml_element(data: ObjectStore) -> etree._Element: + """ + Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree._Element`. + This function is used internally by :meth:`write_aas_xml_file` and shouldn't be + called directly for most use-cases. + + :param data: :class:`ObjectStore ` which contains different objects of + the AAS meta model which should be serialized to an XML file + """ + # separate different kind of objects + asset_administration_shells = [] + submodels = [] + concept_descriptions = [] + for obj in data: + if isinstance(obj, model.AssetAdministrationShell): + asset_administration_shells.append(obj) + elif isinstance(obj, model.Submodel): + submodels.append(obj) + elif isinstance(obj, model.ConceptDescription): + concept_descriptions.append(obj) + + # serialize objects to XML + root = etree.Element(NS_AAS + "environment", nsmap=XML_NS_MAP) + if asset_administration_shells: + et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") + for aas_obj in asset_administration_shells: + et_asset_administration_shells.append( + etree.fromstring(aas_xmlization.to_str(aas_obj))) + root.append(et_asset_administration_shells) + if submodels: + et_submodels = etree.Element(NS_AAS + "submodels") + for sub_obj in submodels: + et_submodels.append(etree.fromstring(aas_xmlization.to_str(sub_obj))) + root.append(et_submodels) + if concept_descriptions: + et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") + for con_obj in concept_descriptions: + et_concept_descriptions.append(etree.fromstring(aas_xmlization.to_str(con_obj))) + root.append(et_concept_descriptions) + return root + + +def write_aas_xml_file(file: PathOrBinaryIO, + data: ObjectStore, + **kwargs) -> None: + """ + Write a set of AAS objects to an Asset Administration Shell XML file according to 'Details of the Asset + Administration Shell', chapter 5.4 + + :param file: A filename or file-like object to write the XML-serialized data to + :param data: :class:`ObjectStore ` which contains different objects of + the AAS meta model which should be serialized to an XML file + :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree._ElementTree.write` + """ + return _write_element(file, object_store_to_xml_element(data), **kwargs) + + +REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]} + + +RE = TypeVar("RE", bound=model.RelationshipElement) + + +def _element_pretty_identifier(element: etree._Element) -> str: + """ + Returns a pretty element identifier for a given XML element. + + If the prefix is known, the namespace in the element tag is replaced by the prefix. + If additionally also the sourceline is known, it is added as a suffix to name. + For example, instead of "{https://admin-shell.io/aas/3/0}assetAdministrationShell" this function would return + "aas:assetAdministrationShell on line $line", if both, prefix and sourceline, are known. + + :param element: The xml element. + :return: The pretty element identifier. + """ + identifier = element.tag + if element.prefix is not None: + # Only replace the namespace by the prefix if it matches our known namespaces, + # so the replacement by the prefix doesn't mask errors such as incorrect namespaces. + namespace, tag = element.tag.split("}", 1) + if namespace[1:] in XML_NS_MAP.values(): + identifier = element.prefix + ":" + tag + if element.sourceline is not None: + identifier += f" on line {element.sourceline}" + return identifier + + +def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree._Element]: + """ + Parse an XML document into an element tree + + :param file: A filename or file-like object to read the XML-serialized data from + :param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception if the document + is malformed, parsing is aborted, an error is logged and None is returned + :param parser_kwargs: Keyword arguments passed to the XMLParser constructor + :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML + :raises KeyError: If a required namespace has not been declared on the XML document + :return: The root element of the element tree + """ + + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, **parser_kwargs) + + try: + root = etree.parse(file, parser).getroot() + except etree.XMLSyntaxError as e: + if failsafe: + logger.error(e) + return None + raise e + + missing_namespaces: Set[str] = REQUIRED_NAMESPACES - set(root.nsmap.values()) + if missing_namespaces: + error_message = f"The following required namespaces are not declared: {' | '.join(missing_namespaces)}" \ + + " - Is the input document of an older version?" + if not failsafe: + raise KeyError(error_message) + logger.error(error_message) + return root + + +def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, + replace_existing: bool = False, ignore_existing: bool = False, + **parser_kwargs: Any) -> Set[str]: + """ + Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 + into a given :class:`ObjectStore `. + + :param object_store: The :class:`ObjectStore ` in which the + :class:`~basyx.aas.model.base.Identifiable` objects should be stored + :param file: A filename or file-like object to read the XML-serialized data from + :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not + :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. + This parameter is ignored if replace_existing is True. + :param parser_kwargs: Keyword arguments passed to the XMLParser constructor + :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML + :raises KeyError: If a required namespace has not been declared on the XML document + :raises KeyError: 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`` + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): Errors during + construction of the objects + :raises TypeError: Encountered an undefined top-level list (e.g. ````) + :return: A set of :class:`Identifiers ` that were added to object_store + """ + ret: Set = set() + + element_constructors: Dict[str, Callable[..., model.Identifiable]] = { + "assetAdministrationShell": aas_xmlization.asset_administration_shell_from_str, + "conceptDescription": aas_xmlization.concept_description_from_str, + "submodel": aas_xmlization.submodel_from_str + } + + element_constructors = {NS_AAS + k: v for k, v in element_constructors.items()} + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, **parser_kwargs) + + root = etree.parse(file, parser).getroot() + + if root is None: + return ret + # Add AAS objects to ObjectStore + for list_ in root: + + element_tag = list_.tag[:-1] + if list_.tag[-1] != "s" or element_tag not in element_constructors: + error_message = f"Unexpected top-level list {_element_pretty_identifier(list_)}!" + + logger.warning(error_message) + continue + + for element in list_: + str = etree.tostring(element).decode("utf-8-sig") + identifiable = element_constructors[element_tag](str) + + if identifiable.id in ret: + error_message = f"{element} has a duplicate identifier already parsed in the document!" + raise KeyError(error_message) + existing_element = object_store.get(identifiable.id) + if existing_element is not None: + if not replace_existing: + error_message = f"object with identifier {identifiable.id} already exists " \ + f"in the object store: {existing_element}!" + if not ignore_existing: + raise KeyError(error_message + f" failed to insert {identifiable}!") + logger.info(error_message + f" skipping insertion of {identifiable}...") + continue + object_store.discard(existing_element) + object_store.add(identifiable) + ret.add(identifiable.id) + + return ret + + +def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> ObjectStore[model.Identifiable]: + """ + A wrapper of :meth:`~basyx.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an + empty :class:`~basyx.ObjectStore`. This function supports + the same keyword arguments as :meth:`~basyx.adapter.xml.xml_deserialization.read_aas_xml_file_into`. + + :param file: A filename or file-like object to read the XML-serialized data from + :param kwargs: Keyword arguments passed to :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` + :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML + :raises KeyError: If a required namespace has not been declared on the XML document + :raises KeyError: Encountered a duplicate identifier + :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): Errors during + construction of the objects + :raises TypeError: Encountered an undefined top-level list (e.g. ````) + :return: A :class:`~basyx.ObjectStore` containing all AAS objects from the XML file + """ + obj_store: ObjectStore[model.Identifiable] = ObjectStore() + read_aas_xml_file_into(obj_store, file, **kwargs) + return obj_store diff --git a/sdk/basyx/adapter/json/__init__.py b/sdk/basyx/adapter/json/__init__.py deleted file mode 100644 index a2b0fde..0000000 --- a/sdk/basyx/adapter/json/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -.. _adapter.json.__init__: - -This package contains functionality for serialization and deserialization of BaSyx Python SDK objects into/from JSON. - -:ref:`json_serialization `: The module offers a function to write an ObjectStore to a -given file and therefore defines the custom JSONEncoder -:class:`~basyx.aas.adapter.json.json_serialization.AASToJsonEncoder` which handles encoding of all BaSyx Python SDK -objects and their attributes by converting them into standard python objects. - -:ref:`json_deserialization `: The module implements custom JSONDecoder classes -:class:`~basyx.aas.adapter.json.json_deserialization.AASFromJsonDecoder` and -:class:`~basyx.aas.adapter.json.json_deserialization.StrictAASFromJsonDecoder`, that — when used with Python's -:mod:`json` module — detect AAS objects in the parsed JSON and convert them into the corresponding BaSyx Python SDK -object. A function :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file` is provided to read all -AAS objects within a JSON file and return them as BaSyx Python SDK -:class:`ObjectStore `. -""" diff --git a/sdk/basyx/adapter/json/json_deserialization.py b/sdk/basyx/adapter/json/json_deserialization.py deleted file mode 100644 index 499a66c..0000000 --- a/sdk/basyx/adapter/json/json_deserialization.py +++ /dev/null @@ -1,107 +0,0 @@ -import contextlib -import json -from typing import Dict, Callable, ContextManager, TypeVar, Type, List, IO, Optional, Set, get_args -from aas_core3.types import AssetAdministrationShell, Submodel, ConceptDescription -import aas_core3.jsonization as aas_jsonization - -from basyx.object_store import ObjectStore, Identifiable -from .._generic import PathOrIO, Path - -T = TypeVar('T') - - -def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: - """ - Helper function for getting an item from a (str→object) dict in a typesafe way. - - The type of the object is checked at runtime and a TypeError is raised, if the object has not the expected type. - - :param dct: The dict - :param key: The key of the item to retrieve - :param type_: The expected type of the item - :return: The item - :raises TypeError: If the item has an unexpected type - :raises KeyError: If the key is not found in the dict (just as usual) - """ - val = dct[key] - if not isinstance(val, type_): - raise TypeError("Dict entry '{}' has unexpected type {}".format(key, type(val).__name__)) - return val - - -def read_aas_json_file_into(object_store: ObjectStore, file: PathOrIO, replace_existing: bool = False, - ignore_existing: bool = False) -> Set[str]: - """ - Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 - into a given object store. - - :param object_store: The :class:`ObjectStore ` in which the - identifiable objects should be stored - :param file: A filename or file-like object to read the JSON-serialized data from - :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not - :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. - This parameter is ignored if replace_existing is ``True``. - :raises KeyError: 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`` - :raises TypeError: **Non-failsafe**: Encountered an element in the wrong list - (e.g. an AssetAdministrationShell in ``submodels``) - :return: A set of :class:`Identifiers ` that were added to object_store - """ - ret: Set[str] = set() - - # json.load() accepts TextIO and BinaryIO - cm: ContextManager[IO] - if isinstance(file, get_args(Path)): - # 'file' is a path, needs to be opened first - cm = open(file, "r", encoding="utf-8-sig") - else: - # 'file' is not a path, thus it must already be IO - # mypy seems to have issues narrowing the type due to get_args() - cm = contextlib.nullcontext(file) # type: ignore[arg-type] - - # read, parse and convert JSON file - with cm as fp: - data = json.load(fp) - - for name, expected_type in (('assetAdministrationShells', AssetAdministrationShell), - ('submodels', Submodel), - ('conceptDescriptions', ConceptDescription)): - try: - lst = _get_ts(data, name, list) - except (KeyError, TypeError): - continue - - for item in lst: - identifiable = aas_jsonization.identifiable_from_jsonable(item) - if identifiable.id in ret: - error_message = f"{item} has a duplicate identifier already parsed in the document!" - raise KeyError(error_message) - existing_element = object_store.get(identifiable.id) - if existing_element is not None: - if not replace_existing: - error_message = f"object with identifier {identifiable.id} already exists " \ - f"in the object store: {existing_element}!" - if not ignore_existing: - raise KeyError(error_message + f" failed to insert {identifiable}!") - object_store.discard(existing_element) - object_store.add(identifiable) - ret.add(identifiable.id) - - return ret - - -def read_aas_json_file(file, **kwargs) -> ObjectStore[Identifiable]: - """ - A wrapper of :meth:`~basyx.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects - in an empty :class:`~basyx.model.provider.DictObjectStore`. This function supports the same keyword arguments as - :meth:`~basyx.adapter.json.json_deserialization.read_aas_json_file_into`. - - :param file: A filename or file-like object to read the JSON-serialized data from - :param kwargs: Keyword arguments passed to :meth:`read_aas_json_file_into` - :raises KeyError: Encountered a duplicate identifier - :return: A :class:`~basyx.ObjectStore` containing all AAS objects from the JSON file - """ - obj_store: ObjectStore[Identifiable] = ObjectStore() - read_aas_json_file_into(obj_store, file, **kwargs) - return obj_store diff --git a/sdk/basyx/adapter/json/json_serialization.py b/sdk/basyx/adapter/json/json_serialization.py deleted file mode 100644 index 4d7b699..0000000 --- a/sdk/basyx/adapter/json/json_serialization.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (c) 2023 the Eclipse BaSyx Authors -# -# This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 which is available -# at https://www.apache.org/licenses/LICENSE-2.0. -# -# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -""" -.. _adapter.json.json_serialization: - -Module for serializing Asset Administration Shell objects to the official JSON format - - -""" -import base64 -import contextlib -import inspect -import io -import time -from typing import ContextManager, List, Dict, Optional, TextIO, Type, Callable, get_args -import json -from basyx.object_store import ObjectStore -from aas_core3.types import AssetAdministrationShell, Submodel, ConceptDescription -from aas_core3.jsonization import to_jsonable -from .. import _generic - -import os -from typing import BinaryIO, Dict, IO, Type, Union - -Path = Union[str, bytes, os.PathLike] -PathOrBinaryIO = Union[Path, BinaryIO] -PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO - - -def _create_dict(data: ObjectStore) -> dict: - # separate different kind of objects - asset_administration_shells: List = [] - submodels: List = [] - concept_descriptions: List = [] - for obj in data: - if isinstance(obj, AssetAdministrationShell): - asset_administration_shells.append(to_jsonable(obj)) - elif isinstance(obj, Submodel): - submodels.append(to_jsonable(obj)) - elif isinstance(obj, ConceptDescription): - concept_descriptions.append(to_jsonable(obj)) - dict_: Dict[str, List] = {} - if asset_administration_shells: - dict_['assetAdministrationShells'] = asset_administration_shells - if submodels: - dict_['submodels'] = submodels - if concept_descriptions: - dict_['conceptDescriptions'] = concept_descriptions - return dict_ - - -class _DetachingTextIOWrapper(io.TextIOWrapper): - """ - Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer. - """ - - def __exit__(self, exc_type, exc_val, exc_tb): - self.detach() - - -def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: - """ - Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset - Administration Shell', chapter 5.5 - - :param file: A filename or file-like object to write the JSON-serialized data to - :param data: :class:`ObjectStore ` which contains different objects of - the AAS meta model which should be serialized to a JSON file - :param kwargs: Additional keyword arguments to be passed to `json.dump()` - """ - - # json.dump() only accepts TextIO - cm: ContextManager[TextIO] - if isinstance(file, get_args(_generic.Path)): - # 'file' is a path, needs to be opened first - cm = open(file, "w", encoding="utf-8") - elif not hasattr(file, "encoding"): - # only TextIO has this attribute, so this must be BinaryIO, which needs to be wrapped - # mypy seems to have issues narrowing the type due to get_args() - cm = _DetachingTextIOWrapper(file, "utf-8", write_through=True) # type: ignore[arg-type] - else: - # we already got TextIO, nothing needs to be done - # mypy seems to have issues narrowing the type due to get_args() - cm = contextlib.nullcontext(file) # type: ignore[arg-type] - # serialize object to json# - - with cm as fp: - json.dump(_create_dict(data), fp, **kwargs) diff --git a/sdk/basyx/adapter/xml/__init__.py b/sdk/basyx/adapter/xml/__init__.py deleted file mode 100644 index 6d2bc75..0000000 --- a/sdk/basyx/adapter/xml/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -.. _adapter.xml.__init__: - -This package contains functionality for serialization and deserialization of BaSyx Python SDK objects into/from XML. - -:ref:`xml_serialization `: The module offers a function to write an -:class:`ObjectStore ` to a given file. - -:ref:`xml_deserialization `: The module offers a function to create an -:class:`ObjectStore ` from a given xml document. -""" diff --git a/sdk/basyx/adapter/xml/xml_deserialization.py b/sdk/basyx/adapter/xml/xml_deserialization.py deleted file mode 100644 index a597ea8..0000000 --- a/sdk/basyx/adapter/xml/xml_deserialization.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) 2023 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 -""" -.. _adapter.xml.xml_deserialization: - -Module for deserializing Asset Administration Shell data from the official XML format - -This module provides the following functions for parsing XML documents: - -- :func:`read_aas_xml_file_into` constructs all elements of an XML document and stores them in a given - :class:`ObjectStore ` -- :func:`read_aas_xml_file` constructs all elements of an XML document and returns them in a - :class:`~basyx.ObjectStore` - - -.. code-block:: - - KeyError: aas:id on line 252 has no attribute with name idType! - -> Failed to construct aas:id on line 252 using construct_identifier! - -> Failed to construct aas:conceptDescription on line 247 using construct_concept_description! - - - -""" - -from aas_core3 import types as model -from lxml import etree -import logging -import aas_core3.xmlization as aas_xmlization -from basyx.object_store import ObjectStore, Identifiable - -from typing import Any, Callable, Dict, Iterable, Optional, Set, Tuple, Type, TypeVar, List -from .._generic import XML_NS_MAP, XML_NS_AAS, PathOrIO - -NS_AAS = XML_NS_AAS -REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]} - -logger = logging.getLogger(__name__) - -T = TypeVar("T") -RE = TypeVar("RE", bound=model.RelationshipElement) - - -def _element_pretty_identifier(element: etree._Element) -> str: - """ - Returns a pretty element identifier for a given XML element. - - If the prefix is known, the namespace in the element tag is replaced by the prefix. - If additionally also the sourceline is known, it is added as a suffix to name. - For example, instead of "{https://admin-shell.io/aas/3/0}assetAdministrationShell" this function would return - "aas:assetAdministrationShell on line $line", if both, prefix and sourceline, are known. - - :param element: The xml element. - :return: The pretty element identifier. - """ - identifier = element.tag - if element.prefix is not None: - # Only replace the namespace by the prefix if it matches our known namespaces, - # so the replacement by the prefix doesn't mask errors such as incorrect namespaces. - namespace, tag = element.tag.split("}", 1) - if namespace[1:] in XML_NS_MAP.values(): - identifier = element.prefix + ":" + tag - if element.sourceline is not None: - identifier += f" on line {element.sourceline}" - return identifier - - -def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree._Element]: - """ - Parse an XML document into an element tree - - :param file: A filename or file-like object to read the XML-serialized data from - :param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception if the document - is malformed, parsing is aborted, an error is logged and None is returned - :param parser_kwargs: Keyword arguments passed to the XMLParser constructor - :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML - :raises KeyError: If a required namespace has not been declared on the XML document - :return: The root element of the element tree - """ - - parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, **parser_kwargs) - - try: - root = etree.parse(file, parser).getroot() - except etree.XMLSyntaxError as e: - if failsafe: - logger.error(e) - return None - raise e - - missing_namespaces: Set[str] = REQUIRED_NAMESPACES - set(root.nsmap.values()) - if missing_namespaces: - error_message = f"The following required namespaces are not declared: {' | '.join(missing_namespaces)}" \ - + " - Is the input document of an older version?" - if not failsafe: - raise KeyError(error_message) - logger.error(error_message) - return root - - -def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, - replace_existing: bool = False, ignore_existing: bool = False, - **parser_kwargs: Any) -> Set[str]: - """ - Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 - into a given :class:`ObjectStore `. - - :param object_store: The :class:`ObjectStore ` in which the - :class:`~basyx.aas.model.base.Identifiable` objects should be stored - :param file: A filename or file-like object to read the XML-serialized data from - :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not - :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. - This parameter is ignored if replace_existing is True. - :param parser_kwargs: Keyword arguments passed to the XMLParser constructor - :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML - :raises KeyError: If a required namespace has not been declared on the XML document - :raises KeyError: 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`` - :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): Errors during - construction of the objects - :raises TypeError: Encountered an undefined top-level list (e.g. ````) - :return: A set of :class:`Identifiers ` that were added to object_store - """ - ret: Set = set() - - element_constructors: Dict[str, Callable[..., model.Identifiable]] = { - "assetAdministrationShell": aas_xmlization.asset_administration_shell_from_str, - "conceptDescription": aas_xmlization.concept_description_from_str, - "submodel": aas_xmlization.submodel_from_str - } - - element_constructors = {NS_AAS + k: v for k, v in element_constructors.items()} - parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, **parser_kwargs) - - root = etree.parse(file, parser).getroot() - - if root is None: - return ret - # Add AAS objects to ObjectStore - for list_ in root: - - element_tag = list_.tag[:-1] - if list_.tag[-1] != "s" or element_tag not in element_constructors: - error_message = f"Unexpected top-level list {_element_pretty_identifier(list_)}!" - - logger.warning(error_message) - continue - - for element in list_: - str = etree.tostring(element).decode("utf-8-sig") - identifiable = element_constructors[element_tag](str) - - if identifiable.id in ret: - error_message = f"{element} has a duplicate identifier already parsed in the document!" - raise KeyError(error_message) - existing_element = object_store.get(identifiable.id) - if existing_element is not None: - if not replace_existing: - error_message = f"object with identifier {identifiable.id} already exists " \ - f"in the object store: {existing_element}!" - if not ignore_existing: - raise KeyError(error_message + f" failed to insert {identifiable}!") - logger.info(error_message + f" skipping insertion of {identifiable}...") - continue - object_store.discard(existing_element) - object_store.add(identifiable) - ret.add(identifiable.id) - - return ret - - -def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> ObjectStore[Identifiable]: - """ - A wrapper of :meth:`~basyx.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an - empty :class:`~basyx.ObjectStore`. This function supports - the same keyword arguments as :meth:`~basyx.adapter.xml.xml_deserialization.read_aas_xml_file_into`. - - :param file: A filename or file-like object to read the XML-serialized data from - :param kwargs: Keyword arguments passed to :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` - :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML - :raises KeyError: If a required namespace has not been declared on the XML document - :raises KeyError: Encountered a duplicate identifier - :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): Errors during - construction of the objects - :raises TypeError: Encountered an undefined top-level list (e.g. ````) - :return: A :class:`~basyx.ObjectStore` containing all AAS objects from the XML file - """ - obj_store: ObjectStore[Identifiable] = ObjectStore() - read_aas_xml_file_into(obj_store, file, **kwargs) - return obj_store diff --git a/sdk/basyx/adapter/xml/xml_serialization.py b/sdk/basyx/adapter/xml/xml_serialization.py deleted file mode 100644 index 1c2e7c3..0000000 --- a/sdk/basyx/adapter/xml/xml_serialization.py +++ /dev/null @@ -1,71 +0,0 @@ -from lxml import etree -from typing import Callable, Dict, Optional, Type -import base64 - -from aas_core3 import types as model -from .. import _generic -from basyx.object_store import ObjectStore -import aas_core3.xmlization as aas_xmlization - -NS_AAS = _generic.XML_NS_AAS - - -def _write_element(file: _generic.PathOrBinaryIO, element: etree._Element, **kwargs) -> None: - etree.ElementTree(element).write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) - - -def object_store_to_xml_element(data: ObjectStore) -> etree._Element: - """ - Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree._Element`. - This function is used internally by :meth:`write_aas_xml_file` and shouldn't be - called directly for most use-cases. - - :param data: :class:`ObjectStore ` which contains different objects of - the AAS meta model which should be serialized to an XML file - """ - # separate different kind of objects - asset_administration_shells = [] - submodels = [] - concept_descriptions = [] - for obj in data: - if isinstance(obj, model.AssetAdministrationShell): - asset_administration_shells.append(obj) - elif isinstance(obj, model.Submodel): - submodels.append(obj) - elif isinstance(obj, model.ConceptDescription): - concept_descriptions.append(obj) - - # serialize objects to XML - root = etree.Element(NS_AAS + "environment", nsmap=_generic.XML_NS_MAP) - if asset_administration_shells: - et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") - for aas_obj in asset_administration_shells: - et_asset_administration_shells.append( - etree.fromstring(aas_xmlization.to_str(aas_obj))) - root.append(et_asset_administration_shells) - if submodels: - et_submodels = etree.Element(NS_AAS + "submodels") - for sub_obj in submodels: - et_submodels.append(etree.fromstring(aas_xmlization.to_str(sub_obj))) - root.append(et_submodels) - if concept_descriptions: - et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") - for con_obj in concept_descriptions: - et_concept_descriptions.append(etree.fromstring(aas_xmlization.to_str(con_obj))) - root.append(et_concept_descriptions) - return root - - -def write_aas_xml_file(file: _generic.PathOrBinaryIO, - data: ObjectStore, - **kwargs) -> None: - """ - Write a set of AAS objects to an Asset Administration Shell XML file according to 'Details of the Asset - Administration Shell', chapter 5.4 - - :param file: A filename or file-like object to write the XML-serialized data to - :param data: :class:`ObjectStore ` which contains different objects of - the AAS meta model which should be serialized to an XML file - :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree._ElementTree.write` - """ - return _write_element(file, object_store_to_xml_element(data), **kwargs) From f14d0d284934e8166f961c85d69c5fb690780346 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Tue, 5 Nov 2024 17:00:19 +0100 Subject: [PATCH 462/474] sdk/basyx/adapter/_generic.py: remove unused file --- sdk/basyx/adapter/_generic.py | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 sdk/basyx/adapter/_generic.py diff --git a/sdk/basyx/adapter/_generic.py b/sdk/basyx/adapter/_generic.py deleted file mode 100644 index d2b7801..0000000 --- a/sdk/basyx/adapter/_generic.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2023 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 -""" -The dicts defined in this module are used in the json and xml modules to translate enum members of our -implementation to the respective string and vice versa. -""" -import os -from typing import BinaryIO, Dict, IO, Type, Union - - -# type aliases for path-like objects and IO -# used by write_aas_xml_file, read_aas_xml_file, write_aas_json_file, read_aas_json_file -Path = Union[str, bytes, os.PathLike] -PathOrBinaryIO = Union[Path, BinaryIO] -PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO - -# XML Namespace definition -XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} -XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" From a63c0850bbc9712c7f74718fb3b7fb5aea663198 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Tue, 5 Nov 2024 17:07:24 +0100 Subject: [PATCH 463/474] move sdk/basyx/aasx.py and fixed various imports --- sdk/basyx/{adapter => }/aasx.py | 0 sdk/basyx/adapter/__init__.py | 9 --------- sdk/test/adapter/aasx/example_aas.py | 2 +- sdk/test/adapter/aasx/test_aasx.py | 2 +- 4 files changed, 2 insertions(+), 11 deletions(-) rename sdk/basyx/{adapter => }/aasx.py (100%) delete mode 100644 sdk/basyx/adapter/__init__.py diff --git a/sdk/basyx/adapter/aasx.py b/sdk/basyx/aasx.py similarity index 100% rename from sdk/basyx/adapter/aasx.py rename to sdk/basyx/aasx.py diff --git a/sdk/basyx/adapter/__init__.py b/sdk/basyx/adapter/__init__.py deleted file mode 100644 index 7f96702..0000000 --- a/sdk/basyx/adapter/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -This package contains different kinds of adapters. - -* :ref:`json `: This package offers an adapter for serialization and deserialization of BaSyx - Python SDK objects to/from JSON. -* :ref:`xml `: This package offers an adapter for serialization and deserialization of BaSyx - Python SDK objects to/from XML. -* :ref:`aasx `: This package offers functions for reading and writing AASX-files. -""" diff --git a/sdk/test/adapter/aasx/example_aas.py b/sdk/test/adapter/aasx/example_aas.py index 26ee28c..b6c4e49 100644 --- a/sdk/test/adapter/aasx/example_aas.py +++ b/sdk/test/adapter/aasx/example_aas.py @@ -1,7 +1,7 @@ from aas_core3 import types as model from basyx.object_store import ObjectStore import aas_core3.types as aas_types -from basyx.adapter.aasx import DictSupplementaryFileContainer +from basyx.aasx import DictSupplementaryFileContainer from pathlib import Path diff --git a/sdk/test/adapter/aasx/test_aasx.py b/sdk/test/adapter/aasx/test_aasx.py index 1821754..abdd244 100644 --- a/sdk/test/adapter/aasx/test_aasx.py +++ b/sdk/test/adapter/aasx/test_aasx.py @@ -15,7 +15,7 @@ import pyecma376_2 from aas_core3 import types as model -from basyx.adapter import aasx +from basyx import aasx from . import example_aas from basyx.object_store import ObjectStore From 2b74f3fed6881164f223fa3937438bd6c576857b Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Tue, 15 Oct 2024 12:35:36 +0200 Subject: [PATCH 464/474] sdk/basyx/tutorial/tutorial_create_simple_aas.py: Add verification step --- .../tutorial/tutorial_create_simple_aas.py | 181 ++++++++++++------ 1 file changed, 124 insertions(+), 57 deletions(-) diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_create_simple_aas.py index 11f5fd8..8000ad3 100644 --- a/sdk/basyx/tutorial/tutorial_create_simple_aas.py +++ b/sdk/basyx/tutorial/tutorial_create_simple_aas.py @@ -1,78 +1,145 @@ -import json +#!/usr/bin/env python3 +# This work is licensed under a Creative Commons CCZero 1.0 Universal License. +# See http://creativecommons.org/publicdomain/zero/1.0/ for more information. +""" +Tutorial for the creation of a simple Asset Administration Shell, containing an AssetInformation object and a Submodel +reference using aas-core3.0-python +""" + +# Import all type classes from the aas-core3.0-python SDK import aas_core3.types as aas_types -import aas_core3.jsonization as aas_jsonization -from basyx.object_store import ObjectStore -from basyx.adapter.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer -import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class. -import datetime -from pathlib import Path # Used for easier handling of auxiliary file's local path - -Referencetype = aas_types.ReferenceTypes("ModelReference") +from aas_core3 import verification + +# In this tutorial, you'll get a step-by-step guide on how to create an Asset Administration Shell (AAS) and all +# required objects within. First, you need an AssetInformation object for which you want to create an AAS. After that, +# an Asset Administration Shell can be created. Then, it's possible to add Submodels to the AAS. The Submodels can +# contain SubmodelElements. + +# Step-by-Step Guide: +# Step 1: create a simple Asset Administration Shell, containing AssetInformation object +# Step 2: create a simple Submodel +# Step 3: create a simple Property and add it to the Submodel + + +############################################################################################ +# Step 1: Create a Simple Asset Administration Shell Containing an AssetInformation object # +############################################################################################ +# Step 1.1: create the AssetInformation object +asset_information = aas_types.AssetInformation( + asset_kind=aas_types.AssetKind.INSTANCE, + global_asset_id='http://acplt.org/Simple_Asset' +) -key_types = aas_types.KeyTypes("Submodel") +# Step 1.2: create the Asset Administration Shell +identifier = 'https://acplt.org/Simple_AAS' +aas = aas_types.AssetAdministrationShell( + id=identifier, # set identifier + asset_information=asset_information, + submodels=[] +) -key = aas_types.Key(value="some-unique-global-identifier", type=key_types) -reference = aas_types.Reference(type=Referencetype, keys=[key]) +############################################################# +# Step 2: Create a Simple Submodel Without SubmodelElements # +############################################################# +# Step 2.1: create the Submodel object +identifier = 'https://acplt.org/Simple_Submodel' submodel = aas_types.Submodel( - id="some-unique-global-identifier", - submodel_elements=[ - aas_types.Property( - id_short="some_property", - value_type=aas_types.DataTypeDefXSD.INT, - value="1984", - semantic_id=reference - ) - ] + id=identifier, + submodel_elements=[] ) -file_store = DictSupplementaryFileContainer() +# Step 2.2: create a reference to that Submodel and add it to the Asset Administration Shell's `submodel` set +submodel_reference = aas_types.Reference( + type=aas_types.ReferenceTypes.MODEL_REFERENCE, + keys=[aas_types.Key( + type=aas_types.KeyTypes.SUBMODEL, + value=identifier + )] +) -with open(Path(__file__).parent / 'data' / 'TestFile.pdf', 'rb') as f: - actual_file_name = file_store.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") +# Warning, this overwrites whatever is in the `aas.submodels` list. +# In your production code, it might make sense to check for already existing content. +aas.submodels = [submodel_reference] -if submodel.submodel_elements is not None: - submodel.submodel_elements.append(aas_types.File(id_short="documentationFile", - content_type="application/pdf", - value=actual_file_name)) -aas = aas_types.AssetAdministrationShell(id="urn:x-test:aas1", - asset_information=aas_types.AssetInformation( - asset_kind=aas_types.AssetKind.TYPE), - submodels=[reference]) +# =============================================================== +# ALTERNATIVE: step 1 and 2 can alternatively be done in one step +# In this version, the Submodel reference is passed to the Asset Administration Shell's constructor. +submodel = aas_types.Submodel( + id='https://acplt.org/Simple_Submodel', + submodel_elements=[] +) +aas = aas_types.AssetAdministrationShell( + id='https://acplt.org/Simple_AAS', + asset_information=asset_information, + submodels=[aas_types.Reference( + type=aas_types.ReferenceTypes.MODEL_REFERENCE, + keys=[aas_types.Key( + type=aas_types.KeyTypes.SUBMODEL, + value='https://acplt.org/Simple_Submodel' + )] + )] +) -obj_store: ObjectStore = ObjectStore() -obj_store.add(aas) -obj_store.add(submodel) +############################################################### +# Step 3: Create a Simple Property and Add it to the Submodel # +############################################################### -# Serialize to a JSON-able mapping -jsonable = aas_jsonization.to_jsonable(submodel) +# Step 3.1: create a global reference to a semantic description of the Property +# A global reference consists of one key which points to the address where the semantic description is stored +semantic_reference = aas_types.Reference( + type=aas_types.ReferenceTypes.MODEL_REFERENCE, + keys=[aas_types.Key( + type=aas_types.KeyTypes.GLOBAL_REFERENCE, + value='http://acplt.org/Properties/SimpleProperty' + )] +) +# Step 3.2: create the simple Property +property_ = aas_types.Property( + id_short='ExampleProperty', # Identifying string of the element within the Submodel namespace + value_type=aas_types.DataTypeDefXSD.STRING, # Data type of the value + value='exampleValue', # Value of the Property + semantic_id=semantic_reference # set the semantic reference +) -meta_data = pyecma376_2.OPCCoreProperties() -meta_data.creator = "Chair of Process Control Engineering" -meta_data.created = datetime.datetime.now() +# Step 3.3: add the Property to the Submodel -with AASXWriter("./MyAASXPackage.aasx") as writer: - writer.write_aas(aas_ids=["urn:x-test:aas1"], - object_store=obj_store, - file_store=file_store, - write_json=False) - writer.write_core_properties(meta_data) +# Warning, this overwrites whatever is in the `submodel_elements` list. +# In your production code, it might make sense to check for already existing content. +submodel.submodel_elements = [property_] -new_object_store: ObjectStore = ObjectStore() -new_file_store = DictSupplementaryFileContainer() -with AASXReader("./MyAASXPackage.aasx") as reader: - # Read all contained AAS objects and all referenced auxiliary files - reader.read_into(object_store=new_object_store, - file_store=new_file_store) +# ===================================================================== +# ALTERNATIVE: step 2 and 3 can also be combined in a single statement: +# Again, we pass the Property to the Submodel's constructor instead of adding it afterward. +submodel = aas_types.Submodel( + id='https://acplt.org/Simple_Submodel', + submodel_elements=[ + aas_types.Property( + id_short='ExampleProperty', + value_type=aas_types.DataTypeDefXSD.STRING, + value='exampleValue', + semantic_id=aas_types.Reference( + type=aas_types.ReferenceTypes.MODEL_REFERENCE, + keys=[aas_types.Key( + type=aas_types.KeyTypes.GLOBAL_REFERENCE, + value='http://acplt.org/Properties/SimpleProperty' + )] + ) + ) + ] +) -print(new_object_store.__len__()) -for item in file_store.__iter__(): - print(item) +########################################################################## +# Step 4: Verify the Asset Administration Shell (AAS) and its components # +########################################################################## +# This step ensures that the AAS conforms to the rules and constraints defined by the AAS metamodel. The fields +# themselves do not underlie any restriction. -for item in new_file_store.__iter__(): - print(item) +# We can use aas_core3.verification.verify(). This method returns an Iterator that we can collect into a list and +# for demonstration reasons assert it to be empty. +assert len(list(verification.verify(aas))) == 0 From 0926132f04e2fdfcd688c10b8d0dcb873e26d539 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Wed, 23 Oct 2024 14:40:13 +0200 Subject: [PATCH 465/474] sdk/basyx/tutorial/tutorial_create_simple_aas.py: Add further explanation of return type --- sdk/basyx/tutorial/tutorial_create_simple_aas.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_create_simple_aas.py index 8000ad3..b0eef9f 100644 --- a/sdk/basyx/tutorial/tutorial_create_simple_aas.py +++ b/sdk/basyx/tutorial/tutorial_create_simple_aas.py @@ -141,5 +141,9 @@ # themselves do not underlie any restriction. # We can use aas_core3.verification.verify(). This method returns an Iterator that we can collect into a list and -# for demonstration reasons assert it to be empty. +# for demonstration reasons assert it to be empty. Each item in this iterator represents a violation of the AAS +# meta-model rules or constraints (e.g. Missing required fields, Invalid types of fields, ...). + +# For more information refer to the official aas-core documentation. + assert len(list(verification.verify(aas))) == 0 From a0154644e5733cb40ea3418991349e2549742050 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Mon, 11 Nov 2024 13:45:51 +0100 Subject: [PATCH 466/474] sdk/test: added unittest to improve coverage. Modified test folder to match project structure. Simplify some functions of aasx.py --- sdk/basyx/aasx.py | 184 +++------------- .../tutorial/tutorial_create_simple_aas.py | 206 +++++++----------- sdk/test/adapter/__init__.py | 0 sdk/test/adapter/aasx/__init__.py | 0 sdk/test/{adapter/aasx => }/example_aas.py | 2 +- sdk/test/{adapter/aasx => }/test_aasx.py | 31 ++- 6 files changed, 134 insertions(+), 289 deletions(-) delete mode 100644 sdk/test/adapter/__init__.py delete mode 100644 sdk/test/adapter/aasx/__init__.py rename sdk/test/{adapter/aasx => }/example_aas.py (94%) rename sdk/test/{adapter/aasx => }/test_aasx.py (81%) diff --git a/sdk/basyx/aasx.py b/sdk/basyx/aasx.py index e155875..e7b2380 100644 --- a/sdk/basyx/aasx.py +++ b/sdk/basyx/aasx.py @@ -64,6 +64,12 @@ # XML Namespace definition XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" +NS_AAS = XML_NS_AAS + + +REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]} +RE = TypeVar("RE", bound=model.RelationshipElement) +T = TypeVar('T') class AASXReader: @@ -137,7 +143,7 @@ def get_thumbnail(self) -> Optional[bytes]: def read_into(self, object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", - override_existing: bool = False, **kwargs) -> Set[id_type]: + replace_existing: bool = False, **kwargs) -> Set[id_type]: """ Read the contents of the AASX package and add them into a given :class:`ObjectStore ` @@ -154,7 +160,7 @@ def read_into(self, object_store: ObjectStore, objects from the AASX file to :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the embedded supplementary files to - :param override_existing: If ``True``, existing objects in the object store are overridden with objects from the + :param replace_existing: If ``True``, existing objects in the object store are overridden with objects from the AASX that have the same Identifier. Default behavior is to skip those objects from the AASX. :return: A set of the Identifiers of all @@ -168,16 +174,15 @@ def read_into(self, object_store: ObjectStore, raise ValueError("Not a valid AASX file: aasx-origin Relationship is missing.") from e read_identifiables: Set[id_type] = set() - # Iterate AAS files for aas_part in self.reader.get_related_parts_by_type(aasx_origin_part)[RELATIONSHIP_TYPE_AAS_SPEC]: self._read_aas_part_into(aas_part, object_store, file_store, - read_identifiables, override_existing, **kwargs) + read_identifiables, replace_existing, **kwargs) # Iterate split parts of AAS file for split_part in self.reader.get_related_parts_by_type(aas_part)[RELATIONSHIP_TYPE_AAS_SPEC_SPLIT]: self._read_aas_part_into(split_part, object_store, file_store, - read_identifiables, override_existing, **kwargs) + read_identifiables, replace_existing, **kwargs) return read_identifiables @@ -197,7 +202,7 @@ def _read_aas_part_into(self, part_name: str, object_store: ObjectStore, file_store: "AbstractSupplementaryFileContainer", read_identifiables: Set[id_type], - override_existing: bool, **kwargs) -> None: + replace_existing: bool, **kwargs) -> None: """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. @@ -210,7 +215,7 @@ def _read_aas_part_into(self, part_name: str, from a File object of this part :param read_identifiables: A set of Identifiers of objects which have already been read. New objects' Identifiers are added to this set. Objects with already known Identifiers are skipped silently. - :param override_existing: If True, existing objects in the object store are overridden with objects from the + :param replace_existing: If True, existing objects in the object store are overridden with objects from the AASX that have the same Identifer. Default behavior is to skip those objects from the AASX. """ @@ -218,12 +223,14 @@ def _read_aas_part_into(self, part_name: str, if obj.id in read_identifiables: continue if obj.id in object_store: - if override_existing: - logger.info("Overriding existing object in ObjectStore with {} ...".format(obj)) - object_store.discard(obj) + if replace_existing: + logger.info("Overriding existing object in ObjectStore with {} ...".format(obj.id)) + existing_object = object_store.get(obj.id) + object_store.discard(existing_object) + else: logger.warning("Skipping {}, since an object with the same id is already contained in the " - "ObjectStore".format(obj)) + "ObjectStore".format(obj.id)) continue object_store.add(obj) read_identifiables.add(obj.id) @@ -890,9 +897,6 @@ def __iter__(self) -> Iterator[str]: return iter(self._name_map) -T = TypeVar('T') - - def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: """ Helper function for getting an item from a (str→object) dict in a typesafe way. @@ -912,17 +916,12 @@ def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: return val -def read_aas_json_file_into(object_store: ObjectStore, file: PathOrIO, replace_existing: bool = False, - ignore_existing: bool = False) -> Set[str]: +def read_aas_json_file(file: PathOrIO) -> ObjectStore[model.Identifiable]: """ Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 into a given object store. - :param object_store: The :class:`ObjectStore ` in which the - identifiable objects should be stored :param file: A filename or file-like object to read the JSON-serialized data from - :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not - :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. This parameter is ignored if replace_existing is ``True``. :raises KeyError: Encountered a duplicate identifier :raises KeyError: Encountered an identifier that already exists in the given ``object_store`` with both @@ -931,8 +930,9 @@ def read_aas_json_file_into(object_store: ObjectStore, file: PathOrIO, replace_e (e.g. an AssetAdministrationShell in ``submodels``) :return: A set of :class:`Identifiers ` that were added to object_store """ - ret: Set[str] = set() + object_store: ObjectStore[model.Identifiable] = ObjectStore() + ret: Set[str] = set() # json.load() accepts TextIO and BinaryIO cm: ContextManager[IO] if isinstance(file, get_args(Path)): @@ -960,34 +960,11 @@ def read_aas_json_file_into(object_store: ObjectStore, file: PathOrIO, replace_e if identifiable.id in ret: error_message = f"{item} has a duplicate identifier already parsed in the document!" raise KeyError(error_message) - existing_element = object_store.get(identifiable.id) - if existing_element is not None: - if not replace_existing: - error_message = f"object with identifier {identifiable.id} already exists " \ - f"in the object store: {existing_element}!" - if not ignore_existing: - raise KeyError(error_message + f" failed to insert {identifiable}!") - object_store.discard(existing_element) + object_store.add(identifiable) ret.add(identifiable.id) - return ret - - -def read_aas_json_file(file, **kwargs) -> ObjectStore[model.Identifiable]: - """ - A wrapper of :meth:`~basyx.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects - in an empty :class:`~basyx.model.provider.DictObjectStore`. This function supports the same keyword arguments as - :meth:`~basyx.adapter.json.json_deserialization.read_aas_json_file_into`. - - :param file: A filename or file-like object to read the JSON-serialized data from - :param kwargs: Keyword arguments passed to :meth:`read_aas_json_file_into` - :raises KeyError: Encountered a duplicate identifier - :return: A :class:`~basyx.ObjectStore` containing all AAS objects from the JSON file - """ - obj_store: ObjectStore[model.Identifiable] = ObjectStore() - read_aas_json_file_into(obj_store, file, **kwargs) - return obj_store + return object_store def _create_dict(data: ObjectStore) -> dict: @@ -1051,9 +1028,6 @@ def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: json.dump(_create_dict(data), fp, **kwargs) -NS_AAS = XML_NS_AAS - - def _write_element(file: PathOrBinaryIO, element: etree._Element, **kwargs) -> None: etree.ElementTree(element).write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) @@ -1115,83 +1089,12 @@ def write_aas_xml_file(file: PathOrBinaryIO, return _write_element(file, object_store_to_xml_element(data), **kwargs) -REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]} - - -RE = TypeVar("RE", bound=model.RelationshipElement) - - -def _element_pretty_identifier(element: etree._Element) -> str: - """ - Returns a pretty element identifier for a given XML element. - - If the prefix is known, the namespace in the element tag is replaced by the prefix. - If additionally also the sourceline is known, it is added as a suffix to name. - For example, instead of "{https://admin-shell.io/aas/3/0}assetAdministrationShell" this function would return - "aas:assetAdministrationShell on line $line", if both, prefix and sourceline, are known. - - :param element: The xml element. - :return: The pretty element identifier. - """ - identifier = element.tag - if element.prefix is not None: - # Only replace the namespace by the prefix if it matches our known namespaces, - # so the replacement by the prefix doesn't mask errors such as incorrect namespaces. - namespace, tag = element.tag.split("}", 1) - if namespace[1:] in XML_NS_MAP.values(): - identifier = element.prefix + ":" + tag - if element.sourceline is not None: - identifier += f" on line {element.sourceline}" - return identifier - - -def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree._Element]: - """ - Parse an XML document into an element tree - - :param file: A filename or file-like object to read the XML-serialized data from - :param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception if the document - is malformed, parsing is aborted, an error is logged and None is returned - :param parser_kwargs: Keyword arguments passed to the XMLParser constructor - :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML - :raises KeyError: If a required namespace has not been declared on the XML document - :return: The root element of the element tree - """ - - parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, **parser_kwargs) - - try: - root = etree.parse(file, parser).getroot() - except etree.XMLSyntaxError as e: - if failsafe: - logger.error(e) - return None - raise e - - missing_namespaces: Set[str] = REQUIRED_NAMESPACES - set(root.nsmap.values()) - if missing_namespaces: - error_message = f"The following required namespaces are not declared: {' | '.join(missing_namespaces)}" \ - + " - Is the input document of an older version?" - if not failsafe: - raise KeyError(error_message) - logger.error(error_message) - return root - - -def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, - replace_existing: bool = False, ignore_existing: bool = False, - **parser_kwargs: Any) -> Set[str]: +def read_aas_xml_file(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Identifiable]: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 into a given :class:`ObjectStore `. - :param object_store: The :class:`ObjectStore ` in which the - :class:`~basyx.aas.model.base.Identifiable` objects should be stored :param file: A filename or file-like object to read the XML-serialized data from - :param replace_existing: Whether to replace existing objects with the same identifier in the object store or not - :param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error. - This parameter is ignored if replace_existing is True. - :param parser_kwargs: Keyword arguments passed to the XMLParser constructor :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML :raises KeyError: If a required namespace has not been declared on the XML document :raises KeyError: Encountered a duplicate identifier @@ -1202,6 +1105,7 @@ def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, :raises TypeError: Encountered an undefined top-level list (e.g. ````) :return: A set of :class:`Identifiers ` that were added to object_store """ + object_store: ObjectStore = ObjectStore() ret: Set = set() element_constructors: Dict[str, Callable[..., model.Identifiable]] = { @@ -1216,13 +1120,13 @@ def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, root = etree.parse(file, parser).getroot() if root is None: - return ret + return object_store # Add AAS objects to ObjectStore for list_ in root: element_tag = list_.tag[:-1] if list_.tag[-1] != "s" or element_tag not in element_constructors: - error_message = f"Unexpected top-level list {_element_pretty_identifier(list_)}!" + error_message = f"Unexpected top-level list {list_.tag}!" logger.warning(error_message) continue @@ -1234,38 +1138,8 @@ def read_aas_xml_file_into(object_store: ObjectStore, file: PathOrIO, if identifiable.id in ret: error_message = f"{element} has a duplicate identifier already parsed in the document!" raise KeyError(error_message) - existing_element = object_store.get(identifiable.id) - if existing_element is not None: - if not replace_existing: - error_message = f"object with identifier {identifiable.id} already exists " \ - f"in the object store: {existing_element}!" - if not ignore_existing: - raise KeyError(error_message + f" failed to insert {identifiable}!") - logger.info(error_message + f" skipping insertion of {identifiable}...") - continue - object_store.discard(existing_element) + object_store.add(identifiable) ret.add(identifiable.id) - return ret - - -def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> ObjectStore[model.Identifiable]: - """ - A wrapper of :meth:`~basyx.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an - empty :class:`~basyx.ObjectStore`. This function supports - the same keyword arguments as :meth:`~basyx.adapter.xml.xml_deserialization.read_aas_xml_file_into`. - - :param file: A filename or file-like object to read the XML-serialized data from - :param kwargs: Keyword arguments passed to :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into` - :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML - :raises KeyError: If a required namespace has not been declared on the XML document - :raises KeyError: Encountered a duplicate identifier - :raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): Errors during - construction of the objects - :raises TypeError: Encountered an undefined top-level list (e.g. ````) - :return: A :class:`~basyx.ObjectStore` containing all AAS objects from the XML file - """ - obj_store: ObjectStore[model.Identifiable] = ObjectStore() - read_aas_xml_file_into(obj_store, file, **kwargs) - return obj_store + return object_store diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_create_simple_aas.py index b0eef9f..2becdba 100644 --- a/sdk/basyx/tutorial/tutorial_create_simple_aas.py +++ b/sdk/basyx/tutorial/tutorial_create_simple_aas.py @@ -1,149 +1,93 @@ -#!/usr/bin/env python3 -# This work is licensed under a Creative Commons CCZero 1.0 Universal License. -# See http://creativecommons.org/publicdomain/zero/1.0/ for more information. -""" -Tutorial for the creation of a simple Asset Administration Shell, containing an AssetInformation object and a Submodel -reference using aas-core3.0-python -""" - -# Import all type classes from the aas-core3.0-python SDK +import json import aas_core3.types as aas_types -from aas_core3 import verification - -# In this tutorial, you'll get a step-by-step guide on how to create an Asset Administration Shell (AAS) and all -# required objects within. First, you need an AssetInformation object for which you want to create an AAS. After that, -# an Asset Administration Shell can be created. Then, it's possible to add Submodels to the AAS. The Submodels can -# contain SubmodelElements. - -# Step-by-Step Guide: -# Step 1: create a simple Asset Administration Shell, containing AssetInformation object -# Step 2: create a simple Submodel -# Step 3: create a simple Property and add it to the Submodel - - -############################################################################################ -# Step 1: Create a Simple Asset Administration Shell Containing an AssetInformation object # -############################################################################################ -# Step 1.1: create the AssetInformation object -asset_information = aas_types.AssetInformation( - asset_kind=aas_types.AssetKind.INSTANCE, - global_asset_id='http://acplt.org/Simple_Asset' -) - -# Step 1.2: create the Asset Administration Shell -identifier = 'https://acplt.org/Simple_AAS' -aas = aas_types.AssetAdministrationShell( - id=identifier, # set identifier - asset_information=asset_information, - submodels=[] -) +import aas_core3.jsonization as aas_jsonization +from basyx.object_store import ObjectStore +from basyx.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer +import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class. +import datetimedocs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ env.X_PYTHON_VERSION }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env.X_PYTHON_VERSION }} + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/docs-requirements.txt + - name: Check documentation for errors + run: | + SPHINXOPTS="-a -E -n -W --keep-going" make -C docs html +from pathlib import Path # Used for easier handling of auxiliary file's local path + +Referencetype = aas_types.ReferenceTypes("ModelReference") + +key_types = aas_types.KeyTypes("Submodel") + +key = aas_types.Key(value="some-unique-global-identifier", type=key_types) + +reference = aas_types.Reference(type=Referencetype, keys=[key]) - -############################################################# -# Step 2: Create a Simple Submodel Without SubmodelElements # -############################################################# - -# Step 2.1: create the Submodel object -identifier = 'https://acplt.org/Simple_Submodel' submodel = aas_types.Submodel( - id=identifier, - submodel_elements=[] + id="some-unique-global-identifier", + submodel_elements=[ + aas_types.Property( + id_short="some_property", + value_type=aas_types.DataTypeDefXSD.INT, + value="1984", + semantic_id=reference + ) + ] ) -# Step 2.2: create a reference to that Submodel and add it to the Asset Administration Shell's `submodel` set -submodel_reference = aas_types.Reference( - type=aas_types.ReferenceTypes.MODEL_REFERENCE, - keys=[aas_types.Key( - type=aas_types.KeyTypes.SUBMODEL, - value=identifier - )] -) +file_store = DictSupplementaryFileContainer() -# Warning, this overwrites whatever is in the `aas.submodels` list. -# In your production code, it might make sense to check for already existing content. -aas.submodels = [submodel_reference] +with open(Path(__file__).parent / 'data' / 'TestFile.pdf', 'rb') as f: + actual_file_name = file_store.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") +if submodel.submodel_elements is not None: + submodel.submodel_elements.append(aas_types.File(id_short="documentationFile", + content_type="application/pdf", + value=actual_file_name)) -# =============================================================== -# ALTERNATIVE: step 1 and 2 can alternatively be done in one step -# In this version, the Submodel reference is passed to the Asset Administration Shell's constructor. -submodel = aas_types.Submodel( - id='https://acplt.org/Simple_Submodel', - submodel_elements=[] -) -aas = aas_types.AssetAdministrationShell( - id='https://acplt.org/Simple_AAS', - asset_information=asset_information, - submodels=[aas_types.Reference( - type=aas_types.ReferenceTypes.MODEL_REFERENCE, - keys=[aas_types.Key( - type=aas_types.KeyTypes.SUBMODEL, - value='https://acplt.org/Simple_Submodel' - )] - )] -) - - -############################################################### -# Step 3: Create a Simple Property and Add it to the Submodel # -############################################################### +aas = aas_types.AssetAdministrationShell(id="urn:x-test:aas1", + asset_information=aas_types.AssetInformation( + asset_kind=aas_types.AssetKind.TYPE), + submodels=[reference]) -# Step 3.1: create a global reference to a semantic description of the Property -# A global reference consists of one key which points to the address where the semantic description is stored -semantic_reference = aas_types.Reference( - type=aas_types.ReferenceTypes.MODEL_REFERENCE, - keys=[aas_types.Key( - type=aas_types.KeyTypes.GLOBAL_REFERENCE, - value='http://acplt.org/Properties/SimpleProperty' - )] -) +obj_store: ObjectStore = ObjectStore() +obj_store.add(aas) +obj_store.add(submodel) -# Step 3.2: create the simple Property -property_ = aas_types.Property( - id_short='ExampleProperty', # Identifying string of the element within the Submodel namespace - value_type=aas_types.DataTypeDefXSD.STRING, # Data type of the value - value='exampleValue', # Value of the Property - semantic_id=semantic_reference # set the semantic reference -) -# Step 3.3: add the Property to the Submodel +# Serialize to a JSON-able mapping +jsonable = aas_jsonization.to_jsonable(submodel) -# Warning, this overwrites whatever is in the `submodel_elements` list. -# In your production code, it might make sense to check for already existing content. -submodel.submodel_elements = [property_] +meta_data = pyecma376_2.OPCCoreProperties() +meta_data.creator = "Chair of Process Control Engineering" +meta_data.created = datetime.datetime.now() -# ===================================================================== -# ALTERNATIVE: step 2 and 3 can also be combined in a single statement: -# Again, we pass the Property to the Submodel's constructor instead of adding it afterward. -submodel = aas_types.Submodel( - id='https://acplt.org/Simple_Submodel', - submodel_elements=[ - aas_types.Property( - id_short='ExampleProperty', - value_type=aas_types.DataTypeDefXSD.STRING, - value='exampleValue', - semantic_id=aas_types.Reference( - type=aas_types.ReferenceTypes.MODEL_REFERENCE, - keys=[aas_types.Key( - type=aas_types.KeyTypes.GLOBAL_REFERENCE, - value='http://acplt.org/Properties/SimpleProperty' - )] - ) - ) - ] -) +with AASXWriter("./MyAASXPackage.aasx") as writer: + writer.write_aas(aas_ids=["urn:x-test:aas1"], + object_store=obj_store, + file_store=file_store, + write_json=False) + writer.write_core_properties(meta_data) -########################################################################## -# Step 4: Verify the Asset Administration Shell (AAS) and its components # -########################################################################## -# This step ensures that the AAS conforms to the rules and constraints defined by the AAS metamodel. The fields -# themselves do not underlie any restriction. +new_object_store: ObjectStore = ObjectStore() +new_file_store = DictSupplementaryFileContainer() -# We can use aas_core3.verification.verify(). This method returns an Iterator that we can collect into a list and -# for demonstration reasons assert it to be empty. Each item in this iterator represents a violation of the AAS -# meta-model rules or constraints (e.g. Missing required fields, Invalid types of fields, ...). +with AASXReader("./MyAASXPackage.aasx") as reader: + # Read all contained AAS objects and all referenced auxiliary files + reader.read_into(object_store=new_object_store, + file_store=new_file_store) -# For more information refer to the official aas-core documentation. +print(new_object_store.__len__()) +for item in file_store.__iter__(): + print(item) -assert len(list(verification.verify(aas))) == 0 +for item in new_file_store.__iter__(): + print(item) diff --git a/sdk/test/adapter/__init__.py b/sdk/test/adapter/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/sdk/test/adapter/aasx/__init__.py b/sdk/test/adapter/aasx/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/sdk/test/adapter/aasx/example_aas.py b/sdk/test/example_aas.py similarity index 94% rename from sdk/test/adapter/aasx/example_aas.py rename to sdk/test/example_aas.py index b6c4e49..08cb1c1 100644 --- a/sdk/test/adapter/aasx/example_aas.py +++ b/sdk/test/example_aas.py @@ -34,7 +34,7 @@ def create_full_example() -> ObjectStore: ) file_store = DictSupplementaryFileContainer() - with open(Path(__file__).parent.parent.parent.parent / 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: + with open(Path(__file__).parent.parent / 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: actual_file_name = file_store.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") if submodel.submodel_elements is not None: diff --git a/sdk/test/adapter/aasx/test_aasx.py b/sdk/test/test_aasx.py similarity index 81% rename from sdk/test/adapter/aasx/test_aasx.py rename to sdk/test/test_aasx.py index abdd244..378111e 100644 --- a/sdk/test/adapter/aasx/test_aasx.py +++ b/sdk/test/test_aasx.py @@ -16,9 +16,12 @@ import pyecma376_2 from aas_core3 import types as model from basyx import aasx + from . import example_aas from basyx.object_store import ObjectStore +from .example_aas import create_full_example + class TestAASXUtils(unittest.TestCase): def test_name_friendlyfier(self) -> None: @@ -30,7 +33,7 @@ def test_name_friendlyfier(self) -> None: def test_supplementary_file_container(self) -> None: container = aasx.DictSupplementaryFileContainer() - with open(Path(__file__).parent.parent.parent.parent / 'basyx' / 'tutorial' / + with open(Path(__file__).parent.parent / 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: new_name = container.add_file("/TestFile.pdf", f, "application/pdf") # Name should not be modified, since there is no conflict @@ -80,7 +83,7 @@ def test_writing_reading_example_aas(self) -> None: # Create example data and file_store data = example_aas.create_full_example() files = aasx.DictSupplementaryFileContainer() - with open(Path(__file__).parent.parent.parent.parent / 'basyx' / + with open(Path(__file__).parent.parent / 'basyx' / 'tutorial' / 'data' / 'TestFile.pdf', 'rb') as f: files.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") f.seek(0) @@ -132,4 +135,28 @@ def test_writing_reading_example_aas(self) -> None: self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), "78450a66f59d74c073bf6858db340090ea72a8b1") + # Override read objects + with aasx.AASXReader(filename) as reader: + reader.read_into(new_data, new_files, replace_existing=True) + new_cp = reader.get_core_properties() + + # Reload objects while expected to skipp all + with self.assertLogs(level='INFO') as log: + with aasx.AASXReader(filename) as reader: + reader.read_into(new_data, new_files) + new_cp = reader.get_core_properties() + assert isinstance(log.output, list) # This should be True due to the record=True parameter + self.assertEqual(len(log.output), 2) + os.unlink(filename) + + # Test AASXReader exceptions + new_data_2: ObjectStore[model.Identifiable] = ObjectStore() + new_files = aasx.DictSupplementaryFileContainer() + with self.assertRaises(FileNotFoundError): + with aasx.AASXReader("/Non_existing_dir") as reader: + reader.read_into(new_data_2, new_files) + with self.assertRaises(Exception): + with open(Path(__file__).parent / "./__init__.py") as f: + with aasx.AASXReader(f) as reader: + pass From 5f101ea49d7cb56490e29368dd8f12c4cbfd6917 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 11 Nov 2024 14:15:27 +0100 Subject: [PATCH 467/474] Update tutorial_create_simple_aas.py --- .../tutorial/tutorial_create_simple_aas.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_create_simple_aas.py index 2becdba..8980e06 100644 --- a/sdk/basyx/tutorial/tutorial_create_simple_aas.py +++ b/sdk/basyx/tutorial/tutorial_create_simple_aas.py @@ -4,22 +4,7 @@ from basyx.object_store import ObjectStore from basyx.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class. -import datetimedocs: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ env.X_PYTHON_VERSION }} - uses: actions/setup-python@v2 - with: - python-version: ${{ env.X_PYTHON_VERSION }} - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install -r docs/docs-requirements.txt - - name: Check documentation for errors - run: | - SPHINXOPTS="-a -E -n -W --keep-going" make -C docs html +import datetime from pathlib import Path # Used for easier handling of auxiliary file's local path Referencetype = aas_types.ReferenceTypes("ModelReference") From 82139cd64f5c44b594de272566aee3eca9eec5fe Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Tue, 19 Nov 2024 14:23:45 +0100 Subject: [PATCH 468/474] sdk/: Remove unused functions and simplify the code. Resolve PR threads. --- sdk/basyx/aasx.py | 296 +++++++++++------------------------------- sdk/test/test_aasx.py | 7 - 2 files changed, 73 insertions(+), 230 deletions(-) diff --git a/sdk/basyx/aasx.py b/sdk/basyx/aasx.py index e7b2380..a1fdb41 100644 --- a/sdk/basyx/aasx.py +++ b/sdk/basyx/aasx.py @@ -7,9 +7,8 @@ """ .. _adapter.aasx: -Functionality for reading and writing AASX files according to "Details of the Asset Administration Shell Part 1 V2.0", -section 7. - +Functionality for reading and writing AASX files according to "Specification of the Asset Administration Shell Part 5: +Package File Format (AASX) v3.0". The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the ``pyecma376_2`` library for low level OPC reading and writing. It currently supports all required features except for embedded digital signatures. @@ -51,9 +50,6 @@ RELATIONSHIP_TYPE_AAS_SPEC_SPLIT = "http://admin-shell.io/aasx/relationships/aas-spec-split" RELATIONSHIP_TYPE_AAS_SUPL = "http://admin-shell.io/aasx/relationships/aas-suppl" -# id_type = model.Identifiable.__annotations__["id"] using this we can refer to the type_hint of "id" of the class -# Identifiable. Doing this leads to problems with mypy... -id_type = str # type aliases for path-like objects and IO # used by write_aas_xml_file, read_aas_xml_file, write_aas_json_file, read_aas_json_file @@ -66,10 +62,11 @@ XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}" NS_AAS = XML_NS_AAS - +# type aliases REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]} RE = TypeVar("RE", bound=model.RelationshipElement) T = TypeVar('T') +id_type = str class AASXReader: @@ -447,64 +444,9 @@ def write_aas(self, self.write_all_aas_objects("/aasx/data.{}".format("json" if write_json else "xml"), objects_to_be_written, file_store, write_json) - # TODO remove `method` parameter in future version. - # Not actually required since you can always create a local dict - def write_aas_objects(self, - part_name: str, - object_ids: Iterable[id_type], - object_store: ObjectStore, - file_store: "AbstractSupplementaryFileContainer", - write_json: bool = False, - split_part: bool = False, - additional_relationships: Iterable[pyecma376_2.OPCRelationship] = ()) -> None: - """ - A thin wrapper around :meth:`write_all_aas_objects` to ensure downwards compatibility - - This method takes the AAS's :class:`~aas_core3.types.Identifiable.id` (as ``aas_id``) to retrieve it - from the given object_store. If the list of written objects includes :class:`~aas_core3.types.Submodel` - objects, Supplementary files which are referenced by :class:`~aas_core3.types.File` objects within - those submodels, are also added to the AASX package. - - .. attention:: - - You must make sure to call this method or :meth:`write_all_aas_objects` only once per unique ``part_name`` - on a single package instance. - - :param part_name: Name of the Part within the AASX package to write the files to. Must be a valid ECMA376-2 - part name and unique within the package. The extension of the part should match the data format (i.e. - '.json' if ``write_json`` else '.xml'). - :param object_ids: A list of :class:`Identifiers ` of the objects to be written - to the AASX package. Only these :class:`~aas_core3.types.Identifiable.id` objects (and included - :class:`~aas_core3.types.Referable` objects) are written to the package. - :param object_store: The objects store to retrieve the :class:`~aas_core3.types.Identifiable` objects from - :param file_store: The - :class:`SupplementaryFileContainer ` - to retrieve supplementary files from (if there are any :class:`~aas_core3.types.File` - objects within the written objects. - :param write_json: If ``True``, the part is written as a JSON file instead of an XML file. Defaults to - ``False``. - :param split_part: If ``True``, no aas-spec relationship is added from the aasx-origin to this part. You must - make sure to reference it via a aas-spec-split relationship from another aas-spec part - :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object - part to be written, in addition to the aas-suppl relationships which are created automatically. - """ - logger.debug("Writing AASX part {} with AAS objects ...".format(part_name)) - - objects: ObjectStore[model.Identifiable] = ObjectStore() - - # Retrieve objects and scan for referenced supplementary files - for identifier in object_ids: - try: - the_object = object_store.get_identifiable(identifier) - except KeyError: - logger.error("Could not find object {} in ObjectStore".format(identifier)) - continue - objects.add(the_object) - - self.write_all_aas_objects(part_name, objects, file_store, write_json, split_part, additional_relationships) - # TODO remove `split_part` parameter in future version. # Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 + def write_all_aas_objects(self, part_name: str, objects: ObjectStore, @@ -689,56 +631,6 @@ def _write_package_relationships(self): self.writer.write_relationships(package_relationships) -# TODO remove in future version. -# Not required anymore since changes from DotAAS version 2.0.1 to 3.0RC01 -class NameFriendlyfier: - """ - A simple helper class to create unique "AAS friendly names" according to DotAAS, section 7.6. - - Objects of this class store the already created friendly names to avoid name collisions within one set of names. - """ - RE_NON_ALPHANUMERICAL = re.compile(r"[^a-zA-Z0-9]") - - def __init__(self) -> None: - self.issued_names: Set[str] = set() - - def get_friendly_name(self, identifier: str): - """ - Generate a friendly name from an AAS identifier. - - TODO: This information is outdated. The whole class is no longer needed. - - According to section 7.6 of "Details of the Asset Administration Shell", all non-alphanumerical characters are - replaced with underscores. We also replace all non-ASCII characters to generate valid URIs as the result. - If this replacement results in a collision with a previously generated friendly name of this NameFriendlifier, - a number is appended with underscore to the friendly name. - - Example: - - .. code-block:: python - - friendlyfier = NameFriendlyfier() - friendlyfier.get_friendly_name("http://example.com/AAS-a") - > "http___example_com_AAS_a" - - friendlyfier.get_friendly_name("http://example.com/AAS+a") - > "http___example_com_AAS_a_1" - - """ - # friendlify name - raw_name = self.RE_NON_ALPHANUMERICAL.sub('_', identifier) - - # Unify name (avoid collisions) - amended_name = raw_name - i = 1 - while amended_name in self.issued_names: - amended_name = "{}_{}".format(raw_name, i) - i += 1 - - self.issued_names.add(amended_name) - return amended_name - - class AbstractSupplementaryFileContainer(metaclass=abc.ABCMeta): """ Abstract interface for containers of supplementary files for AASs. @@ -897,32 +789,12 @@ def __iter__(self) -> Iterator[str]: return iter(self._name_map) -def _get_ts(dct: Dict[str, object], key: str, type_: Type[T]) -> T: - """ - Helper function for getting an item from a (str→object) dict in a typesafe way. - - The type of the object is checked at runtime and a TypeError is raised, if the object has not the expected type. - - :param dct: The dict - :param key: The key of the item to retrieve - :param type_: The expected type of the item - :return: The item - :raises TypeError: If the item has an unexpected type - :raises KeyError: If the key is not found in the dict (just as usual) - """ - val = dct[key] - if not isinstance(val, type_): - raise TypeError("Dict entry '{}' has unexpected type {}".format(key, type(val).__name__)) - return val - - def read_aas_json_file(file: PathOrIO) -> ObjectStore[model.Identifiable]: """ Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 into a given object store. :param file: A filename or file-like object to read the JSON-serialized data from - This parameter is ignored if replace_existing is ``True``. :raises KeyError: 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`` @@ -951,7 +823,7 @@ def read_aas_json_file(file: PathOrIO) -> ObjectStore[model.Identifiable]: ('submodels', model.Submodel), ('conceptDescriptions', model.ConceptDescription)): try: - lst = _get_ts(data, name, list) + lst: list = data[name] except (KeyError, TypeError): continue @@ -967,37 +839,6 @@ def read_aas_json_file(file: PathOrIO) -> ObjectStore[model.Identifiable]: return object_store -def _create_dict(data: ObjectStore) -> dict: - # separate different kind of objects - asset_administration_shells: List = [] - submodels: List = [] - concept_descriptions: List = [] - for obj in data: - if isinstance(obj, model.AssetAdministrationShell): - asset_administration_shells.append(aas_jsonization.to_jsonable(obj)) - elif isinstance(obj, model.Submodel): - submodels.append(aas_jsonization.to_jsonable(obj)) - elif isinstance(obj, model.ConceptDescription): - concept_descriptions.append(aas_jsonization.to_jsonable(obj)) - dict_: Dict[str, List] = {} - if asset_administration_shells: - dict_['assetAdministrationShells'] = asset_administration_shells - if submodels: - dict_['submodels'] = submodels - if concept_descriptions: - dict_['conceptDescriptions'] = concept_descriptions - return dict_ - - -class _DetachingTextIOWrapper(io.TextIOWrapper): - """ - Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer. - """ - - def __exit__(self, exc_type, exc_val, exc_tb): - self.detach() - - def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: """ Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset @@ -1022,77 +863,34 @@ def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: # we already got TextIO, nothing needs to be done # mypy seems to have issues narrowing the type due to get_args() cm = contextlib.nullcontext(file) # type: ignore[arg-type] - # serialize object to json# - with cm as fp: - json.dump(_create_dict(data), fp, **kwargs) - - -def _write_element(file: PathOrBinaryIO, element: etree._Element, **kwargs) -> None: - etree.ElementTree(element).write(file, encoding="UTF-8", xml_declaration=True, method="xml", **kwargs) - - -def object_store_to_xml_element(data: ObjectStore) -> etree._Element: - """ - Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree._Element`. - This function is used internally by :meth:`write_aas_xml_file` and shouldn't be - called directly for most use-cases. - - :param data: :class:`ObjectStore ` which contains different objects of - the AAS meta model which should be serialized to an XML file - """ - # separate different kind of objects - asset_administration_shells = [] - submodels = [] - concept_descriptions = [] + # serialize object to json# + asset_administration_shells: List = [] + submodels: List = [] + concept_descriptions: List = [] for obj in data: if isinstance(obj, model.AssetAdministrationShell): - asset_administration_shells.append(obj) + asset_administration_shells.append(aas_jsonization.to_jsonable(obj)) elif isinstance(obj, model.Submodel): - submodels.append(obj) + submodels.append(aas_jsonization.to_jsonable(obj)) elif isinstance(obj, model.ConceptDescription): - concept_descriptions.append(obj) - - # serialize objects to XML - root = etree.Element(NS_AAS + "environment", nsmap=XML_NS_MAP) + concept_descriptions.append(aas_jsonization.to_jsonable(obj)) + dict_: Dict[str, List] = {} if asset_administration_shells: - et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") - for aas_obj in asset_administration_shells: - et_asset_administration_shells.append( - etree.fromstring(aas_xmlization.to_str(aas_obj))) - root.append(et_asset_administration_shells) + dict_['assetAdministrationShells'] = asset_administration_shells if submodels: - et_submodels = etree.Element(NS_AAS + "submodels") - for sub_obj in submodels: - et_submodels.append(etree.fromstring(aas_xmlization.to_str(sub_obj))) - root.append(et_submodels) + dict_['submodels'] = submodels if concept_descriptions: - et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") - for con_obj in concept_descriptions: - et_concept_descriptions.append(etree.fromstring(aas_xmlization.to_str(con_obj))) - root.append(et_concept_descriptions) - return root - - -def write_aas_xml_file(file: PathOrBinaryIO, - data: ObjectStore, - **kwargs) -> None: - """ - Write a set of AAS objects to an Asset Administration Shell XML file according to 'Details of the Asset - Administration Shell', chapter 5.4 + dict_['conceptDescriptions'] = concept_descriptions - :param file: A filename or file-like object to write the XML-serialized data to - :param data: :class:`ObjectStore ` which contains different objects of - the AAS meta model which should be serialized to an XML file - :param kwargs: Additional keyword arguments to be passed to :meth:`~lxml.etree._ElementTree.write` - """ - return _write_element(file, object_store_to_xml_element(data), **kwargs) + with cm as fp: + json.dump(dict_, fp, **kwargs) def read_aas_xml_file(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Identifiable]: """ - Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 - into a given :class:`ObjectStore `. + Able to parse the official schema files into a given + :class:`ObjectStore `. :param file: A filename or file-like object to read the XML-serialized data from :raises ~lxml.etree.XMLSyntaxError: If the given file(-handle) has invalid XML @@ -1143,3 +941,55 @@ def read_aas_xml_file(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Iden ret.add(identifiable.id) return object_store + + +def write_aas_xml_file(file: PathOrIO, data: ObjectStore) -> None: + """ + Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree._Element`. + This function is used internally by :meth:`write_aas_xml_file` and shouldn't be + called directly for most use-cases. + + :param file: A filename or file-like object to read the JSON-serialized data from + :param data: :class:`ObjectStore ` which contains different objects of + the AAS meta model which should be serialized to an XML file + """ + # separate different kind of objects + asset_administration_shells = [] + submodels = [] + concept_descriptions = [] + for obj in data: + if isinstance(obj, model.AssetAdministrationShell): + asset_administration_shells.append(obj) + elif isinstance(obj, model.Submodel): + submodels.append(obj) + elif isinstance(obj, model.ConceptDescription): + concept_descriptions.append(obj) + + # serialize objects to XML + root = etree.Element(NS_AAS + "environment", nsmap=XML_NS_MAP) + if asset_administration_shells: + et_asset_administration_shells = etree.Element(NS_AAS + "assetAdministrationShells") + for aas_obj in asset_administration_shells: + et_asset_administration_shells.append( + etree.fromstring(aas_xmlization.to_str(aas_obj))) + root.append(et_asset_administration_shells) + if submodels: + et_submodels = etree.Element(NS_AAS + "submodels") + for sub_obj in submodels: + et_submodels.append(etree.fromstring(aas_xmlization.to_str(sub_obj))) + root.append(et_submodels) + if concept_descriptions: + et_concept_descriptions = etree.Element(NS_AAS + "conceptDescriptions") + for con_obj in concept_descriptions: + et_concept_descriptions.append(etree.fromstring(aas_xmlization.to_str(con_obj))) + root.append(et_concept_descriptions) + etree.ElementTree(root).write(file, encoding="UTF-8", xml_declaration=True, method="xml") + + +class _DetachingTextIOWrapper(io.TextIOWrapper): + """ + Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer. + """ + + def __exit__(self, exc_type, exc_val, exc_tb): + self.detach() diff --git a/sdk/test/test_aasx.py b/sdk/test/test_aasx.py index 378111e..17cc238 100644 --- a/sdk/test/test_aasx.py +++ b/sdk/test/test_aasx.py @@ -24,13 +24,6 @@ class TestAASXUtils(unittest.TestCase): - def test_name_friendlyfier(self) -> None: - friendlyfier = aasx.NameFriendlyfier() - name1 = friendlyfier.get_friendly_name("http://example.com/AAS-a") - self.assertEqual("http___example_com_AAS_a", name1) - name2 = friendlyfier.get_friendly_name("http://example.com/AAS+a") - self.assertEqual("http___example_com_AAS_a_1", name2) - def test_supplementary_file_container(self) -> None: container = aasx.DictSupplementaryFileContainer() with open(Path(__file__).parent.parent / 'basyx' / 'tutorial' / From d618cb225fae0c0821124bef93d4dce2ba703eb3 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Fri, 29 Nov 2024 16:28:37 +0100 Subject: [PATCH 469/474] aasx.py: Rename a few functions and check the docstrings. tutorial_create_simple_aas.py: Implement tutorial comparable to the tutorial in the basyx python sdk --- sdk/basyx/aasx.py | 22 +-- sdk/basyx/object_store.py | 7 +- .../tutorial/tutorial_create_simple_aas.py | 165 +++++++++++++----- sdk/basyx/tutorial/tutorial_objectstore.py | 2 +- 4 files changed, 132 insertions(+), 64 deletions(-) diff --git a/sdk/basyx/aasx.py b/sdk/basyx/aasx.py index a1fdb41..aaa0453 100644 --- a/sdk/basyx/aasx.py +++ b/sdk/basyx/aasx.py @@ -52,7 +52,7 @@ # type aliases for path-like objects and IO -# used by write_aas_xml_file, read_aas_xml_file, write_aas_json_file, read_aas_json_file +# used by parse_obj_store_to_xml, parse_xml_to_obj_store, parse_obj_store_to_json, parse_json_to_obj_store Path = Union[str, bytes, os.PathLike] PathOrBinaryIO = Union[Path, BinaryIO] PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO @@ -203,8 +203,6 @@ def _read_aas_part_into(self, part_name: str, """ Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file. - This method primarily checks for duplicate objects. It uses ``_parse_aas_parse()`` to do the actual parsing and - ``_collect_supplementary_files()`` for supplementary file processing of non-duplicate objects. :param part_name: The OPC part name to read :param object_store: An ObjectStore to add the AAS objects from the AASX file to @@ -249,13 +247,13 @@ def _parse_aas_part(self, part_name: str, **kwargs) -> ObjectStore: if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml": logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_xml_file(p, **kwargs) + return parse_xml_to_obj_store(p, **kwargs) elif content_type.split(";")[0] in ("text/json", "application/json") \ or content_type == "" and extension == "json": logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name)) with self.reader.open_part(part_name) as p: - return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs) + return parse_json_to_obj_store(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs) else: logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}" .format(part_name, content_type, extension)) @@ -504,9 +502,9 @@ def write_all_aas_objects(self, # TODO allow writing xml *and* JSON part with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p: if write_json: - write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects) + parse_obj_store_to_json(io.TextIOWrapper(p, encoding='utf-8'), objects) else: - write_aas_xml_file(p, objects) + parse_obj_store_to_xml(p, objects) # Write submodel's supplementary files to AASX file supplementary_file_names = [] @@ -789,7 +787,7 @@ def __iter__(self) -> Iterator[str]: return iter(self._name_map) -def read_aas_json_file(file: PathOrIO) -> ObjectStore[model.Identifiable]: +def parse_json_to_obj_store(file: PathOrIO) -> ObjectStore[model.Identifiable]: """ Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 into a given object store. @@ -839,7 +837,7 @@ def read_aas_json_file(file: PathOrIO) -> ObjectStore[model.Identifiable]: return object_store -def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: +def parse_obj_store_to_json(file: PathOrIO, data: ObjectStore, **kwargs) -> None: """ Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5 @@ -887,7 +885,7 @@ def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None: json.dump(dict_, fp, **kwargs) -def read_aas_xml_file(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Identifiable]: +def parse_xml_to_obj_store(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Identifiable]: """ Able to parse the official schema files into a given :class:`ObjectStore `. @@ -943,11 +941,9 @@ def read_aas_xml_file(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Iden return object_store -def write_aas_xml_file(file: PathOrIO, data: ObjectStore) -> None: +def parse_obj_store_to_xml(file: PathOrIO, data: ObjectStore) -> None: """ Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree._Element`. - This function is used internally by :meth:`write_aas_xml_file` and shouldn't be - called directly for most use-cases. :param file: A filename or file-like object to read the JSON-serialized data from :param data: :class:`ObjectStore ` which contains different objects of diff --git a/sdk/basyx/object_store.py b/sdk/basyx/object_store.py index aee4712..88cc8e8 100644 --- a/sdk/basyx/object_store.py +++ b/sdk/basyx/object_store.py @@ -149,13 +149,12 @@ def get_referable(self, identifier: str, id_short: str) -> Referable: """ referable: Referable identifiable = self.get_identifiable(identifier) - for referable in identifiable.descend(): + for element in identifiable.descend(): if ( - issubclass(type(referable), Referable) - and id_short in referable.id_short + isinstance(element, Referable) and id_short == element.id_short ): - return referable + return element raise KeyError("Referable object with short_id {} does not exist for identifiable object with id {}" .format(id_short, identifier)) diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_create_simple_aas.py index 8980e06..4baa09c 100644 --- a/sdk/basyx/tutorial/tutorial_create_simple_aas.py +++ b/sdk/basyx/tutorial/tutorial_create_simple_aas.py @@ -1,78 +1,151 @@ -import json -import aas_core3.types as aas_types -import aas_core3.jsonization as aas_jsonization -from basyx.object_store import ObjectStore -from basyx.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer -import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class. +#!/usr/bin/env python3 +""" +Tutorial for exporting Asset Administration Shells with related objects and auxiliary files to AASX package files, using +the :mod:`~basyx.aasx` module from the Eclipse BaSyx Python Framework. + +""" + import datetime from pathlib import Path # Used for easier handling of auxiliary file's local path -Referencetype = aas_types.ReferenceTypes("ModelReference") +import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class. +from aas_core3 import types as model +from basyx.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer +from basyx.object_store import ObjectStore + +# step 1: Setting up an SupplementaryFileContainer and AAS & submodel with File objects +# step 2: Writing AAS objects and auxiliary files to an AASX package +# step 3: Reading AAS objects and auxiliary files from an AASX package + + +######################################################################################## +# Step 1: Setting up a SupplementaryFileContainer and AAS & submodel with File objects # +######################################################################################## -key_types = aas_types.KeyTypes("Submodel") +# Let's first create a basic Asset Administration Shell with a simple submodel. +# See `tutorial_create_simple_aas.py` for more details. +submodel = model.Submodel(id='https://acplt.org/Submodel', submodel_elements=[]) -key = aas_types.Key(value="some-unique-global-identifier", type=key_types) +key_types = model.KeyTypes("Submodel") +Referencetype = model.ReferenceTypes("ModelReference") +key = model.Key(value='https://acplt.org/Submodel', type=key_types) +reference = model.Reference(type=Referencetype, keys=[key]) -reference = aas_types.Reference(type=Referencetype, keys=[key]) +aas = model.AssetAdministrationShell(id='https://acplt.org/Simple_AAS', + asset_information=model.AssetInformation( + asset_kind=model.AssetKind.TYPE), + submodels=[reference]) -submodel = aas_types.Submodel( - id="some-unique-global-identifier", - submodel_elements=[ - aas_types.Property( - id_short="some_property", - value_type=aas_types.DataTypeDefXSD.INT, - value="1984", - semantic_id=reference - ) - ] +# Another submodel, which is not related to the AAS: +unrelated_submodel = model.Submodel( + id='https://acplt.org/Unrelated_Submodel' ) +# We add these objects to an ObjectStore for easy retrieval by id. +object_store: ObjectStore = ObjectStore([unrelated_submodel, submodel, aas]) + + +# For holding auxiliary files, which will eventually be added to an AASX package, we need a SupplementaryFileContainer. +# The `DictSupplementaryFileContainer` is a simple SupplementaryFileContainer that stores the files' contents in simple +# bytes objects in memory. file_store = DictSupplementaryFileContainer() +# Now, we add an example file from our local filesystem to the SupplementaryFileContainer. +# +# For this purpose, we need to specify the file's name in the SupplementaryFileContainer. This name is used to reference +# the file in the container and will later be used as the filename in the AASX package file. Thus, this file must begin +# with a slash and should begin with `/aasx/`. Here, we use `/aasx/suppl/MyExampleFile.pdf`. The +# SupplementaryFileContainer's add_file() method will ensure uniqueness of the name by adding a suffix if an equally +# named file with different contents exists. The final name is returned. +# +# In addition, we need to specify the MIME type of the file, which is later used in the metadata of the AASX package. +# (This is actually a requirement of the underlying Open Packaging Conventions (ECMA376-2) format, which imposes the +# specification of the MIME type ("content type") of every single file within the package.) + with open(Path(__file__).parent / 'data' / 'TestFile.pdf', 'rb') as f: actual_file_name = file_store.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf") -if submodel.submodel_elements is not None: - submodel.submodel_elements.append(aas_types.File(id_short="documentationFile", - content_type="application/pdf", - value=actual_file_name)) -aas = aas_types.AssetAdministrationShell(id="urn:x-test:aas1", - asset_information=aas_types.AssetInformation( - asset_kind=aas_types.AssetKind.TYPE), - submodels=[reference]) +# With the actual_file_name in the SupplementaryFileContainer, we can create a reference to that file in our AAS +# Submodel, in the form of a `File` object: -obj_store: ObjectStore = ObjectStore() -obj_store.add(aas) -obj_store.add(submodel) +model.File(id_short="documentationFile", content_type="application/pdf", value=actual_file_name) +file = model.File(id_short="documentationFile", content_type="application/pdf", value=actual_file_name) +if submodel.submodel_elements is not None: + submodel.submodel_elements.append(file) -# Serialize to a JSON-able mapping -jsonable = aas_jsonization.to_jsonable(submodel) +###################################################################### +# Step 2: Writing AAS objects and auxiliary files to an AASX package # +###################################################################### -meta_data = pyecma376_2.OPCCoreProperties() -meta_data.creator = "Chair of Process Control Engineering" -meta_data.created = datetime.datetime.now() +# After setting everything up in Step 1, writing the AAS, including the Submodel objects and the auxiliary file +# to an AASX package is simple. +# Open an AASXWriter with the destination file name and use it as a context handler, to make sure it is properly closed +# after doing the modifications: with AASXWriter("./MyAASXPackage.aasx") as writer: - writer.write_aas(aas_ids=["urn:x-test:aas1"], - object_store=obj_store, - file_store=file_store, - write_json=False) + # Write the AAS and everything belonging to it to the AASX package + # The `write_aas()` method will automatically fetch the AAS object with the given id + # and all referenced Submodel objects from the ObjectStore. It will also scan every object for + # semanticIds referencing ConceptDescription, fetch them from the ObjectStore, and scan all sbmodels for `File` + # objects and fetch the referenced auxiliary files from the SupplementaryFileContainer. + # In order to add more than one AAS to the package, we can simply add more Identifiers to the `aas_ids` list. + # + # ATTENTION: As of Version 3.0 RC01 of Details of the Asset Administration Shell, it is no longer valid to add more + # than one "aas-spec" part (JSON/XML part with AAS objects) to an AASX package. Thus, `write_aas` MUST + # only be called once per AASX package! + writer.write_aas(aas_ids=['https://acplt.org/Simple_AAS'], + object_store=object_store, + file_store=file_store) + + # Alternatively, we can use a more low-level interface to add a JSON/XML part with any Identifiable objects (not + # only an AAS and referenced objects) in the AASX package manually. `write_aas_objects()` will also take care of + # adding referenced auxiliary files by scanning all submodel objects for contained `File` objects. + # + # ATTENTION: As of Version 3.0 RC01 of Details of the Asset Administration Shell, it is no longer valid to add more + # than one "aas-spec" part (JSON/XML part with AAS objects) to an AASX package. Thus, `write_all_aas_objects` SHALL + # only be used as an alternative to `write_aas` and SHALL only be called once! + objects_to_be_written: ObjectStore[model.Identifiable] = ObjectStore([unrelated_submodel]) + writer.write_all_aas_objects(part_name="/aasx/my_aas_part.xml", + objects=objects_to_be_written, + file_store=file_store) + + # We can also add a thumbnail image to the package (using `writer.write_thumbnail()`) or add metadata: + meta_data = pyecma376_2.OPCCoreProperties() + meta_data.creator = "Chair of Process Control Engineering" + meta_data.created = datetime.datetime.now() writer.write_core_properties(meta_data) +# Closing the AASXWriter will write some required parts with relationships and MIME types to the AASX package file and +# close the package file afterward. Make sure, to always call `AASXWriter.close()` or use the AASXWriter in a `with` +# statement (as a context manager) as shown above. + + +######################################################################## +# Step 3: Reading AAS objects and auxiliary files from an AASX package # +######################################################################## + +# Let's read the AASX package file, we have just written. +# We'll use a fresh ObjectStore and SupplementaryFileContainer to read AAS objects and auxiliary files into. new_object_store: ObjectStore = ObjectStore() new_file_store = DictSupplementaryFileContainer() -with AASXReader("./MyAASXPackage.aasx") as reader: +# Again, we need to use the AASXReader as a context manager (or call `.close()` in the end) to make sure the AASX +# package file is properly closed when we are finished. +with AASXReader("MyAASXPackage.aasx") as reader: # Read all contained AAS objects and all referenced auxiliary files reader.read_into(object_store=new_object_store, file_store=new_file_store) -print(new_object_store.__len__()) -for item in file_store.__iter__(): - print(item) + # We can also read the metadata + new_meta_data = reader.get_core_properties() + + # We could also read the thumbnail image, using `reader.get_thumbnail()` + -for item in new_file_store.__iter__(): - print(item) +# Some quick checks to make sure, reading worked as expected +assert 'https://acplt.org/Submodel' in new_object_store +assert actual_file_name in new_file_store +assert new_meta_data.creator == "Chair of Process Control Engineering" diff --git a/sdk/basyx/tutorial/tutorial_objectstore.py b/sdk/basyx/tutorial/tutorial_objectstore.py index ac75b31..e61fee0 100644 --- a/sdk/basyx/tutorial/tutorial_objectstore.py +++ b/sdk/basyx/tutorial/tutorial_objectstore.py @@ -5,7 +5,7 @@ # # SPDX-License-Identifier: MIT -from basyx.objectstore import ObjectStore +from basyx.object_store import ObjectStore from aas_core3.types import Identifiable, AssetAdministrationShell, AssetInformation, AssetKind import aas_core3.types as aas_types From 7e0e4ef602276503aef64ba40d2b34c146b75b6b Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Mon, 9 Dec 2024 19:05:21 +0100 Subject: [PATCH 470/474] sdk/basyx/__init__.py: Import aas-core3 in init file to shortcut its import. sdk/basyx/aasx.py: Include sample codeblock for adapter usage. Update imports in tutorial files --- sdk/basyx/__init__.py | 1 + sdk/basyx/aasx.py | 23 ++++++++++++++++++- .../tutorial/tutorial_create_simple_aas.py | 2 +- sdk/basyx/tutorial/tutorial_objectstore.py | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/sdk/basyx/__init__.py b/sdk/basyx/__init__.py index 34dfc9e..1e02231 100644 --- a/sdk/basyx/__init__.py +++ b/sdk/basyx/__init__.py @@ -1 +1,2 @@ from .object_store import * +from aas_core3 import types as model diff --git a/sdk/basyx/aasx.py b/sdk/basyx/aasx.py index aaa0453..2612530 100644 --- a/sdk/basyx/aasx.py +++ b/sdk/basyx/aasx.py @@ -11,7 +11,28 @@ Package File Format (AASX) v3.0". The AASX file format is built upon the Open Packaging Conventions (OPC; ECMA 376-2). We use the ``pyecma376_2`` library for low level OPC reading and writing. It currently supports all required features except for embedded digital -signatures. +signatures. The following codeblock contains the required imports and functions to use the adapter. + + .. code-block:: python + + from basyx import model # Shortcut to import aas_core3 which should be installed after installing requirements + from basyx.aasx import AASXWriter + from basyx.object_store import ObjectStore + + object_store: ObjectStore = ObjectStore() + file_store = DictSupplementaryFileContainer() + + aas = model.AssetAdministrationShell(id='https://acplt.org/Simple_AAS', + asset_information=model.AssetInformation( + asset_kind=model.AssetKind.TYPE)) + + with AASXWriter("./MyAASXPackage.aasx") as writer: + + writer.write_aas(aas_ids=['https://acplt.org/Simple_AAS'], + object_store=object_store, + file_store=file_store) + .. + Writing and reading of AASX packages is performed through the :class:`~.AASXReader` and :class:`~.AASXWriter` classes. Each instance of these classes wraps an existing AASX file resp. a file to be created and allows to read/write the diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_create_simple_aas.py index 4baa09c..0c2f699 100644 --- a/sdk/basyx/tutorial/tutorial_create_simple_aas.py +++ b/sdk/basyx/tutorial/tutorial_create_simple_aas.py @@ -9,7 +9,7 @@ from pathlib import Path # Used for easier handling of auxiliary file's local path import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class. -from aas_core3 import types as model +from basyx import model from basyx.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer from basyx.object_store import ObjectStore diff --git a/sdk/basyx/tutorial/tutorial_objectstore.py b/sdk/basyx/tutorial/tutorial_objectstore.py index e61fee0..7f425b4 100644 --- a/sdk/basyx/tutorial/tutorial_objectstore.py +++ b/sdk/basyx/tutorial/tutorial_objectstore.py @@ -7,7 +7,7 @@ from basyx.object_store import ObjectStore from aas_core3.types import Identifiable, AssetAdministrationShell, AssetInformation, AssetKind -import aas_core3.types as aas_types +from basyx import model as aas_types aas = AssetAdministrationShell(id="urn:x-test:aas1", asset_information=AssetInformation(asset_kind=AssetKind.TYPE)) From 8c53860cfb1bf8d5599f6138bee9d60cae0639ae Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 12 Dec 2024 13:16:36 +0100 Subject: [PATCH 471/474] tutorial_create_simple_aas.py rename to tutorial_aasx.py --- .../tutorial/{tutorial_create_simple_aas.py => tutorial_aasx.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sdk/basyx/tutorial/{tutorial_create_simple_aas.py => tutorial_aasx.py} (100%) diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_aasx.py similarity index 100% rename from sdk/basyx/tutorial/tutorial_create_simple_aas.py rename to sdk/basyx/tutorial/tutorial_aasx.py From 84f798c8bf819dafbb836618f89cfd79e431d9d5 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 12 Dec 2024 13:28:18 +0100 Subject: [PATCH 472/474] tutorial_create_simple_aas.py: restored the original tutorial --- .../tutorial/tutorial_create_simple_aas.py | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 sdk/basyx/tutorial/tutorial_create_simple_aas.py diff --git a/sdk/basyx/tutorial/tutorial_create_simple_aas.py b/sdk/basyx/tutorial/tutorial_create_simple_aas.py new file mode 100644 index 0000000..43b8995 --- /dev/null +++ b/sdk/basyx/tutorial/tutorial_create_simple_aas.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# This work is licensed under a Creative Commons CCZero 1.0 Universal License. +# See http://creativecommons.org/publicdomain/zero/1.0/ for more information. +""" +Tutorial for the creation of a simple Asset Administration Shell, containing an AssetInformation object and a Submodel +reference using aas-core3.0-python +""" + +# Import all type classes from the aas-core3.0-python SDK +import aas_core3.types as aas_types + +# In this tutorial, you'll get a step-by-step guide on how to create an Asset Administration Shell (AAS) and all +# required objects within. First, you need an AssetInformation object for which you want to create an AAS. After that, +# an Asset Administration Shell can be created. Then, it's possible to add Submodels to the AAS. The Submodels can +# contain SubmodelElements. + +# Step-by-Step Guide: +# Step 1: create a simple Asset Administration Shell, containing AssetInformation object +# Step 2: create a simple Submodel +# Step 3: create a simple Property and add it to the Submodel + + +############################################################################################ +# Step 1: Create a Simple Asset Administration Shell Containing an AssetInformation object # +############################################################################################ +# Step 1.1: create the AssetInformation object +asset_information = aas_types.AssetInformation( + asset_kind=aas_types.AssetKind.INSTANCE, + global_asset_id='http://acplt.org/Simple_Asset' +) + +# Step 1.2: create the Asset Administration Shell +identifier = 'https://acplt.org/Simple_AAS' +aas = aas_types.AssetAdministrationShell( + id=identifier, # set identifier + asset_information=asset_information, + submodels=[] +) + + +############################################################# +# Step 2: Create a Simple Submodel Without SubmodelElements # +############################################################# + +# Step 2.1: create the Submodel object +identifier = 'https://acplt.org/Simple_Submodel' +submodel = aas_types.Submodel( + id=identifier, + submodel_elements=[] +) + +# Step 2.2: create a reference to that Submodel and add it to the Asset Administration Shell's `submodel` set +submodel_reference = aas_types.Reference( + type=aas_types.ReferenceTypes.MODEL_REFERENCE, + keys=[aas_types.Key( + type=aas_types.KeyTypes.SUBMODEL, + value=identifier + )] +) + +# Warning, this overwrites whatever is in the `aas.submodels` list. +# In your production code, it might make sense to check for already existing content. +aas.submodels = [submodel_reference] + + +# =============================================================== +# ALTERNATIVE: step 1 and 2 can alternatively be done in one step +# In this version, the Submodel reference is passed to the Asset Administration Shell's constructor. +submodel = aas_types.Submodel( + id='https://acplt.org/Simple_Submodel', + submodel_elements=[] +) +aas = aas_types.AssetAdministrationShell( + id='https://acplt.org/Simple_AAS', + asset_information=asset_information, + submodels=[aas_types.Reference( + type=aas_types.ReferenceTypes.MODEL_REFERENCE, + keys=[aas_types.Key( + type=aas_types.KeyTypes.SUBMODEL, + value='https://acplt.org/Simple_Submodel' + )] + )] +) + + +############################################################### +# Step 3: Create a Simple Property and Add it to the Submodel # +############################################################### + +# Step 3.1: create a global reference to a semantic description of the Property +# A global reference consists of one key which points to the address where the semantic description is stored +semantic_reference = aas_types.Reference( + type=aas_types.ReferenceTypes.MODEL_REFERENCE, + keys=[aas_types.Key( + type=aas_types.KeyTypes.GLOBAL_REFERENCE, + value='http://acplt.org/Properties/SimpleProperty' + )] +) + +# Step 3.2: create the simple Property +property_ = aas_types.Property( + id_short='ExampleProperty', # Identifying string of the element within the Submodel namespace + value_type=aas_types.DataTypeDefXSD.STRING, # Data type of the value + value='exampleValue', # Value of the Property + semantic_id=semantic_reference # set the semantic reference +) + +# Step 3.3: add the Property to the Submodel + +# Warning, this overwrites whatever is in the `submodel_elements` list. +# In your production code, it might make sense to check for already existing content. +submodel.submodel_elements = [property_] + + +# ===================================================================== +# ALTERNATIVE: step 2 and 3 can also be combined in a single statement: +# Again, we pass the Property to the Submodel's constructor instead of adding it afterward. +submodel = aas_types.Submodel( + id='https://acplt.org/Simple_Submodel', + submodel_elements=[ + aas_types.Property( + id_short='ExampleProperty', + value_type=aas_types.DataTypeDefXSD.STRING, + value='exampleValue', + semantic_id=aas_types.Reference( + type=aas_types.ReferenceTypes.MODEL_REFERENCE, + keys=[aas_types.Key( + type=aas_types.KeyTypes.GLOBAL_REFERENCE, + value='http://acplt.org/Properties/SimpleProperty' + )] + ) + ) + ] +) From 54b32cc31174d669f08cb072d30d0d76fb01a1fd Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Thu, 12 Dec 2024 13:33:39 +0100 Subject: [PATCH 473/474] sdk/basyx/__init__.py: added documentation for aas-core3 shortcut --- sdk/basyx/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/basyx/__init__.py b/sdk/basyx/__init__.py index 1e02231..8991815 100644 --- a/sdk/basyx/__init__.py +++ b/sdk/basyx/__init__.py @@ -1,2 +1,4 @@ from .object_store import * + +# Shortcut to import aas_core3 which should be installed after installing requirements from aas_core3 import types as model From a08971f815e95a6ed1d1885516001d694f2ac903 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Fri, 13 Dec 2024 10:11:34 +0100 Subject: [PATCH 474/474] Update sdk/basyx/__init__.py --- sdk/basyx/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/basyx/__init__.py b/sdk/basyx/__init__.py index 8991815..a407fdf 100644 --- a/sdk/basyx/__init__.py +++ b/sdk/basyx/__init__.py @@ -1,4 +1,5 @@ from .object_store import * -# Shortcut to import aas_core3 which should be installed after installing requirements +# Alias `types` as `model` to maintain consistency with the import style of the predecessor project (BaSyx-Python SDK), +# ensuring a familiar experience for users transitioning to the new BaSyx-Python Framework. from aas_core3 import types as model