Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
103 changes: 71 additions & 32 deletions sdk/basyx/aas/adapter/aasx.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
RELATIONSHIP_TYPE_AAS_SUPL = "http://admin-shell.io/aasx/relationships/aas-suppl"


class AASXReader:
class AASXReader():
Comment thread
Frosty2500 marked this conversation as resolved.
Outdated
"""
An AASXReader wraps an existing AASX package file to allow reading its contents and metadata.

Expand All @@ -60,7 +60,7 @@ class AASXReader:
reader.read_into(objects, files)

"""
def __init__(self, file: Union[os.PathLike, str, IO]):
def __init__(self, file: Union[os.PathLike, str, IO], failsafe: bool = True):
"""
Open an AASX reader for the given filename or file handle

Expand All @@ -69,9 +69,12 @@ 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
: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.
:raises FileNotFoundError: If the file does not exist
:raises ValueError: If the file is not a valid OPC zip package
"""
self.failsafe = failsafe
Comment thread
Frosty2500 marked this conversation as resolved.
Outdated
try:
logger.debug("Opening {} as AASX pacakge for reading ...".format(file))
self.reader = pyecma376_2.ZipPackageReader(file)
Expand Down Expand Up @@ -159,7 +162,10 @@ def read_into(self, object_store: model.AbstractObjectStore,
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")
if self.failsafe:
logger.warning("No AAS files found in AASX package")
else:
raise ValueError("No AAS files found in AASX package")

return read_identifiables

Expand Down Expand Up @@ -203,9 +209,12 @@ def _read_aas_part_into(self, part_name: str,
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
if self.failsafe:
logger.warning("Skipping {}, since an object with the same id is already contained in the "
"ObjectStore".format(obj))
Comment thread
Frosty2500 marked this conversation as resolved.
Outdated
continue
else:
raise ValueError("Object with id {} is already contained in the ObjectStore".format(obj))
object_store.add(obj)
read_identifiables.add(obj.id)
if isinstance(obj, model.Submodel):
Expand All @@ -225,15 +234,19 @@ 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, failsafe=self.failsafe, **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 read_aas_json_file(io.TextIOWrapper(p, encoding='utf-8-sig'), failsafe=self.failsafe, **kwargs)
else:
logger.error("Could not determine part format of AASX part {} (Content Type: {}, extension: {}"
.format(part_name, content_type, extension))
error_message = ("Could not determine part format of AASX part {} (Content Type: {}, extension: {}"
.format(part_name, content_type, extension))
if self.failsafe:
logger.error(error_message)
else:
raise ValueError(error_message)
return model.DictObjectStore()

def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel,
Expand Down Expand Up @@ -265,7 +278,7 @@ def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel,
element.value = final_name


class AASXWriter:
class AASXWriter():
Comment thread
Frosty2500 marked this conversation as resolved.
Outdated
"""
An AASXWriter wraps a new AASX package file to write its contents to it piece by piece.

Expand Down Expand Up @@ -295,16 +308,19 @@ class AASXWriter:
"""
AASX_ORIGIN_PART_NAME = "/aasx/aasx-origin"

def __init__(self, file: Union[os.PathLike, str, IO]):
def __init__(self, file: Union[os.PathLike, str, IO], failsafe: bool = True):
"""
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 failsafe: If ``True``, the document is written in a failsafe way: Missing attributes and elements are
logged instead of causing exceptions. Defect objects are skipped.
:param file: filename, path, or binary file handle opened for writing
"""
self.failsafe = failsafe
Comment thread
Frosty2500 marked this conversation as resolved.
Outdated
# names of aas-spec parts, used by `_write_aasx_origin_relationships()`
self._aas_part_names: List[str] = []
# name of the thumbnail part (if any)
Expand Down Expand Up @@ -378,12 +394,15 @@ def write_aas(self,
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 AssetAdministrationShell object but to "
f"{aas!r}")
if not isinstance(aas, model.AssetAdministrationShell):
raise TypeError(f"Identifier {aas_id} does not belong to an AssetAdministrationShell object but to "
f"{aas!r}")
except (KeyError, TypeError) as e:
if self.failsafe:
logger.error(f"Skipping AAS {aas_id}: {e}")
continue
else:
raise

# Add the AssetAdministrationShell object to the data part
objects_to_be_written.add(aas)
Expand All @@ -393,8 +412,11 @@ def write_aas(self,
try:
submodel = submodel_ref.resolve(object_store)
except KeyError:
logger.warning("Could not find submodel %s. Skipping it.", str(submodel_ref))
continue
if self.failsafe:
logger.warning("Could not find submodel %s. Skipping it.", str(submodel_ref))
Comment thread
Frosty2500 marked this conversation as resolved.
Outdated
continue
else:
raise KeyError(f"Could not find submodel {submodel_ref!r}")
objects_to_be_written.add(submodel)

# Traverse object tree and check if semanticIds are referencing to existing ConceptDescriptions in the
Expand All @@ -410,13 +432,20 @@ def write_aas(self,
try:
cd = semantic_id.resolve(object_store)
except KeyError:
logger.warning("ConceptDescription for semanticId %s not found in object store. Skipping it.",
str(semantic_id))
continue
if self.failsafe:
logger.warning("ConceptDescription for semanticId %s not found in object store. Skipping it.",
str(semantic_id))
continue
else:
raise KeyError(f"ConceptDescription for semanticId {semantic_id!r} not found in object store.")
except model.UnexpectedTypeError as e:
Comment thread
Frosty2500 marked this conversation as resolved.
logger.error("semanticId %s resolves to %s, which is not a ConceptDescription. Skipping it.",
str(semantic_id), e.value)
continue
if self.failsafe:
logger.error("semanticId %s resolves to %s, which is not a ConceptDescription. Skipping it.",
str(semantic_id), e.value)
continue
else:
raise TypeError(f"semanticId {semantic_id!r} resolves to {e.value!r}, which is not a"
f" ConceptDescription.") from e
concept_descriptions.append(cd)
objects_to_be_written.update(concept_descriptions)

Expand Down Expand Up @@ -474,8 +503,11 @@ def write_aas_objects(self,
try:
the_object = object_store.get_identifiable(identifier)
except KeyError:
logger.error("Could not find object {} in ObjectStore".format(identifier))
continue
if self.failsafe:
logger.error("Could not find object {} in ObjectStore".format(identifier))
continue
else:
raise KeyError(f"Could not find object {identifier!r} in ObjectStore")
objects.add(the_object)

self.write_all_aas_objects(part_name, objects, file_store, write_json, split_part, additional_relationships)
Expand Down Expand Up @@ -550,14 +582,21 @@ def write_all_aas_objects(self,
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
if self.failsafe:
logger.warning("Could not find file {} in file store.".format(file_name))
continue
else:
raise KeyError(f"Could not find file {file_name} in file store.")
Comment thread
Frosty2500 marked this conversation as resolved.
Outdated
# 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))
if self.failsafe:
logger.error("Trying to write supplementary file {} to AASX twice with different contents"
.format(file_name))
else:
raise ValueError(f"Trying to write supplementary file {file_name} to AASX twice with"
f" different contents")
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)
Expand Down
6 changes: 4 additions & 2 deletions sdk/basyx/aas/adapter/json/json_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,13 +885,15 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathO
return ret


def read_aas_json_file(file: PathOrIO, **kwargs) -> model.DictObjectStore[model.Identifiable]:
def read_aas_json_file(file: PathOrIO, failsafe: bool = True, **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
: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 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.
: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**:
Expand All @@ -901,5 +903,5 @@ def read_aas_json_file(file: PathOrIO, **kwargs) -> model.DictObjectStore[model.
: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)
read_aas_json_file_into(object_store, file, failsafe=failsafe, **kwargs)
return object_store
7 changes: 5 additions & 2 deletions sdk/basyx/aas/adapter/xml/xml_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -1502,13 +1502,16 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif
return ret


def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]:
def read_aas_xml_file(file: PathOrIO, failsafe: bool = True, **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
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 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.
: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
Expand All @@ -1519,5 +1522,5 @@ def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> model.DictObjectStore[mo
: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)
read_aas_xml_file_into(object_store, file, failsafe=failsafe, **kwargs)
return object_store