Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 59 additions & 28 deletions sdk/basyx/aas/adapter/aasx.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ def _read_aas_part_into(self, part_name: str,
read_identifiables.add(obj.id)
if isinstance(obj, model.Submodel):
self._collect_supplementary_files(part_name, obj, file_store)
elif isinstance(obj, model.AssetAdministrationShell):
self._collect_supplementary_files(part_name, obj, file_store)

def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore:
"""
Expand Down Expand Up @@ -261,33 +263,55 @@ def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore:
raise ValueError(error_message)
return model.DictObjectStore()

def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel,
def _collect_supplementary_files(self, part_name: str, root_element: Union[model.AssetAdministrationShell, 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.
Helper function to search File objects within a single parsed AssetAdministrationShell or Submodel.
Resolve their absolute paths, and update the corresponding File/Thumbnail objects 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
:param part_name: The OPC part name of the part the root_element has been parsed from. This is used to resolve
relative file paths.
:param submodel: The Submodel to process
:param root_element: The AssetAdministrationShell or 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(f"Skipping supplementary file {element.value}, since it seems to be an absolute URI or "
f"network-path URI reference")
continue
absolute_name = pyecma376_2.package_model.part_realpath(element.value, part_name)
logger.debug(f"Reading supplementary file {absolute_name} from AASX package ...")
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
if isinstance(root_element, model.AssetAdministrationShell):
if (root_element.asset_information.default_thumbnail and
root_element.asset_information.default_thumbnail.path):
file_name = self._add_supplementary_file(part_name, root_element.asset_information.default_thumbnail.path, file_store)
if file_name:
root_element.asset_information.default_thumbnail.path = file_name
if isinstance(root_element, model.Submodel):
for element in traversal.walk_submodel(root_element):
if isinstance(element, model.File):
if element.value is None:
continue
final_name = self._add_supplementary_file(part_name, element.value, file_store)
if final_name:
element.value = final_name

def _add_supplementary_file(self, part_name: str, file_path: str,
file_store: "AbstractSupplementaryFileContainer") -> Optional[str]:
"""
Helper function to extract a single referenced supplementary file and return the absolute path within the AASX package.

:param part_name: The OPC part name of the part the root_element has been parsed from. This is used to resolve
relative file paths.
:param file_path: The file path or URI reference of the supplementary file to be extracted
:param file_store: The SupplementaryFileContainer to add the extracted supplementary files to
:return: The stored file name as returned by *file_store*, or ``None`` if the reference was skipped.
"""
# 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 file_path.startswith('//') or ':' in file_path.split('/')[0]:
logger.info(f"Skipping supplementary file {file_path}, since it seems to be an absolute URI or network-path URI reference")
return None
absolute_name = pyecma376_2.package_model.part_realpath(file_path, part_name)
logger.debug(f"Reading supplementary file {absolute_name} from AASX package ...")
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))
return final_name


class AASXWriter:
Expand Down Expand Up @@ -541,7 +565,8 @@ def write_all_aas_objects(self,
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.
and added to the AASX package. If the ObjectStore contains a thumbnail referenced by
:class:`~basyx.aas.model.asset.AssetInformation.default_thumbnail`, it is also added to the AASX package.

.. attention::

Expand All @@ -563,17 +588,23 @@ def write_all_aas_objects(self,
logger.debug(f"Writing AASX part {part_name} with AAS objects ...")
supplementary_files: List[str] = []

def _collect_supplementary_file(file_name: str) -> None:
# 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]:
return
supplementary_files.append(file_name)

# Retrieve objects and scan for referenced supplementary files
for the_object in objects:
if isinstance(the_object, model.AssetAdministrationShell):
if (the_object.asset_information.default_thumbnail and
the_object.asset_information.default_thumbnail.path):
_collect_supplementary_file(the_object.asset_information.default_thumbnail.path)
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)
_collect_supplementary_file(element.value)

# Add aas-spec relationship
if not split_part:
Expand Down
Loading