Skip to content

Commit d618cb2

Browse files
committed
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
1 parent 82139cd commit d618cb2

4 files changed

Lines changed: 132 additions & 64 deletions

File tree

sdk/basyx/aasx.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353

5454
# type aliases for path-like objects and IO
55-
# used by write_aas_xml_file, read_aas_xml_file, write_aas_json_file, read_aas_json_file
55+
# used by parse_obj_store_to_xml, parse_xml_to_obj_store, parse_obj_store_to_json, parse_json_to_obj_store
5656
Path = Union[str, bytes, os.PathLike]
5757
PathOrBinaryIO = Union[Path, BinaryIO]
5858
PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO
@@ -203,8 +203,6 @@ def _read_aas_part_into(self, part_name: str,
203203
"""
204204
Helper function for :meth:`read_into()` to read and process the contents of an AAS-spec part of the AASX file.
205205
206-
This method primarily checks for duplicate objects. It uses ``_parse_aas_parse()`` to do the actual parsing and
207-
``_collect_supplementary_files()`` for supplementary file processing of non-duplicate objects.
208206
209207
:param part_name: The OPC part name to read
210208
: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:
249247
if content_type.split(";")[0] in ("text/xml", "application/xml") or content_type == "" and extension == "xml":
250248
logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(part_name))
251249
with self.reader.open_part(part_name) as p:
252-
return read_aas_xml_file(p, **kwargs)
250+
return parse_xml_to_obj_store(p, **kwargs)
253251
elif content_type.split(";")[0] in ("text/json", "application/json") \
254252
or content_type == "" and extension == "json":
255253
logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(part_name))
256254

257255
with self.reader.open_part(part_name) as p:
258-
return read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs)
256+
return parse_json_to_obj_store(io.TextIOWrapper(p, encoding='utf-8-sig'), **kwargs)
259257
else:
260258
logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}"
261259
.format(part_name, content_type, extension))
@@ -504,9 +502,9 @@ def write_all_aas_objects(self,
504502
# TODO allow writing xml *and* JSON part
505503
with self.writer.open_part(part_name, "application/json" if write_json else "application/xml") as p:
506504
if write_json:
507-
write_aas_json_file(io.TextIOWrapper(p, encoding='utf-8'), objects)
505+
parse_obj_store_to_json(io.TextIOWrapper(p, encoding='utf-8'), objects)
508506
else:
509-
write_aas_xml_file(p, objects)
507+
parse_obj_store_to_xml(p, objects)
510508

511509
# Write submodel's supplementary files to AASX file
512510
supplementary_file_names = []
@@ -789,7 +787,7 @@ def __iter__(self) -> Iterator[str]:
789787
return iter(self._name_map)
790788

791789

792-
def read_aas_json_file(file: PathOrIO) -> ObjectStore[model.Identifiable]:
790+
def parse_json_to_obj_store(file: PathOrIO) -> ObjectStore[model.Identifiable]:
793791
"""
794792
Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5
795793
into a given object store.
@@ -839,7 +837,7 @@ def read_aas_json_file(file: PathOrIO) -> ObjectStore[model.Identifiable]:
839837
return object_store
840838

841839

842-
def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None:
840+
def parse_obj_store_to_json(file: PathOrIO, data: ObjectStore, **kwargs) -> None:
843841
"""
844842
Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset
845843
Administration Shell', chapter 5.5
@@ -887,7 +885,7 @@ def write_aas_json_file(file: PathOrIO, data: ObjectStore, **kwargs) -> None:
887885
json.dump(dict_, fp, **kwargs)
888886

889887

890-
def read_aas_xml_file(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Identifiable]:
888+
def parse_xml_to_obj_store(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Identifiable]:
891889
"""
892890
Able to parse the official schema files into a given
893891
:class:`ObjectStore <basyx.aas.model.provider.AbstractObjectStore>`.
@@ -943,11 +941,9 @@ def read_aas_xml_file(file: PathOrIO, **parser_kwargs) -> ObjectStore[model.Iden
943941
return object_store
944942

945943

946-
def write_aas_xml_file(file: PathOrIO, data: ObjectStore) -> None:
944+
def parse_obj_store_to_xml(file: PathOrIO, data: ObjectStore) -> None:
947945
"""
948946
Serialize a set of AAS objects to an Asset Administration Shell as :class:`~lxml.etree._Element`.
949-
This function is used internally by :meth:`write_aas_xml_file` and shouldn't be
950-
called directly for most use-cases.
951947
952948
:param file: A filename or file-like object to read the JSON-serialized data from
953949
:param data: :class:`ObjectStore <basyx.ObjectStore>` which contains different objects of

sdk/basyx/object_store.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,12 @@ def get_referable(self, identifier: str, id_short: str) -> Referable:
149149
"""
150150
referable: Referable
151151
identifiable = self.get_identifiable(identifier)
152-
for referable in identifiable.descend():
152+
for element in identifiable.descend():
153153

154154
if (
155-
issubclass(type(referable), Referable)
156-
and id_short in referable.id_short
155+
isinstance(element, Referable) and id_short == element.id_short
157156
):
158-
return referable
157+
return element
159158
raise KeyError("Referable object with short_id {} does not exist for identifiable object with id {}"
160159
.format(id_short, identifier))
161160

Lines changed: 119 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,151 @@
1-
import json
2-
import aas_core3.types as aas_types
3-
import aas_core3.jsonization as aas_jsonization
4-
from basyx.object_store import ObjectStore
5-
from basyx.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer
6-
import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class.
1+
#!/usr/bin/env python3
2+
"""
3+
Tutorial for exporting Asset Administration Shells with related objects and auxiliary files to AASX package files, using
4+
the :mod:`~basyx.aasx` module from the Eclipse BaSyx Python Framework.
5+
6+
"""
7+
78
import datetime
89
from pathlib import Path # Used for easier handling of auxiliary file's local path
910

10-
Referencetype = aas_types.ReferenceTypes("ModelReference")
11+
import pyecma376_2 # The base library for Open Packaging Specifications. We will use the OPCCoreProperties class.
12+
from aas_core3 import types as model
13+
from basyx.aasx import AASXWriter, AASXReader, DictSupplementaryFileContainer
14+
from basyx.object_store import ObjectStore
15+
16+
# step 1: Setting up an SupplementaryFileContainer and AAS & submodel with File objects
17+
# step 2: Writing AAS objects and auxiliary files to an AASX package
18+
# step 3: Reading AAS objects and auxiliary files from an AASX package
19+
20+
21+
########################################################################################
22+
# Step 1: Setting up a SupplementaryFileContainer and AAS & submodel with File objects #
23+
########################################################################################
1124

12-
key_types = aas_types.KeyTypes("Submodel")
25+
# Let's first create a basic Asset Administration Shell with a simple submodel.
26+
# See `tutorial_create_simple_aas.py` for more details.
27+
submodel = model.Submodel(id='https://acplt.org/Submodel', submodel_elements=[])
1328

14-
key = aas_types.Key(value="some-unique-global-identifier", type=key_types)
29+
key_types = model.KeyTypes("Submodel")
30+
Referencetype = model.ReferenceTypes("ModelReference")
31+
key = model.Key(value='https://acplt.org/Submodel', type=key_types)
32+
reference = model.Reference(type=Referencetype, keys=[key])
1533

16-
reference = aas_types.Reference(type=Referencetype, keys=[key])
34+
aas = model.AssetAdministrationShell(id='https://acplt.org/Simple_AAS',
35+
asset_information=model.AssetInformation(
36+
asset_kind=model.AssetKind.TYPE),
37+
submodels=[reference])
1738

18-
submodel = aas_types.Submodel(
19-
id="some-unique-global-identifier",
20-
submodel_elements=[
21-
aas_types.Property(
22-
id_short="some_property",
23-
value_type=aas_types.DataTypeDefXSD.INT,
24-
value="1984",
25-
semantic_id=reference
26-
)
27-
]
39+
# Another submodel, which is not related to the AAS:
40+
unrelated_submodel = model.Submodel(
41+
id='https://acplt.org/Unrelated_Submodel'
2842
)
2943

44+
# We add these objects to an ObjectStore for easy retrieval by id.
45+
object_store: ObjectStore = ObjectStore([unrelated_submodel, submodel, aas])
46+
47+
48+
# For holding auxiliary files, which will eventually be added to an AASX package, we need a SupplementaryFileContainer.
49+
# The `DictSupplementaryFileContainer` is a simple SupplementaryFileContainer that stores the files' contents in simple
50+
# bytes objects in memory.
3051
file_store = DictSupplementaryFileContainer()
3152

53+
# Now, we add an example file from our local filesystem to the SupplementaryFileContainer.
54+
#
55+
# For this purpose, we need to specify the file's name in the SupplementaryFileContainer. This name is used to reference
56+
# the file in the container and will later be used as the filename in the AASX package file. Thus, this file must begin
57+
# with a slash and should begin with `/aasx/`. Here, we use `/aasx/suppl/MyExampleFile.pdf`. The
58+
# SupplementaryFileContainer's add_file() method will ensure uniqueness of the name by adding a suffix if an equally
59+
# named file with different contents exists. The final name is returned.
60+
#
61+
# In addition, we need to specify the MIME type of the file, which is later used in the metadata of the AASX package.
62+
# (This is actually a requirement of the underlying Open Packaging Conventions (ECMA376-2) format, which imposes the
63+
# specification of the MIME type ("content type") of every single file within the package.)
64+
3265
with open(Path(__file__).parent / 'data' / 'TestFile.pdf', 'rb') as f:
3366
actual_file_name = file_store.add_file("/aasx/suppl/MyExampleFile.pdf", f, "application/pdf")
3467

35-
if submodel.submodel_elements is not None:
36-
submodel.submodel_elements.append(aas_types.File(id_short="documentationFile",
37-
content_type="application/pdf",
38-
value=actual_file_name))
3968

40-
aas = aas_types.AssetAdministrationShell(id="urn:x-test:aas1",
41-
asset_information=aas_types.AssetInformation(
42-
asset_kind=aas_types.AssetKind.TYPE),
43-
submodels=[reference])
69+
# With the actual_file_name in the SupplementaryFileContainer, we can create a reference to that file in our AAS
70+
# Submodel, in the form of a `File` object:
4471

45-
obj_store: ObjectStore = ObjectStore()
46-
obj_store.add(aas)
47-
obj_store.add(submodel)
72+
model.File(id_short="documentationFile", content_type="application/pdf", value=actual_file_name)
4873

74+
file = model.File(id_short="documentationFile", content_type="application/pdf", value=actual_file_name)
75+
if submodel.submodel_elements is not None:
76+
submodel.submodel_elements.append(file)
4977

50-
# Serialize to a JSON-able mapping
51-
jsonable = aas_jsonization.to_jsonable(submodel)
5278

79+
######################################################################
80+
# Step 2: Writing AAS objects and auxiliary files to an AASX package #
81+
######################################################################
5382

54-
meta_data = pyecma376_2.OPCCoreProperties()
55-
meta_data.creator = "Chair of Process Control Engineering"
56-
meta_data.created = datetime.datetime.now()
83+
# After setting everything up in Step 1, writing the AAS, including the Submodel objects and the auxiliary file
84+
# to an AASX package is simple.
5785

86+
# Open an AASXWriter with the destination file name and use it as a context handler, to make sure it is properly closed
87+
# after doing the modifications:
5888
with AASXWriter("./MyAASXPackage.aasx") as writer:
59-
writer.write_aas(aas_ids=["urn:x-test:aas1"],
60-
object_store=obj_store,
61-
file_store=file_store,
62-
write_json=False)
89+
# Write the AAS and everything belonging to it to the AASX package
90+
# The `write_aas()` method will automatically fetch the AAS object with the given id
91+
# and all referenced Submodel objects from the ObjectStore. It will also scan every object for
92+
# semanticIds referencing ConceptDescription, fetch them from the ObjectStore, and scan all sbmodels for `File`
93+
# objects and fetch the referenced auxiliary files from the SupplementaryFileContainer.
94+
# In order to add more than one AAS to the package, we can simply add more Identifiers to the `aas_ids` list.
95+
#
96+
# ATTENTION: As of Version 3.0 RC01 of Details of the Asset Administration Shell, it is no longer valid to add more
97+
# than one "aas-spec" part (JSON/XML part with AAS objects) to an AASX package. Thus, `write_aas` MUST
98+
# only be called once per AASX package!
99+
writer.write_aas(aas_ids=['https://acplt.org/Simple_AAS'],
100+
object_store=object_store,
101+
file_store=file_store)
102+
103+
# Alternatively, we can use a more low-level interface to add a JSON/XML part with any Identifiable objects (not
104+
# only an AAS and referenced objects) in the AASX package manually. `write_aas_objects()` will also take care of
105+
# adding referenced auxiliary files by scanning all submodel objects for contained `File` objects.
106+
#
107+
# ATTENTION: As of Version 3.0 RC01 of Details of the Asset Administration Shell, it is no longer valid to add more
108+
# than one "aas-spec" part (JSON/XML part with AAS objects) to an AASX package. Thus, `write_all_aas_objects` SHALL
109+
# only be used as an alternative to `write_aas` and SHALL only be called once!
110+
objects_to_be_written: ObjectStore[model.Identifiable] = ObjectStore([unrelated_submodel])
111+
writer.write_all_aas_objects(part_name="/aasx/my_aas_part.xml",
112+
objects=objects_to_be_written,
113+
file_store=file_store)
114+
115+
# We can also add a thumbnail image to the package (using `writer.write_thumbnail()`) or add metadata:
116+
meta_data = pyecma376_2.OPCCoreProperties()
117+
meta_data.creator = "Chair of Process Control Engineering"
118+
meta_data.created = datetime.datetime.now()
63119
writer.write_core_properties(meta_data)
64120

121+
# Closing the AASXWriter will write some required parts with relationships and MIME types to the AASX package file and
122+
# close the package file afterward. Make sure, to always call `AASXWriter.close()` or use the AASXWriter in a `with`
123+
# statement (as a context manager) as shown above.
124+
125+
126+
########################################################################
127+
# Step 3: Reading AAS objects and auxiliary files from an AASX package #
128+
########################################################################
129+
130+
# Let's read the AASX package file, we have just written.
131+
# We'll use a fresh ObjectStore and SupplementaryFileContainer to read AAS objects and auxiliary files into.
65132
new_object_store: ObjectStore = ObjectStore()
66133
new_file_store = DictSupplementaryFileContainer()
67134

68-
with AASXReader("./MyAASXPackage.aasx") as reader:
135+
# Again, we need to use the AASXReader as a context manager (or call `.close()` in the end) to make sure the AASX
136+
# package file is properly closed when we are finished.
137+
with AASXReader("MyAASXPackage.aasx") as reader:
69138
# Read all contained AAS objects and all referenced auxiliary files
70139
reader.read_into(object_store=new_object_store,
71140
file_store=new_file_store)
72141

73-
print(new_object_store.__len__())
74-
for item in file_store.__iter__():
75-
print(item)
142+
# We can also read the metadata
143+
new_meta_data = reader.get_core_properties()
144+
145+
# We could also read the thumbnail image, using `reader.get_thumbnail()`
146+
76147

77-
for item in new_file_store.__iter__():
78-
print(item)
148+
# Some quick checks to make sure, reading worked as expected
149+
assert 'https://acplt.org/Submodel' in new_object_store
150+
assert actual_file_name in new_file_store
151+
assert new_meta_data.creator == "Chair of Process Control Engineering"

sdk/basyx/tutorial/tutorial_objectstore.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#
66
# SPDX-License-Identifier: MIT
77

8-
from basyx.objectstore import ObjectStore
8+
from basyx.object_store import ObjectStore
99
from aas_core3.types import Identifiable, AssetAdministrationShell, AssetInformation, AssetKind
1010
import aas_core3.types as aas_types
1111

0 commit comments

Comments
 (0)