2828import itertools
2929import logging
3030import os
31- import re
3231from typing import Dict , Tuple , IO , Union , List , Set , Optional , Iterable , Iterator
3332
3433from .xml import read_aas_xml_file , write_aas_xml_file
@@ -60,7 +59,7 @@ class AASXReader:
6059 reader.read_into(objects, files)
6160
6261 """
63- def __init__ (self , file : Union [os .PathLike , str , IO ]):
62+ def __init__ (self , file : Union [os .PathLike , str , IO ], failsafe : bool = True ):
6463 """
6564 Open an AASX reader for the given filename or file handle
6665
@@ -69,16 +68,19 @@ def __init__(self, file: Union[os.PathLike, str, IO]):
6968 closing under any circumstances.
7069
7170 :param file: A filename, file path or an open file-like object in binary mode
71+ :param failsafe: If ``True``, the document is parsed in a failsafe way: Missing attributes and elements are
72+ logged instead of causing exceptions. Defect objects are skipped.
7273 :raises FileNotFoundError: If the file does not exist
7374 :raises ValueError: If the file is not a valid OPC zip package
7475 """
76+ self .failsafe : bool = failsafe
7577 try :
76- logger .debug ("Opening {} as AASX pacakge for reading ..." . format ( file ) )
78+ logger .debug (f "Opening { file } as AASX package for reading ..." )
7779 self .reader = pyecma376_2 .ZipPackageReader (file )
7880 except FileNotFoundError :
7981 raise
8082 except Exception as e :
81- raise ValueError ("{ } is not a valid ECMA376-2 (OPC) file: {}" . format ( file , e ) ) from e
83+ raise ValueError (f" { file } is not a valid ECMA376-2 (OPC) file: { e } " ) from e
8284
8385 def get_core_properties (self ) -> pyecma376_2 .OPCCoreProperties :
8486 """
@@ -132,7 +134,7 @@ def read_into(self, object_store: model.AbstractObjectStore,
132134 objects from the AASX file to
133135 :param file_store: A :class:`SupplementaryFileContainer <.AbstractSupplementaryFileContainer>` to add the
134136 embedded supplementary files to
135- :param override_existing: If ``True``, existing objects in the object store are overridden with objects from the
137+ :param override_existing: If ``True``, existing objects in the ObjectStore are overridden with objects from the
136138 AASX that have the same :class:`~basyx.aas.model.base.Identifier`. Default behavior is to skip those objects
137139 from the AASX.
138140 :return: A set of the :class:`Identifiers <basyx.aas.model.base.Identifier>` of all
@@ -172,7 +174,10 @@ def read_into(self, object_store: model.AbstractObjectStore,
172174 self ._read_aas_part_into (split_part , object_store , file_store ,
173175 read_identifiables , override_existing , ** kwargs )
174176 if no_aas_files_found :
175- logger .warning ("No AAS files found in AASX package" )
177+ if self .failsafe :
178+ logger .warning ("No AAS files found in AASX package" )
179+ else :
180+ raise ValueError ("No AAS files found in AASX package" )
176181
177182 return read_identifiables
178183
@@ -205,20 +210,23 @@ def _read_aas_part_into(self, part_name: str,
205210 from a File object of this part
206211 :param read_identifiables: A set of Identifiers of objects which have already been read. New objects'
207212 Identifiers are added to this set. Objects with already known Identifiers are skipped silently.
208- :param override_existing: If True, existing objects in the object store are overridden with objects from the
213+ :param override_existing: If True, existing objects in the ObjectStore are overridden with objects from the
209214 AASX that have the same Identifier. Default behavior is to skip those objects from the AASX.
210215 """
211216 for obj in self ._parse_aas_part (part_name , ** kwargs ):
212217 if obj .id in read_identifiables :
213218 continue
214219 if obj .id in object_store :
215220 if override_existing :
216- logger .info ("Overriding existing object in ObjectStore with {} ..." . format ( obj ) )
221+ logger .info (f "Overriding existing object in ObjectStore with { obj } ..." )
217222 object_store .discard (obj )
218223 else :
219- logger .warning ("Skipping {}, since an object with the same id is already contained in the "
220- "ObjectStore" .format (obj ))
221- continue
224+ if self .failsafe :
225+ logger .warning (f"Skipping { obj } , since an object with the same id is already contained in the "
226+ "ObjectStore" )
227+ continue
228+ else :
229+ raise ValueError (f"Object with id { obj } is already contained in the ObjectStore" )
222230 object_store .add (obj )
223231 read_identifiables .add (obj .id )
224232 if isinstance (obj , model .Submodel ):
@@ -236,17 +244,21 @@ def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore:
236244 content_type = self .reader .get_content_type (part_name )
237245 extension = part_name .split ("/" )[- 1 ].split ("." )[- 1 ]
238246 if content_type .split (";" )[0 ] in ("text/xml" , "application/xml" ) or content_type == "" and extension == "xml" :
239- logger .debug ("Parsing AAS objects from XML stream in OPC part {} ..." . format ( part_name ) )
247+ logger .debug (f "Parsing AAS objects from XML stream in OPC part { part_name } ..." )
240248 with self .reader .open_part (part_name ) as p :
241- return read_aas_xml_file (p , ** kwargs )
249+ return read_aas_xml_file (p , failsafe = self . failsafe , ** kwargs )
242250 elif content_type .split (";" )[0 ] in ("text/json" , "application/json" ) \
243251 or content_type == "" and extension == "json" :
244- logger .debug ("Parsing AAS objects from JSON stream in OPC part {} ..." . format ( part_name ) )
252+ logger .debug (f "Parsing AAS objects from JSON stream in OPC part { part_name } ..." )
245253 with self .reader .open_part (part_name ) as p :
246- return read_aas_json_file (io .TextIOWrapper (p , encoding = 'utf-8-sig' ), ** kwargs )
254+ return read_aas_json_file (io .TextIOWrapper (p , encoding = 'utf-8-sig' ), failsafe = self . failsafe , ** kwargs )
247255 else :
248- logger .error ("Could not determine part format of AASX part {} (Content Type: {}, extension: {}"
249- .format (part_name , content_type , extension ))
256+ error_message = (f"Could not determine part format of AASX part { part_name } (Content Type: { content_type } ,"
257+ f" extension: { extension } " )
258+ if self .failsafe :
259+ logger .error (error_message )
260+ else :
261+ raise ValueError (error_message )
250262 return model .DictObjectStore ()
251263
252264 def _collect_supplementary_files (self , part_name : str , submodel : model .Submodel ,
@@ -255,7 +267,7 @@ def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel,
255267 Helper function to search File objects within a single parsed Submodel, extract the referenced supplementary
256268 files and update the File object's values with the absolute path.
257269
258- :param part_name: The OPC part name of the part the submodel has been parsed from. This is used to resolve
270+ :param part_name: The OPC part name of the part the Submodel has been parsed from. This is used to resolve
259271 relative file paths.
260272 :param submodel: The Submodel to process
261273 :param file_store: The SupplementaryFileContainer to add the extracted supplementary files to
@@ -268,11 +280,11 @@ def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel,
268280 # to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute
269281 # URIs and network-path references)
270282 if element .value .startswith ('//' ) or ':' in element .value .split ('/' )[0 ]:
271- logger .info ("Skipping supplementary file %s , since it seems to be an absolute URI or network-path "
272- " URI reference", element . value )
283+ logger .info (f "Skipping supplementary file { element . value } , since it seems to be an absolute URI or "
284+ f"network-path URI reference" )
273285 continue
274286 absolute_name = pyecma376_2 .package_model .part_realpath (element .value , part_name )
275- logger .debug ("Reading supplementary file {} from AASX package ..." . format ( absolute_name ) )
287+ logger .debug (f "Reading supplementary file { absolute_name } from AASX package ..." )
276288 with self .reader .open_part (absolute_name ) as p :
277289 final_name = file_store .add_file (absolute_name , p , self .reader .get_content_type (absolute_name ))
278290 element .value = final_name
@@ -308,16 +320,19 @@ class AASXWriter:
308320 """
309321 AASX_ORIGIN_PART_NAME = "/aasx/aasx-origin"
310322
311- def __init__ (self , file : Union [os .PathLike , str , IO ]):
323+ def __init__ (self , file : Union [os .PathLike , str , IO ], failsafe : bool = True ):
312324 """
313325 Create a new AASX package in the given file and open the AASXWriter to add contents to the package.
314326
315327 Make sure to call ``AASXWriter.close()`` after writing all contents to write the aas-spec relationships for all
316328 AAS parts to the file and close the underlying ZIP file writer. You may also use the AASXWriter as a context
317329 manager to ensure closing under any circumstances.
318330
331+ :param failsafe: If ``True``, the document is written in a failsafe way: Missing attributes and elements are
332+ logged instead of causing exceptions. Defect objects are skipped.
319333 :param file: filename, path, or binary file handle opened for writing
320334 """
335+ self .failsafe : bool = failsafe
321336 # names of aas-spec parts, used by `_write_aasx_origin_relationships()`
322337 self ._aas_part_names : List [str ] = []
323338 # name of the thumbnail part (if any)
@@ -377,7 +392,7 @@ def write_aas(self,
377392 :param write_json: If ``True``, JSON parts are created for the AAS and each
378393 :class:`~basyx.aas.model.submodel.Submodel` in the AASX package file instead of XML parts.
379394 Defaults to ``False``.
380- :raises KeyError: If one of the AAS could not be retrieved from the object store (unresolvable
395+ :raises KeyError: If one of the AAS could not be retrieved from the ObjectStore (unresolvable
381396 :class:`Submodels <basyx.aas.model.submodel.Submodel>` and
382397 :class:`ConceptDescriptions <basyx.aas.model.concept.ConceptDescription>` are skipped, logging a
383398 warning/info message)
@@ -391,12 +406,15 @@ def write_aas(self,
391406 for aas_id in aas_ids :
392407 try :
393408 aas = object_store .get_identifiable (aas_id )
394- # TODO add failsafe mode
395- except KeyError :
396- raise
397- if not isinstance (aas , model .AssetAdministrationShell ):
398- raise TypeError (f"Identifier { aas_id } does not belong to an AssetAdministrationShell object but to "
399- f"{ aas !r} " )
409+ if not isinstance (aas , model .AssetAdministrationShell ):
410+ raise TypeError (f"Identifier { aas_id } does not belong to an AssetAdministrationShell object but to "
411+ f"{ aas !r} " )
412+ except (KeyError , TypeError ) as e :
413+ if self .failsafe :
414+ logger .error (f"Skipping AAS { aas_id } : { e } " )
415+ continue
416+ else :
417+ raise
400418
401419 # Add the AssetAdministrationShell object to the data part
402420 objects_to_be_written .add (aas )
@@ -406,8 +424,11 @@ def write_aas(self,
406424 try :
407425 submodel = submodel_ref .resolve (object_store )
408426 except KeyError :
409- logger .warning ("Could not find submodel %s. Skipping it." , str (submodel_ref ))
410- continue
427+ if self .failsafe :
428+ logger .warning (f"Could not find Submodel { submodel_ref } . Skipping it." )
429+ continue
430+ else :
431+ raise KeyError (f"Could not find Submodel { submodel_ref !r} " )
411432 objects_to_be_written .add (submodel )
412433
413434 # Traverse object tree and check if semanticIds are referencing to existing ConceptDescriptions in the
@@ -423,13 +444,20 @@ def write_aas(self,
423444 try :
424445 cd = semantic_id .resolve (object_store )
425446 except KeyError :
426- logger .warning ("ConceptDescription for semanticId %s not found in object store. Skipping it." ,
427- str (semantic_id ))
428- continue
447+ if self .failsafe :
448+ logger .warning (f"ConceptDescription for semanticId { semantic_id } not found in ObjectStore. "
449+ f"Skipping it." )
450+ continue
451+ else :
452+ raise KeyError (f"ConceptDescription for semanticId { semantic_id !r} not found in ObjectStore." )
429453 except model .UnexpectedTypeError as e :
430- logger .error ("semanticId %s resolves to %s, which is not a ConceptDescription. Skipping it." ,
431- str (semantic_id ), e .value )
432- continue
454+ if self .failsafe :
455+ logger .error (f"semanticId { semantic_id } resolves to { e .value } , "
456+ f"which is not a ConceptDescription. Skipping it." )
457+ continue
458+ else :
459+ raise TypeError (f"semanticId { semantic_id !r} resolves to { e .value !r} , which is not a"
460+ f" ConceptDescription." ) from e
433461 concept_descriptions .append (cd )
434462 objects_to_be_written .update (concept_descriptions )
435463
@@ -453,7 +481,7 @@ def write_aas_objects(self,
453481 This method takes the AAS's :class:`~basyx.aas.model.base.Identifier` (as ``aas_id``) to retrieve it
454482 from the given object_store. If the list of written objects includes :class:`~basyx.aas.model.submodel.Submodel`
455483 objects, Supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within
456- those submodels , are also added to the AASX package.
484+ those Submodels , are also added to the AASX package.
457485
458486 .. attention::
459487
@@ -478,7 +506,7 @@ def write_aas_objects(self,
478506 :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object
479507 part to be written, in addition to the aas-suppl relationships which are created automatically.
480508 """
481- logger .debug ("Writing AASX part {} with AAS objects ..." . format ( part_name ) )
509+ logger .debug (f "Writing AASX part { part_name } with AAS objects ..." )
482510
483511 objects : model .DictObjectStore [model .Identifiable ] = model .DictObjectStore ()
484512
@@ -487,8 +515,11 @@ def write_aas_objects(self,
487515 try :
488516 the_object = object_store .get_identifiable (identifier )
489517 except KeyError :
490- logger .error ("Could not find object {} in ObjectStore" .format (identifier ))
491- continue
518+ if self .failsafe :
519+ logger .error (f"Could not find object { identifier } in ObjectStore" )
520+ continue
521+ else :
522+ raise KeyError (f"Could not find object { identifier !r} in ObjectStore" )
492523 objects .add (the_object )
493524
494525 self .write_all_aas_objects (part_name , objects , file_store , write_json , split_part , additional_relationships )
@@ -529,7 +560,7 @@ def write_all_aas_objects(self,
529560 :param additional_relationships: Optional OPC/ECMA376 relationships which should originate at the AAS object
530561 part to be written, in addition to the aas-suppl relationships which are created automatically.
531562 """
532- logger .debug ("Writing AASX part {} with AAS objects ..." . format ( part_name ) )
563+ logger .debug (f "Writing AASX part { part_name } with AAS objects ..." )
533564 supplementary_files : List [str ] = []
534565
535566 # Retrieve objects and scan for referenced supplementary files
@@ -556,29 +587,36 @@ def write_all_aas_objects(self,
556587 else :
557588 write_aas_xml_file (p , objects )
558589
559- # Write submodel 's supplementary files to AASX file
590+ # Write Submodel 's supplementary files to AASX file
560591 supplementary_file_names = []
561592 for file_name in supplementary_files :
562593 try :
563594 content_type = file_store .get_content_type (file_name )
564595 hash = file_store .get_sha256 (file_name )
565596 except KeyError :
566- logger .warning ("Could not find file {} in file store." .format (file_name ))
567- continue
597+ if self .failsafe :
598+ logger .warning (f"Could not find file { file_name } in FileStore." )
599+ continue
600+ else :
601+ raise KeyError (f"Could not find file { file_name } in FileStore." )
568602 # Check if this supplementary file has already been written to the AASX package or has a name conflict
569603 if self ._supplementary_part_names .get (file_name ) == hash :
570604 continue
571605 elif file_name in self ._supplementary_part_names :
572- logger .error ("Trying to write supplementary file {} to AASX twice with different contents"
573- .format (file_name ))
574- logger .debug ("Writing supplementary file {} to AASX package ..." .format (file_name ))
606+ if self .failsafe :
607+ logger .error (f"Trying to write supplementary file { file_name } to AASX "
608+ f"twice with different contents" )
609+ else :
610+ raise ValueError (f"Trying to write supplementary file { file_name } to AASX twice with"
611+ f" different contents" )
612+ logger .debug (f"Writing supplementary file { file_name } to AASX package ..." )
575613 with self .writer .open_part (file_name , content_type ) as p :
576614 file_store .write_file (file_name , p )
577615 supplementary_file_names .append (pyecma376_2 .package_model .normalize_part_name (file_name ))
578616 self ._supplementary_part_names [file_name ] = hash
579617
580- # Add relationships from submodel to supplementary parts
581- logger .debug ("Writing aas-suppl relationships for AAS object part {} to AASX package ..." . format ( part_name ) )
618+ # Add relationships from Submodel to supplementary parts
619+ logger .debug (f "Writing aas-suppl relationships for AAS object part { part_name } to AASX package ..." )
582620 self .writer .write_relationships (
583621 itertools .chain (
584622 (pyecma376_2 .OPCRelationship ("r{}" .format (i ),
@@ -617,7 +655,7 @@ def write_thumbnail(self, name: str, data: bytearray, content_type: str):
617655 :param content_type: OPC content type (MIME type) of the image file
618656 """
619657 if self ._thumbnail_part is not None :
620- raise RuntimeError ("package thumbnail has already been written to {}." . format ( self ._thumbnail_part ) )
658+ raise RuntimeError (f "package thumbnail has already been written to { self ._thumbnail_part } ." )
621659 with self .writer .open_part (name , content_type ) as p :
622660 p .write (data )
623661 self ._thumbnail_part = name
0 commit comments