Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix typos and refactor #343

Merged
merged 12 commits into from
Dec 1, 2024
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ The following guidelines are for the commit or PR message text:

## Codestyle and Testing

Our code follows the [PEP 8 -- Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/).
Our code follows the [PEP 8 -- Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/)
with the following exceptions:
- Line length is allowed to be up to 120 characters, though lines up to 100 characters are preferred.

Additionally, we use [PEP 484 -- Type Hints](https://www.python.org/dev/peps/pep-0484/) throughout the code to enable type checking the code.

Before submitting any changes, make sure to let `mypy` and `pycodestyle` check your code and run the unit tests with
Expand Down
4 changes: 2 additions & 2 deletions sdk/basyx/aas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
The package consists of the python implementation of the AssetAdministrationShell, as defined in the
'Details of the Asset Administration Shell' specification of Plattform Industrie 4.0.

The subpackage 'model' is an implementation of the meta-model of the AAS,
The subpackage 'model' is an implementation of the metamodel of the AAS,
in 'adapter', you can find JSON and XML adapters to translate between BaSyx Python SDK objects and JSON/XML schemas;
and in 'util', some helpful functionality to actually use the AAS meta-model you created with 'model' is located.
and in 'util', some helpful functionality to actually use the AAS metamodel you created with 'model' is located.
"""

__version__ = "1.0.0"
86 changes: 19 additions & 67 deletions sdk/basyx/aas/adapter/aasx.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ def __init__(self, file: Union[os.PathLike, str, IO]):

def get_core_properties(self) -> pyecma376_2.OPCCoreProperties:
"""
Retrieve the OPC Core Properties (meta data) of the AASX package file.
Retrieve the OPC Core Properties (metadata) of the AASX package file.

If no meta data is provided in the package file, an emtpy OPCCoreProperties object is returned.
If no metadata is provided in the package file, an emtpy OPCCoreProperties object is returned.

:return: The AASX package's meta data
:return: The AASX package's metadata
"""
return self.reader.get_core_properties()

Expand Down Expand Up @@ -147,17 +147,19 @@ def read_into(self, object_store: model.AbstractObjectStore,

read_identifiables: Set[model.Identifier] = set()

no_aas_files_found = True
# 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]:
no_aas_files_found = False
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)
if no_aas_files_found:
logger.warning("No AAS files found in AASX package")

return read_identifiables

Expand Down Expand Up @@ -191,7 +193,7 @@ def _read_aas_part_into(self, part_name: str,
: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.
AASX that have the same Identifier. 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:
Expand Down Expand Up @@ -380,7 +382,7 @@ def write_aas(self,
except KeyError:
raise
if not isinstance(aas, model.AssetAdministrationShell):
raise TypeError(f"Identifier {aas_id} does not belong to an AssetAdminstrationShell object but to "
raise TypeError(f"Identifier {aas_id} does not belong to an AssetAdministrationShell object but to "
f"{aas!r}")

# Add the AssetAdministrationShell object to the data part
Expand All @@ -407,10 +409,10 @@ def write_aas(self,
try:
cd = semantic_id.resolve(object_store)
except KeyError:
logger.info("ConceptDescription for semantidId %s not found in object store.", str(semantic_id))
logger.info("ConceptDescription for semanticId %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",
logger.error("semanticId %s resolves to %s, which is not a ConceptDescription",
str(semantic_id), e.value)
continue
concept_descriptions.append(cd)
Expand Down Expand Up @@ -457,7 +459,7 @@ def write_aas_objects(self,
: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
make sure to reference it via an 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.
"""
Expand Down Expand Up @@ -508,7 +510,7 @@ def write_all_aas_objects(self,
``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
sure to reference it via an 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.
"""
Expand Down Expand Up @@ -574,12 +576,12 @@ def write_all_aas_objects(self,

def write_core_properties(self, core_properties: pyecma376_2.OPCCoreProperties):
"""
Write OPC Core Properties (meta data) to the AASX package file.
Write OPC Core Properties (metadata) 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
:param core_properties: The OPCCoreProperties object with the metadata to be written to the package file
"""
if self._properties_part is not None:
raise RuntimeError("Core Properties have already been written.")
Expand Down Expand Up @@ -662,56 +664,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: 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.
Expand All @@ -720,7 +672,7 @@ class AbstractSupplementaryFileContainer(metaclass=abc.ABCMeta):
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
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).
Expand All @@ -738,7 +690,7 @@ 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
:return: The file name as stored in the SupplementaryFileContainer. Typically, ``name`` or a modified version of
``name`` to resolve conflicts.
"""
pass # pragma: no cover
Expand Down
4 changes: 1 addition & 3 deletions sdk/basyx/aas/adapter/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,9 +721,7 @@ def handle_request(self, request: Request):

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]
return endpoint(request, values, response_t=response_t, 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
Expand Down
16 changes: 6 additions & 10 deletions sdk/basyx/aas/adapter/json/json_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
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
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.
Expand Down Expand Up @@ -83,12 +83,12 @@ def _expect_type(object_: object, type_: Type, context: str, failsafe: bool) ->
if _expect_type(element, model.SubmodelElement, str(submodel), failsafe):
submodel.submodel_element.add(element)

:param object_: The object to by type-checked
:param object_: The object to be 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
:return: True if the object is of the expected type
:raises TypeError: If the object is not of the expected type and the failsafe mode is not active
"""
if isinstance(object_, type_):
Expand Down Expand Up @@ -226,7 +226,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object:
@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,
Utility method to add the optional attributes of the abstract metaclasses 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
Expand Down Expand Up @@ -414,7 +414,7 @@ def _construct_value_reference_pair(cls, dct: Dict[str, object],
# 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
# These constructor methods create objects that *are* identified by a 'modelType' JSON attribute, so they can
# be called from the object_hook() method directly.

@classmethod
Expand Down Expand Up @@ -613,8 +613,6 @@ def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operati
@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)))
Expand All @@ -625,8 +623,6 @@ def _construct_relationship_element(
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)),
Expand Down
8 changes: 4 additions & 4 deletions sdk/basyx/aas/adapter/json/json_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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`
The module provides a 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).
Expand Down Expand Up @@ -103,7 +103,7 @@ def default(self, obj: object) -> object:
@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
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
Expand Down Expand Up @@ -722,7 +722,7 @@ def object_store_to_json(data: model.AbstractObjectStore, stripped: bool = False
chapter 5.5

:param data: :class:`ObjectStore <basyx.aas.model.provider.AbstractObjectStore>` which contains different objects of
the AAS meta model which should be serialized to a JSON file
the AAS metamodel 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.
Expand Down Expand Up @@ -750,7 +750,7 @@ 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 <basyx.aas.model.provider.AbstractObjectStore>` which contains different objects of
the AAS meta model which should be serialized to a JSON file
the AAS metamodel 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.
Expand Down
Loading
Loading