Skip to content

Commit

Permalink
aasx.py: Rename a few functions and check the docstrings. tutorial_cr…
Browse files Browse the repository at this point in the history
…eate_simple_aas.py: Implement tutorial comparable to the tutorial in the basyx python sdk
  • Loading branch information
somsonson committed Dec 9, 2024
1 parent 82139cd commit d618cb2
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 64 deletions.
22 changes: 9 additions & 13 deletions sdk/basyx/aasx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <basyx.aas.model.provider.AbstractObjectStore>`.
Expand Down Expand Up @@ -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 <basyx.ObjectStore>` which contains different objects of
Expand Down
7 changes: 3 additions & 4 deletions sdk/basyx/object_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
165 changes: 119 additions & 46 deletions sdk/basyx/tutorial/tutorial_create_simple_aas.py
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion sdk/basyx/tutorial/tutorial_objectstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit d618cb2

Please sign in to comment.