3434import json
3535import logging
3636import pprint
37- from typing import Dict , Callable , ContextManager , TypeVar , Type , List , IO , Optional , Set , get_args
37+ from typing import (Dict , Callable , ContextManager , TypeVar , Type ,
38+ List , IO , Optional , Set , get_args , Tuple , Iterable , Any )
3839
3940from basyx .aas import model
4041from .._generic import MODELLING_KIND_INVERSE , ASSET_KIND_INVERSE , KEY_TYPES_INVERSE , ENTITY_TYPES_INVERSE , \
4142 IEC61360_DATA_TYPES_INVERSE , IEC61360_LEVEL_TYPES_INVERSE , KEY_TYPES_CLASSES_INVERSE , REFERENCE_TYPES_INVERSE , \
42- DIRECTION_INVERSE , STATE_OF_EVENT_INVERSE , QUALIFIER_KIND_INVERSE , PathOrIO , Path
43+ DIRECTION_INVERSE , STATE_OF_EVENT_INVERSE , QUALIFIER_KIND_INVERSE , PathOrIO , Path , JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES
4344
4445logger = logging .getLogger (__name__ )
4546
@@ -154,19 +155,20 @@ def __init__(self, *args, **kwargs):
154155 json .JSONDecoder .__init__ (self , object_hook = self .object_hook , * args , ** kwargs )
155156
156157 @classmethod
157- def object_hook (cls , dct : Dict [str , object ]) -> object :
158- # Check if JSON object seems to be a deserializable AAS object (i.e. it has a modelType). Otherwise, the JSON
159- # object is returned as is, so it's possible to mix AAS objects with other data within a JSON structure.
160- if 'modelType' not in dct :
161- return dct
158+ def _get_aas_class_parsers (cls ) -> Dict [str , Callable [[Dict [str , object ]], object ]]:
159+ """
160+ Returns the dictionary of AAS class parsers.
161+
162+ The following dict specifies a constructor method for all AAS classes that may be identified using the
163+ ``modelType`` attribute in their JSON representation. Each of those constructor functions takes the JSON
164+ representation of an object and tries to construct a Python object from it. Embedded objects that have a
165+ modelType themselves are expected to be converted to the correct PythonType already. Additionally, each
166+ function takes a bool parameter ``failsafe``, which indicates weather to log errors and skip defective objects
167+ instead of raising an Exception.
162168
163- # The following dict specifies a constructor method for all AAS classes that may be identified using the
164- # ``modelType`` attribute in their JSON representation. Each of those constructor functions takes the JSON
165- # representation of an object and tries to construct a Python object from it. Embedded objects that have a
166- # modelType themselves are expected to be converted to the correct PythonType already. Additionally, each
167- # function takes a bool parameter ``failsafe``, which indicates weather to log errors and skip defective objects
168- # instead of raising an Exception.
169- AAS_CLASS_PARSERS : Dict [str , Callable [[Dict [str , object ]], object ]] = {
169+ :return: The dictionary of AAS class parsers
170+ """
171+ aas_class_parsers : Dict [str , Callable [[Dict [str , object ]], object ]] = {
170172 'AssetAdministrationShell' : cls ._construct_asset_administration_shell ,
171173 'AssetInformation' : cls ._construct_asset_information ,
172174 'SpecificAssetId' : cls ._construct_specific_asset_id ,
@@ -189,6 +191,16 @@ def object_hook(cls, dct: Dict[str, object]) -> object:
189191 'ReferenceElement' : cls ._construct_reference_element ,
190192 'DataSpecificationIec61360' : cls ._construct_data_specification_iec61360 ,
191193 }
194+ return aas_class_parsers
195+
196+ @classmethod
197+ def object_hook (cls , dct : Dict [str , object ]) -> object :
198+ # Check if JSON object seems to be a deserializable AAS object (i.e. it has a modelType). Otherwise, the JSON
199+ # object is returned as is, so it's possible to mix AAS objects with other data within a JSON structure.
200+ if 'modelType' not in dct :
201+ return dct
202+
203+ AAS_CLASS_PARSERS = cls ._get_aas_class_parsers ()
192204
193205 # Get modelType and constructor function
194206 if not isinstance (dct ['modelType' ], str ):
@@ -799,7 +811,9 @@ def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFr
799811
800812def read_aas_json_file_into (object_store : model .AbstractObjectStore , file : PathOrIO , replace_existing : bool = False ,
801813 ignore_existing : bool = False , failsafe : bool = True , stripped : bool = False ,
802- decoder : Optional [Type [AASFromJsonDecoder ]] = None ) -> Set [model .Identifier ]:
814+ decoder : Optional [Type [AASFromJsonDecoder ]] = None ,
815+ keys_to_types : Iterable [Tuple [str , Any ]] = JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES ) \
816+ -> Set [model .Identifier ]:
803817 """
804818 Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5
805819 into a given object store.
@@ -817,6 +831,7 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathO
817831 See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91
818832 This parameter is ignored if a decoder class is specified.
819833 :param decoder: The decoder class used to decode the JSON objects
834+ :param keys_to_types: A dictionary of JSON keys to expected types. This is used to check the type of the objects
820835 :raises KeyError: **Non-failsafe**: Encountered a duplicate identifier
821836 :raises KeyError: Encountered an identifier that already exists in the given ``object_store`` with both
822837 ``replace_existing`` and ``ignore_existing`` set to ``False``
@@ -843,45 +858,43 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathO
843858 with cm as fp :
844859 data = json .load (fp , cls = decoder_ )
845860
846- for name , expected_type in (('assetAdministrationShells' , model .AssetAdministrationShell ),
847- ('submodels' , model .Submodel ),
848- ('conceptDescriptions' , model .ConceptDescription )):
861+ for name , expected_type in keys_to_types :
849862 try :
850863 lst = _get_ts (data , name , list )
851864 except (KeyError , TypeError ):
852865 continue
853866
854867 for item in lst :
855- error_message = "Expected a {} in list '{}', but found {}" .format (
856- expected_type .__name__ , name , repr (item ))
868+ error_msg = f"Expected a { expected_type .__name__ } in list '{ name } ', but found { repr (item )} ."
857869 if isinstance (item , model .Identifiable ):
858870 if not isinstance (item , expected_type ):
859- if decoder_ .failsafe :
860- logger . warning ( "{ } was in wrong list '{}'; nevertheless, we'll use it" . format ( item , name ) )
861- else :
862- raise TypeError ( error_message )
871+ if not decoder_ .failsafe :
872+ raise TypeError ( f" { item } was in the wrong list '{ name } '" )
873+ logger . warning ( f" { item } was in the wrong list ' { name } '; nevertheless, we'll use it" )
874+
863875 if item .id in ret :
864- error_message = f"{ item } has a duplicate identifier already parsed in the document!"
876+ error_msg = f"{ item } has a duplicate identifier already parsed in the document!"
865877 if not decoder_ .failsafe :
866- raise KeyError (error_message )
867- logger .error (error_message + " skipping it..." )
878+ raise KeyError (error_msg )
879+ logger .error (f" { error_msg } Skipping it..." )
868880 continue
881+
869882 existing_element = object_store .get (item .id )
870883 if existing_element is not None :
871884 if not replace_existing :
872- error_message = f"object with identifier { item .id } already exists " \
873- f"in the object store: { existing_element } !"
885+ error_msg = f"Object with id '{ item .id } ' already exists in store: { existing_element } !"
874886 if not ignore_existing :
875- raise KeyError (error_message + f" failed to insert { item } !" )
876- logger .info (error_message + f" skipping insertion of { item } ..." )
887+ raise KeyError (f" { error_msg } Failed to insert { item } !" )
888+ logger .info (f" { error_msg } Skipping { item } ..." )
877889 continue
878890 object_store .discard (existing_element )
891+
879892 object_store .add (item )
880893 ret .add (item .id )
881894 elif decoder_ .failsafe :
882- logger .error (error_message )
895+ logger .error (f" { error_msg } Skipping it..." )
883896 else :
884- raise TypeError (error_message )
897+ raise TypeError (error_msg )
885898 return ret
886899
887900
0 commit comments