From 5f001dfa6056590db8881f9f453fea152a4771ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 18 Feb 2020 16:14:12 +0100 Subject: [PATCH 01/27] add first xml deserialization working draft --- aas/adapter/xml/xml_deserialization.py | 115 +++++++++++++++++++++++++ aas/adapter/xml/xml_serialization.py | 22 +++++ 2 files changed, 137 insertions(+) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index c2c4c59..47499aa 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -11,3 +11,118 @@ """ Module for deserializing Asset Administration Shell data from the official XML format """ + +# TODO: add more constructors +# TODO: implement error handling / failsafe parsing +# TODO: add better (more useful) helper functions + +from ... import model +import xml.etree.ElementTree as ElTree + +from typing import Dict, IO, Optional +from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI, MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ + KEY_TYPES, IDENTIFIER_TYPES, ENTITY_TYPES, IEC61360_DATA_TYPES, IEC61360_LEVEL_TYPES + +MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} +ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} +KEY_ELEMENTS_INVERSE: Dict[str, model.KeyElements] = {v: k for k, v in KEY_ELEMENTS.items()} +KEY_TYPES_INVERSE: Dict[str, model.KeyType] = {v: k for k, v in KEY_TYPES.items()} +IDENTIFIER_TYPES_INVERSE: Dict[str, model.IdentifierType] = {v: k for k, v in IDENTIFIER_TYPES.items()} +ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()} +KEY_ELEMENTS_CLASSES_INVERSE: Dict[model.KeyElements, type] = {v: k for k, v in model.KEY_ELEMENTS_CLASSES.items()} +IEC61360_DATA_TYPES_INVERSE: Dict[str, model.concept.IEC61360DataType] = {v: k for k, v in IEC61360_DATA_TYPES.items()} +IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.concept.IEC61360LevelType] = \ + {v: k for k, v in IEC61360_LEVEL_TYPES.items()} + + +def _get_text_or_none(_dict: Dict[str, ElTree.Element], key: str) -> Optional[str]: + return _dict[key].text if key in _dict else None + + +def construct_key(element: ElTree.Element) -> model.Key: + return model.Key( + KEY_ELEMENTS_INVERSE[element.attrib["type"]], + element.attrib["local"] == "True", + element.text, + KEY_TYPES_INVERSE[element.attrib["idType"]] + ) + + +def construct_reference(element: ElTree.Element) -> model.Reference: + return model.Reference( + [construct_key(k) for k in element["keys"]] + ) + + +def construct_administrative_information(element: ElTree.Element) -> model.AdministrativeInformation: + return model.AdministrativeInformation( + _get_text_or_none(element, NS_AAS + "version"), + _get_text_or_none(element, NS_AAS + "revision") + ) + + +def construct_lang_string_set(element: ElTree.Element) -> model.LangStringSet: + lss = model.LangStringSet + for lang_string in element: + if lang_string.tag != NS_IEC + "langString" or not lang_string.attrib["lang"]: + continue + lss[lang_string.attrib["lang"]] = lang_string.text + return lss + + +def construct_qualifier(element: ElTree.Element) -> model.Qualifier: + q = model.Qualifier( + _get_text_or_none(element, "type"), + _get_text_or_none(element, "valueType"), + _get_text_or_none(element, "value"), + construct_reference(element["valueId"]) + ) + amend_abstract_attributes(q, element) + return q + + +def construct_formula(element: ElTree.Element) -> model.Formula: + return model.Formula( + set(construct_reference(ref) for ref in element) + ) + + +def construct_constraint(element: ElTree.Element) -> model.Constraint: + if element.tag == NS_AAS + "qualifier": + return construct_qualifier(element) + if element.tag == NS_AAS + "formula": + return construct_formula(element) + raise TypeError("Given element is neither a qualifier nor a formula!") + + +def amend_abstract_attributes(obj: object, element: ElTree.Element) -> None: + if isinstance(obj, model.Referable): + if NS_AAS + "category" in element: + obj.category = element[NS_AAS + "category"].text + if NS_AAS + "description" in element: + obj.description = construct_lang_string_set(element[NS_AAS + "description"]) + if isinstance(obj, model.Identifiable): + if NS_AAS + "idShort" in element: + obj.id_short = element[NS_AAS + "idShort"].text + if NS_AAS + "administration" in element: + obj.administration = construct_administrative_information(element[NS_AAS + "administration"]) + if isinstance(obj, model.HasSemantics): + if NS_AAS + "semanticId" in element: + obj.semantic_id = construct_reference(element[NS_AAS + "semanticId"]) + if isinstance(obj, model.Qualifiable): + if NS_AAS + "qualifiers" in element: + for constraint in element[NS_AAS + "qualifiers"]: + obj.qualifier.add(construct_constraint(constraint)) + + +def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: + """ + Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 + + :param file: A file-like object to read the XML-serialized data from + :param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception for missing + attributes and wrong types, errors are logged and defective objects are skipped + :return: A DictObjectStore containing all AAS objects from the XML file + """ + + diff --git a/aas/adapter/xml/xml_serialization.py b/aas/adapter/xml/xml_serialization.py index 6622417..464a673 100644 --- a/aas/adapter/xml/xml_serialization.py +++ b/aas/adapter/xml/xml_serialization.py @@ -136,6 +136,28 @@ ENTITY_TYPES: Dict[model.EntityType, str] = { model.EntityType.CO_MANAGED_ENTITY: 'CoManagedEntity', model.EntityType.SELF_MANAGED_ENTITY: 'SelfManagedEntity'} +IEC61360_DATA_TYPES: Dict[model.concept.IEC61360DataType, str] = { + model.concept.IEC61360DataType.DATE: 'DATE', + model.concept.IEC61360DataType.STRING: 'STRING', + model.concept.IEC61360DataType.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE', + model.concept.IEC61360DataType.REAL_MEASURE: 'REAL_MEASURE', + model.concept.IEC61360DataType.REAL_COUNT: 'REAL_COUNT', + model.concept.IEC61360DataType.REAL_CURRENCY: 'REAL_CURRENCY', + model.concept.IEC61360DataType.BOOLEAN: 'BOOLEAN', + model.concept.IEC61360DataType.URL: 'URL', + model.concept.IEC61360DataType.RATIONAL: 'RATIONAL', + model.concept.IEC61360DataType.RATIONAL_MEASURE: 'RATIONAL_MEASURE', + model.concept.IEC61360DataType.TIME: 'TIME', + model.concept.IEC61360DataType.TIMESTAMP: 'TIMESTAMP', +} + +IEC61360_LEVEL_TYPES: Dict[model.concept.IEC61360LevelType, str] = { + model.concept.IEC61360LevelType.MIN: 'Min', + model.concept.IEC61360LevelType.MAX: 'Max', + model.concept.IEC61360LevelType.NOM: 'Nom', + model.concept.IEC61360LevelType.TYP: 'Typ', +} + # ############################################################## # transformation functions to serialize abstract classes from model.base -- GitLab From 8d84710ce2cc276edd809686471f99d8a1b85149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 26 Feb 2020 20:13:15 +0100 Subject: [PATCH 02/27] adapter.xml: fix ci errors --- aas/adapter/xml/xml_deserialization.py | 150 ++++++++++++++++++------- 1 file changed, 109 insertions(+), 41 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 47499aa..6466835 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -18,11 +18,14 @@ Module for deserializing Asset Administration Shell data from the official XML f from ... import model import xml.etree.ElementTree as ElTree +import logging -from typing import Dict, IO, Optional +from typing import Dict, IO, Optional, Set, TypeVar, Callable, Type, Tuple from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI, MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ KEY_TYPES, IDENTIFIER_TYPES, ENTITY_TYPES, IEC61360_DATA_TYPES, IEC61360_LEVEL_TYPES +logger = logging.getLogger(__name__) + MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} KEY_ELEMENTS_INVERSE: Dict[str, model.KeyElements] = {v: k for k, v in KEY_ELEMENTS.items()} @@ -35,11 +38,34 @@ IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.concept.IEC61360LevelType] = \ {v: k for k, v in IEC61360_LEVEL_TYPES.items()} -def _get_text_or_none(_dict: Dict[str, ElTree.Element], key: str) -> Optional[str]: - return _dict[key].text if key in _dict else None +def _get_child_text_or_none(element: ElTree.Element, child: str) -> Optional[str]: + optional_child = element.find(child) + return optional_child.text if optional_child else None + + +T = TypeVar('T') + + +def object_from_xml(constructor: Callable[[ElTree.Element, bool], T], element: ElTree.Element, failsafe: bool) ->\ + Optional[T]: + try: + return constructor(element, failsafe) + except(TypeError, KeyError) as e: + error_message = "{} while converting XML Element with tag {} to type {}: {}".format( + type(e).__name__, + element.tag, + constructor.__name__, + e + ) + if failsafe: + logger.error(error_message) + return None + raise type(e)(error_message) -def construct_key(element: ElTree.Element) -> model.Key: +def construct_key(element: ElTree.Element, failsafe: bool) -> model.Key: + if element.text is None: + raise TypeError("XML Key Element has no text!") return model.Key( KEY_ELEMENTS_INVERSE[element.attrib["type"]], element.attrib["local"] == "True", @@ -48,71 +74,115 @@ def construct_key(element: ElTree.Element) -> model.Key: ) -def construct_reference(element: ElTree.Element) -> model.Reference: +def construct_reference(element: ElTree.Element, failsafe: bool) -> model.Reference: return model.Reference( - [construct_key(k) for k in element["keys"]] + tuple(key for key in [object_from_xml(construct_key, el, failsafe) for el in element.find("keys") or []] + if key is not None) ) -def construct_administrative_information(element: ElTree.Element) -> model.AdministrativeInformation: +def construct_administrative_information(element: ElTree.Element, failsafe: bool) -> model.AdministrativeInformation: return model.AdministrativeInformation( - _get_text_or_none(element, NS_AAS + "version"), - _get_text_or_none(element, NS_AAS + "revision") + _get_child_text_or_none(element, NS_AAS + "version"), + _get_child_text_or_none(element, NS_AAS + "revision") ) -def construct_lang_string_set(element: ElTree.Element) -> model.LangStringSet: - lss = model.LangStringSet +def construct_lang_string_set(element: ElTree.Element, failsafe: bool) -> model.LangStringSet: + lss: model.LangStringSet for lang_string in element: - if lang_string.tag != NS_IEC + "langString" or not lang_string.attrib["lang"]: + if lang_string.tag != NS_IEC + "langString" or not lang_string.attrib["lang"] or lang_string.text is None: + logger.warning(f"Skipping invalid XML Element with tag {lang_string.tag}") continue lss[lang_string.attrib["lang"]] = lang_string.text return lss -def construct_qualifier(element: ElTree.Element) -> model.Qualifier: +def construct_qualifier(element: ElTree.Element, failsafe: bool) -> model.Qualifier: + type_ = _get_child_text_or_none(element, "type") + value_type = _get_child_text_or_none(element, "valueType") + if not type_ or not value_type: + raise TypeError("XML Qualifier Element has no type or valueType") q = model.Qualifier( - _get_text_or_none(element, "type"), - _get_text_or_none(element, "valueType"), - _get_text_or_none(element, "value"), - construct_reference(element["valueId"]) + type_, + value_type, + _get_child_text_or_none(element, "value") ) - amend_abstract_attributes(q, element) + value_id = element.find("valueId") + if value_id: + value_id_obj = object_from_xml( + construct_reference, + value_id, + failsafe + ) + if value_id_obj: + q.value_id = value_id_obj + amend_abstract_attributes(q, element, failsafe) return q -def construct_formula(element: ElTree.Element) -> model.Formula: - return model.Formula( - set(construct_reference(ref) for ref in element) - ) +def construct_formula(element: ElTree.Element, failsafe: bool) -> model.Formula: + ref_set: Set[model.Reference] = set() + for ref in element: + obj = object_from_xml(construct_reference, ref, failsafe) + if not obj: + logger.warning(f"Skipping invalid XML Element with tag {ref.tag}") + continue + ref_set.add(obj) + return model.Formula(ref_set) -def construct_constraint(element: ElTree.Element) -> model.Constraint: +def construct_constraint(element: ElTree.Element, failsafe: bool) -> model.Constraint: if element.tag == NS_AAS + "qualifier": - return construct_qualifier(element) + return construct_qualifier(element, failsafe) if element.tag == NS_AAS + "formula": - return construct_formula(element) + return construct_formula(element, failsafe) raise TypeError("Given element is neither a qualifier nor a formula!") -def amend_abstract_attributes(obj: object, element: ElTree.Element) -> None: +def amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: if isinstance(obj, model.Referable): - if NS_AAS + "category" in element: - obj.category = element[NS_AAS + "category"].text - if NS_AAS + "description" in element: - obj.description = construct_lang_string_set(element[NS_AAS + "description"]) + if element.find(NS_AAS + "category"): + obj.category = _get_child_text_or_none(element, NS_AAS + "category") + description = element.find(NS_AAS + "description") + if description: + obj.description = object_from_xml( + construct_lang_string_set, + description, + failsafe + ) if isinstance(obj, model.Identifiable): - if NS_AAS + "idShort" in element: - obj.id_short = element[NS_AAS + "idShort"].text - if NS_AAS + "administration" in element: - obj.administration = construct_administrative_information(element[NS_AAS + "administration"]) + if element.find(NS_AAS + "idShort"): + obj.id_short = _get_child_text_or_none(element, NS_AAS + "idShort") + administration = element.find(NS_AAS + "administration") + if administration: + obj.administration = object_from_xml( + construct_administrative_information, + administration, + failsafe + ) if isinstance(obj, model.HasSemantics): - if NS_AAS + "semanticId" in element: - obj.semantic_id = construct_reference(element[NS_AAS + "semanticId"]) + semantic_id = element.find(NS_AAS + "semanticId") + if semantic_id: + obj.semantic_id = object_from_xml( + construct_reference, + semantic_id, + failsafe + ) if isinstance(obj, model.Qualifiable): - if NS_AAS + "qualifiers" in element: - for constraint in element[NS_AAS + "qualifiers"]: - obj.qualifier.add(construct_constraint(constraint)) + for constraint in element: + if constraint.tag != NS_AAS + "qualifiers": + logger.warning(f"Skipping XML Element with invalid tag {constraint.tag}") + continue + constraint_obj = object_from_xml( + construct_constraint, + constraint, + failsafe + ) + if not constraint_obj: + logger.warning(f"Skipping invalid XML Element with tag {constraint.tag}") + continue + obj.qualifier.add(constraint_obj) def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: @@ -124,5 +194,3 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: attributes and wrong types, errors are logged and defective objects are skipped :return: A DictObjectStore containing all AAS objects from the XML file """ - - -- GitLab From d1cfeca97e373541c710026f44b5690e25e5c58a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 27 Feb 2020 15:04:44 +0100 Subject: [PATCH 03/27] requirements: remove lxml for now --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac7ff5f..b3fc7e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ jsonschema>=3.2,<4.0 -lxml -- GitLab From f5050a6040720e6d402932ff83f72e1b466e8293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 27 Feb 2020 16:40:42 +0100 Subject: [PATCH 04/27] adapter.xml: add main parsing function prefix some function parameters with _ to suppress an unused variable warning remove unused typing.{Type,Tuple} imports fix reference before assignment warning in construct_lang_string_set --- aas/adapter/xml/xml_deserialization.py | 64 +++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 6466835..9dbc0e9 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -13,14 +13,12 @@ Module for deserializing Asset Administration Shell data from the official XML f """ # TODO: add more constructors -# TODO: implement error handling / failsafe parsing -# TODO: add better (more useful) helper functions from ... import model import xml.etree.ElementTree as ElTree import logging -from typing import Dict, IO, Optional, Set, TypeVar, Callable, Type, Tuple +from typing import Dict, IO, Optional, Set, TypeVar, Callable from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI, MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ KEY_TYPES, IDENTIFIER_TYPES, ENTITY_TYPES, IEC61360_DATA_TYPES, IEC61360_LEVEL_TYPES @@ -63,7 +61,7 @@ def object_from_xml(constructor: Callable[[ElTree.Element, bool], T], element: E raise type(e)(error_message) -def construct_key(element: ElTree.Element, failsafe: bool) -> model.Key: +def construct_key(element: ElTree.Element, _failsafe: bool) -> model.Key: if element.text is None: raise TypeError("XML Key Element has no text!") return model.Key( @@ -81,15 +79,15 @@ def construct_reference(element: ElTree.Element, failsafe: bool) -> model.Refere ) -def construct_administrative_information(element: ElTree.Element, failsafe: bool) -> model.AdministrativeInformation: +def construct_administrative_information(element: ElTree.Element, _failsafe: bool) -> model.AdministrativeInformation: return model.AdministrativeInformation( _get_child_text_or_none(element, NS_AAS + "version"), _get_child_text_or_none(element, NS_AAS + "revision") ) -def construct_lang_string_set(element: ElTree.Element, failsafe: bool) -> model.LangStringSet: - lss: model.LangStringSet +def construct_lang_string_set(element: ElTree.Element, _failsafe: bool) -> model.LangStringSet: + lss: model.LangStringSet = {} for lang_string in element: if lang_string.tag != NS_IEC + "langString" or not lang_string.attrib["lang"] or lang_string.text is None: logger.warning(f"Skipping invalid XML Element with tag {lang_string.tag}") @@ -185,6 +183,22 @@ def amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bo obj.qualifier.add(constraint_obj) +def construct_asset_administration_shell(element: ElTree.Element, failsafe: bool) -> model.AssetAdministrationShell: + pass + + +def construct_asset(element: ElTree.Element, failsafe: bool) -> model.Asset: + pass + + +def construct_submodel(element: ElTree.Element, failsafe: bool) -> model.Submodel: + pass + + +def construct_concept_description(element: ElTree.Element, failsafe: bool) -> model.ConceptDescription: + pass + + def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 @@ -194,3 +208,39 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: attributes and wrong types, errors are logged and defective objects are skipped :return: A DictObjectStore containing all AAS objects from the XML file """ + + tag_parser_map = { + NS_AAS + "assetAdministrationShell": construct_asset_administration_shell, + NS_AAS + "asset": construct_asset, + NS_AAS + "submodel": construct_submodel, + NS_AAS + "conceptDescription": construct_concept_description + } + + tree = ElTree.parse(file) + root = tree.getroot() + + # Add AAS objects to ObjectStore + ret: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + for list_ in root: + try: + if list_.tag[-1] != "s": + raise TypeError(f"Unexpected list {list_.tag}") + constructor = tag_parser_map[list_.tag[:-1]] + for element in list_: + if element.tag not in tag_parser_map.keys(): + error_message = f"Unexpected element {element.tag} in list {list_.tag}" + if failsafe: + logger.warning(error_message) + else: + raise TypeError(error_message) + parsed = object_from_xml(constructor, element, failsafe) + # parsed is always Identifiable, because the tag is checked earlier + # this is just to satisfy the typechecker and to make sure no error occured while parsing + if parsed and isinstance(parsed, model.Identifiable): + ret.add(parsed) + except (KeyError, TypeError) as e: + error_message = f"{type(e).__name__} while parsing XML List with tag {list_.tag}: {e}" + if not failsafe: + raise type(e)(error_message) + logger.error(error_message) + return ret -- GitLab From 3c63d564856b319650843ad55a7fd57f7215a02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 27 Feb 2020 16:44:33 +0100 Subject: [PATCH 05/27] Revert "requirements: remove lxml for now" This reverts commit d1cfeca97e373541c710026f44b5690e25e5c58a. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index b3fc7e0..ac7ff5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ jsonschema>=3.2,<4.0 +lxml -- GitLab From e40beead93848c8e85711d517e596e9a19ae53ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 28 Feb 2020 20:01:42 +0100 Subject: [PATCH 06/27] adapter.xml: refactor helper functions add conceptDescription + identification parsing --- aas/adapter/xml/xml_deserialization.py | 204 +++++++++++++------------ 1 file changed, 108 insertions(+), 96 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 9dbc0e9..93e5065 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -18,7 +18,7 @@ from ... import model import xml.etree.ElementTree as ElTree import logging -from typing import Dict, IO, Optional, Set, TypeVar, Callable +from typing import Any, Callable, Dict, IO, List, Optional, Set, TypeVar from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI, MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ KEY_TYPES, IDENTIFIER_TYPES, ENTITY_TYPES, IEC61360_DATA_TYPES, IEC61360_LEVEL_TYPES @@ -35,170 +35,182 @@ IEC61360_DATA_TYPES_INVERSE: Dict[str, model.concept.IEC61360DataType] = {v: k f IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.concept.IEC61360LevelType] = \ {v: k for k, v in IEC61360_LEVEL_TYPES.items()} +T = TypeVar('T') -def _get_child_text_or_none(element: ElTree.Element, child: str) -> Optional[str]: - optional_child = element.find(child) - return optional_child.text if optional_child else None +def _unwrap(monad: Optional[Any]) -> Any: + if monad is not None: + return monad + raise Exception(f"Unwrap failed for value {monad}!") -T = TypeVar('T') +def _get_text_or_none(element: Optional[ElTree.Element]) -> Optional[str]: + return element.text if element is not None else None + + +def _get_text_mandatory(element: Optional[ElTree.Element]) -> str: + # unwrap value here so an exception is thrown if the element is None + text = _get_text_or_none(_unwrap(element)) + if not text: + # ignore type here as the "None case" cannot occur because of _unwrap() + raise TypeError(f"XML element {element.tag} has no text!") # type: ignore + return text + + +def _objects_from_xml_elements(elements: List[ElTree.Element], constructor: Callable[[ElTree.Element, bool], T], + failsafe: bool) -> List[T]: + ret: List[T] = [] + for element in elements: + try: + ret.append(constructor(element, failsafe)) + except (KeyError, TypeError) as e: + error_message = "{} while converting XML element with tag {} to type {}: {}".format( + type(e).__name__, + element.tag, + constructor.__name__, + e + ) + if failsafe: + logger.error(error_message) + continue + raise type(e)(error_message) + return ret + + +def _object_from_xml_element(element: ElTree.Element, constructor: Callable[[ElTree.Element, bool], T], failsafe: bool)\ + -> Optional[T]: + objects = _objects_from_xml_elements([element], constructor, failsafe) + return objects[0] if objects else None -def object_from_xml(constructor: Callable[[ElTree.Element, bool], T], element: ElTree.Element, failsafe: bool) ->\ - Optional[T]: - try: - return constructor(element, failsafe) - except(TypeError, KeyError) as e: - error_message = "{} while converting XML Element with tag {} to type {}: {}".format( - type(e).__name__, - element.tag, - constructor.__name__, - e - ) - if failsafe: - logger.error(error_message) - return None - raise type(e)(error_message) + +def _object_from_xml_element_mandatory(parent: ElTree.Element, tag: str, + constructor: Callable[[ElTree.Element, bool], T]) -> T: + element = parent.find(tag) + if element is None: + raise TypeError(f"No such element {tag} found in {parent.tag}!") + return _unwrap(_object_from_xml_element(element, constructor, False)) def construct_key(element: ElTree.Element, _failsafe: bool) -> model.Key: - if element.text is None: - raise TypeError("XML Key Element has no text!") return model.Key( KEY_ELEMENTS_INVERSE[element.attrib["type"]], element.attrib["local"] == "True", - element.text, + _get_text_mandatory(element), KEY_TYPES_INVERSE[element.attrib["idType"]] ) def construct_reference(element: ElTree.Element, failsafe: bool) -> model.Reference: return model.Reference( - tuple(key for key in [object_from_xml(construct_key, el, failsafe) for el in element.find("keys") or []] - if key is not None) + tuple(_objects_from_xml_elements(_unwrap(element.find(NS_AAS + "keys")).findall(NS_AAS + "key"), construct_key, + failsafe)) ) def construct_administrative_information(element: ElTree.Element, _failsafe: bool) -> model.AdministrativeInformation: return model.AdministrativeInformation( - _get_child_text_or_none(element, NS_AAS + "version"), - _get_child_text_or_none(element, NS_AAS + "revision") + _get_text_or_none(element.find(NS_AAS + "version")), + _get_text_or_none(element.find(NS_AAS + "revision")) ) def construct_lang_string_set(element: ElTree.Element, _failsafe: bool) -> model.LangStringSet: lss: model.LangStringSet = {} - for lang_string in element: - if lang_string.tag != NS_IEC + "langString" or not lang_string.attrib["lang"] or lang_string.text is None: - logger.warning(f"Skipping invalid XML Element with tag {lang_string.tag}") - continue - lss[lang_string.attrib["lang"]] = lang_string.text + for lang_string in element.findall(NS_IEC + "langString"): + lss[lang_string.attrib["lang"]] = _get_text_mandatory(lang_string) return lss def construct_qualifier(element: ElTree.Element, failsafe: bool) -> model.Qualifier: - type_ = _get_child_text_or_none(element, "type") - value_type = _get_child_text_or_none(element, "valueType") - if not type_ or not value_type: - raise TypeError("XML Qualifier Element has no type or valueType") q = model.Qualifier( - type_, - value_type, - _get_child_text_or_none(element, "value") + _get_text_mandatory(element.find(NS_AAS + "type")), + _get_text_mandatory(element.find(NS_AAS + "valueType")), + _get_text_or_none(element.find(NS_AAS + "value")) ) - value_id = element.find("valueId") + value_id = element.find(NS_AAS + "valueId") if value_id: - value_id_obj = object_from_xml( - construct_reference, - value_id, - failsafe - ) - if value_id_obj: - q.value_id = value_id_obj + q.value_id = _unwrap(_object_from_xml_element(value_id, construct_reference, failsafe)) amend_abstract_attributes(q, element, failsafe) return q def construct_formula(element: ElTree.Element, failsafe: bool) -> model.Formula: ref_set: Set[model.Reference] = set() + for ref in element: - obj = object_from_xml(construct_reference, ref, failsafe) + obj = _object_from_xml_element(ref, construct_reference, failsafe) if not obj: - logger.warning(f"Skipping invalid XML Element with tag {ref.tag}") + logger.warning(f"Skipping invalid XML element with tag {ref.tag}") continue ref_set.add(obj) return model.Formula(ref_set) def construct_constraint(element: ElTree.Element, failsafe: bool) -> model.Constraint: - if element.tag == NS_AAS + "qualifier": - return construct_qualifier(element, failsafe) - if element.tag == NS_AAS + "formula": - return construct_formula(element, failsafe) - raise TypeError("Given element is neither a qualifier nor a formula!") + return { + NS_AAS + "qualifier": construct_qualifier, + NS_AAS + "formula": construct_formula + }[element.tag](element, failsafe) + + +def construct_identification(element: ElTree.Element, _failsafe: bool) -> model.Identifier: + return model.Identifier( + _get_text_mandatory(element), + IDENTIFIER_TYPES_INVERSE[element.attrib["idType"]] + ) + + +def construct_asset_administration_shell(element: ElTree.Element, failsafe: bool) -> model.AssetAdministrationShell: + pass + + +def construct_asset(element: ElTree.Element, failsafe: bool) -> model.Asset: + pass + + +def construct_submodel(element: ElTree.Element, failsafe: bool) -> model.Submodel: + pass + + +def construct_concept_description(element: ElTree.Element, failsafe: bool) -> model.ConceptDescription: + cd = model.ConceptDescription( + _object_from_xml_element_mandatory(element, NS_AAS + "identification", construct_identification), + set(_objects_from_xml_elements(element.findall(NS_AAS + "isCaseOf"), construct_reference, failsafe)) + ) + amend_abstract_attributes(cd, element, failsafe) + return cd def amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: if isinstance(obj, model.Referable): if element.find(NS_AAS + "category"): - obj.category = _get_child_text_or_none(element, NS_AAS + "category") + obj.category = _get_text_or_none(element.find(NS_AAS + "category")) description = element.find(NS_AAS + "description") if description: - obj.description = object_from_xml( - construct_lang_string_set, - description, - failsafe - ) + obj.description = _object_from_xml_element(description, construct_lang_string_set, failsafe) if isinstance(obj, model.Identifiable): if element.find(NS_AAS + "idShort"): - obj.id_short = _get_child_text_or_none(element, NS_AAS + "idShort") + obj.id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) administration = element.find(NS_AAS + "administration") if administration: - obj.administration = object_from_xml( - construct_administrative_information, - administration, - failsafe - ) + obj.administration = _object_from_xml_element(administration, construct_administrative_information, + failsafe) if isinstance(obj, model.HasSemantics): semantic_id = element.find(NS_AAS + "semanticId") if semantic_id: - obj.semantic_id = object_from_xml( - construct_reference, - semantic_id, - failsafe - ) + obj.semantic_id = _object_from_xml_element(semantic_id, construct_reference, failsafe) if isinstance(obj, model.Qualifiable): for constraint in element: if constraint.tag != NS_AAS + "qualifiers": - logger.warning(f"Skipping XML Element with invalid tag {constraint.tag}") + logger.warning(f"Skipping XML element with invalid tag {constraint.tag}") continue - constraint_obj = object_from_xml( - construct_constraint, - constraint, - failsafe - ) + constraint_obj = _object_from_xml_element(constraint, construct_constraint, failsafe) if not constraint_obj: - logger.warning(f"Skipping invalid XML Element with tag {constraint.tag}") + logger.warning(f"Skipping invalid XML element with tag {constraint.tag}") continue obj.qualifier.add(constraint_obj) -def construct_asset_administration_shell(element: ElTree.Element, failsafe: bool) -> model.AssetAdministrationShell: - pass - - -def construct_asset(element: ElTree.Element, failsafe: bool) -> model.Asset: - pass - - -def construct_submodel(element: ElTree.Element, failsafe: bool) -> model.Submodel: - pass - - -def construct_concept_description(element: ElTree.Element, failsafe: bool) -> model.ConceptDescription: - pass - - def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 @@ -233,9 +245,9 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: logger.warning(error_message) else: raise TypeError(error_message) - parsed = object_from_xml(constructor, element, failsafe) + parsed = _object_from_xml_element(element, constructor, failsafe) # parsed is always Identifiable, because the tag is checked earlier - # this is just to satisfy the typechecker and to make sure no error occured while parsing + # this is just to satisfy the type checker and to make sure no error occurred while parsing if parsed and isinstance(parsed, model.Identifiable): ret.add(parsed) except (KeyError, TypeError) as e: -- GitLab From 1957711cc6ba9e5fbd421cc685b107ad78c24d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 1 Mar 2020 02:40:36 +0100 Subject: [PATCH 07/27] adapter.xml: give better type name in error messages remove #type: ignore prefix constructor functions with _ --- aas/adapter/xml/xml_deserialization.py | 75 +++++++++++++------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 93e5065..54a671c 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -41,7 +41,11 @@ T = TypeVar('T') def _unwrap(monad: Optional[Any]) -> Any: if monad is not None: return monad - raise Exception(f"Unwrap failed for value {monad}!") + raise TypeError(f"Unwrap failed for value {monad}!") + + +def _constructor_name_to_typename(constructor: Callable[[ElTree.Element, bool], T]) -> str: + return "".join([s[0].upper() + s[1:] for s in constructor.__name__.split("_")[2:]]) def _get_text_or_none(element: Optional[ElTree.Element]) -> Optional[str]: @@ -50,10 +54,10 @@ def _get_text_or_none(element: Optional[ElTree.Element]) -> Optional[str]: def _get_text_mandatory(element: Optional[ElTree.Element]) -> str: # unwrap value here so an exception is thrown if the element is None - text = _get_text_or_none(_unwrap(element)) + element_unwrapped = _unwrap(element) + text = _get_text_or_none(element_unwrapped) if not text: - # ignore type here as the "None case" cannot occur because of _unwrap() - raise TypeError(f"XML element {element.tag} has no text!") # type: ignore + raise TypeError(f"XML element {element_unwrapped.tag} has no text!") return text @@ -67,7 +71,7 @@ def _objects_from_xml_elements(elements: List[ElTree.Element], constructor: Call error_message = "{} while converting XML element with tag {} to type {}: {}".format( type(e).__name__, element.tag, - constructor.__name__, + _constructor_name_to_typename(constructor), e ) if failsafe: @@ -91,7 +95,7 @@ def _object_from_xml_element_mandatory(parent: ElTree.Element, tag: str, return _unwrap(_object_from_xml_element(element, constructor, False)) -def construct_key(element: ElTree.Element, _failsafe: bool) -> model.Key: +def _construct_key(element: ElTree.Element, _failsafe: bool) -> model.Key: return model.Key( KEY_ELEMENTS_INVERSE[element.attrib["type"]], element.attrib["local"] == "True", @@ -100,28 +104,28 @@ def construct_key(element: ElTree.Element, _failsafe: bool) -> model.Key: ) -def construct_reference(element: ElTree.Element, failsafe: bool) -> model.Reference: +def _construct_reference(element: ElTree.Element, failsafe: bool) -> model.Reference: return model.Reference( - tuple(_objects_from_xml_elements(_unwrap(element.find(NS_AAS + "keys")).findall(NS_AAS + "key"), construct_key, + tuple(_objects_from_xml_elements(_unwrap(element.find(NS_AAS + "keys")).findall(NS_AAS + "key"), _construct_key, failsafe)) ) -def construct_administrative_information(element: ElTree.Element, _failsafe: bool) -> model.AdministrativeInformation: +def _construct_administrative_information(element: ElTree.Element, _failsafe: bool) -> model.AdministrativeInformation: return model.AdministrativeInformation( _get_text_or_none(element.find(NS_AAS + "version")), _get_text_or_none(element.find(NS_AAS + "revision")) ) -def construct_lang_string_set(element: ElTree.Element, _failsafe: bool) -> model.LangStringSet: +def _construct_lang_string_set(element: ElTree.Element, _failsafe: bool) -> model.LangStringSet: lss: model.LangStringSet = {} for lang_string in element.findall(NS_IEC + "langString"): lss[lang_string.attrib["lang"]] = _get_text_mandatory(lang_string) return lss -def construct_qualifier(element: ElTree.Element, failsafe: bool) -> model.Qualifier: +def _construct_qualifier(element: ElTree.Element, failsafe: bool) -> model.Qualifier: q = model.Qualifier( _get_text_mandatory(element.find(NS_AAS + "type")), _get_text_mandatory(element.find(NS_AAS + "valueType")), @@ -129,16 +133,15 @@ def construct_qualifier(element: ElTree.Element, failsafe: bool) -> model.Qualif ) value_id = element.find(NS_AAS + "valueId") if value_id: - q.value_id = _unwrap(_object_from_xml_element(value_id, construct_reference, failsafe)) - amend_abstract_attributes(q, element, failsafe) + q.value_id = _unwrap(_object_from_xml_element(value_id, _construct_reference, failsafe)) + _amend_abstract_attributes(q, element, failsafe) return q -def construct_formula(element: ElTree.Element, failsafe: bool) -> model.Formula: +def _construct_formula(element: ElTree.Element, failsafe: bool) -> model.Formula: ref_set: Set[model.Reference] = set() - for ref in element: - obj = _object_from_xml_element(ref, construct_reference, failsafe) + obj = _object_from_xml_element(ref, _construct_reference, failsafe) if not obj: logger.warning(f"Skipping invalid XML element with tag {ref.tag}") continue @@ -146,65 +149,65 @@ def construct_formula(element: ElTree.Element, failsafe: bool) -> model.Formula: return model.Formula(ref_set) -def construct_constraint(element: ElTree.Element, failsafe: bool) -> model.Constraint: +def _construct_constraint(element: ElTree.Element, failsafe: bool) -> model.Constraint: return { - NS_AAS + "qualifier": construct_qualifier, - NS_AAS + "formula": construct_formula + NS_AAS + "qualifier": _construct_qualifier, + NS_AAS + "formula": _construct_formula }[element.tag](element, failsafe) -def construct_identification(element: ElTree.Element, _failsafe: bool) -> model.Identifier: +def _construct_identification(element: ElTree.Element, _failsafe: bool) -> model.Identifier: return model.Identifier( _get_text_mandatory(element), IDENTIFIER_TYPES_INVERSE[element.attrib["idType"]] ) -def construct_asset_administration_shell(element: ElTree.Element, failsafe: bool) -> model.AssetAdministrationShell: +def _construct_asset_administration_shell(element: ElTree.Element, failsafe: bool) -> model.AssetAdministrationShell: pass -def construct_asset(element: ElTree.Element, failsafe: bool) -> model.Asset: +def _construct_asset(element: ElTree.Element, failsafe: bool) -> model.Asset: pass -def construct_submodel(element: ElTree.Element, failsafe: bool) -> model.Submodel: +def _construct_submodel(element: ElTree.Element, failsafe: bool) -> model.Submodel: pass -def construct_concept_description(element: ElTree.Element, failsafe: bool) -> model.ConceptDescription: +def _construct_concept_description(element: ElTree.Element, failsafe: bool) -> model.ConceptDescription: cd = model.ConceptDescription( - _object_from_xml_element_mandatory(element, NS_AAS + "identification", construct_identification), - set(_objects_from_xml_elements(element.findall(NS_AAS + "isCaseOf"), construct_reference, failsafe)) + _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identification), + set(_objects_from_xml_elements(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe)) ) - amend_abstract_attributes(cd, element, failsafe) + _amend_abstract_attributes(cd, element, failsafe) return cd -def amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: +def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: if isinstance(obj, model.Referable): if element.find(NS_AAS + "category"): obj.category = _get_text_or_none(element.find(NS_AAS + "category")) description = element.find(NS_AAS + "description") if description: - obj.description = _object_from_xml_element(description, construct_lang_string_set, failsafe) + obj.description = _object_from_xml_element(description, _construct_lang_string_set, failsafe) if isinstance(obj, model.Identifiable): if element.find(NS_AAS + "idShort"): obj.id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) administration = element.find(NS_AAS + "administration") if administration: - obj.administration = _object_from_xml_element(administration, construct_administrative_information, + obj.administration = _object_from_xml_element(administration, _construct_administrative_information, failsafe) if isinstance(obj, model.HasSemantics): semantic_id = element.find(NS_AAS + "semanticId") if semantic_id: - obj.semantic_id = _object_from_xml_element(semantic_id, construct_reference, failsafe) + obj.semantic_id = _object_from_xml_element(semantic_id, _construct_reference, failsafe) if isinstance(obj, model.Qualifiable): for constraint in element: if constraint.tag != NS_AAS + "qualifiers": logger.warning(f"Skipping XML element with invalid tag {constraint.tag}") continue - constraint_obj = _object_from_xml_element(constraint, construct_constraint, failsafe) + constraint_obj = _object_from_xml_element(constraint, _construct_constraint, failsafe) if not constraint_obj: logger.warning(f"Skipping invalid XML element with tag {constraint.tag}") continue @@ -222,10 +225,10 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: """ tag_parser_map = { - NS_AAS + "assetAdministrationShell": construct_asset_administration_shell, - NS_AAS + "asset": construct_asset, - NS_AAS + "submodel": construct_submodel, - NS_AAS + "conceptDescription": construct_concept_description + NS_AAS + "assetAdministrationShell": _construct_asset_administration_shell, + NS_AAS + "asset": _construct_asset, + NS_AAS + "submodel": _construct_submodel, + NS_AAS + "conceptDescription": _construct_concept_description } tree = ElTree.parse(file) -- GitLab From 94d067eb2345e1737146ae01ab433b07edf1b786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 2 Mar 2020 02:18:49 +0100 Subject: [PATCH 08/27] adapter.xml: add constructors for complete construction of AssetAdministrationShell + Asset shorten read_xml_aas_file() minor changes to helper functions --- aas/adapter/xml/xml_deserialization.py | 166 ++++++++++++++++++------- 1 file changed, 122 insertions(+), 44 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 54a671c..fac0cde 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -12,13 +12,13 @@ Module for deserializing Asset Administration Shell data from the official XML format """ -# TODO: add more constructors +# TODO: add constructor for submodel + all classes required by submodel from ... import model import xml.etree.ElementTree as ElTree import logging -from typing import Any, Callable, Dict, IO, List, Optional, Set, TypeVar +from typing import Callable, Dict, IO, List, Optional, Set, Tuple, Type, TypeVar from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI, MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ KEY_TYPES, IDENTIFIER_TYPES, ENTITY_TYPES, IEC61360_DATA_TYPES, IEC61360_LEVEL_TYPES @@ -38,7 +38,7 @@ IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.concept.IEC61360LevelType] = \ T = TypeVar('T') -def _unwrap(monad: Optional[Any]) -> Any: +def _unwrap(monad: Optional[T]) -> T: if monad is not None: return monad raise TypeError(f"Unwrap failed for value {monad}!") @@ -53,7 +53,7 @@ def _get_text_or_none(element: Optional[ElTree.Element]) -> Optional[str]: def _get_text_mandatory(element: Optional[ElTree.Element]) -> str: - # unwrap value here so an exception is thrown if the element is None + # unwrap value here so a TypeError is thrown if the element is None element_unwrapped = _unwrap(element) text = _get_text_or_none(element_unwrapped) if not text: @@ -81,8 +81,10 @@ def _objects_from_xml_elements(elements: List[ElTree.Element], constructor: Call return ret -def _object_from_xml_element(element: ElTree.Element, constructor: Callable[[ElTree.Element, bool], T], failsafe: bool)\ - -> Optional[T]: +def _object_from_xml_element(element: Optional[ElTree.Element], constructor: Callable[[ElTree.Element, bool], T], + failsafe: bool) -> Optional[T]: + if element is None: + return None objects = _objects_from_xml_elements([element], constructor, failsafe) return objects[0] if objects else None @@ -104,11 +106,35 @@ def _construct_key(element: ElTree.Element, _failsafe: bool) -> model.Key: ) +def _construct_key_tuple(element: ElTree.Element, failsafe: bool) -> Tuple[model.Key, ...]: + return tuple(_objects_from_xml_elements(_unwrap(element.find(NS_AAS + "keys")).findall(NS_AAS + "key"), + _construct_key, failsafe)) + + def _construct_reference(element: ElTree.Element, failsafe: bool) -> model.Reference: - return model.Reference( - tuple(_objects_from_xml_elements(_unwrap(element.find(NS_AAS + "keys")).findall(NS_AAS + "key"), _construct_key, - failsafe)) - ) + return model.Reference(_construct_key_tuple(element, failsafe)) + + +def _construct_submodel_reference(element: ElTree.Element, failsafe: bool) -> model.AASReference[model.Submodel]: + return model.AASReference(_construct_key_tuple(element, failsafe), model.Submodel) + + +def _construct_asset_reference(element: ElTree.Element, failsafe: bool) -> model.AASReference[model.Asset]: + return model.AASReference(_construct_key_tuple(element, failsafe), model.Asset) + + +def _construct_aas_reference(element: ElTree.Element, failsafe: bool)\ + -> model.AASReference[model.AssetAdministrationShell]: + return model.AASReference(_construct_key_tuple(element, failsafe), model.AssetAdministrationShell) + + +def _construct_contained_element_ref(element: ElTree.Element, failsafe: bool) -> model.AASReference[model.Referable]: + return model.AASReference(_construct_key_tuple(element, failsafe), model.Referable) + + +def _construct_concept_description_ref(element: ElTree.Element, failsafe: bool)\ + -> model.AASReference[model.ConceptDescription]: + return model.AASReference(_construct_key_tuple(element, failsafe), model.ConceptDescription) def _construct_administrative_information(element: ElTree.Element, _failsafe: bool) -> model.AdministrativeInformation: @@ -131,9 +157,9 @@ def _construct_qualifier(element: ElTree.Element, failsafe: bool) -> model.Quali _get_text_mandatory(element.find(NS_AAS + "valueType")), _get_text_or_none(element.find(NS_AAS + "value")) ) - value_id = element.find(NS_AAS + "valueId") - if value_id: - q.value_id = _unwrap(_object_from_xml_element(value_id, _construct_reference, failsafe)) + value_id = _object_from_xml_element(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) + if value_id is not None: + q.value_id = value_id _amend_abstract_attributes(q, element, failsafe) return q @@ -156,19 +182,79 @@ def _construct_constraint(element: ElTree.Element, failsafe: bool) -> model.Cons }[element.tag](element, failsafe) -def _construct_identification(element: ElTree.Element, _failsafe: bool) -> model.Identifier: +def _construct_identifier(element: ElTree.Element, _failsafe: bool) -> model.Identifier: return model.Identifier( _get_text_mandatory(element), IDENTIFIER_TYPES_INVERSE[element.attrib["idType"]] ) +def _construct_security(_element: ElTree.Element, _failsafe: bool) -> model.Security: + return model.Security() + + +def _construct_view(element: ElTree.Element, failsafe: bool) -> model.View: + view = model.View(_get_text_mandatory(element.find(NS_AAS + "idShort"))) + contained_elements = element.find(NS_AAS + "containedElements") + if contained_elements is not None: + view.contained_element = set( + _objects_from_xml_elements(contained_elements.findall(NS_AAS + "containedElementRef"), + _construct_contained_element_ref, failsafe) + ) + _amend_abstract_attributes(view, element, failsafe) + return view + + +def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool) -> model.ConceptDictionary: + cd = model.ConceptDictionary(_get_text_mandatory(element.find(NS_AAS + "idShort"))) + concept_description = element.find(NS_AAS + "conceptDescriptionRefs") + if concept_description is not None: + cd.concept_description = set(_objects_from_xml_elements( + concept_description.findall(NS_AAS + "conceptDescriptionRef"), + _construct_concept_description_ref, + failsafe + )) + _amend_abstract_attributes(cd, element, failsafe) + return cd + + def _construct_asset_administration_shell(element: ElTree.Element, failsafe: bool) -> model.AssetAdministrationShell: - pass + aas = model.AssetAdministrationShell( + _object_from_xml_element_mandatory(element, NS_AAS + "assetRef", _construct_asset_reference), + _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) + ) + aas.security_ = _object_from_xml_element(element.find(NS_ABAC + "security"), _construct_security, failsafe) + submodels = element.find(NS_AAS + "submodelRefs") + if submodels is not None: + aas.submodel_ = set(_objects_from_xml_elements(submodels.findall(NS_AAS + "submodelRef"), + _construct_submodel_reference, failsafe)) + views = element.find(NS_AAS + "views") + if views is not None: + for view in _objects_from_xml_elements(views.findall(NS_AAS + "view"), _construct_view, failsafe): + aas.view.add(view) + concept_dictionaries = element.find(NS_AAS + "conceptDictionaries") + if concept_dictionaries is not None: + for cd in _objects_from_xml_elements(concept_dictionaries.findall(NS_AAS + "conceptDictionary"), + _construct_concept_dictionary, failsafe): + aas.concept_dictionary.add(cd) + derived_from = element.find(NS_AAS + "derivedFrom") + if derived_from is not None: + aas.derived_from = _object_from_xml_element(element, _construct_aas_reference, failsafe) + _amend_abstract_attributes(aas, element, failsafe) + return aas def _construct_asset(element: ElTree.Element, failsafe: bool) -> model.Asset: - pass + asset = model.Asset( + ASSET_KIND_INVERSE[_get_text_mandatory(element.find(NS_AAS + "kind"))], + _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) + ) + asset.asset_identification_model = _object_from_xml_element(element.find(NS_AAS + "assetIdentificationModelRef"), + _construct_submodel_reference, failsafe) + asset.bill_of_material = _object_from_xml_element(element.find(NS_AAS + "billOfMaterialRef"), + _construct_submodel_reference, failsafe) + _amend_abstract_attributes(asset, element, failsafe) + return asset def _construct_submodel(element: ElTree.Element, failsafe: bool) -> model.Submodel: @@ -177,23 +263,27 @@ def _construct_submodel(element: ElTree.Element, failsafe: bool) -> model.Submod def _construct_concept_description(element: ElTree.Element, failsafe: bool) -> model.ConceptDescription: cd = model.ConceptDescription( - _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identification), - set(_objects_from_xml_elements(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe)) + _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) ) + is_case_of = set(_objects_from_xml_elements(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe)) + if len(is_case_of) != 0: + cd.is_case_of = is_case_of _amend_abstract_attributes(cd, element, failsafe) return cd def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: if isinstance(obj, model.Referable): - if element.find(NS_AAS + "category"): - obj.category = _get_text_or_none(element.find(NS_AAS + "category")) + category = element.find(NS_AAS + "category") + if category: + obj.category = _get_text_or_none(category) description = element.find(NS_AAS + "description") if description: obj.description = _object_from_xml_element(description, _construct_lang_string_set, failsafe) if isinstance(obj, model.Identifiable): - if element.find(NS_AAS + "idShort"): - obj.id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) + id_short = element.find(NS_AAS + "idShort") + if id_short: + obj.id_short = _get_text_or_none(id_short) administration = element.find(NS_AAS + "administration") if administration: obj.administration = _object_from_xml_element(administration, _construct_administrative_information, @@ -224,7 +314,7 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: :return: A DictObjectStore containing all AAS objects from the XML file """ - tag_parser_map = { + element_constructors = { NS_AAS + "assetAdministrationShell": _construct_asset_administration_shell, NS_AAS + "asset": _construct_asset, NS_AAS + "submodel": _construct_submodel, @@ -237,25 +327,13 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: # Add AAS objects to ObjectStore ret: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() for list_ in root: - try: - if list_.tag[-1] != "s": - raise TypeError(f"Unexpected list {list_.tag}") - constructor = tag_parser_map[list_.tag[:-1]] - for element in list_: - if element.tag not in tag_parser_map.keys(): - error_message = f"Unexpected element {element.tag} in list {list_.tag}" - if failsafe: - logger.warning(error_message) - else: - raise TypeError(error_message) - parsed = _object_from_xml_element(element, constructor, failsafe) - # parsed is always Identifiable, because the tag is checked earlier - # this is just to satisfy the type checker and to make sure no error occurred while parsing - if parsed and isinstance(parsed, model.Identifiable): - ret.add(parsed) - except (KeyError, TypeError) as e: - error_message = f"{type(e).__name__} while parsing XML List with tag {list_.tag}: {e}" - if not failsafe: - raise type(e)(error_message) - logger.error(error_message) + element_tag = list_.tag[:-1] + if list_.tag[-1] != "s" or element_tag not in element_constructors.keys(): + raise TypeError(f"Unexpected list {list_.tag}") + constructor = element_constructors[element_tag] + for element in _objects_from_xml_elements(list_.findall(element_tag), constructor, failsafe): + # element is always Identifiable, because the tag is checked earlier + # this is just to satisfy the type checker + if isinstance(element, model.Identifiable): + ret.add(element) return ret -- GitLab From f20a3f1080d20518b63e05d949463ddbcb808776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 5 Mar 2020 03:04:21 +0100 Subject: [PATCH 09/27] adapter.xml: move exception handling to _object_from_xml_element some minor changes mentioned in the meeting yesterday --- aas/adapter/xml/xml_deserialization.py | 39 +++++++++++--------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index fac0cde..1b2e983 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -18,7 +18,7 @@ from ... import model import xml.etree.ElementTree as ElTree import logging -from typing import Callable, Dict, IO, List, Optional, Set, Tuple, Type, TypeVar +from typing import Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI, MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ KEY_TYPES, IDENTIFIER_TYPES, ENTITY_TYPES, IEC61360_DATA_TYPES, IEC61360_LEVEL_TYPES @@ -56,44 +56,39 @@ def _get_text_mandatory(element: Optional[ElTree.Element]) -> str: # unwrap value here so a TypeError is thrown if the element is None element_unwrapped = _unwrap(element) text = _get_text_or_none(element_unwrapped) - if not text: + if text is None: raise TypeError(f"XML element {element_unwrapped.tag} has no text!") return text -def _objects_from_xml_elements(elements: List[ElTree.Element], constructor: Callable[[ElTree.Element, bool], T], - failsafe: bool) -> List[T]: - ret: List[T] = [] +def _objects_from_xml_elements(elements: Iterable[ElTree.Element], constructor: Callable[[ElTree.Element, bool], T], + failsafe: bool) -> Iterable[T]: for element in elements: - try: - ret.append(constructor(element, failsafe)) - except (KeyError, TypeError) as e: - error_message = "{} while converting XML element with tag {} to type {}: {}".format( - type(e).__name__, - element.tag, - _constructor_name_to_typename(constructor), - e - ) - if failsafe: - logger.error(error_message) - continue - raise type(e)(error_message) - return ret + parsed = _object_from_xml_element(element, constructor, failsafe) + if parsed is not None: + yield parsed def _object_from_xml_element(element: Optional[ElTree.Element], constructor: Callable[[ElTree.Element, bool], T], failsafe: bool) -> Optional[T]: if element is None: return None - objects = _objects_from_xml_elements([element], constructor, failsafe) - return objects[0] if objects else None + try: + return constructor(element, failsafe) + except (KeyError, TypeError) as e: + error_message = f"{type(e).__name__} while converting XML element with tag {element.tag} to " \ + f"type {_constructor_name_to_typename(constructor)}: {e}" + if not failsafe: + raise type(e)(error_message) from e + logger.error(error_message) + return None def _object_from_xml_element_mandatory(parent: ElTree.Element, tag: str, constructor: Callable[[ElTree.Element, bool], T]) -> T: element = parent.find(tag) if element is None: - raise TypeError(f"No such element {tag} found in {parent.tag}!") + raise KeyError(f"No such element {tag} found in {parent.tag}!") return _unwrap(_object_from_xml_element(element, constructor, False)) -- GitLab From ec9f91ac48d4b0c15badde572889d5f99dd615b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Mar 2020 17:55:36 +0100 Subject: [PATCH 10/27] adapter.xml: allow passing of optional keyword arguments to constructor functions adjust _construct_qualifier() and _construct_asset_administration_shell() for merged master --- aas/adapter/xml/xml_deserialization.py | 81 ++++++++++++++------------ 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 1b2e983..217ae63 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -18,8 +18,9 @@ from ... import model import xml.etree.ElementTree as ElTree import logging -from typing import Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar -from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI, MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ +from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar +from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI +from .._generic import MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ KEY_TYPES, IDENTIFIER_TYPES, ENTITY_TYPES, IEC61360_DATA_TYPES, IEC61360_LEVEL_TYPES logger = logging.getLogger(__name__) @@ -61,20 +62,20 @@ def _get_text_mandatory(element: Optional[ElTree.Element]) -> str: return text -def _objects_from_xml_elements(elements: Iterable[ElTree.Element], constructor: Callable[[ElTree.Element, bool], T], - failsafe: bool) -> Iterable[T]: +def _objects_from_xml_elements(elements: Iterable[ElTree.Element], constructor: Callable[..., T], + failsafe: bool, **kwargs: Any) -> Iterable[T]: for element in elements: - parsed = _object_from_xml_element(element, constructor, failsafe) + parsed = _object_from_xml_element(element, constructor, failsafe, **kwargs) if parsed is not None: yield parsed -def _object_from_xml_element(element: Optional[ElTree.Element], constructor: Callable[[ElTree.Element, bool], T], - failsafe: bool) -> Optional[T]: +def _object_from_xml_element(element: Optional[ElTree.Element], constructor: Callable[..., T], + failsafe: bool, **kwargs: Any) -> Optional[T]: if element is None: return None try: - return constructor(element, failsafe) + return constructor(element, failsafe, **kwargs) except (KeyError, TypeError) as e: error_message = f"{type(e).__name__} while converting XML element with tag {element.tag} to " \ f"type {_constructor_name_to_typename(constructor)}: {e}" @@ -85,14 +86,14 @@ def _object_from_xml_element(element: Optional[ElTree.Element], constructor: Cal def _object_from_xml_element_mandatory(parent: ElTree.Element, tag: str, - constructor: Callable[[ElTree.Element, bool], T]) -> T: + constructor: Callable[..., T], **kwargs: Any) -> T: element = parent.find(tag) if element is None: raise KeyError(f"No such element {tag} found in {parent.tag}!") - return _unwrap(_object_from_xml_element(element, constructor, False)) + return _unwrap(_object_from_xml_element(element, constructor, False, **kwargs)) -def _construct_key(element: ElTree.Element, _failsafe: bool) -> model.Key: +def _construct_key(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Key: return model.Key( KEY_ELEMENTS_INVERSE[element.attrib["type"]], element.attrib["local"] == "True", @@ -101,55 +102,58 @@ def _construct_key(element: ElTree.Element, _failsafe: bool) -> model.Key: ) -def _construct_key_tuple(element: ElTree.Element, failsafe: bool) -> Tuple[model.Key, ...]: +def _construct_key_tuple(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> Tuple[model.Key, ...]: return tuple(_objects_from_xml_elements(_unwrap(element.find(NS_AAS + "keys")).findall(NS_AAS + "key"), _construct_key, failsafe)) -def _construct_reference(element: ElTree.Element, failsafe: bool) -> model.Reference: +def _construct_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Reference: return model.Reference(_construct_key_tuple(element, failsafe)) -def _construct_submodel_reference(element: ElTree.Element, failsafe: bool) -> model.AASReference[model.Submodel]: +def _construct_submodel_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ + -> model.AASReference[model.Submodel]: return model.AASReference(_construct_key_tuple(element, failsafe), model.Submodel) -def _construct_asset_reference(element: ElTree.Element, failsafe: bool) -> model.AASReference[model.Asset]: +def _construct_asset_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.AASReference[model.Asset]: return model.AASReference(_construct_key_tuple(element, failsafe), model.Asset) -def _construct_aas_reference(element: ElTree.Element, failsafe: bool)\ +def _construct_asset_administration_shell_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ -> model.AASReference[model.AssetAdministrationShell]: return model.AASReference(_construct_key_tuple(element, failsafe), model.AssetAdministrationShell) -def _construct_contained_element_ref(element: ElTree.Element, failsafe: bool) -> model.AASReference[model.Referable]: +def _construct_referable_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ + -> model.AASReference[model.Referable]: return model.AASReference(_construct_key_tuple(element, failsafe), model.Referable) -def _construct_concept_description_ref(element: ElTree.Element, failsafe: bool)\ +def _construct_concept_description_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ -> model.AASReference[model.ConceptDescription]: return model.AASReference(_construct_key_tuple(element, failsafe), model.ConceptDescription) -def _construct_administrative_information(element: ElTree.Element, _failsafe: bool) -> model.AdministrativeInformation: +def _construct_administrative_information(element: ElTree.Element, _failsafe: bool, **_kwargs: Any)\ + -> model.AdministrativeInformation: return model.AdministrativeInformation( _get_text_or_none(element.find(NS_AAS + "version")), _get_text_or_none(element.find(NS_AAS + "revision")) ) -def _construct_lang_string_set(element: ElTree.Element, _failsafe: bool) -> model.LangStringSet: +def _construct_lang_string_set(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.LangStringSet: lss: model.LangStringSet = {} for lang_string in element.findall(NS_IEC + "langString"): lss[lang_string.attrib["lang"]] = _get_text_mandatory(lang_string) return lss -def _construct_qualifier(element: ElTree.Element, failsafe: bool) -> model.Qualifier: +def _construct_qualifier(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Qualifier: q = model.Qualifier( _get_text_mandatory(element.find(NS_AAS + "type")), - _get_text_mandatory(element.find(NS_AAS + "valueType")), + model.datatypes.XSD_TYPE_CLASSES[_get_text_mandatory(element.find(NS_AAS + "valueType"))], _get_text_or_none(element.find(NS_AAS + "value")) ) value_id = _object_from_xml_element(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) @@ -159,7 +163,7 @@ def _construct_qualifier(element: ElTree.Element, failsafe: bool) -> model.Quali return q -def _construct_formula(element: ElTree.Element, failsafe: bool) -> model.Formula: +def _construct_formula(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Formula: ref_set: Set[model.Reference] = set() for ref in element: obj = _object_from_xml_element(ref, _construct_reference, failsafe) @@ -170,58 +174,59 @@ def _construct_formula(element: ElTree.Element, failsafe: bool) -> model.Formula return model.Formula(ref_set) -def _construct_constraint(element: ElTree.Element, failsafe: bool) -> model.Constraint: +def _construct_constraint(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Constraint: return { NS_AAS + "qualifier": _construct_qualifier, NS_AAS + "formula": _construct_formula }[element.tag](element, failsafe) -def _construct_identifier(element: ElTree.Element, _failsafe: bool) -> model.Identifier: +def _construct_identifier(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Identifier: return model.Identifier( _get_text_mandatory(element), IDENTIFIER_TYPES_INVERSE[element.attrib["idType"]] ) -def _construct_security(_element: ElTree.Element, _failsafe: bool) -> model.Security: +def _construct_security(_element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Security: return model.Security() -def _construct_view(element: ElTree.Element, failsafe: bool) -> model.View: +def _construct_view(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.View: view = model.View(_get_text_mandatory(element.find(NS_AAS + "idShort"))) contained_elements = element.find(NS_AAS + "containedElements") if contained_elements is not None: view.contained_element = set( _objects_from_xml_elements(contained_elements.findall(NS_AAS + "containedElementRef"), - _construct_contained_element_ref, failsafe) + _construct_referable_reference, failsafe) ) _amend_abstract_attributes(view, element, failsafe) return view -def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool) -> model.ConceptDictionary: +def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDictionary: cd = model.ConceptDictionary(_get_text_mandatory(element.find(NS_AAS + "idShort"))) concept_description = element.find(NS_AAS + "conceptDescriptionRefs") if concept_description is not None: cd.concept_description = set(_objects_from_xml_elements( concept_description.findall(NS_AAS + "conceptDescriptionRef"), - _construct_concept_description_ref, + _construct_concept_description_reference, failsafe )) _amend_abstract_attributes(cd, element, failsafe) return cd -def _construct_asset_administration_shell(element: ElTree.Element, failsafe: bool) -> model.AssetAdministrationShell: +def _construct_asset_administration_shell(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ + -> model.AssetAdministrationShell: aas = model.AssetAdministrationShell( _object_from_xml_element_mandatory(element, NS_AAS + "assetRef", _construct_asset_reference), _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) ) - aas.security_ = _object_from_xml_element(element.find(NS_ABAC + "security"), _construct_security, failsafe) + aas.security = _object_from_xml_element(element.find(NS_ABAC + "security"), _construct_security, failsafe) submodels = element.find(NS_AAS + "submodelRefs") if submodels is not None: - aas.submodel_ = set(_objects_from_xml_elements(submodels.findall(NS_AAS + "submodelRef"), + aas.submodel = set(_objects_from_xml_elements(submodels.findall(NS_AAS + "submodelRef"), _construct_submodel_reference, failsafe)) views = element.find(NS_AAS + "views") if views is not None: @@ -234,12 +239,12 @@ def _construct_asset_administration_shell(element: ElTree.Element, failsafe: boo aas.concept_dictionary.add(cd) derived_from = element.find(NS_AAS + "derivedFrom") if derived_from is not None: - aas.derived_from = _object_from_xml_element(element, _construct_aas_reference, failsafe) + aas.derived_from = _object_from_xml_element(element, _construct_asset_administration_shell_reference, failsafe) _amend_abstract_attributes(aas, element, failsafe) return aas -def _construct_asset(element: ElTree.Element, failsafe: bool) -> model.Asset: +def _construct_asset(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Asset: asset = model.Asset( ASSET_KIND_INVERSE[_get_text_mandatory(element.find(NS_AAS + "kind"))], _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) @@ -252,11 +257,11 @@ def _construct_asset(element: ElTree.Element, failsafe: bool) -> model.Asset: return asset -def _construct_submodel(element: ElTree.Element, failsafe: bool) -> model.Submodel: +def _construct_submodel(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Submodel: pass -def _construct_concept_description(element: ElTree.Element, failsafe: bool) -> model.ConceptDescription: +def _construct_concept_description(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: cd = model.ConceptDescription( _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) ) @@ -267,7 +272,7 @@ def _construct_concept_description(element: ElTree.Element, failsafe: bool) -> m return cd -def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: +def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> None: if isinstance(obj, model.Referable): category = element.find(NS_AAS + "category") if category: -- GitLab From fb6d458cff48ce63387c94f662a0da654c33490c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Mar 2020 18:00:52 +0100 Subject: [PATCH 11/27] adapter.xml: break too long line adjust indent in continuation line --- aas/adapter/xml/xml_deserialization.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 217ae63..8b924df 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -116,7 +116,8 @@ def _construct_submodel_reference(element: ElTree.Element, failsafe: bool, **_kw return model.AASReference(_construct_key_tuple(element, failsafe), model.Submodel) -def _construct_asset_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.AASReference[model.Asset]: +def _construct_asset_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ + -> model.AASReference[model.Asset]: return model.AASReference(_construct_key_tuple(element, failsafe), model.Asset) @@ -227,7 +228,7 @@ def _construct_asset_administration_shell(element: ElTree.Element, failsafe: boo submodels = element.find(NS_AAS + "submodelRefs") if submodels is not None: aas.submodel = set(_objects_from_xml_elements(submodels.findall(NS_AAS + "submodelRef"), - _construct_submodel_reference, failsafe)) + _construct_submodel_reference, failsafe)) views = element.find(NS_AAS + "views") if views is not None: for view in _objects_from_xml_elements(views.findall(NS_AAS + "view"), _construct_view, failsafe): -- GitLab From 4580bbc67f366d2af65bb8e77cc3a40e8d34ee82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 26 Mar 2020 17:43:05 +0100 Subject: [PATCH 12/27] adapter.xml: import inverse dicts from _generic in deserialization --- aas/adapter/xml/xml_deserialization.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 8b924df..5a62218 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -20,22 +20,12 @@ import logging from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI -from .._generic import MODELING_KIND, ASSET_KIND, KEY_ELEMENTS,\ - KEY_TYPES, IDENTIFIER_TYPES, ENTITY_TYPES, IEC61360_DATA_TYPES, IEC61360_LEVEL_TYPES +from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ + IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ + KEY_ELEMENTS_CLASSES_INVERSE logger = logging.getLogger(__name__) -MODELING_KIND_INVERSE: Dict[str, model.ModelingKind] = {v: k for k, v in MODELING_KIND.items()} -ASSET_KIND_INVERSE: Dict[str, model.AssetKind] = {v: k for k, v in ASSET_KIND.items()} -KEY_ELEMENTS_INVERSE: Dict[str, model.KeyElements] = {v: k for k, v in KEY_ELEMENTS.items()} -KEY_TYPES_INVERSE: Dict[str, model.KeyType] = {v: k for k, v in KEY_TYPES.items()} -IDENTIFIER_TYPES_INVERSE: Dict[str, model.IdentifierType] = {v: k for k, v in IDENTIFIER_TYPES.items()} -ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()} -KEY_ELEMENTS_CLASSES_INVERSE: Dict[model.KeyElements, type] = {v: k for k, v in model.KEY_ELEMENTS_CLASSES.items()} -IEC61360_DATA_TYPES_INVERSE: Dict[str, model.concept.IEC61360DataType] = {v: k for k, v in IEC61360_DATA_TYPES.items()} -IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.concept.IEC61360LevelType] = \ - {v: k for k, v in IEC61360_LEVEL_TYPES.items()} - T = TypeVar('T') -- GitLab From 8076caf2630be2ce9322c3b4d58851797a9b3d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 26 Mar 2020 18:14:19 +0100 Subject: [PATCH 13/27] adapter.xml: refactor helper functions and improve error handling remove _unwrap() as it produced error messages that weren't helpful add _get_text_mandatory_mapped() and _get_attrib_mandatory_mapped() These are helper functions for retrieving an attribute or the text of an xml element and then using the value as the key in a specified dict. These are useful since they will raise a helpful error if the attribute or text does not exist in the dict as a key. add _get_child_mandatory() + _get_attribute_mandatory() Functions for retrieving a mandatory child element or a mandatory attribute and raising an error with a helpful message if the child/attribute doesn't exist. _get_text_mandatory()'s signature is now similar to _get_child_mandatory() and _get_attribute_mandatory(). It won't accept None anymore, and it now also raises a KeyError instead of a TypeError. change _failsafe_construct() This function will now handle Key and Value errors. It will now print the whole error cause chain. change constructor functions depending on these helper functions respectively add a check if type of last key matches reference type when constructing AAS References --- aas/adapter/xml/xml_deserialization.py | 248 ++++++++++++++----------- 1 file changed, 143 insertions(+), 105 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 5a62218..6ac3297 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -18,7 +18,7 @@ from ... import model import xml.etree.ElementTree as ElTree import logging -from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar +from typing import Any, Callable, Dict, IO, Iterable, List, Optional, Set, Tuple, Type, TypeVar from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ @@ -29,101 +29,141 @@ logger = logging.getLogger(__name__) T = TypeVar('T') -def _unwrap(monad: Optional[T]) -> T: - if monad is not None: - return monad - raise TypeError(f"Unwrap failed for value {monad}!") +def _get_child_mandatory(element: ElTree.Element, child_tag: str) -> ElTree.Element: + child = element.find(child_tag) + if child is None: + raise KeyError(f"XML element {element.tag} has no child {child_tag}!") + return child -def _constructor_name_to_typename(constructor: Callable[[ElTree.Element, bool], T]) -> str: - return "".join([s[0].upper() + s[1:] for s in constructor.__name__.split("_")[2:]]) +def _get_attrib_mandatory(element: ElTree.Element, attrib: str) -> str: + if attrib not in element.attrib: + raise KeyError(f"XML element {element.tag} has no attribute with name {attrib}!") + return element.attrib[attrib] + + +def _get_attrib_mandatory_mapped(element: ElTree.Element, attrib: str, dct: Dict[str, T]) -> T: + attrib_value = _get_attrib_mandatory(element, attrib) + if attrib_value not in dct: + raise ValueError(f"Attribute {attrib} of XML element {element.tag} has invalid value: {attrib_value}") + return dct[attrib_value] def _get_text_or_none(element: Optional[ElTree.Element]) -> Optional[str]: return element.text if element is not None else None -def _get_text_mandatory(element: Optional[ElTree.Element]) -> str: - # unwrap value here so a TypeError is thrown if the element is None - element_unwrapped = _unwrap(element) - text = _get_text_or_none(element_unwrapped) +def _get_text_mandatory(element: ElTree.Element) -> str: + text = element.text if text is None: - raise TypeError(f"XML element {element_unwrapped.tag} has no text!") + raise KeyError(f"XML element {element.tag} has no text!") return text -def _objects_from_xml_elements(elements: Iterable[ElTree.Element], constructor: Callable[..., T], - failsafe: bool, **kwargs: Any) -> Iterable[T]: - for element in elements: - parsed = _object_from_xml_element(element, constructor, failsafe, **kwargs) - if parsed is not None: - yield parsed +def _get_text_mandatory_mapped(element: ElTree.Element, dct: Dict[str, T]) -> T: + text = _get_text_mandatory(element) + if text not in dct: + raise ValueError(f"Text of XML element {element.tag} is invalid: {text}") + return dct[text] + + +def _constructor_name_to_typename(constructor: Callable[[ElTree.Element, bool], T]) -> str: + return "".join([s[0].upper() + s[1:] for s in constructor.__name__.split("_")[2:]]) -def _object_from_xml_element(element: Optional[ElTree.Element], constructor: Callable[..., T], - failsafe: bool, **kwargs: Any) -> Optional[T]: +def _exception_to_str(exception: BaseException) -> str: + string = str(exception) + return string[1:-1] if isinstance(exception, KeyError) else string + + +def _failsafe_construct(element: Optional[ElTree.Element], constructor: Callable[..., T], failsafe: bool, + **kwargs: Any) -> Optional[T]: if element is None: return None try: return constructor(element, failsafe, **kwargs) - except (KeyError, TypeError) as e: - error_message = f"{type(e).__name__} while converting XML element with tag {element.tag} to " \ - f"type {_constructor_name_to_typename(constructor)}: {e}" + except (KeyError, ValueError) as e: + error_message = f"while converting XML element with tag {element.tag} to "\ + f"type {_constructor_name_to_typename(constructor)}" if not failsafe: raise type(e)(error_message) from e - logger.error(error_message) + error_type = type(e).__name__ + cause: Optional[BaseException] = e + while cause is not None: + error_message = _exception_to_str(cause) + "\n -> " + error_message + cause = cause.__cause__ + logger.error(error_type + ": " + error_message) + logger.error(f"Failed to construct {_constructor_name_to_typename(constructor)}!") return None -def _object_from_xml_element_mandatory(parent: ElTree.Element, tag: str, - constructor: Callable[..., T], **kwargs: Any) -> T: - element = parent.find(tag) - if element is None: - raise KeyError(f"No such element {tag} found in {parent.tag}!") - return _unwrap(_object_from_xml_element(element, constructor, False, **kwargs)) +def _failsafe_construct_multiple(elements: Iterable[ElTree.Element], constructor: Callable[..., T], failsafe: bool, + **kwargs: Any) -> Iterable[T]: + for element in elements: + parsed = _failsafe_construct(element, constructor, failsafe, **kwargs) + if parsed is not None: + yield parsed + + +def _find_and_construct_mandatory(element: ElTree.Element, child_tag: str, constructor: Callable[..., T], + **kwargs: Any) -> T: + constructed = _failsafe_construct(_get_child_mandatory(element, child_tag), constructor, False, **kwargs) + if constructed is None: + raise TypeError("The result of a non-failsafe _failsafe_construct() call was None! " + "This is a bug in the pyAAS XML deserialization, please report it!") + return constructed def _construct_key(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Key: return model.Key( - KEY_ELEMENTS_INVERSE[element.attrib["type"]], - element.attrib["local"] == "True", + _get_attrib_mandatory_mapped(element, "type", KEY_ELEMENTS_INVERSE), + _get_attrib_mandatory(element, "local").lower() == "true", _get_text_mandatory(element), - KEY_TYPES_INVERSE[element.attrib["idType"]] + _get_attrib_mandatory_mapped(element, "idType", KEY_TYPES_INVERSE) ) def _construct_key_tuple(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> Tuple[model.Key, ...]: - return tuple(_objects_from_xml_elements(_unwrap(element.find(NS_AAS + "keys")).findall(NS_AAS + "key"), - _construct_key, failsafe)) + keys = _get_child_mandatory(element, NS_AAS + "keys") + return tuple(_failsafe_construct_multiple(keys.findall(NS_AAS + "key"), _construct_key, failsafe)) def _construct_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Reference: return model.Reference(_construct_key_tuple(element, failsafe)) -def _construct_submodel_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_aas_reference(element: ElTree.Element, failsafe: bool, type_: Type[model.base._RT], **_kwargs: Any)\ + -> model.AASReference[model.base._RT]: + keys = _construct_key_tuple(element, failsafe) + if len(keys) != 0 and not issubclass(KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): + logger.warning(f"Type {keys[-1].type.name} of last key of reference to {' / '.join(str(k) for k in keys)} " + f"does not match reference type {type_.__name__}") + return model.AASReference(keys, type_) + + +def _construct_submodel_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.Submodel]: - return model.AASReference(_construct_key_tuple(element, failsafe), model.Submodel) + return _construct_aas_reference(element, failsafe, model.Submodel, **kwargs) -def _construct_asset_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_asset_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.Asset]: - return model.AASReference(_construct_key_tuple(element, failsafe), model.Asset) + return _construct_aas_reference(element, failsafe, model.Asset, **kwargs) -def _construct_asset_administration_shell_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_asset_administration_shell_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.AssetAdministrationShell]: - return model.AASReference(_construct_key_tuple(element, failsafe), model.AssetAdministrationShell) + return _construct_aas_reference(element, failsafe, model.AssetAdministrationShell, **kwargs) -def _construct_referable_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_referable_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.Referable]: - return model.AASReference(_construct_key_tuple(element, failsafe), model.Referable) + return _construct_aas_reference(element, failsafe, model.Referable, **kwargs) -def _construct_concept_description_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_concept_description_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.ConceptDescription]: - return model.AASReference(_construct_key_tuple(element, failsafe), model.ConceptDescription) + return _construct_aas_reference(element, failsafe, model.ConceptDescription, **kwargs) def _construct_administrative_information(element: ElTree.Element, _failsafe: bool, **_kwargs: Any)\ @@ -137,17 +177,20 @@ def _construct_administrative_information(element: ElTree.Element, _failsafe: bo def _construct_lang_string_set(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.LangStringSet: lss: model.LangStringSet = {} for lang_string in element.findall(NS_IEC + "langString"): - lss[lang_string.attrib["lang"]] = _get_text_mandatory(lang_string) + lss[_get_attrib_mandatory(lang_string, "lang")] = _get_text_mandatory(lang_string) return lss def _construct_qualifier(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Qualifier: q = model.Qualifier( - _get_text_mandatory(element.find(NS_AAS + "type")), - model.datatypes.XSD_TYPE_CLASSES[_get_text_mandatory(element.find(NS_AAS + "valueType"))], - _get_text_or_none(element.find(NS_AAS + "value")) + _get_text_mandatory(_get_child_mandatory(element, NS_AAS + "type")), + _get_text_mandatory_mapped(_get_child_mandatory(element, NS_AAS + "valueType"), + model.datatypes.XSD_TYPE_CLASSES) ) - value_id = _object_from_xml_element(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) + value = element.find(NS_AAS + "value") + if value is not None: + q.value = value.text + value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) if value_id is not None: q.value_id = value_id _amend_abstract_attributes(q, element, failsafe) @@ -156,12 +199,10 @@ def _construct_qualifier(element: ElTree.Element, failsafe: bool, **_kwargs: Any def _construct_formula(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Formula: ref_set: Set[model.Reference] = set() - for ref in element: - obj = _object_from_xml_element(ref, _construct_reference, failsafe) - if not obj: - logger.warning(f"Skipping invalid XML element with tag {ref.tag}") - continue - ref_set.add(obj) + depends_on_refs = element.find(NS_AAS + "dependsOnRefs") + if depends_on_refs is not None: + ref_set = set(_failsafe_construct_multiple(depends_on_refs.findall(NS_AAS + "reference"), _construct_reference, + failsafe)) return model.Formula(ref_set) @@ -175,7 +216,7 @@ def _construct_constraint(element: ElTree.Element, failsafe: bool, **_kwargs: An def _construct_identifier(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Identifier: return model.Identifier( _get_text_mandatory(element), - IDENTIFIER_TYPES_INVERSE[element.attrib["idType"]] + _get_attrib_mandatory_mapped(element, "idType", IDENTIFIER_TYPES_INVERSE) ) @@ -184,22 +225,22 @@ def _construct_security(_element: ElTree.Element, _failsafe: bool, **_kwargs: An def _construct_view(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.View: - view = model.View(_get_text_mandatory(element.find(NS_AAS + "idShort"))) + view = model.View(_get_text_mandatory(_get_child_mandatory(element, NS_AAS + "idShort"))) contained_elements = element.find(NS_AAS + "containedElements") if contained_elements is not None: view.contained_element = set( - _objects_from_xml_elements(contained_elements.findall(NS_AAS + "containedElementRef"), - _construct_referable_reference, failsafe) + _failsafe_construct_multiple(contained_elements.findall(NS_AAS + "containedElementRef"), + _construct_referable_reference, failsafe) ) _amend_abstract_attributes(view, element, failsafe) return view def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDictionary: - cd = model.ConceptDictionary(_get_text_mandatory(element.find(NS_AAS + "idShort"))) + cd = model.ConceptDictionary(_get_text_mandatory(_get_child_mandatory(element, NS_AAS + "idShort"))) concept_description = element.find(NS_AAS + "conceptDescriptionRefs") if concept_description is not None: - cd.concept_description = set(_objects_from_xml_elements( + cd.concept_description = set(_failsafe_construct_multiple( concept_description.findall(NS_AAS + "conceptDescriptionRef"), _construct_concept_description_reference, failsafe @@ -211,39 +252,42 @@ def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool, **_kw def _construct_asset_administration_shell(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ -> model.AssetAdministrationShell: aas = model.AssetAdministrationShell( - _object_from_xml_element_mandatory(element, NS_AAS + "assetRef", _construct_asset_reference), - _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) + _find_and_construct_mandatory(element, NS_AAS + "assetRef", _construct_asset_reference), + _find_and_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) ) - aas.security = _object_from_xml_element(element.find(NS_ABAC + "security"), _construct_security, failsafe) + security = _failsafe_construct(element.find(NS_ABAC + "security"), _construct_security, failsafe) + if security is not None: + aas.security = security submodels = element.find(NS_AAS + "submodelRefs") if submodels is not None: - aas.submodel = set(_objects_from_xml_elements(submodels.findall(NS_AAS + "submodelRef"), - _construct_submodel_reference, failsafe)) + aas.submodel = set(_failsafe_construct_multiple(submodels.findall(NS_AAS + "submodelRef"), + _construct_submodel_reference, failsafe)) views = element.find(NS_AAS + "views") if views is not None: - for view in _objects_from_xml_elements(views.findall(NS_AAS + "view"), _construct_view, failsafe): + for view in _failsafe_construct_multiple(views.findall(NS_AAS + "view"), _construct_view, failsafe): aas.view.add(view) concept_dictionaries = element.find(NS_AAS + "conceptDictionaries") if concept_dictionaries is not None: - for cd in _objects_from_xml_elements(concept_dictionaries.findall(NS_AAS + "conceptDictionary"), - _construct_concept_dictionary, failsafe): + for cd in _failsafe_construct_multiple(concept_dictionaries.findall(NS_AAS + "conceptDictionary"), + _construct_concept_dictionary, failsafe): aas.concept_dictionary.add(cd) - derived_from = element.find(NS_AAS + "derivedFrom") + derived_from = _failsafe_construct(element.find(NS_AAS + "derivedFrom"), + _construct_asset_administration_shell_reference, failsafe) if derived_from is not None: - aas.derived_from = _object_from_xml_element(element, _construct_asset_administration_shell_reference, failsafe) + aas.derived_from = derived_from _amend_abstract_attributes(aas, element, failsafe) return aas def _construct_asset(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Asset: asset = model.Asset( - ASSET_KIND_INVERSE[_get_text_mandatory(element.find(NS_AAS + "kind"))], - _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) + _get_text_mandatory_mapped(_get_child_mandatory(element, NS_AAS + "kind"), ASSET_KIND_INVERSE), + _find_and_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) ) - asset.asset_identification_model = _object_from_xml_element(element.find(NS_AAS + "assetIdentificationModelRef"), - _construct_submodel_reference, failsafe) - asset.bill_of_material = _object_from_xml_element(element.find(NS_AAS + "billOfMaterialRef"), - _construct_submodel_reference, failsafe) + asset.asset_identification_model = _failsafe_construct(element.find(NS_AAS + "assetIdentificationModelRef"), + _construct_submodel_reference, failsafe) + asset.bill_of_material = _failsafe_construct(element.find(NS_AAS + "billOfMaterialRef"), + _construct_submodel_reference, failsafe) _amend_abstract_attributes(asset, element, failsafe) return asset @@ -254,9 +298,9 @@ def _construct_submodel(element: ElTree.Element, failsafe: bool, **_kwargs: Any) def _construct_concept_description(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: cd = model.ConceptDescription( - _object_from_xml_element_mandatory(element, NS_AAS + "identification", _construct_identifier) + _find_and_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) ) - is_case_of = set(_objects_from_xml_elements(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe)) + is_case_of = set(_failsafe_construct_multiple(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe)) if len(is_case_of) != 0: cd.is_case_of = is_case_of _amend_abstract_attributes(cd, element, failsafe) @@ -265,34 +309,28 @@ def _construct_concept_description(element: ElTree.Element, failsafe: bool, **_k def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> None: if isinstance(obj, model.Referable): - category = element.find(NS_AAS + "category") - if category: - obj.category = _get_text_or_none(category) - description = element.find(NS_AAS + "description") - if description: - obj.description = _object_from_xml_element(description, _construct_lang_string_set, failsafe) + category = _get_text_or_none(element.find(NS_AAS + "category")) + if category is not None: + obj.category = category + description = _failsafe_construct(element.find(NS_AAS + "description"), _construct_lang_string_set, failsafe) + if description is not None: + obj.description = description if isinstance(obj, model.Identifiable): - id_short = element.find(NS_AAS + "idShort") - if id_short: - obj.id_short = _get_text_or_none(id_short) - administration = element.find(NS_AAS + "administration") + id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) + if id_short is not None: + obj.id_short = id_short + administration = _failsafe_construct(element.find(NS_AAS + "administration"), + _construct_administrative_information, failsafe) if administration: - obj.administration = _object_from_xml_element(administration, _construct_administrative_information, - failsafe) + obj.administration = administration if isinstance(obj, model.HasSemantics): - semantic_id = element.find(NS_AAS + "semanticId") - if semantic_id: - obj.semantic_id = _object_from_xml_element(semantic_id, _construct_reference, failsafe) + semantic_id = _failsafe_construct(element.find(NS_AAS + "semanticId"), _construct_reference, failsafe) + if semantic_id is not None: + obj.semantic_id = semantic_id if isinstance(obj, model.Qualifiable): - for constraint in element: - if constraint.tag != NS_AAS + "qualifiers": - logger.warning(f"Skipping XML element with invalid tag {constraint.tag}") - continue - constraint_obj = _object_from_xml_element(constraint, _construct_constraint, failsafe) - if not constraint_obj: - logger.warning(f"Skipping invalid XML element with tag {constraint.tag}") - continue - obj.qualifier.add(constraint_obj) + for constraint in _failsafe_construct_multiple(element.findall(NS_AAS + "qualifiers"), _construct_constraint, + failsafe): + obj.qualifier.add(constraint) def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: @@ -322,7 +360,7 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: if list_.tag[-1] != "s" or element_tag not in element_constructors.keys(): raise TypeError(f"Unexpected list {list_.tag}") constructor = element_constructors[element_tag] - for element in _objects_from_xml_elements(list_.findall(element_tag), constructor, failsafe): + for element in _failsafe_construct_multiple(list_.findall(element_tag), constructor, failsafe): # element is always Identifiable, because the tag is checked earlier # this is just to satisfy the type checker if isinstance(element, model.Identifiable): -- GitLab From f0a50e3f840c9223616b75f3fcc616a633650695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 27 Mar 2020 09:46:42 +0100 Subject: [PATCH 14/27] adapter.xml: add docstrings to all non-constructor functions I think adding docstrings to every constructor function would be unnecessary, since they all do nearly the same task, just for other xml elements. extend the module docstring move _amend_abstract_attributes() to the top of the module (where the other helper functions are) do not raise a TypeError in read_xml_aas_file() when an unexpected top-level list is encountered in failsafe mode --- aas/adapter/xml/xml_deserialization.py | 210 +++++++++++++++++++++---- 1 file changed, 182 insertions(+), 28 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 6ac3297..6e1ac18 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -10,6 +10,26 @@ # specific language governing permissions and limitations under the License. """ Module for deserializing Asset Administration Shell data from the official XML format + +Use this module by calling read_xml_aas_file(file, failsafe). +The function returns a DictObjectStore containing all parsed elements. + +Unlike the JSON deserialization, parsing is done top-down. Elements with a specific tag are searched on the level +directly below the level of the current xml element (in terms of parent and child relation) and parsed when +found. Constructor functions of these elements will then again search for mandatory and optional child elements +and construct them if available, and so on. + +This module supports parsing in failsafe and non-failsafe mode. +In failsafe mode errors regarding missing attributes and elements or invalid values are caught and logged. +In non-failsafe mode any error would abort parsing. +Error handling is done only by _failsafe_construct() in this module. Nearly all constructor functions are called +by other constructor functions via _failsafe_construct(), so an error chain is constructed in the error case, +which allows printing stacktrace-like error messages like the following in the error case (in failsafe mode of course): +KeyError: XML element {http://www.admin-shell.io/aas/2/0}identification has no attribute with name idType! + -> while converting XML element with tag {http://www.admin-shell.io/aas/2/0}identification to type Identifier + -> while converting XML element with tag {http://www.admin-shell.io/aas/2/0}assetAdministrationShell to type + AssetAdministrationShell +Failed to construct AssetAdministrationShell! """ # TODO: add constructor for submodel + all classes required by submodel @@ -30,6 +50,14 @@ T = TypeVar('T') def _get_child_mandatory(element: ElTree.Element, child_tag: str) -> ElTree.Element: + """ + A helper function for getting a mandatory child element. + + :param element: The parent element. + :param child_tag: The tag of the child element to return. + :return: The child element. + :raises KeyError: If the parent element has no child element with the given tag. + """ child = element.find(child_tag) if child is None: raise KeyError(f"XML element {element.tag} has no child {child_tag}!") @@ -37,12 +65,34 @@ def _get_child_mandatory(element: ElTree.Element, child_tag: str) -> ElTree.Elem def _get_attrib_mandatory(element: ElTree.Element, attrib: str) -> str: + """ + A helper function for getting a mandatory attribute of an element. + + :param element: The xml element. + :param attrib: The name of the attribute. + :return: The value of the attribute. + :raises KeyError: If the attribute does not exist. + """ if attrib not in element.attrib: raise KeyError(f"XML element {element.tag} has no attribute with name {attrib}!") return element.attrib[attrib] def _get_attrib_mandatory_mapped(element: ElTree.Element, attrib: str, dct: Dict[str, T]) -> T: + """ + A helper function for getting a mapped mandatory attribute of an xml element. + + It first gets the attribute value using _get_attrib_mandatory(), which raises a KeyError if the attribute + does not exist. + Then it returns dct[] and raises a ValueError, if the attribute value does not exist in the dict. + + :param element: The xml element. + :param attrib: The name of the attribute. + :param dct: The dictionary that is used to map the attribute value. + :return: The mapped value of the attribute. + :raises KeyError: If the attribute does not exist. + :raises ValueError: If the value of the attribute does not exist in dct. + """ attrib_value = _get_attrib_mandatory(element, attrib) if attrib_value not in dct: raise ValueError(f"Attribute {attrib} of XML element {element.tag} has invalid value: {attrib_value}") @@ -50,10 +100,29 @@ def _get_attrib_mandatory_mapped(element: ElTree.Element, attrib: str, dct: Dict def _get_text_or_none(element: Optional[ElTree.Element]) -> Optional[str]: + """ + A helper function for getting the text of an element, when it's not clear whether the element exists or not. + + This function is useful whenever the text of an optional child element is needed. + Then the text can be get with: text = _get_text_or_none(element.find("childElement") + element.find() returns either the element or None, if it doesn't exist. This is why this function accepts + an optional element, to reduce the amount of code in the constructor functions below. + + :param element: The xml element or None. + :return: The text of the xml element if the xml element is not None and if the xml element has a text. + None otherwise. + """ return element.text if element is not None else None def _get_text_mandatory(element: ElTree.Element) -> str: + """ + A helper function for getting the mandatory text of an element. + + :param element: The xml element. + :return: The text of the xml element. + :raises KeyError: If the xml element has no text. + """ text = element.text if text is None: raise KeyError(f"XML element {element.tag} has no text!") @@ -61,6 +130,19 @@ def _get_text_mandatory(element: ElTree.Element) -> str: def _get_text_mandatory_mapped(element: ElTree.Element, dct: Dict[str, T]) -> T: + """ + A helper function for getting the mapped mandatory text of an element. + + It first gets the text of the element using _get_text_mandatory(), + which raises a KeyError if the element has no text. + Then it returns dct[] and raises a ValueError, if the text of the element does not exist in the dict. + + :param element: The xml element. + :param dct: The dictionary that is used to map the text. + :return: The mapped text of the element. + :raises KeyError: If the element has no text. + :raises ValueError: If the text of the xml element does not exist in dct. + """ text = _get_text_mandatory(element) if text not in dct: raise ValueError(f"Text of XML element {element.tag} is invalid: {text}") @@ -68,16 +150,49 @@ def _get_text_mandatory_mapped(element: ElTree.Element, dct: Dict[str, T]) -> T: def _constructor_name_to_typename(constructor: Callable[[ElTree.Element, bool], T]) -> str: + """ + A helper function for converting the name of a constructor function to the respective type name. + + _construct_some_type -> SomeType + + :param constructor: The constructor function. + :return: The name of the type the constructor function constructs. + """ return "".join([s[0].upper() + s[1:] for s in constructor.__name__.split("_")[2:]]) def _exception_to_str(exception: BaseException) -> str: + """ + A helper function used to stringify exceptions. + + It removes the quotation marks '' that are put around str(KeyError), otherwise it's just calls str(exception). + + :param exception: The exception to stringify. + :return: The stringified exception. + """ string = str(exception) return string[1:-1] if isinstance(exception, KeyError) else string def _failsafe_construct(element: Optional[ElTree.Element], constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Optional[T]: + """ + A wrapper function that is used to handle exceptions raised in constructor functions. + + This is the only function of this module where exceptions are caught. + This is why constructor functions should (in almost all cases) call other constructor functions using this function, + so errors can be caught and logged in failsafe mode. + The functions accepts None as a valid value for element for the same reason _get_text_or_none() does, so it can be + called like _failsafe_construct(element.find("childElement"), ...), since element.find() can return None. + This function will also return None in this case. + + :param element: The xml element or None. + :param constructor: The constructor function to apply on the element. + :param failsafe: Indicates whether errors should be caught or re-raised. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: The constructed class instance, if construction was successful. + None if the element was None or if the construction failed. + """ if element is None: return None try: @@ -99,6 +214,17 @@ def _failsafe_construct(element: Optional[ElTree.Element], constructor: Callable def _failsafe_construct_multiple(elements: Iterable[ElTree.Element], constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Iterable[T]: + """ + A generator function that applies _failsafe_construct() to multiple elements. + + :param elements: Any iterable containing any number of xml elements. + :param constructor: The constructor function to apply on the xml elements. + :param failsafe: Indicates whether errors should be caught or re-raised. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: An iterator over the successfully constructed elements. + If an error occurred while constructing an element and while in failsafe mode, + this element will be skipped. + """ for element in elements: parsed = _failsafe_construct(element, constructor, failsafe, **kwargs) if parsed is not None: @@ -107,6 +233,21 @@ def _failsafe_construct_multiple(elements: Iterable[ElTree.Element], constructor def _find_and_construct_mandatory(element: ElTree.Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any) -> T: + """ + A helper function that finds a mandatory child element and applies a constructor function to it + in non-failsafe mode, meaning that any errors will not be caught. + + Useful when constructing mandatory child elements while not knowing whether their respective xml elements exist + in the first place. + + :param element: The parent xml element. + :param child_tag: The tag of the child element. + :param constructor: The constructor function to apply on the xml element. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: The constructed child element. + :raises TypeError: If the result of _failsafe_construct() in non-failsafe mode was None. + This shouldn't be possible and if it happens, indicates a bug in _failsafe_construct(). + """ constructed = _failsafe_construct(_get_child_mandatory(element, child_tag), constructor, False, **kwargs) if constructed is None: raise TypeError("The result of a non-failsafe _failsafe_construct() call was None! " @@ -114,6 +255,41 @@ def _find_and_construct_mandatory(element: ElTree.Element, child_tag: str, const return constructed +def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: + """ + A helper function that amends optional attributes to already constructed class instances, if they inherit + from an abstract class like Referable, Identifiable, HasSemantics or Qualifiable. + + :param obj: The constructed class instance. + :param element: The respective xml element. + :param failsafe: Indicates whether errors should be caught or re-raised. + :return: None + """ + if isinstance(obj, model.Referable): + category = _get_text_or_none(element.find(NS_AAS + "category")) + if category is not None: + obj.category = category + description = _failsafe_construct(element.find(NS_AAS + "description"), _construct_lang_string_set, failsafe) + if description is not None: + obj.description = description + if isinstance(obj, model.Identifiable): + id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) + if id_short is not None: + obj.id_short = id_short + administration = _failsafe_construct(element.find(NS_AAS + "administration"), + _construct_administrative_information, failsafe) + if administration: + obj.administration = administration + if isinstance(obj, model.HasSemantics): + semantic_id = _failsafe_construct(element.find(NS_AAS + "semanticId"), _construct_reference, failsafe) + if semantic_id is not None: + obj.semantic_id = semantic_id + if isinstance(obj, model.Qualifiable): + for constraint in _failsafe_construct_multiple(element.findall(NS_AAS + "qualifiers"), _construct_constraint, + failsafe): + obj.qualifier.add(constraint) + + def _construct_key(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Key: return model.Key( _get_attrib_mandatory_mapped(element, "type", KEY_ELEMENTS_INVERSE), @@ -307,32 +483,6 @@ def _construct_concept_description(element: ElTree.Element, failsafe: bool, **_k return cd -def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> None: - if isinstance(obj, model.Referable): - category = _get_text_or_none(element.find(NS_AAS + "category")) - if category is not None: - obj.category = category - description = _failsafe_construct(element.find(NS_AAS + "description"), _construct_lang_string_set, failsafe) - if description is not None: - obj.description = description - if isinstance(obj, model.Identifiable): - id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) - if id_short is not None: - obj.id_short = id_short - administration = _failsafe_construct(element.find(NS_AAS + "administration"), - _construct_administrative_information, failsafe) - if administration: - obj.administration = administration - if isinstance(obj, model.HasSemantics): - semantic_id = _failsafe_construct(element.find(NS_AAS + "semanticId"), _construct_reference, failsafe) - if semantic_id is not None: - obj.semantic_id = semantic_id - if isinstance(obj, model.Qualifiable): - for constraint in _failsafe_construct_multiple(element.findall(NS_AAS + "qualifiers"), _construct_constraint, - failsafe): - obj.qualifier.add(constraint) - - def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 @@ -357,8 +507,12 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: ret: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() for list_ in root: element_tag = list_.tag[:-1] - if list_.tag[-1] != "s" or element_tag not in element_constructors.keys(): - raise TypeError(f"Unexpected list {list_.tag}") + if list_.tag[-1] != "s" or element_tag not in element_constructors: + error_message = f"Unexpected top-level list {list_.tag}" + if not failsafe: + raise TypeError(error_message) + logger.warning(error_message) + continue constructor = element_constructors[element_tag] for element in _failsafe_construct_multiple(list_.findall(element_tag), constructor, failsafe): # element is always Identifiable, because the tag is checked earlier -- GitLab From c62d7823ef268e31163217062c0151a702f32791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 28 Mar 2020 05:05:16 +0100 Subject: [PATCH 15/27] adapter.xml: begin work on the submodel constructor add a helper function for setting the modeling kind fix construction of qualifiers and formulas --- aas/adapter/xml/xml_deserialization.py | 57 ++++++++++++++++++++------ 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 6e1ac18..f0d9475 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -38,7 +38,7 @@ from ... import model import xml.etree.ElementTree as ElTree import logging -from typing import Any, Callable, Dict, IO, Iterable, List, Optional, Set, Tuple, Type, TypeVar +from typing import Any, Callable, Dict, IO, Iterable, List, Optional, Set, Tuple, Type, TypedDict, TypeVar from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ @@ -46,7 +46,7 @@ from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_I logger = logging.getLogger(__name__) -T = TypeVar('T') +T = TypeVar("T") def _get_child_mandatory(element: ElTree.Element, child_tag: str) -> ElTree.Element: @@ -285,9 +285,40 @@ def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: b if semantic_id is not None: obj.semantic_id = semantic_id if isinstance(obj, model.Qualifiable): - for constraint in _failsafe_construct_multiple(element.findall(NS_AAS + "qualifiers"), _construct_constraint, - failsafe): - obj.qualifier.add(constraint) + qualifiers = element.find(NS_AAS + "qualifiers") + if qualifiers is not None: + for formula in _failsafe_construct_multiple(qualifiers.findall(NS_AAS + "formula"), + _construct_formula, failsafe): + obj.qualifier.add(formula) + for qualifier in _failsafe_construct_multiple(qualifiers.findall(NS_AAS + "qualifier"), + _construct_qualifier, failsafe): + obj.qualifier.add(qualifier) + + +class ModelingKindKwArg(TypedDict, total=False): + kind: model.ModelingKind + + +def _get_modeling_kind_kwarg(element: ElTree.Element) -> ModelingKindKwArg: + """ + A helper function that creates a dict containing the modeling kind or nothing for a given xml element. + + Since the modeling kind can only be set in the __init__ method of a class that inherits from model.HasKind, + the dict returned by this function can be passed directly to the classes __init__ method. + An alternative to this function would be returning the modeling kind directly and falling back to the default + value if no "kind" xml element is present, but in this case the default value would have to be defined here as well. + In my opinion defining what the default value is, should be the task of the __init__ method, not the task of any + function in the deserialization. + + :param element: The xml element. + :return: A dict containing {"kind": }, if a kind element was found. + An empty dict if not. + """ + kwargs: ModelingKindKwArg = ModelingKindKwArg() + kind = element.find(NS_AAS + "kind") + if kind is not None: + kwargs["kind"] = _get_text_mandatory_mapped(kind, MODELING_KIND_INVERSE) + return kwargs def _construct_key(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Key: @@ -382,13 +413,6 @@ def _construct_formula(element: ElTree.Element, failsafe: bool, **_kwargs: Any) return model.Formula(ref_set) -def _construct_constraint(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Constraint: - return { - NS_AAS + "qualifier": _construct_qualifier, - NS_AAS + "formula": _construct_formula - }[element.tag](element, failsafe) - - def _construct_identifier(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Identifier: return model.Identifier( _get_text_mandatory(element), @@ -469,7 +493,14 @@ def _construct_asset(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> def _construct_submodel(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Submodel: - pass + submodel_elements = _get_child_mandatory(element, NS_AAS + "submodelElements") + submodel = model.Submodel( + _find_and_construct_mandatory(element, NS_AAS + "identification", _construct_identifier), + **_get_modeling_kind_kwarg(element) + ) + # TODO: continue here + _amend_abstract_attributes(submodel, element, failsafe) + return submodel def _construct_concept_description(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: -- GitLab From e9173004734342fe0f7abef75c4d047071b92e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 28 Mar 2020 05:53:21 +0100 Subject: [PATCH 16/27] adapter.xml: import TypedDict from mypy_extensions remove unused import List --- aas/adapter/xml/xml_deserialization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index f0d9475..ee9c572 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -38,7 +38,8 @@ from ... import model import xml.etree.ElementTree as ElTree import logging -from typing import Any, Callable, Dict, IO, Iterable, List, Optional, Set, Tuple, Type, TypedDict, TypeVar +from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar +from mypy_extensions import TypedDict # TODO: import this from typing should we require python 3.8+ at some point from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ -- GitLab From cba3ced8b36821a22395448a1384aa2c1857aee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 28 Mar 2020 20:55:36 +0100 Subject: [PATCH 17/27] adapter.xml: add constructor for submodel and most submodel elements refactor some helper functions for better naming add _child_* helper functions fix constructing constraints --- aas/adapter/xml/xml_deserialization.py | 346 ++++++++++++++++++++----- 1 file changed, 276 insertions(+), 70 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index ee9c572..2b0e975 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -25,6 +25,7 @@ In non-failsafe mode any error would abort parsing. Error handling is done only by _failsafe_construct() in this module. Nearly all constructor functions are called by other constructor functions via _failsafe_construct(), so an error chain is constructed in the error case, which allows printing stacktrace-like error messages like the following in the error case (in failsafe mode of course): + KeyError: XML element {http://www.admin-shell.io/aas/2/0}identification has no attribute with name idType! -> while converting XML element with tag {http://www.admin-shell.io/aas/2/0}identification to type Identifier -> while converting XML element with tag {http://www.admin-shell.io/aas/2/0}assetAdministrationShell to type @@ -32,11 +33,10 @@ KeyError: XML element {http://www.admin-shell.io/aas/2/0}identification has no a Failed to construct AssetAdministrationShell! """ -# TODO: add constructor for submodel + all classes required by submodel - from ... import model import xml.etree.ElementTree as ElTree import logging +import base64 from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar from mypy_extensions import TypedDict # TODO: import this from typing should we require python 3.8+ at some point @@ -50,18 +50,18 @@ logger = logging.getLogger(__name__) T = TypeVar("T") -def _get_child_mandatory(element: ElTree.Element, child_tag: str) -> ElTree.Element: +def _get_child_mandatory(parent: ElTree.Element, child_tag: str) -> ElTree.Element: """ A helper function for getting a mandatory child element. - :param element: The parent element. + :param parent: The parent element. :param child_tag: The tag of the child element to return. :return: The child element. :raises KeyError: If the parent element has no child element with the given tag. """ - child = element.find(child_tag) + child = parent.find(child_tag) if child is None: - raise KeyError(f"XML element {element.tag} has no child {child_tag}!") + raise KeyError(f"XML element {parent.tag} has no child {child_tag}!") return child @@ -91,7 +91,6 @@ def _get_attrib_mandatory_mapped(element: ElTree.Element, attrib: str, dct: Dict :param attrib: The name of the attribute. :param dct: The dictionary that is used to map the attribute value. :return: The mapped value of the attribute. - :raises KeyError: If the attribute does not exist. :raises ValueError: If the value of the attribute does not exist in dct. """ attrib_value = _get_attrib_mandatory(element, attrib) @@ -141,7 +140,6 @@ def _get_text_mandatory_mapped(element: ElTree.Element, dct: Dict[str, T]) -> T: :param element: The xml element. :param dct: The dictionary that is used to map the text. :return: The mapped text of the element. - :raises KeyError: If the element has no text. :raises ValueError: If the text of the xml element does not exist in dct. """ text = _get_text_mandatory(element) @@ -213,6 +211,25 @@ def _failsafe_construct(element: Optional[ElTree.Element], constructor: Callable return None +def _failsafe_construct_mandatory(element: ElTree.Element, constructor: Callable[..., T], + **kwargs: Any) -> T: + """ + _failsafe_construct() but not failsafe and it returns T instead of Optional[T] + + :param element: The xml element. + :param constructor: The constructor function to apply on the xml element. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: The constructed child element. + :raises TypeError: If the result of _failsafe_construct() in non-failsafe mode was None. + This shouldn't be possible and if it happens, indicates a bug in _failsafe_construct(). + """ + constructed = _failsafe_construct(element, constructor, False, **kwargs) + if constructed is None: + raise TypeError("The result of a non-failsafe _failsafe_construct() call was None! " + "This is a bug in the pyAAS XML deserialization, please report it!") + return constructed + + def _failsafe_construct_multiple(elements: Iterable[ElTree.Element], constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Iterable[T]: """ @@ -232,28 +249,41 @@ def _failsafe_construct_multiple(elements: Iterable[ElTree.Element], constructor yield parsed -def _find_and_construct_mandatory(element: ElTree.Element, child_tag: str, constructor: Callable[..., T], - **kwargs: Any) -> T: +def _child_construct_mandatory(parent: ElTree.Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any)\ + -> T: """ - A helper function that finds a mandatory child element and applies a constructor function to it - in non-failsafe mode, meaning that any errors will not be caught. - - Useful when constructing mandatory child elements while not knowing whether their respective xml elements exist - in the first place. + Shorthand for _failsafe_construct_mandatory() in combination with _get_child_mandatory(). - :param element: The parent xml element. - :param child_tag: The tag of the child element. - :param constructor: The constructor function to apply on the xml element. + :param parent: The xml element where the child element is searched. + :param child_tag: The tag of the child element to construct. + :param constructor: The constructor function for the child element. :param kwargs: Optional keyword arguments that are passed to the constructor function. :return: The constructed child element. - :raises TypeError: If the result of _failsafe_construct() in non-failsafe mode was None. - This shouldn't be possible and if it happens, indicates a bug in _failsafe_construct(). """ - constructed = _failsafe_construct(_get_child_mandatory(element, child_tag), constructor, False, **kwargs) - if constructed is None: - raise TypeError("The result of a non-failsafe _failsafe_construct() call was None! " - "This is a bug in the pyAAS XML deserialization, please report it!") - return constructed + return _failsafe_construct_mandatory(_get_child_mandatory(parent, child_tag), constructor, **kwargs) + + +def _child_text_mandatory(parent: ElTree.Element, child_tag: str) -> str: + """ + Shorthand for _get_text_mandatory() in combination with _get_child_mandatory(). + + :param parent: The xml element where the child element is searched. + :param child_tag: The tag of the child element to get the text from. + :return: The text of the child element. + """ + return _get_text_mandatory(_get_child_mandatory(parent, child_tag)) + + +def _child_text_mandatory_mapped(parent: ElTree.Element, child_tag: str, dct: Dict[str, T]) -> T: + """ + Shorthand for _get_text_mandatory_mapped() in combination with _get_child_mandatory(). + + :param parent: The xml element where the child element is searched. + :param child_tag: The tag of the child element to get the text from. + :param dct: The dictionary that is used to map the text of the child element. + :return: The mapped text of the child element. + """ + return _get_text_mandatory_mapped(_get_child_mandatory(parent, child_tag), dct) def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: @@ -288,12 +318,8 @@ def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: b if isinstance(obj, model.Qualifiable): qualifiers = element.find(NS_AAS + "qualifiers") if qualifiers is not None: - for formula in _failsafe_construct_multiple(qualifiers.findall(NS_AAS + "formula"), - _construct_formula, failsafe): - obj.qualifier.add(formula) - for qualifier in _failsafe_construct_multiple(qualifiers.findall(NS_AAS + "qualifier"), - _construct_qualifier, failsafe): - obj.qualifier.add(qualifier) + for constraint in _failsafe_construct_multiple(qualifiers, _construct_constraint, failsafe): + obj.qualifier.add(constraint) class ModelingKindKwArg(TypedDict, total=False): @@ -374,6 +400,11 @@ def _construct_concept_description_reference(element: ElTree.Element, failsafe: return _construct_aas_reference(element, failsafe, model.ConceptDescription, **kwargs) +def _construct_data_element_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ + -> model.AASReference[model.DataElement]: + return _construct_aas_reference(element, failsafe, model.DataElement, **kwargs) + + def _construct_administrative_information(element: ElTree.Element, _failsafe: bool, **_kwargs: Any)\ -> model.AdministrativeInformation: return model.AdministrativeInformation( @@ -391,13 +422,12 @@ def _construct_lang_string_set(element: ElTree.Element, _failsafe: bool, **_kwar def _construct_qualifier(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Qualifier: q = model.Qualifier( - _get_text_mandatory(_get_child_mandatory(element, NS_AAS + "type")), - _get_text_mandatory_mapped(_get_child_mandatory(element, NS_AAS + "valueType"), - model.datatypes.XSD_TYPE_CLASSES) + _child_text_mandatory(element, NS_AAS + "type"), + _child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) - value = element.find(NS_AAS + "value") + value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: - q.value = value.text + q.value = value value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) if value_id is not None: q.value_id = value_id @@ -426,43 +456,213 @@ def _construct_security(_element: ElTree.Element, _failsafe: bool, **_kwargs: An def _construct_view(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.View: - view = model.View(_get_text_mandatory(_get_child_mandatory(element, NS_AAS + "idShort"))) + view = model.View(_child_text_mandatory(element, NS_AAS + "idShort")) contained_elements = element.find(NS_AAS + "containedElements") if contained_elements is not None: - view.contained_element = set( - _failsafe_construct_multiple(contained_elements.findall(NS_AAS + "containedElementRef"), - _construct_referable_reference, failsafe) - ) + for ce in _failsafe_construct_multiple(contained_elements.findall(NS_AAS + "containedElementRef"), + _construct_referable_reference, failsafe): + view.contained_element.add(ce) _amend_abstract_attributes(view, element, failsafe) return view def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDictionary: - cd = model.ConceptDictionary(_get_text_mandatory(_get_child_mandatory(element, NS_AAS + "idShort"))) + concept_dictionary = model.ConceptDictionary(_child_text_mandatory(element, NS_AAS + "idShort")) concept_description = element.find(NS_AAS + "conceptDescriptionRefs") if concept_description is not None: - cd.concept_description = set(_failsafe_construct_multiple( - concept_description.findall(NS_AAS + "conceptDescriptionRef"), - _construct_concept_description_reference, - failsafe - )) - _amend_abstract_attributes(cd, element, failsafe) - return cd + for cd in _failsafe_construct_multiple(concept_description.findall(NS_AAS + "conceptDescriptionRef"), + _construct_concept_description_reference, failsafe): + concept_dictionary.concept_description.add(cd) + _amend_abstract_attributes(concept_dictionary, element, failsafe) + return concept_dictionary + + +def _construct_submodel_element(element: ElTree.Element, failsafe: bool, **kwargs: Any) -> model.SubmodelElement: + submodel_elements: Dict[str, Callable[..., model.SubmodelElement]] = {NS_AAS + k: v for k, v in { + "annotatedRelationshipElement": _construct_annotated_relationship_element, + "basicEvent": _construct_basic_event, + "blob": _construct_blob, + "capability": _construct_capability, + "entity": _construct_entity, + "file": _construct_file, + "multiLanguageProperty": _construct_multi_language_property, + "operation": _construct_operation, + "property": _construct_property, + "range": _construct_range, + "referenceElement": _construct_reference_element, + "relationshipElement": _construct_relationship_element, + "submodelElementCollection": _construct_submodel_element_collection + }.items()} + if element.tag not in submodel_elements: + raise KeyError(f"XML element {element.tag} is not a valid submodel element!") + return submodel_elements[element.tag](element, failsafe, **kwargs) + + +def _construct_constraint(element: ElTree.Element, failsafe: bool, **kwargs: Any) -> model.Constraint: + constraints: Dict[str, Callable[..., model.Constraint]] = {NS_AAS + k: v for k, v in { + "formula": _construct_formula, + "qualifier": _construct_qualifier + }.items()} + if element.tag not in constraints: + raise KeyError(f"XML element {element.tag} is not a valid constraint!") + return constraints[element.tag](element, failsafe, **kwargs) + + +def _construct_operation_variable(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.OperationVariable: + value = _get_child_mandatory(element, NS_AAS + "value") + if len(value) == 0: + raise KeyError("Value of operation variable has no submodel element!") + if len(value) > 1: + logger.warning("Value of operation variable has more than one submodel element, using the first one...") + return model.OperationVariable( + _failsafe_construct_mandatory(value[0], _construct_submodel_element) + ) + + +def _construct_annotated_relationship_element(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ + -> model.AnnotatedRelationshipElement: + annotated_relationship_element = model.AnnotatedRelationshipElement( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_construct_mandatory(element, NS_AAS + "first", _construct_referable_reference), + _child_construct_mandatory(element, NS_AAS + "second", _construct_referable_reference), + **_get_modeling_kind_kwarg(element) + ) + annotations = _get_child_mandatory(element, NS_AAS + "annotations") + for data_element_ref in _failsafe_construct_multiple(annotations.findall(NS_AAS + "reference"), + _construct_data_element_reference, failsafe): + annotated_relationship_element.annotation.add(data_element_ref) + _amend_abstract_attributes(annotated_relationship_element, element, failsafe) + return annotated_relationship_element + + +def _construct_basic_event(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.BasicEvent: + basic_event = model.BasicEvent( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_construct_mandatory(element, NS_AAS + "observed", _construct_referable_reference), + **_get_modeling_kind_kwarg(element) + ) + _amend_abstract_attributes(basic_event, element, failsafe) + return basic_event + + +def _construct_blob(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Blob: + blob = model.Blob( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_text_mandatory(element, NS_AAS + "mimeType"), + **_get_modeling_kind_kwarg(element) + ) + value = element.find(NS_AAS + "value") + if value is not None: + blob.value = base64.b64decode(_get_text_mandatory(value)) + _amend_abstract_attributes(blob, element, failsafe) + return blob + + +def _construct_capability(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Capability: + capability = model.Capability( + _child_text_mandatory(element, NS_AAS + "idShort"), + **_get_modeling_kind_kwarg(element) + ) + _amend_abstract_attributes(capability, element, failsafe) + return capability + + +def _construct_entity(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Entity: + entity = model.Entity( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), + **_get_modeling_kind_kwarg(element) + ) + asset_ref = _failsafe_construct(element.find(NS_AAS + "assetRef"), _construct_asset_reference, failsafe) + if asset_ref is not None: + entity.asset = asset_ref + for stmt in _failsafe_construct_multiple(_get_child_mandatory(element, NS_AAS + "statements"), + _construct_submodel_element, failsafe): + entity.statement.add(stmt) + _amend_abstract_attributes(entity, element, failsafe) + return entity + + +def _construct_file(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.File: + file = model.File( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_text_mandatory(element, NS_AAS + "idShort"), + **_get_modeling_kind_kwarg(element) + ) + value = element.find(NS_AAS + "value") + if value is not None: + file.value = _get_text_mandatory(value) + _amend_abstract_attributes(file, element, failsafe) + return file + + +def _construct_multi_language_property(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ + -> model.MultiLanguageProperty: + multi_language_property = model.MultiLanguageProperty( + _child_text_mandatory(element, NS_AAS + "idShort"), + **_get_modeling_kind_kwarg(element) + ) + value = _failsafe_construct(element.find(NS_AAS + "value"), _construct_lang_string_set, failsafe) + if value is not None: + multi_language_property.value = value + value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) + if value_id is not None: + multi_language_property.value_id = value_id + _amend_abstract_attributes(multi_language_property, element, failsafe) + return multi_language_property + + +def _construct_operation(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Operation: + operation = model.Operation( + _child_text_mandatory(element, NS_AAS + "idShort"), + **_get_modeling_kind_kwarg(element) + ) + # TODO + _amend_abstract_attributes(operation, element, failsafe) + return operation + + +def _construct_property(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Property: + # TODO + pass + + +def _construct_range(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Range: + # TODO + pass + + +def _construct_reference_element(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ReferenceElement: + # TODO + pass + + +def _construct_relationship_element(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ + -> model.RelationshipElement: + # TODO + pass + + +def _construct_submodel_element_collection(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ + -> model.SubmodelElementCollection: + # TODO + pass def _construct_asset_administration_shell(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ -> model.AssetAdministrationShell: aas = model.AssetAdministrationShell( - _find_and_construct_mandatory(element, NS_AAS + "assetRef", _construct_asset_reference), - _find_and_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) + _child_construct_mandatory(element, NS_AAS + "assetRef", _construct_asset_reference), + _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) ) security = _failsafe_construct(element.find(NS_ABAC + "security"), _construct_security, failsafe) if security is not None: aas.security = security submodels = element.find(NS_AAS + "submodelRefs") if submodels is not None: - aas.submodel = set(_failsafe_construct_multiple(submodels.findall(NS_AAS + "submodelRef"), - _construct_submodel_reference, failsafe)) + for sm in _failsafe_construct_multiple(submodels.findall(NS_AAS + "submodelRef"), _construct_submodel_reference, + failsafe): + aas.submodel.add(sm) views = element.find(NS_AAS + "views") if views is not None: for view in _failsafe_construct_multiple(views.findall(NS_AAS + "view"), _construct_view, failsafe): @@ -482,31 +682,37 @@ def _construct_asset_administration_shell(element: ElTree.Element, failsafe: boo def _construct_asset(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Asset: asset = model.Asset( - _get_text_mandatory_mapped(_get_child_mandatory(element, NS_AAS + "kind"), ASSET_KIND_INVERSE), - _find_and_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) + _child_text_mandatory_mapped(element, NS_AAS + "kind", ASSET_KIND_INVERSE), + _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) ) - asset.asset_identification_model = _failsafe_construct(element.find(NS_AAS + "assetIdentificationModelRef"), - _construct_submodel_reference, failsafe) - asset.bill_of_material = _failsafe_construct(element.find(NS_AAS + "billOfMaterialRef"), - _construct_submodel_reference, failsafe) + asset_identification_model = _failsafe_construct(element.find(NS_AAS + "assetIdentificationModelRef"), + _construct_submodel_reference, failsafe) + if asset_identification_model is not None: + asset.asset_identification_model = asset_identification_model + bill_of_material = _failsafe_construct(element.find(NS_AAS + "billOfMaterialRef"), _construct_submodel_reference, + failsafe) + if bill_of_material is not None: + asset.bill_of_material = bill_of_material _amend_abstract_attributes(asset, element, failsafe) return asset def _construct_submodel(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Submodel: - submodel_elements = _get_child_mandatory(element, NS_AAS + "submodelElements") submodel = model.Submodel( - _find_and_construct_mandatory(element, NS_AAS + "identification", _construct_identifier), + _failsafe_construct_mandatory(_get_child_mandatory(element, NS_AAS + "identification"), _construct_identifier), **_get_modeling_kind_kwarg(element) ) - # TODO: continue here + for submodel_element in _get_child_mandatory(element, NS_AAS + "submodelElements"): + constructed = _failsafe_construct(submodel_element, _construct_submodel_element, failsafe) + if constructed is not None: + submodel.submodel_element.add(constructed) _amend_abstract_attributes(submodel, element, failsafe) return submodel def _construct_concept_description(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: cd = model.ConceptDescription( - _find_and_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) + _failsafe_construct_mandatory(_get_child_mandatory(element, NS_AAS + "identification"), _construct_identifier) ) is_case_of = set(_failsafe_construct_multiple(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe)) if len(is_case_of) != 0: @@ -525,12 +731,12 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: :return: A DictObjectStore containing all AAS objects from the XML file """ - element_constructors = { - NS_AAS + "assetAdministrationShell": _construct_asset_administration_shell, - NS_AAS + "asset": _construct_asset, - NS_AAS + "submodel": _construct_submodel, - NS_AAS + "conceptDescription": _construct_concept_description - } + element_constructors = {NS_AAS + k: v for k, v in { + "assetAdministrationShell": _construct_asset_administration_shell, + "asset": _construct_asset, + "submodel": _construct_submodel, + "conceptDescription": _construct_concept_description + }.items()} tree = ElTree.parse(file) root = tree.getroot() @@ -540,7 +746,7 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: for list_ in root: element_tag = list_.tag[:-1] if list_.tag[-1] != "s" or element_tag not in element_constructors: - error_message = f"Unexpected top-level list {list_.tag}" + error_message = f"Unexpected top-level list {list_.tag}!" if not failsafe: raise TypeError(error_message) logger.warning(error_message) -- GitLab From 0a3aace596bec0c3d1763af3a728864550063e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 29 Mar 2020 06:42:30 +0200 Subject: [PATCH 18/27] adapter.xml: add final constructors for submodel elements mark _construct_security() as stub implementation make some variable names more explicit _construct_submodel(), _construct_concept_description(): use _child_construct_mandatory() shorthand added in previous commit --- aas/adapter/xml/xml_deserialization.py | 134 ++++++++++++++++++------- 1 file changed, 98 insertions(+), 36 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 2b0e975..68ffe01 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -197,8 +197,9 @@ def _failsafe_construct(element: Optional[ElTree.Element], constructor: Callable try: return constructor(element, failsafe, **kwargs) except (KeyError, ValueError) as e: + type_name = _constructor_name_to_typename(constructor) error_message = f"while converting XML element with tag {element.tag} to "\ - f"type {_constructor_name_to_typename(constructor)}" + f"type {type_name}" if not failsafe: raise type(e)(error_message) from e error_type = type(e).__name__ @@ -207,7 +208,7 @@ def _failsafe_construct(element: Optional[ElTree.Element], constructor: Callable error_message = _exception_to_str(cause) + "\n -> " + error_message cause = cause.__cause__ logger.error(error_type + ": " + error_message) - logger.error(f"Failed to construct {_constructor_name_to_typename(constructor)}!") + logger.error(f"Failed to construct {type_name}!") return None @@ -421,27 +422,28 @@ def _construct_lang_string_set(element: ElTree.Element, _failsafe: bool, **_kwar def _construct_qualifier(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Qualifier: - q = model.Qualifier( + qualifier = model.Qualifier( _child_text_mandatory(element, NS_AAS + "type"), _child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: - q.value = value + qualifier.value = model.datatypes.from_xsd(value, qualifier.value_type) value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) if value_id is not None: - q.value_id = value_id - _amend_abstract_attributes(q, element, failsafe) - return q + qualifier.value_id = value_id + _amend_abstract_attributes(qualifier, element, failsafe) + return qualifier def _construct_formula(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Formula: - ref_set: Set[model.Reference] = set() + formula = model.Formula() depends_on_refs = element.find(NS_AAS + "dependsOnRefs") if depends_on_refs is not None: - ref_set = set(_failsafe_construct_multiple(depends_on_refs.findall(NS_AAS + "reference"), _construct_reference, - failsafe)) - return model.Formula(ref_set) + for ref in _failsafe_construct_multiple(depends_on_refs.findall(NS_AAS + "reference"), _construct_reference, + failsafe): + formula.depends_on.add(ref) + return formula def _construct_identifier(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Identifier: @@ -452,6 +454,9 @@ def _construct_identifier(element: ElTree.Element, _failsafe: bool, **_kwargs: A def _construct_security(_element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Security: + """ + TODO: this is just a stub implementation + """ return model.Security() @@ -459,9 +464,9 @@ def _construct_view(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> view = model.View(_child_text_mandatory(element, NS_AAS + "idShort")) contained_elements = element.find(NS_AAS + "containedElements") if contained_elements is not None: - for ce in _failsafe_construct_multiple(contained_elements.findall(NS_AAS + "containedElementRef"), - _construct_referable_reference, failsafe): - view.contained_element.add(ce) + for ref in _failsafe_construct_multiple(contained_elements.findall(NS_AAS + "containedElementRef"), + _construct_referable_reference, failsafe): + view.contained_element.add(ref) _amend_abstract_attributes(view, element, failsafe) return view @@ -470,9 +475,9 @@ def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool, **_kw concept_dictionary = model.ConceptDictionary(_child_text_mandatory(element, NS_AAS + "idShort")) concept_description = element.find(NS_AAS + "conceptDescriptionRefs") if concept_description is not None: - for cd in _failsafe_construct_multiple(concept_description.findall(NS_AAS + "conceptDescriptionRef"), - _construct_concept_description_reference, failsafe): - concept_dictionary.concept_description.add(cd) + for ref in _failsafe_construct_multiple(concept_description.findall(NS_AAS + "conceptDescriptionRef"), + _construct_concept_description_reference, failsafe): + concept_dictionary.concept_description.add(ref) _amend_abstract_attributes(concept_dictionary, element, failsafe) return concept_dictionary @@ -617,36 +622,94 @@ def _construct_operation(element: ElTree.Element, failsafe: bool, **_kwargs: Any _child_text_mandatory(element, NS_AAS + "idShort"), **_get_modeling_kind_kwarg(element) ) - # TODO + in_output_variable = element.find(NS_AAS + "inoutputVariable") + if in_output_variable is not None: + for var in _failsafe_construct_multiple(in_output_variable.findall(NS_AAS + "operationVariable"), + _construct_operation_variable, failsafe): + operation.in_output_variable.append(var) + input_variable = element.find(NS_AAS + "inputVariable") + if input_variable is not None: + for var in _failsafe_construct_multiple(input_variable.findall(NS_AAS + "operationVariable"), + _construct_operation_variable, failsafe): + operation.input_variable.append(var) + output_variable = element.find(NS_AAS + "outputVariable") + if output_variable is not None: + for var in _failsafe_construct_multiple(output_variable.findall(NS_AAS + "operationVariable"), + _construct_operation_variable, failsafe): + operation.output_variable.append(var) _amend_abstract_attributes(operation, element, failsafe) return operation def _construct_property(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Property: - # TODO - pass + property = model.Property( + _child_text_mandatory(element, NS_AAS + "idShort"), + value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), + **_get_modeling_kind_kwarg(element) + ) + value = _get_text_or_none(element.find(NS_AAS + "value")) + if value is not None: + property.value = model.datatypes.from_xsd(value, property.value_type) + value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) + if value_id is not None: + property.value_id = value_id + _amend_abstract_attributes(property, element, failsafe) + return property def _construct_range(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Range: - # TODO - pass + range = model.Range( + _child_text_mandatory(element, NS_AAS + "idShort"), + value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), + **_get_modeling_kind_kwarg(element) + ) + max = _get_text_or_none(element.find(NS_AAS + "max")) + if max is not None: + range.max = model.datatypes.from_xsd(max, range.value_type) + min = _get_text_or_none(element.find(NS_AAS + "min")) + if min is not None: + range.min = model.datatypes.from_xsd(min, range.value_type) + _amend_abstract_attributes(range, element, failsafe) + return range def _construct_reference_element(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ReferenceElement: - # TODO - pass + reference_element = model.ReferenceElement( + _child_text_mandatory(element, NS_AAS + "idShort"), + **_get_modeling_kind_kwarg(element) + ) + value = _failsafe_construct(element.find(NS_AAS + "value"), _construct_referable_reference, failsafe) + if value is not None: + reference_element.value = value + _amend_abstract_attributes(reference_element, element, failsafe) + return reference_element def _construct_relationship_element(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ -> model.RelationshipElement: - # TODO - pass + relationship_element = model.RelationshipElement( + _child_text_mandatory(element, NS_AAS + "idShort"), + _child_construct_mandatory(element, NS_AAS + "first", _construct_referable_reference), + _child_construct_mandatory(element, NS_AAS + "second", _construct_referable_reference), + **_get_modeling_kind_kwarg(element) + ) + _amend_abstract_attributes(relationship_element, element, failsafe) + return relationship_element def _construct_submodel_element_collection(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ -> model.SubmodelElementCollection: - # TODO - pass + ordered = _child_text_mandatory(element, NS_AAS + "ordered").lower() == "true" + collection_type = model.SubmodelElementCollectionOrdered if ordered else model.SubmodelElementCollectionUnordered + collection = collection_type( + _child_text_mandatory(element, NS_AAS + "idShort"), + **_get_modeling_kind_kwarg(element) + ) + value = _get_child_mandatory(element, NS_AAS + "value") + for se in _failsafe_construct_multiple(value, _construct_submodel_element, failsafe): + collection.value.add(se) + _amend_abstract_attributes(collection, element, failsafe) + return collection def _construct_asset_administration_shell(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ @@ -660,9 +723,9 @@ def _construct_asset_administration_shell(element: ElTree.Element, failsafe: boo aas.security = security submodels = element.find(NS_AAS + "submodelRefs") if submodels is not None: - for sm in _failsafe_construct_multiple(submodels.findall(NS_AAS + "submodelRef"), _construct_submodel_reference, - failsafe): - aas.submodel.add(sm) + for ref in _failsafe_construct_multiple(submodels.findall(NS_AAS + "submodelRef"), + _construct_submodel_reference, failsafe): + aas.submodel.add(ref) views = element.find(NS_AAS + "views") if views is not None: for view in _failsafe_construct_multiple(views.findall(NS_AAS + "view"), _construct_view, failsafe): @@ -699,7 +762,7 @@ def _construct_asset(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> def _construct_submodel(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Submodel: submodel = model.Submodel( - _failsafe_construct_mandatory(_get_child_mandatory(element, NS_AAS + "identification"), _construct_identifier), + _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier), **_get_modeling_kind_kwarg(element) ) for submodel_element in _get_child_mandatory(element, NS_AAS + "submodelElements"): @@ -712,11 +775,10 @@ def _construct_submodel(element: ElTree.Element, failsafe: bool, **_kwargs: Any) def _construct_concept_description(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: cd = model.ConceptDescription( - _failsafe_construct_mandatory(_get_child_mandatory(element, NS_AAS + "identification"), _construct_identifier) + _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) ) - is_case_of = set(_failsafe_construct_multiple(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe)) - if len(is_case_of) != 0: - cd.is_case_of = is_case_of + for ref in _failsafe_construct_multiple(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe): + cd.is_case_of.add(ref) _amend_abstract_attributes(cd, element, failsafe) return cd -- GitLab From 4ec8cc86e9cb5c2d28c6843a241f230d4f7ca49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 29 Mar 2020 19:21:07 +0200 Subject: [PATCH 19/27] adapter.xml: switch to lxml remove unused imports --- aas/adapter/xml/xml_deserialization.py | 121 +++++++++++++------------ 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 68ffe01..4e4fe40 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -34,23 +34,22 @@ Failed to construct AssetAdministrationShell! """ from ... import model -import xml.etree.ElementTree as ElTree +from lxml import etree # type: ignore import logging import base64 -from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar +from typing import Any, Callable, Dict, IO, Iterable, Optional, Tuple, Type, TypeVar from mypy_extensions import TypedDict # TODO: import this from typing should we require python 3.8+ at some point -from .xml_serialization import NS_AAS, NS_AAS_COMMON, NS_ABAC, NS_IEC, NS_XSI +from .xml_serialization import NS_AAS, NS_ABAC, NS_IEC from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ - IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE,\ - KEY_ELEMENTS_CLASSES_INVERSE + IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, KEY_ELEMENTS_CLASSES_INVERSE logger = logging.getLogger(__name__) T = TypeVar("T") -def _get_child_mandatory(parent: ElTree.Element, child_tag: str) -> ElTree.Element: +def _get_child_mandatory(parent: etree.Element, child_tag: str) -> etree.Element: """ A helper function for getting a mandatory child element. @@ -65,7 +64,7 @@ def _get_child_mandatory(parent: ElTree.Element, child_tag: str) -> ElTree.Eleme return child -def _get_attrib_mandatory(element: ElTree.Element, attrib: str) -> str: +def _get_attrib_mandatory(element: etree.Element, attrib: str) -> str: """ A helper function for getting a mandatory attribute of an element. @@ -79,7 +78,7 @@ def _get_attrib_mandatory(element: ElTree.Element, attrib: str) -> str: return element.attrib[attrib] -def _get_attrib_mandatory_mapped(element: ElTree.Element, attrib: str, dct: Dict[str, T]) -> T: +def _get_attrib_mandatory_mapped(element: etree.Element, attrib: str, dct: Dict[str, T]) -> T: """ A helper function for getting a mapped mandatory attribute of an xml element. @@ -99,7 +98,7 @@ def _get_attrib_mandatory_mapped(element: ElTree.Element, attrib: str, dct: Dict return dct[attrib_value] -def _get_text_or_none(element: Optional[ElTree.Element]) -> Optional[str]: +def _get_text_or_none(element: Optional[etree.Element]) -> Optional[str]: """ A helper function for getting the text of an element, when it's not clear whether the element exists or not. @@ -115,7 +114,7 @@ def _get_text_or_none(element: Optional[ElTree.Element]) -> Optional[str]: return element.text if element is not None else None -def _get_text_mandatory(element: ElTree.Element) -> str: +def _get_text_mandatory(element: etree.Element) -> str: """ A helper function for getting the mandatory text of an element. @@ -129,7 +128,7 @@ def _get_text_mandatory(element: ElTree.Element) -> str: return text -def _get_text_mandatory_mapped(element: ElTree.Element, dct: Dict[str, T]) -> T: +def _get_text_mandatory_mapped(element: etree.Element, dct: Dict[str, T]) -> T: """ A helper function for getting the mapped mandatory text of an element. @@ -148,7 +147,7 @@ def _get_text_mandatory_mapped(element: ElTree.Element, dct: Dict[str, T]) -> T: return dct[text] -def _constructor_name_to_typename(constructor: Callable[[ElTree.Element, bool], T]) -> str: +def _constructor_name_to_typename(constructor: Callable[[etree.Element, bool], T]) -> str: """ A helper function for converting the name of a constructor function to the respective type name. @@ -173,7 +172,7 @@ def _exception_to_str(exception: BaseException) -> str: return string[1:-1] if isinstance(exception, KeyError) else string -def _failsafe_construct(element: Optional[ElTree.Element], constructor: Callable[..., T], failsafe: bool, +def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Optional[T]: """ A wrapper function that is used to handle exceptions raised in constructor functions. @@ -212,7 +211,7 @@ def _failsafe_construct(element: Optional[ElTree.Element], constructor: Callable return None -def _failsafe_construct_mandatory(element: ElTree.Element, constructor: Callable[..., T], +def _failsafe_construct_mandatory(element: etree.Element, constructor: Callable[..., T], **kwargs: Any) -> T: """ _failsafe_construct() but not failsafe and it returns T instead of Optional[T] @@ -231,7 +230,7 @@ def _failsafe_construct_mandatory(element: ElTree.Element, constructor: Callable return constructed -def _failsafe_construct_multiple(elements: Iterable[ElTree.Element], constructor: Callable[..., T], failsafe: bool, +def _failsafe_construct_multiple(elements: Iterable[etree.Element], constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Iterable[T]: """ A generator function that applies _failsafe_construct() to multiple elements. @@ -250,7 +249,7 @@ def _failsafe_construct_multiple(elements: Iterable[ElTree.Element], constructor yield parsed -def _child_construct_mandatory(parent: ElTree.Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any)\ +def _child_construct_mandatory(parent: etree.Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any)\ -> T: """ Shorthand for _failsafe_construct_mandatory() in combination with _get_child_mandatory(). @@ -264,7 +263,7 @@ def _child_construct_mandatory(parent: ElTree.Element, child_tag: str, construct return _failsafe_construct_mandatory(_get_child_mandatory(parent, child_tag), constructor, **kwargs) -def _child_text_mandatory(parent: ElTree.Element, child_tag: str) -> str: +def _child_text_mandatory(parent: etree.Element, child_tag: str) -> str: """ Shorthand for _get_text_mandatory() in combination with _get_child_mandatory(). @@ -275,7 +274,7 @@ def _child_text_mandatory(parent: ElTree.Element, child_tag: str) -> str: return _get_text_mandatory(_get_child_mandatory(parent, child_tag)) -def _child_text_mandatory_mapped(parent: ElTree.Element, child_tag: str, dct: Dict[str, T]) -> T: +def _child_text_mandatory_mapped(parent: etree.Element, child_tag: str, dct: Dict[str, T]) -> T: """ Shorthand for _get_text_mandatory_mapped() in combination with _get_child_mandatory(). @@ -287,7 +286,7 @@ def _child_text_mandatory_mapped(parent: ElTree.Element, child_tag: str, dct: Di return _get_text_mandatory_mapped(_get_child_mandatory(parent, child_tag), dct) -def _amend_abstract_attributes(obj: object, element: ElTree.Element, failsafe: bool) -> None: +def _amend_abstract_attributes(obj: object, element: etree.Element, failsafe: bool) -> None: """ A helper function that amends optional attributes to already constructed class instances, if they inherit from an abstract class like Referable, Identifiable, HasSemantics or Qualifiable. @@ -327,7 +326,7 @@ class ModelingKindKwArg(TypedDict, total=False): kind: model.ModelingKind -def _get_modeling_kind_kwarg(element: ElTree.Element) -> ModelingKindKwArg: +def _get_modeling_kind_kwarg(element: etree.Element) -> ModelingKindKwArg: """ A helper function that creates a dict containing the modeling kind or nothing for a given xml element. @@ -349,7 +348,7 @@ def _get_modeling_kind_kwarg(element: ElTree.Element) -> ModelingKindKwArg: return kwargs -def _construct_key(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Key: +def _construct_key(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.Key: return model.Key( _get_attrib_mandatory_mapped(element, "type", KEY_ELEMENTS_INVERSE), _get_attrib_mandatory(element, "local").lower() == "true", @@ -358,16 +357,16 @@ def _construct_key(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> ) -def _construct_key_tuple(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> Tuple[model.Key, ...]: +def _construct_key_tuple(element: etree.Element, failsafe: bool, **_kwargs: Any) -> Tuple[model.Key, ...]: keys = _get_child_mandatory(element, NS_AAS + "keys") return tuple(_failsafe_construct_multiple(keys.findall(NS_AAS + "key"), _construct_key, failsafe)) -def _construct_reference(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Reference: +def _construct_reference(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Reference: return model.Reference(_construct_key_tuple(element, failsafe)) -def _construct_aas_reference(element: ElTree.Element, failsafe: bool, type_: Type[model.base._RT], **_kwargs: Any)\ +def _construct_aas_reference(element: etree.Element, failsafe: bool, type_: Type[model.base._RT], **_kwargs: Any)\ -> model.AASReference[model.base._RT]: keys = _construct_key_tuple(element, failsafe) if len(keys) != 0 and not issubclass(KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): @@ -376,37 +375,37 @@ def _construct_aas_reference(element: ElTree.Element, failsafe: bool, type_: Typ return model.AASReference(keys, type_) -def _construct_submodel_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_submodel_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.Submodel]: return _construct_aas_reference(element, failsafe, model.Submodel, **kwargs) -def _construct_asset_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_asset_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.Asset]: return _construct_aas_reference(element, failsafe, model.Asset, **kwargs) -def _construct_asset_administration_shell_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_asset_administration_shell_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.AssetAdministrationShell]: return _construct_aas_reference(element, failsafe, model.AssetAdministrationShell, **kwargs) -def _construct_referable_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_referable_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.Referable]: return _construct_aas_reference(element, failsafe, model.Referable, **kwargs) -def _construct_concept_description_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_concept_description_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.ConceptDescription]: return _construct_aas_reference(element, failsafe, model.ConceptDescription, **kwargs) -def _construct_data_element_reference(element: ElTree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_data_element_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ -> model.AASReference[model.DataElement]: return _construct_aas_reference(element, failsafe, model.DataElement, **kwargs) -def _construct_administrative_information(element: ElTree.Element, _failsafe: bool, **_kwargs: Any)\ +def _construct_administrative_information(element: etree.Element, _failsafe: bool, **_kwargs: Any)\ -> model.AdministrativeInformation: return model.AdministrativeInformation( _get_text_or_none(element.find(NS_AAS + "version")), @@ -414,14 +413,14 @@ def _construct_administrative_information(element: ElTree.Element, _failsafe: bo ) -def _construct_lang_string_set(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.LangStringSet: +def _construct_lang_string_set(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.LangStringSet: lss: model.LangStringSet = {} for lang_string in element.findall(NS_IEC + "langString"): lss[_get_attrib_mandatory(lang_string, "lang")] = _get_text_mandatory(lang_string) return lss -def _construct_qualifier(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Qualifier: +def _construct_qualifier(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Qualifier: qualifier = model.Qualifier( _child_text_mandatory(element, NS_AAS + "type"), _child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) @@ -436,7 +435,7 @@ def _construct_qualifier(element: ElTree.Element, failsafe: bool, **_kwargs: Any return qualifier -def _construct_formula(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Formula: +def _construct_formula(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Formula: formula = model.Formula() depends_on_refs = element.find(NS_AAS + "dependsOnRefs") if depends_on_refs is not None: @@ -446,21 +445,21 @@ def _construct_formula(element: ElTree.Element, failsafe: bool, **_kwargs: Any) return formula -def _construct_identifier(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Identifier: +def _construct_identifier(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.Identifier: return model.Identifier( _get_text_mandatory(element), _get_attrib_mandatory_mapped(element, "idType", IDENTIFIER_TYPES_INVERSE) ) -def _construct_security(_element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.Security: +def _construct_security(_element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.Security: """ TODO: this is just a stub implementation """ return model.Security() -def _construct_view(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.View: +def _construct_view(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.View: view = model.View(_child_text_mandatory(element, NS_AAS + "idShort")) contained_elements = element.find(NS_AAS + "containedElements") if contained_elements is not None: @@ -471,7 +470,7 @@ def _construct_view(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> return view -def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDictionary: +def _construct_concept_dictionary(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDictionary: concept_dictionary = model.ConceptDictionary(_child_text_mandatory(element, NS_AAS + "idShort")) concept_description = element.find(NS_AAS + "conceptDescriptionRefs") if concept_description is not None: @@ -482,7 +481,7 @@ def _construct_concept_dictionary(element: ElTree.Element, failsafe: bool, **_kw return concept_dictionary -def _construct_submodel_element(element: ElTree.Element, failsafe: bool, **kwargs: Any) -> model.SubmodelElement: +def _construct_submodel_element(element: etree.Element, failsafe: bool, **kwargs: Any) -> model.SubmodelElement: submodel_elements: Dict[str, Callable[..., model.SubmodelElement]] = {NS_AAS + k: v for k, v in { "annotatedRelationshipElement": _construct_annotated_relationship_element, "basicEvent": _construct_basic_event, @@ -503,7 +502,7 @@ def _construct_submodel_element(element: ElTree.Element, failsafe: bool, **kwarg return submodel_elements[element.tag](element, failsafe, **kwargs) -def _construct_constraint(element: ElTree.Element, failsafe: bool, **kwargs: Any) -> model.Constraint: +def _construct_constraint(element: etree.Element, failsafe: bool, **kwargs: Any) -> model.Constraint: constraints: Dict[str, Callable[..., model.Constraint]] = {NS_AAS + k: v for k, v in { "formula": _construct_formula, "qualifier": _construct_qualifier @@ -513,7 +512,7 @@ def _construct_constraint(element: ElTree.Element, failsafe: bool, **kwargs: Any return constraints[element.tag](element, failsafe, **kwargs) -def _construct_operation_variable(element: ElTree.Element, _failsafe: bool, **_kwargs: Any) -> model.OperationVariable: +def _construct_operation_variable(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.OperationVariable: value = _get_child_mandatory(element, NS_AAS + "value") if len(value) == 0: raise KeyError("Value of operation variable has no submodel element!") @@ -524,7 +523,7 @@ def _construct_operation_variable(element: ElTree.Element, _failsafe: bool, **_k ) -def _construct_annotated_relationship_element(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_annotated_relationship_element(element: etree.Element, failsafe: bool, **_kwargs: Any)\ -> model.AnnotatedRelationshipElement: annotated_relationship_element = model.AnnotatedRelationshipElement( _child_text_mandatory(element, NS_AAS + "idShort"), @@ -540,7 +539,7 @@ def _construct_annotated_relationship_element(element: ElTree.Element, failsafe: return annotated_relationship_element -def _construct_basic_event(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.BasicEvent: +def _construct_basic_event(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.BasicEvent: basic_event = model.BasicEvent( _child_text_mandatory(element, NS_AAS + "idShort"), _child_construct_mandatory(element, NS_AAS + "observed", _construct_referable_reference), @@ -550,7 +549,7 @@ def _construct_basic_event(element: ElTree.Element, failsafe: bool, **_kwargs: A return basic_event -def _construct_blob(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Blob: +def _construct_blob(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Blob: blob = model.Blob( _child_text_mandatory(element, NS_AAS + "idShort"), _child_text_mandatory(element, NS_AAS + "mimeType"), @@ -563,7 +562,7 @@ def _construct_blob(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> return blob -def _construct_capability(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Capability: +def _construct_capability(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Capability: capability = model.Capability( _child_text_mandatory(element, NS_AAS + "idShort"), **_get_modeling_kind_kwarg(element) @@ -572,7 +571,7 @@ def _construct_capability(element: ElTree.Element, failsafe: bool, **_kwargs: An return capability -def _construct_entity(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Entity: +def _construct_entity(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Entity: entity = model.Entity( _child_text_mandatory(element, NS_AAS + "idShort"), _child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), @@ -588,7 +587,7 @@ def _construct_entity(element: ElTree.Element, failsafe: bool, **_kwargs: Any) - return entity -def _construct_file(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.File: +def _construct_file(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.File: file = model.File( _child_text_mandatory(element, NS_AAS + "idShort"), _child_text_mandatory(element, NS_AAS + "idShort"), @@ -601,7 +600,7 @@ def _construct_file(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> return file -def _construct_multi_language_property(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_multi_language_property(element: etree.Element, failsafe: bool, **_kwargs: Any)\ -> model.MultiLanguageProperty: multi_language_property = model.MultiLanguageProperty( _child_text_mandatory(element, NS_AAS + "idShort"), @@ -617,7 +616,7 @@ def _construct_multi_language_property(element: ElTree.Element, failsafe: bool, return multi_language_property -def _construct_operation(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Operation: +def _construct_operation(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Operation: operation = model.Operation( _child_text_mandatory(element, NS_AAS + "idShort"), **_get_modeling_kind_kwarg(element) @@ -641,7 +640,7 @@ def _construct_operation(element: ElTree.Element, failsafe: bool, **_kwargs: Any return operation -def _construct_property(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Property: +def _construct_property(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Property: property = model.Property( _child_text_mandatory(element, NS_AAS + "idShort"), value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), @@ -657,7 +656,7 @@ def _construct_property(element: ElTree.Element, failsafe: bool, **_kwargs: Any) return property -def _construct_range(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Range: +def _construct_range(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Range: range = model.Range( _child_text_mandatory(element, NS_AAS + "idShort"), value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), @@ -673,7 +672,7 @@ def _construct_range(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> return range -def _construct_reference_element(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ReferenceElement: +def _construct_reference_element(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.ReferenceElement: reference_element = model.ReferenceElement( _child_text_mandatory(element, NS_AAS + "idShort"), **_get_modeling_kind_kwarg(element) @@ -685,7 +684,7 @@ def _construct_reference_element(element: ElTree.Element, failsafe: bool, **_kwa return reference_element -def _construct_relationship_element(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_relationship_element(element: etree.Element, failsafe: bool, **_kwargs: Any)\ -> model.RelationshipElement: relationship_element = model.RelationshipElement( _child_text_mandatory(element, NS_AAS + "idShort"), @@ -697,7 +696,7 @@ def _construct_relationship_element(element: ElTree.Element, failsafe: bool, **_ return relationship_element -def _construct_submodel_element_collection(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_submodel_element_collection(element: etree.Element, failsafe: bool, **_kwargs: Any)\ -> model.SubmodelElementCollection: ordered = _child_text_mandatory(element, NS_AAS + "ordered").lower() == "true" collection_type = model.SubmodelElementCollectionOrdered if ordered else model.SubmodelElementCollectionUnordered @@ -712,7 +711,7 @@ def _construct_submodel_element_collection(element: ElTree.Element, failsafe: bo return collection -def _construct_asset_administration_shell(element: ElTree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_asset_administration_shell(element: etree.Element, failsafe: bool, **_kwargs: Any)\ -> model.AssetAdministrationShell: aas = model.AssetAdministrationShell( _child_construct_mandatory(element, NS_AAS + "assetRef", _construct_asset_reference), @@ -743,7 +742,7 @@ def _construct_asset_administration_shell(element: ElTree.Element, failsafe: boo return aas -def _construct_asset(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Asset: +def _construct_asset(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Asset: asset = model.Asset( _child_text_mandatory_mapped(element, NS_AAS + "kind", ASSET_KIND_INVERSE), _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) @@ -760,7 +759,7 @@ def _construct_asset(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> return asset -def _construct_submodel(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.Submodel: +def _construct_submodel(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Submodel: submodel = model.Submodel( _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier), **_get_modeling_kind_kwarg(element) @@ -773,7 +772,7 @@ def _construct_submodel(element: ElTree.Element, failsafe: bool, **_kwargs: Any) return submodel -def _construct_concept_description(element: ElTree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: +def _construct_concept_description(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: cd = model.ConceptDescription( _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) ) @@ -787,7 +786,7 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 - :param file: A file-like object to read the XML-serialized data from + :param file: A filename or file-like object to read the XML-serialized data from :param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception for missing attributes and wrong types, errors are logged and defective objects are skipped :return: A DictObjectStore containing all AAS objects from the XML file @@ -800,7 +799,9 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: "conceptDescription": _construct_concept_description }.items()} - tree = ElTree.parse(file) + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) + + tree = etree.parse(file, parser) root = tree.getroot() # Add AAS objects to ObjectStore -- GitLab From 5e68f17d6f69db9d5a4614c13a93d2290639cbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 31 Mar 2020 06:21:13 +0200 Subject: [PATCH 20/27] adapter.xml: make error messages prettier - if possible, replace namespace in front of element tag with the prefix used in the xml document - include source line of element in error messages catch XMLSyntaxErrors that can be thrown while parsing and just log them in failsafe mode add _child_construct_multiple() - iterate over all child elements of a given parent - compare each tag to a given tag - raise an error or log a warning if tags don't match, else construct the element --- aas/adapter/xml/xml_deserialization.py | 264 ++++++++++++++++--------- 1 file changed, 172 insertions(+), 92 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 4e4fe40..9e4302f 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -26,11 +26,9 @@ Error handling is done only by _failsafe_construct() in this module. Nearly all by other constructor functions via _failsafe_construct(), so an error chain is constructed in the error case, which allows printing stacktrace-like error messages like the following in the error case (in failsafe mode of course): -KeyError: XML element {http://www.admin-shell.io/aas/2/0}identification has no attribute with name idType! - -> while converting XML element with tag {http://www.admin-shell.io/aas/2/0}identification to type Identifier - -> while converting XML element with tag {http://www.admin-shell.io/aas/2/0}assetAdministrationShell to type - AssetAdministrationShell -Failed to construct AssetAdministrationShell! +KeyError: aas:identification on line 252 has no attribute with name idType! + -> Failed to convert aas:identification on line 252 to type Identifier! + -> Failed to convert aas:conceptDescription on line 247 to type ConceptDescription! """ from ... import model @@ -41,7 +39,7 @@ import base64 from typing import Any, Callable, Dict, IO, Iterable, Optional, Tuple, Type, TypeVar from mypy_extensions import TypedDict # TODO: import this from typing should we require python 3.8+ at some point from .xml_serialization import NS_AAS, NS_ABAC, NS_IEC -from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE,\ +from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE, \ IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, KEY_ELEMENTS_CLASSES_INVERSE logger = logging.getLogger(__name__) @@ -49,6 +47,66 @@ logger = logging.getLogger(__name__) T = TypeVar("T") +def _tag_replace_namespace(tag: str, nsmap: Dict[str, str]) -> str: + """ + Attempts to replace the namespace in front of a tag with the prefix used in the xml document. + + :param tag: The tag of an xml element. + :param nsmap: A dict mapping prefixes to namespaces. + :return: The modified element tag. If the namespace wasn't found in nsmap, the unmodified tag is returned. + """ + split = tag.split("}") + for prefix, namespace in nsmap.items(): + if namespace == split[0][1:]: + return prefix + ":" + split[1] + return tag + + +def _element_pretty_identifier(element: etree.Element) -> str: + """ + Returns a pretty element identifier for a given XML element. + + If the prefix is known, the namespace in the element tag is replaced by the prefix. + If additionally also the sourceline is known, is is added as a suffix to name. + For example, instead of "{http://www.admin-shell.io/aas/2/0}assetAdministrationShell" this function would return + "aas:assetAdministrationShell on line $line", if both, prefix and sourceline, are known. + + :param element: The xml element. + :return: The pretty element identifier. + """ + identifier = element.tag + if element.prefix is not None: + identifier = element.prefix + ":" + element.tag.split("}")[1] + if element.sourceline is not None: + identifier += f" on line {element.sourceline}" + return identifier + + +def _constructor_name_to_typename(constructor: Callable[[etree.Element, bool], T]) -> str: + """ + A helper function for converting the name of a constructor function to the respective type name. + + _construct_some_type -> SomeType + + :param constructor: The constructor function. + :return: The name of the type the constructor function constructs. + """ + return "".join([s[0].upper() + s[1:] for s in constructor.__name__.split("_")[2:]]) + + +def _exception_to_str(exception: BaseException) -> str: + """ + A helper function used to stringify exceptions. + + It removes the quotation marks '' that are put around str(KeyError), otherwise it's just calls str(exception). + + :param exception: The exception to stringify. + :return: The stringified exception. + """ + string = str(exception) + return string[1:-1] if isinstance(exception, KeyError) else string + + def _get_child_mandatory(parent: etree.Element, child_tag: str) -> etree.Element: """ A helper function for getting a mandatory child element. @@ -60,10 +118,34 @@ def _get_child_mandatory(parent: etree.Element, child_tag: str) -> etree.Element """ child = parent.find(child_tag) if child is None: - raise KeyError(f"XML element {parent.tag} has no child {child_tag}!") + raise KeyError(_element_pretty_identifier(parent) + + f" has no child {_tag_replace_namespace(child_tag, parent.nsmap)}!") return child +def _get_child_multiple(parent: etree.Element, exppected_tag: str, failsafe: bool) -> Iterable[etree.Element]: + """ + Iterates over all children, matching the tag. + + not failsafe: Throws an error if a child element doesn't match. + failsafe: Logs a warning if a child element doesn't match. + + :param parent: The parent element. + :param exppected_tag: The tag of the children. + :return: An iterator over all child elements that match child_tag. + :raises KeyError: If the tag of a child element doesn't match and failsafe is true. + """ + for child in parent: + if child.tag != exppected_tag: + error_message = f"{_element_pretty_identifier(child)}, child of {_element_pretty_identifier(parent)}, " \ + f"doesn't match the expected tag {_tag_replace_namespace(exppected_tag, child.nsmap)}!" + if not failsafe: + raise KeyError(error_message) + logger.warning(error_message) + continue + yield child + + def _get_attrib_mandatory(element: etree.Element, attrib: str) -> str: """ A helper function for getting a mandatory attribute of an element. @@ -74,7 +156,7 @@ def _get_attrib_mandatory(element: etree.Element, attrib: str) -> str: :raises KeyError: If the attribute does not exist. """ if attrib not in element.attrib: - raise KeyError(f"XML element {element.tag} has no attribute with name {attrib}!") + raise KeyError(f"{_element_pretty_identifier(element)} has no attribute with name {attrib}!") return element.attrib[attrib] @@ -94,7 +176,8 @@ def _get_attrib_mandatory_mapped(element: etree.Element, attrib: str, dct: Dict[ """ attrib_value = _get_attrib_mandatory(element, attrib) if attrib_value not in dct: - raise ValueError(f"Attribute {attrib} of XML element {element.tag} has invalid value: {attrib_value}") + raise ValueError(f"Attribute {attrib} of {_element_pretty_identifier(element)} " + f"has invalid value: {attrib_value}") return dct[attrib_value] @@ -124,7 +207,7 @@ def _get_text_mandatory(element: etree.Element) -> str: """ text = element.text if text is None: - raise KeyError(f"XML element {element.tag} has no text!") + raise KeyError(_element_pretty_identifier(element) + " has no text!") return text @@ -143,35 +226,10 @@ def _get_text_mandatory_mapped(element: etree.Element, dct: Dict[str, T]) -> T: """ text = _get_text_mandatory(element) if text not in dct: - raise ValueError(f"Text of XML element {element.tag} is invalid: {text}") + raise ValueError(_element_pretty_identifier(element) + f" has invalid text: {text}") return dct[text] -def _constructor_name_to_typename(constructor: Callable[[etree.Element, bool], T]) -> str: - """ - A helper function for converting the name of a constructor function to the respective type name. - - _construct_some_type -> SomeType - - :param constructor: The constructor function. - :return: The name of the type the constructor function constructs. - """ - return "".join([s[0].upper() + s[1:] for s in constructor.__name__.split("_")[2:]]) - - -def _exception_to_str(exception: BaseException) -> str: - """ - A helper function used to stringify exceptions. - - It removes the quotation marks '' that are put around str(KeyError), otherwise it's just calls str(exception). - - :param exception: The exception to stringify. - :return: The stringified exception. - """ - string = str(exception) - return string[1:-1] if isinstance(exception, KeyError) else string - - def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[..., T], failsafe: bool, **kwargs: Any) -> Optional[T]: """ @@ -197,8 +255,7 @@ def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[ return constructor(element, failsafe, **kwargs) except (KeyError, ValueError) as e: type_name = _constructor_name_to_typename(constructor) - error_message = f"while converting XML element with tag {element.tag} to "\ - f"type {type_name}" + error_message = f"Failed to convert {_element_pretty_identifier(element)} to type {type_name}!" if not failsafe: raise type(e)(error_message) from e error_type = type(e).__name__ @@ -207,12 +264,10 @@ def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[ error_message = _exception_to_str(cause) + "\n -> " + error_message cause = cause.__cause__ logger.error(error_type + ": " + error_message) - logger.error(f"Failed to construct {type_name}!") return None -def _failsafe_construct_mandatory(element: etree.Element, constructor: Callable[..., T], - **kwargs: Any) -> T: +def _failsafe_construct_mandatory(element: etree.Element, constructor: Callable[..., T], **kwargs: Any) -> T: """ _failsafe_construct() but not failsafe and it returns T instead of Optional[T] @@ -241,7 +296,7 @@ def _failsafe_construct_multiple(elements: Iterable[etree.Element], constructor: :param kwargs: Optional keyword arguments that are passed to the constructor function. :return: An iterator over the successfully constructed elements. If an error occurred while constructing an element and while in failsafe mode, - this element will be skipped. + the respective element will be skipped. """ for element in elements: parsed = _failsafe_construct(element, constructor, failsafe, **kwargs) @@ -249,7 +304,7 @@ def _failsafe_construct_multiple(elements: Iterable[etree.Element], constructor: yield parsed -def _child_construct_mandatory(parent: etree.Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any)\ +def _child_construct_mandatory(parent: etree.Element, child_tag: str, constructor: Callable[..., T], **kwargs: Any) \ -> T: """ Shorthand for _failsafe_construct_mandatory() in combination with _get_child_mandatory(). @@ -263,6 +318,23 @@ def _child_construct_mandatory(parent: etree.Element, child_tag: str, constructo return _failsafe_construct_mandatory(_get_child_mandatory(parent, child_tag), constructor, **kwargs) +def _child_construct_multiple(parent: etree.Element, expected_tag: str, constructor: Callable[..., T], failsafe: bool, + **kwargs: Any) -> Iterable[T]: + """ + Shorthand for _failsafe_construct_multiple() in combination with _get_child_multiple(). + + :param parent: The xml element where child elements are searched. + :param expected_tag: The expected tag of the child elements. + :param constructor: The constructor function for the child element. + :param kwargs: Optional keyword arguments that are passed to the constructor function. + :return: An iterator over successfully constructed child elements. + If an error occurred while constructing an element and while in failsafe mode, + the respective element will be skipped. + """ + return _failsafe_construct_multiple(_get_child_multiple(parent, expected_tag, failsafe), constructor, failsafe, + **kwargs) + + def _child_text_mandatory(parent: etree.Element, child_tag: str) -> str: """ Shorthand for _get_text_mandatory() in combination with _get_child_mandatory(). @@ -359,14 +431,14 @@ def _construct_key(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> m def _construct_key_tuple(element: etree.Element, failsafe: bool, **_kwargs: Any) -> Tuple[model.Key, ...]: keys = _get_child_mandatory(element, NS_AAS + "keys") - return tuple(_failsafe_construct_multiple(keys.findall(NS_AAS + "key"), _construct_key, failsafe)) + return tuple(_child_construct_multiple(keys, NS_AAS + "key", _construct_key, failsafe)) def _construct_reference(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Reference: return model.Reference(_construct_key_tuple(element, failsafe)) -def _construct_aas_reference(element: etree.Element, failsafe: bool, type_: Type[model.base._RT], **_kwargs: Any)\ +def _construct_aas_reference(element: etree.Element, failsafe: bool, type_: Type[model.base._RT], **_kwargs: Any) \ -> model.AASReference[model.base._RT]: keys = _construct_key_tuple(element, failsafe) if len(keys) != 0 and not issubclass(KEY_ELEMENTS_CLASSES_INVERSE.get(keys[-1].type, type(None)), type_): @@ -375,37 +447,37 @@ def _construct_aas_reference(element: etree.Element, failsafe: bool, type_: Type return model.AASReference(keys, type_) -def _construct_submodel_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_submodel_reference(element: etree.Element, failsafe: bool, **kwargs: Any) \ -> model.AASReference[model.Submodel]: return _construct_aas_reference(element, failsafe, model.Submodel, **kwargs) -def _construct_asset_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_asset_reference(element: etree.Element, failsafe: bool, **kwargs: Any) \ -> model.AASReference[model.Asset]: return _construct_aas_reference(element, failsafe, model.Asset, **kwargs) -def _construct_asset_administration_shell_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_asset_administration_shell_reference(element: etree.Element, failsafe: bool, **kwargs: Any) \ -> model.AASReference[model.AssetAdministrationShell]: return _construct_aas_reference(element, failsafe, model.AssetAdministrationShell, **kwargs) -def _construct_referable_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_referable_reference(element: etree.Element, failsafe: bool, **kwargs: Any) \ -> model.AASReference[model.Referable]: return _construct_aas_reference(element, failsafe, model.Referable, **kwargs) -def _construct_concept_description_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_concept_description_reference(element: etree.Element, failsafe: bool, **kwargs: Any) \ -> model.AASReference[model.ConceptDescription]: return _construct_aas_reference(element, failsafe, model.ConceptDescription, **kwargs) -def _construct_data_element_reference(element: etree.Element, failsafe: bool, **kwargs: Any)\ +def _construct_data_element_reference(element: etree.Element, failsafe: bool, **kwargs: Any) \ -> model.AASReference[model.DataElement]: return _construct_aas_reference(element, failsafe, model.DataElement, **kwargs) -def _construct_administrative_information(element: etree.Element, _failsafe: bool, **_kwargs: Any)\ +def _construct_administrative_information(element: etree.Element, _failsafe: bool, **_kwargs: Any) \ -> model.AdministrativeInformation: return model.AdministrativeInformation( _get_text_or_none(element.find(NS_AAS + "version")), @@ -413,9 +485,9 @@ def _construct_administrative_information(element: etree.Element, _failsafe: boo ) -def _construct_lang_string_set(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.LangStringSet: +def _construct_lang_string_set(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.LangStringSet: lss: model.LangStringSet = {} - for lang_string in element.findall(NS_IEC + "langString"): + for lang_string in _get_child_multiple(element, NS_AAS + "langString", failsafe): lss[_get_attrib_mandatory(lang_string, "lang")] = _get_text_mandatory(lang_string) return lss @@ -498,7 +570,7 @@ def _construct_submodel_element(element: etree.Element, failsafe: bool, **kwargs "submodelElementCollection": _construct_submodel_element_collection }.items()} if element.tag not in submodel_elements: - raise KeyError(f"XML element {element.tag} is not a valid submodel element!") + raise KeyError(_element_pretty_identifier(element) + " is not a valid submodel element!") return submodel_elements[element.tag](element, failsafe, **kwargs) @@ -508,22 +580,23 @@ def _construct_constraint(element: etree.Element, failsafe: bool, **kwargs: Any) "qualifier": _construct_qualifier }.items()} if element.tag not in constraints: - raise KeyError(f"XML element {element.tag} is not a valid constraint!") + raise KeyError(_element_pretty_identifier(element) + " is not a valid constraint!") return constraints[element.tag](element, failsafe, **kwargs) def _construct_operation_variable(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.OperationVariable: value = _get_child_mandatory(element, NS_AAS + "value") if len(value) == 0: - raise KeyError("Value of operation variable has no submodel element!") + raise KeyError(f"{_element_pretty_identifier(value)} has no submodel element!") if len(value) > 1: - logger.warning("Value of operation variable has more than one submodel element, using the first one...") + logger.warning(f"{_element_pretty_identifier(value)} has more than one submodel element," + "using the first one...") return model.OperationVariable( _failsafe_construct_mandatory(value[0], _construct_submodel_element) ) -def _construct_annotated_relationship_element(element: etree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_annotated_relationship_element(element: etree.Element, failsafe: bool, **_kwargs: Any) \ -> model.AnnotatedRelationshipElement: annotated_relationship_element = model.AnnotatedRelationshipElement( _child_text_mandatory(element, NS_AAS + "idShort"), @@ -600,7 +673,7 @@ def _construct_file(element: etree.Element, failsafe: bool, **_kwargs: Any) -> m return file -def _construct_multi_language_property(element: etree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_multi_language_property(element: etree.Element, failsafe: bool, **_kwargs: Any) \ -> model.MultiLanguageProperty: multi_language_property = model.MultiLanguageProperty( _child_text_mandatory(element, NS_AAS + "idShort"), @@ -623,53 +696,53 @@ def _construct_operation(element: etree.Element, failsafe: bool, **_kwargs: Any) ) in_output_variable = element.find(NS_AAS + "inoutputVariable") if in_output_variable is not None: - for var in _failsafe_construct_multiple(in_output_variable.findall(NS_AAS + "operationVariable"), - _construct_operation_variable, failsafe): + for var in _child_construct_multiple(in_output_variable, NS_AAS + "operationVariable", + _construct_operation_variable, failsafe): operation.in_output_variable.append(var) input_variable = element.find(NS_AAS + "inputVariable") if input_variable is not None: - for var in _failsafe_construct_multiple(input_variable.findall(NS_AAS + "operationVariable"), - _construct_operation_variable, failsafe): + for var in _child_construct_multiple(input_variable, NS_AAS + "operationVariable", + _construct_operation_variable, failsafe): operation.input_variable.append(var) output_variable = element.find(NS_AAS + "outputVariable") if output_variable is not None: - for var in _failsafe_construct_multiple(output_variable.findall(NS_AAS + "operationVariable"), - _construct_operation_variable, failsafe): + for var in _child_construct_multiple(output_variable, NS_AAS + "operationVariable", + _construct_operation_variable, failsafe): operation.output_variable.append(var) _amend_abstract_attributes(operation, element, failsafe) return operation def _construct_property(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Property: - property = model.Property( + property_ = model.Property( _child_text_mandatory(element, NS_AAS + "idShort"), value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), **_get_modeling_kind_kwarg(element) ) value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: - property.value = model.datatypes.from_xsd(value, property.value_type) + property_.value = model.datatypes.from_xsd(value, property_.value_type) value_id = _failsafe_construct(element.find(NS_AAS + "valueId"), _construct_reference, failsafe) if value_id is not None: - property.value_id = value_id - _amend_abstract_attributes(property, element, failsafe) - return property + property_.value_id = value_id + _amend_abstract_attributes(property_, element, failsafe) + return property_ def _construct_range(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Range: - range = model.Range( + range_ = model.Range( _child_text_mandatory(element, NS_AAS + "idShort"), value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), **_get_modeling_kind_kwarg(element) ) - max = _get_text_or_none(element.find(NS_AAS + "max")) - if max is not None: - range.max = model.datatypes.from_xsd(max, range.value_type) - min = _get_text_or_none(element.find(NS_AAS + "min")) - if min is not None: - range.min = model.datatypes.from_xsd(min, range.value_type) - _amend_abstract_attributes(range, element, failsafe) - return range + max_ = _get_text_or_none(element.find(NS_AAS + "max")) + if max_ is not None: + range_.max = model.datatypes.from_xsd(max_, range_.value_type) + min_ = _get_text_or_none(element.find(NS_AAS + "min")) + if min_ is not None: + range_.min = model.datatypes.from_xsd(min_, range_.value_type) + _amend_abstract_attributes(range_, element, failsafe) + return range_ def _construct_reference_element(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.ReferenceElement: @@ -684,7 +757,7 @@ def _construct_reference_element(element: etree.Element, failsafe: bool, **_kwar return reference_element -def _construct_relationship_element(element: etree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_relationship_element(element: etree.Element, failsafe: bool, **_kwargs: Any) \ -> model.RelationshipElement: relationship_element = model.RelationshipElement( _child_text_mandatory(element, NS_AAS + "idShort"), @@ -696,7 +769,7 @@ def _construct_relationship_element(element: etree.Element, failsafe: bool, **_k return relationship_element -def _construct_submodel_element_collection(element: etree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_submodel_element_collection(element: etree.Element, failsafe: bool, **_kwargs: Any) \ -> model.SubmodelElementCollection: ordered = _child_text_mandatory(element, NS_AAS + "ordered").lower() == "true" collection_type = model.SubmodelElementCollectionOrdered if ordered else model.SubmodelElementCollectionUnordered @@ -711,7 +784,7 @@ def _construct_submodel_element_collection(element: etree.Element, failsafe: boo return collection -def _construct_asset_administration_shell(element: etree.Element, failsafe: bool, **_kwargs: Any)\ +def _construct_asset_administration_shell(element: etree.Element, failsafe: bool, **_kwargs: Any) \ -> model.AssetAdministrationShell: aas = model.AssetAdministrationShell( _child_construct_mandatory(element, NS_AAS + "assetRef", _construct_asset_reference), @@ -722,17 +795,17 @@ def _construct_asset_administration_shell(element: etree.Element, failsafe: bool aas.security = security submodels = element.find(NS_AAS + "submodelRefs") if submodels is not None: - for ref in _failsafe_construct_multiple(submodels.findall(NS_AAS + "submodelRef"), - _construct_submodel_reference, failsafe): + for ref in _child_construct_multiple(submodels, NS_AAS + "submodelRef", _construct_submodel_reference, + failsafe): aas.submodel.add(ref) views = element.find(NS_AAS + "views") if views is not None: - for view in _failsafe_construct_multiple(views.findall(NS_AAS + "view"), _construct_view, failsafe): + for view in _child_construct_multiple(views, NS_AAS + "view", _construct_view, failsafe): aas.view.add(view) concept_dictionaries = element.find(NS_AAS + "conceptDictionaries") if concept_dictionaries is not None: - for cd in _failsafe_construct_multiple(concept_dictionaries.findall(NS_AAS + "conceptDictionary"), - _construct_concept_dictionary, failsafe): + for cd in _child_construct_multiple(concept_dictionaries, NS_AAS + "conceptDictionary", + _construct_concept_dictionary, failsafe): aas.concept_dictionary.add(cd) derived_from = _failsafe_construct(element.find(NS_AAS + "derivedFrom"), _construct_asset_administration_shell_reference, failsafe) @@ -799,23 +872,30 @@ def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: "conceptDescription": _construct_concept_description }.items()} + ret: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) - tree = etree.parse(file, parser) + try: + tree = etree.parse(file, parser) + except etree.XMLSyntaxError as e: + if failsafe: + logger.error(e) + return ret + raise e + root = tree.getroot() # Add AAS objects to ObjectStore - ret: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() for list_ in root: element_tag = list_.tag[:-1] if list_.tag[-1] != "s" or element_tag not in element_constructors: - error_message = f"Unexpected top-level list {list_.tag}!" + error_message = f"Unexpected top-level list {_element_pretty_identifier(list_)}!" if not failsafe: raise TypeError(error_message) logger.warning(error_message) continue constructor = element_constructors[element_tag] - for element in _failsafe_construct_multiple(list_.findall(element_tag), constructor, failsafe): + for element in _child_construct_multiple(list_, element_tag, constructor, failsafe): # element is always Identifiable, because the tag is checked earlier # this is just to satisfy the type checker if isinstance(element, model.Identifiable): -- GitLab From 8c95bfb097c8ac24922aa3ce5c5cd4a32024c743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 1 Apr 2020 13:46:36 +0200 Subject: [PATCH 21/27] adapter.xml: add _str_to_bool() function _str_to_bool() converts the strings "false" and "true" to their respective boolean value. Any string other than "false" and "true" will result in a ValueError. change error message in _failsafe_construct() --- aas/adapter/xml/xml_deserialization.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 9e4302f..a3e76a0 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -47,6 +47,21 @@ logger = logging.getLogger(__name__) T = TypeVar("T") +def _str_to_bool(string: str) -> bool: + """ + XML only allows "false" and "true" (case-sensitive) as valid values for a boolean. + + This function checks the string and raises a ValueError if the string is neither "true" nor "false". + + :param string: String representation of a boolean. ("true" or "false") + :return: The respective boolean value. + :raises ValueError: If string is neither "true" nor "false". + """ + if string not in ("true", "false"): + raise ValueError(f"{string} is not a valid boolean! Only true and false are allowed.") + return string == "true" + + def _tag_replace_namespace(tag: str, nsmap: Dict[str, str]) -> str: """ Attempts to replace the namespace in front of a tag with the prefix used in the xml document. @@ -255,7 +270,7 @@ def _failsafe_construct(element: Optional[etree.Element], constructor: Callable[ return constructor(element, failsafe, **kwargs) except (KeyError, ValueError) as e: type_name = _constructor_name_to_typename(constructor) - error_message = f"Failed to convert {_element_pretty_identifier(element)} to type {type_name}!" + error_message = f"Failed to create {type_name} from {_element_pretty_identifier(element)}!" if not failsafe: raise type(e)(error_message) from e error_type = type(e).__name__ @@ -423,7 +438,7 @@ def _get_modeling_kind_kwarg(element: etree.Element) -> ModelingKindKwArg: def _construct_key(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.Key: return model.Key( _get_attrib_mandatory_mapped(element, "type", KEY_ELEMENTS_INVERSE), - _get_attrib_mandatory(element, "local").lower() == "true", + _str_to_bool(_get_attrib_mandatory(element, "local")), _get_text_mandatory(element), _get_attrib_mandatory_mapped(element, "idType", KEY_TYPES_INVERSE) ) @@ -771,7 +786,7 @@ def _construct_relationship_element(element: etree.Element, failsafe: bool, **_k def _construct_submodel_element_collection(element: etree.Element, failsafe: bool, **_kwargs: Any) \ -> model.SubmodelElementCollection: - ordered = _child_text_mandatory(element, NS_AAS + "ordered").lower() == "true" + ordered = _str_to_bool(_child_text_mandatory(element, NS_AAS + "ordered")) collection_type = model.SubmodelElementCollectionOrdered if ordered else model.SubmodelElementCollectionUnordered collection = collection_type( _child_text_mandatory(element, NS_AAS + "idShort"), -- GitLab From 90b8e11e87783dd22a3951a145c2cbe2574059f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Apr 2020 19:18:05 +0200 Subject: [PATCH 22/27] adapter.xml: add support for IEC61360 Concept Descriptions add _get_text_mapped_or_none() rename _get_child_multiple() -> _get_all_children_expect_tag() allow passing the namespace to _construct_key_tuple(), _construct_reference() and _construct_lang_string_set() minor fixes in _construct_entity() and _construct_file() update copyright year to 2020 --- aas/adapter/xml/xml_deserialization.py | 165 +++++++++++++++++++++---- 1 file changed, 140 insertions(+), 25 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index a3e76a0..0b62510 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -1,4 +1,4 @@ -# Copyright 2019 PyI40AAS Contributors +# Copyright 2020 PyI40AAS Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at @@ -40,7 +40,8 @@ from typing import Any, Callable, Dict, IO, Iterable, Optional, Tuple, Type, Typ from mypy_extensions import TypedDict # TODO: import this from typing should we require python 3.8+ at some point from .xml_serialization import NS_AAS, NS_ABAC, NS_IEC from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE, \ - IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, KEY_ELEMENTS_CLASSES_INVERSE + IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, \ + KEY_ELEMENTS_CLASSES_INVERSE logger = logging.getLogger(__name__) @@ -138,7 +139,7 @@ def _get_child_mandatory(parent: etree.Element, child_tag: str) -> etree.Element return child -def _get_child_multiple(parent: etree.Element, exppected_tag: str, failsafe: bool) -> Iterable[etree.Element]: +def _get_all_children_expect_tag(parent: etree.Element, exppected_tag: str, failsafe: bool) -> Iterable[etree.Element]: """ Iterates over all children, matching the tag. @@ -212,6 +213,20 @@ def _get_text_or_none(element: Optional[etree.Element]) -> Optional[str]: return element.text if element is not None else None +def _get_text_mapped_or_none(element: Optional[etree.Element], dct: Dict[str, T]) -> Optional[T]: + """ + Returns dct[element.text] or None, if the element is None, has no text or the text is not in dct. + + :param element: The xml element or None. + :param dct: The dictionary that is used to map the text. + :return: The mapped text or None. + """ + text = _get_text_or_none(element) + if text is None or text not in dct: + return None + return dct[text] + + def _get_text_mandatory(element: etree.Element) -> str: """ A helper function for getting the mandatory text of an element. @@ -346,8 +361,8 @@ def _child_construct_multiple(parent: etree.Element, expected_tag: str, construc If an error occurred while constructing an element and while in failsafe mode, the respective element will be skipped. """ - return _failsafe_construct_multiple(_get_child_multiple(parent, expected_tag, failsafe), constructor, failsafe, - **kwargs) + return _failsafe_construct_multiple(_get_all_children_expect_tag(parent, expected_tag, failsafe), constructor, + failsafe, **kwargs) def _child_text_mandatory(parent: etree.Element, child_tag: str) -> str: @@ -429,9 +444,9 @@ def _get_modeling_kind_kwarg(element: etree.Element) -> ModelingKindKwArg: An empty dict if not. """ kwargs: ModelingKindKwArg = ModelingKindKwArg() - kind = element.find(NS_AAS + "kind") + kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), MODELING_KIND_INVERSE) if kind is not None: - kwargs["kind"] = _get_text_mandatory_mapped(kind, MODELING_KIND_INVERSE) + kwargs["kind"] = kind return kwargs @@ -444,13 +459,15 @@ def _construct_key(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> m ) -def _construct_key_tuple(element: etree.Element, failsafe: bool, **_kwargs: Any) -> Tuple[model.Key, ...]: - keys = _get_child_mandatory(element, NS_AAS + "keys") - return tuple(_child_construct_multiple(keys, NS_AAS + "key", _construct_key, failsafe)) +def _construct_key_tuple(element: etree.Element, failsafe: bool, namespace: str = NS_AAS, **_kwargs: Any)\ + -> Tuple[model.Key, ...]: + keys = _get_child_mandatory(element, namespace + "keys") + return tuple(_child_construct_multiple(keys, namespace + "key", _construct_key, failsafe)) -def _construct_reference(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Reference: - return model.Reference(_construct_key_tuple(element, failsafe)) +def _construct_reference(element: etree.Element, failsafe: bool, namespace: str = NS_AAS, **_kwargs: Any) \ + -> model.Reference: + return model.Reference(_construct_key_tuple(element, failsafe, namespace=namespace)) def _construct_aas_reference(element: etree.Element, failsafe: bool, type_: Type[model.base._RT], **_kwargs: Any) \ @@ -500,9 +517,10 @@ def _construct_administrative_information(element: etree.Element, _failsafe: boo ) -def _construct_lang_string_set(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.LangStringSet: +def _construct_lang_string_set(element: etree.Element, failsafe: bool, namespace: str = NS_AAS, **_kwargs: Any) \ + -> model.LangStringSet: lss: model.LangStringSet = {} - for lang_string in _get_child_multiple(element, NS_AAS + "langString", failsafe): + for lang_string in _get_all_children_expect_tag(element, namespace + "langString", failsafe): lss[_get_attrib_mandatory(lang_string, "lang")] = _get_text_mandatory(lang_string) return lss @@ -643,9 +661,9 @@ def _construct_blob(element: etree.Element, failsafe: bool, **_kwargs: Any) -> m _child_text_mandatory(element, NS_AAS + "mimeType"), **_get_modeling_kind_kwarg(element) ) - value = element.find(NS_AAS + "value") + value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: - blob.value = base64.b64decode(_get_text_mandatory(value)) + blob.value = base64.b64decode(value) _amend_abstract_attributes(blob, element, failsafe) return blob @@ -663,11 +681,10 @@ def _construct_entity(element: etree.Element, failsafe: bool, **_kwargs: Any) -> entity = model.Entity( _child_text_mandatory(element, NS_AAS + "idShort"), _child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), + # pass the asset to the constructor, because self managed entities need asset references + asset=_failsafe_construct(element.find(NS_AAS + "assetRef"), _construct_asset_reference, failsafe), **_get_modeling_kind_kwarg(element) ) - asset_ref = _failsafe_construct(element.find(NS_AAS + "assetRef"), _construct_asset_reference, failsafe) - if asset_ref is not None: - entity.asset = asset_ref for stmt in _failsafe_construct_multiple(_get_child_mandatory(element, NS_AAS + "statements"), _construct_submodel_element, failsafe): entity.statement.add(stmt) @@ -678,12 +695,12 @@ def _construct_entity(element: etree.Element, failsafe: bool, **_kwargs: Any) -> def _construct_file(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.File: file = model.File( _child_text_mandatory(element, NS_AAS + "idShort"), - _child_text_mandatory(element, NS_AAS + "idShort"), + _child_text_mandatory(element, NS_AAS + "mimeType"), **_get_modeling_kind_kwarg(element) ) - value = element.find(NS_AAS + "value") + value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: - file.value = _get_text_mandatory(value) + file.value = value _amend_abstract_attributes(file, element, failsafe) return file @@ -860,10 +877,108 @@ def _construct_submodel(element: etree.Element, failsafe: bool, **_kwargs: Any) return submodel -def _construct_concept_description(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: - cd = model.ConceptDescription( - _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) +def _construct_value_reference_pair(element: etree.Element, _failsafe: bool, + value_format: Optional[model.DataTypeDef] = None, **_kwargs: Any) \ + -> model.ValueReferencePair: + if value_format is None: + raise ValueError("No value format given!") + return model.ValueReferencePair( + value_format, + model.datatypes.from_xsd(_child_text_mandatory(element, NS_IEC + "value"), value_format), + _child_construct_mandatory(element, NS_IEC + "valueId", _construct_reference, namespace=NS_IEC) ) + + +def _construct_value_list(element: etree.Element, failsafe: bool, + value_format: Optional[model.DataTypeDef] = None, **_kwargs: Any) \ + -> model.ValueList: + return set( + _child_construct_multiple(element, NS_IEC + "valueReferencePair", _construct_value_reference_pair, failsafe, + value_format=value_format) + ) + + +def _construct_iec61360_concept_description(element: etree.Element, failsafe: bool, + identifier: Optional[model.Identifier] = None, **_kwargs: Any) \ + -> model.IEC61360ConceptDescription: + if identifier is None: + raise ValueError("No identifier given!") + cd = model.IEC61360ConceptDescription( + identifier, + _child_construct_mandatory(element, NS_IEC + "preferredName", _construct_lang_string_set, namespace=NS_IEC), + _child_text_mandatory_mapped(element, NS_IEC + "dataType", IEC61360_DATA_TYPES_INVERSE) + ) + definition = _failsafe_construct(element.find(NS_IEC + "definition"), _construct_lang_string_set, failsafe, + namespace=NS_IEC) + if definition is not None: + cd.definition = definition + short_name = _failsafe_construct(element.find(NS_IEC + "shortName"), _construct_lang_string_set, failsafe, + namespace=NS_IEC) + if short_name is not None: + cd.short_name = short_name + unit = _get_text_or_none(element.find(NS_IEC + "unit")) + if unit is not None: + cd.unit = unit + unit_id = _failsafe_construct(element.find(NS_IEC + "unitId"), _construct_reference, failsafe, namespace=NS_IEC) + if unit_id is not None: + cd.unit_id = unit_id + source_of_definition = _get_text_or_none(element.find(NS_IEC + "sourceOfDefinition")) + if source_of_definition is not None: + cd.source_of_definition = source_of_definition + symbol = _get_text_or_none(element.find(NS_IEC + "symbol")) + if symbol is not None: + cd.symbol = symbol + value_format = _get_text_mapped_or_none(element.find(NS_IEC + "valueFormat"), + model.datatypes.XSD_TYPE_CLASSES) + if value_format is not None: + cd.value_format = value_format + value_list = _failsafe_construct(element.find(NS_IEC + "valueList"), _construct_value_list, failsafe, + value_format=value_format) + if value_list is not None: + cd.value_list = value_list + value = _get_text_or_none(element.find(NS_IEC + "value")) + if value is not None and value_format is not None: + cd.value = model.datatypes.from_xsd(value, value_format) + value_id = _failsafe_construct(element.find(NS_IEC + "valueId"), _construct_reference, failsafe, namespace=NS_IEC) + if value_id is not None: + cd.value_id = value_id + for level_type_element in element.findall(NS_IEC + "levelType"): + level_type = _get_text_mapped_or_none(level_type_element, IEC61360_LEVEL_TYPES_INVERSE) + if level_type is None: + error_message = f"{_element_pretty_identifier(level_type_element)} has invalid value: " \ + + str(level_type_element.text) + if not failsafe: + raise ValueError(error_message) + logger.warning(error_message) + continue + cd.level_types.add(level_type) + return cd + + +def _construct_concept_description(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: + """ + TODO: our model doesn't support more than one embeddedDataSpecification (yet) + """ + cd: Optional[model.ConceptDescription] = None + identifier = _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) + # Hack to detect IEC61360ConceptDescriptions, which are represented using dataSpecification according to DotAAS + dspec_tag = NS_AAS + "embeddedDataSpecification" + dspecs = element.findall(dspec_tag) + if len(dspecs) > 1: + logger.warning(f"{_element_pretty_identifier(element)} has more than one " + f"{_tag_replace_namespace(dspec_tag, element.nsmap)}. This model currently supports only one " + f"per {_tag_replace_namespace(element.tag, element.nsmap)}!") + if len(dspecs) > 0: + dspec = dspecs[0] + dspec_content = dspec.find(NS_AAS + "dataSpecificationContent") + if dspec_content is not None: + dspec_ref = _failsafe_construct(dspec.find(NS_AAS + "dataSpecification"), _construct_reference, failsafe) + if dspec_ref is not None and len(dspec_ref.key) > 0 and dspec_ref.key[0].value == \ + "http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/2/0": + cd = _failsafe_construct(dspec_content.find(NS_AAS + "dataSpecificationIEC61360"), + _construct_iec61360_concept_description, failsafe, identifier=identifier) + if cd is None: + cd = model.ConceptDescription(identifier) for ref in _failsafe_construct_multiple(element.findall(NS_AAS + "isCaseOf"), _construct_reference, failsafe): cd.is_case_of.add(ref) _amend_abstract_attributes(cd, element, failsafe) -- GitLab From bfbb745547f5c5d0e01e99ecb7cc40169ad2eca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Apr 2020 19:19:45 +0200 Subject: [PATCH 23/27] test: add serialize and deserialize test for xml --- .../test_xml_serialization_deserialization.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/adapter/xml/test_xml_serialization_deserialization.py diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py new file mode 100644 index 0000000..bf09064 --- /dev/null +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -0,0 +1,61 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import io +import unittest + +from aas import model +from aas.adapter.xml import xml_serialization, xml_deserialization + +from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ + example_aas_mandatory_attributes, example_aas, example_concept_description +from aas.examples.data._helper import AASDataChecker + + +def _serialize_and_deserialize(data: model.DictObjectStore) -> model.DictObjectStore: + file = io.BytesIO() + xml_serialization.write_aas_xml_file(file=file, data=data) + + # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml_deserialization + # module + file.seek(0) + return xml_deserialization.read_xml_aas_file(file, failsafe=False) + + +class XMLSerializationDeserializationTest(unittest.TestCase): + def test_example_serialization_deserialization(self) -> None: + object_store = _serialize_and_deserialize(example_aas.create_full_example()) + checker = AASDataChecker(raise_immediately=True) + example_aas.check_full_example(checker, object_store) + + def test_example_mandatory_attributes_serialization_deserialization(self) -> None: + object_store = _serialize_and_deserialize(example_aas_mandatory_attributes.create_full_example()) + checker = AASDataChecker(raise_immediately=True) + example_aas_mandatory_attributes.check_full_example(checker, object_store) + + def test_example_missing_attributes_serialization_deserialization(self) -> None: + object_store = _serialize_and_deserialize(example_aas_missing_attributes.create_full_example()) + checker = AASDataChecker(raise_immediately=True) + example_aas_missing_attributes.check_full_example(checker, object_store) + + def test_example_submodel_template_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_submodel_template.create_example_submodel_template()) + object_store = _serialize_and_deserialize(data) + checker = AASDataChecker(raise_immediately=True) + example_submodel_template.check_full_example(checker, object_store) + + def test_example_iec61360_concept_description_serialization_deserialization(self) -> None: + data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore() + data.add(example_concept_description.create_iec61360_concept_description()) + object_store = _serialize_and_deserialize(data) + checker = AASDataChecker(raise_immediately=True) + example_concept_description.check_full_example(checker, object_store) -- GitLab From c948c081ac888ed56a8bf7f5bf3d2239b1548ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 9 Apr 2020 23:53:36 +0200 Subject: [PATCH 24/27] adapter.xml: rename read_xml_aas_file to read_aas_xml_file Re-publish read_aas_xml_file in adapter.xml.__init__. --- aas/adapter/xml/__init__.py | 1 + aas/adapter/xml/xml_deserialization.py | 2 +- test/adapter/xml/test_xml_serialization_deserialization.py | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/aas/adapter/xml/__init__.py b/aas/adapter/xml/__init__.py index 795a355..8c8e62c 100644 --- a/aas/adapter/xml/__init__.py +++ b/aas/adapter/xml/__init__.py @@ -9,3 +9,4 @@ xml_deserialization.py """ from .xml_serialization import write_aas_xml_file +from .xml_deserialization import read_aas_xml_file diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index 0b62510..b30f1f9 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -985,7 +985,7 @@ def _construct_concept_description(element: etree.Element, failsafe: bool, **_kw return cd -def read_xml_aas_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: +def read_aas_xml_file(file: IO, failsafe: bool = True) -> model.DictObjectStore: """ Read an Asset Administration Shell XML file according to 'Details of the Asset Administration Shell', chapter 5.4 diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index bf09064..de6aecd 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -13,7 +13,7 @@ import io import unittest from aas import model -from aas.adapter.xml import xml_serialization, xml_deserialization +from aas.adapter.xml import write_aas_xml_file, read_aas_xml_file from aas.examples.data import example_aas_missing_attributes, example_submodel_template, \ example_aas_mandatory_attributes, example_aas, example_concept_description @@ -22,12 +22,12 @@ from aas.examples.data._helper import AASDataChecker def _serialize_and_deserialize(data: model.DictObjectStore) -> model.DictObjectStore: file = io.BytesIO() - xml_serialization.write_aas_xml_file(file=file, data=data) + write_aas_xml_file(file=file, data=data) # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml_deserialization # module file.seek(0) - return xml_deserialization.read_xml_aas_file(file, failsafe=False) + return read_aas_xml_file(file, failsafe=False) class XMLSerializationDeserializationTest(unittest.TestCase): -- GitLab From f8286e1f95b3b0635aac73597265c95474308d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 10 Apr 2020 01:47:20 +0200 Subject: [PATCH 25/27] test: add xml_deserialization tests --- test/adapter/xml/test_xml_deserialization.py | 261 ++++++++++++++++++ .../test_xml_serialization_deserialization.py | 3 +- 2 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 test/adapter/xml/test_xml_deserialization.py diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py new file mode 100644 index 0000000..beb2cff --- /dev/null +++ b/test/adapter/xml/test_xml_deserialization.py @@ -0,0 +1,261 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import io +import logging +import unittest + +from aas.adapter.xml import read_aas_xml_file +from lxml import etree # type: ignore +from typing import Iterable, Type, Union + + +def _xml_wrap(xml: str) -> str: + return \ + """""" \ + """""" \ + + xml + """""" + + +def _root_cause(exception: BaseException) -> BaseException: + while exception.__cause__ is not None: + exception = exception.__cause__ + return exception + + +class XMLDeserializationTest(unittest.TestCase): + def _assertInExceptionAndLog(self, xml: str, strings: Union[Iterable[str], str], error_type: Type[BaseException], + log_level: int) -> None: + """ + Runs read_xml_aas_file in failsafe mode and checks if each string is contained in the first message logged. + Then runs it in non-failsafe mode and checks if each string is contained in the first error raised. + + :param xml: The xml document to parse. + :param strings: One or more strings to match. + :param error_type: The expected error type. + :param log_level: The log level on which the string is expected. + """ + if isinstance(strings, str): + strings = [strings] + bytes_io = io.BytesIO(xml.encode("utf-8")) + with self.assertLogs(logging.getLogger(), level=log_level) as log_ctx: + read_aas_xml_file(bytes_io, True) + for s in strings: + self.assertIn(s, log_ctx.output[0]) + with self.assertRaises(error_type) as err_ctx: + read_aas_xml_file(bytes_io, False) + cause = _root_cause(err_ctx.exception) + for s in strings: + self.assertIn(s, str(cause)) + + def test_malformed_xml(self) -> None: + xml = ( + "invalid xml", + _xml_wrap("<<>>><<<<<"), + _xml_wrap("") + ) + for s in xml: + bytes_io = io.BytesIO(s.encode("utf-8")) + with self.assertRaises(etree.XMLSyntaxError): + read_aas_xml_file(bytes_io, False) + with self.assertLogs(logging.getLogger(), level=logging.ERROR): + read_aas_xml_file(bytes_io, True) + + def test_invalid_list_name(self) -> None: + xml = _xml_wrap("") + self._assertInExceptionAndLog(xml, "aas:invalidList", TypeError, logging.WARNING) + + def test_invalid_element_in_list(self) -> None: + xml = _xml_wrap(""" + + + + """) + self._assertInExceptionAndLog(xml, ["aas:invalidElement", "aas:assets"], KeyError, logging.WARNING) + + def test_missing_identification_attribute(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_asset + Instance + + + """) + self._assertInExceptionAndLog(xml, "idType", KeyError, logging.ERROR) + + def test_invalid_identification_attribute_value(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_asset + Instance + + + """) + self._assertInExceptionAndLog(xml, ["idType", "invalid"], ValueError, logging.ERROR) + + def test_missing_asset_kind(self) -> None: + xml = _xml_wrap(""" + + + + + """) + self._assertInExceptionAndLog(xml, "aas:kind", KeyError, logging.ERROR) + + def test_missing_asset_kind_text(self) -> None: + xml = _xml_wrap(""" + + + + + + """) + self._assertInExceptionAndLog(xml, "aas:kind", KeyError, logging.ERROR) + + def test_invalid_asset_kind_text(self) -> None: + xml = _xml_wrap(""" + + + invalidKind + + + """) + self._assertInExceptionAndLog(xml, ["aas:kind", "invalidKind"], ValueError, logging.ERROR) + + def test_invalid_boolean(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_asset + + + http://acplt.org/test_ref + + + + + """) + self._assertInExceptionAndLog(xml, "False", ValueError, logging.ERROR) + + def test_no_modeling_kind(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + + """) + # should get parsed successfully + read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + + def test_reference_kind_mismatch(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_aas + + + http://acplt.org/test_ref + + + + + """) + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: + read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + self.assertIn("GLOBAL_REFERENCE", context.output[0]) + self.assertIn("IRI=http://acplt.org/test_ref", context.output[0]) + self.assertIn("Asset", context.output[0]) + + def test_invalid_submodel_element(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + + + + """) + self._assertInExceptionAndLog(xml, "aas:invalidSubmodelElement", KeyError, logging.ERROR) + + def test_invalid_constraint(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + + + + + """) + self._assertInExceptionAndLog(xml, "aas:invalidConstraint", KeyError, logging.ERROR) + + def test_operation_variable_no_submodel_element(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + test_operation + + + + + + + + + + """) + self._assertInExceptionAndLog(xml, "aas:value", KeyError, logging.ERROR) + + def test_operation_variable_too_many_submodel_elements(self) -> None: + xml = _xml_wrap(""" + + + http://acplt.org/test_submodel + + + test_operation + + + + + test_file + application/problem+xml + + + test_file2 + application/problem+xml + + + + + + + + + """) + with self.assertLogs(logging.getLogger(), level=logging.WARNING) as context: + read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + self.assertIn("aas:value", context.output[0]) diff --git a/test/adapter/xml/test_xml_serialization_deserialization.py b/test/adapter/xml/test_xml_serialization_deserialization.py index de6aecd..093de30 100644 --- a/test/adapter/xml/test_xml_serialization_deserialization.py +++ b/test/adapter/xml/test_xml_serialization_deserialization.py @@ -24,8 +24,7 @@ def _serialize_and_deserialize(data: model.DictObjectStore) -> model.DictObjectS file = io.BytesIO() write_aas_xml_file(file=file, data=data) - # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml_deserialization - # module + # try deserializing the xml document into a DictObjectStore of AAS objects with help of the xml module file.seek(0) return read_aas_xml_file(file, failsafe=False) -- GitLab From 2b875550a5dde5573cf4da0a9f19a7e01f6e234b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 14 Apr 2020 14:10:00 +0200 Subject: [PATCH 26/27] adapter: explicitly define INSTANCE as ModelingKind default value test this behavior in the respective test remove TODO docstring from _construct_concept_description() --- aas/adapter/xml/xml_deserialization.py | 57 +++++++------------- test/adapter/xml/test_xml_deserialization.py | 7 ++- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/aas/adapter/xml/xml_deserialization.py b/aas/adapter/xml/xml_deserialization.py index b30f1f9..f370e67 100644 --- a/aas/adapter/xml/xml_deserialization.py +++ b/aas/adapter/xml/xml_deserialization.py @@ -37,7 +37,6 @@ import logging import base64 from typing import Any, Callable, Dict, IO, Iterable, Optional, Tuple, Type, TypeVar -from mypy_extensions import TypedDict # TODO: import this from typing should we require python 3.8+ at some point from .xml_serialization import NS_AAS, NS_ABAC, NS_IEC from .._generic import MODELING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_ELEMENTS_INVERSE, KEY_TYPES_INVERSE, \ IDENTIFIER_TYPES_INVERSE, ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, \ @@ -424,30 +423,15 @@ def _amend_abstract_attributes(obj: object, element: etree.Element, failsafe: bo obj.qualifier.add(constraint) -class ModelingKindKwArg(TypedDict, total=False): - kind: model.ModelingKind - - -def _get_modeling_kind_kwarg(element: etree.Element) -> ModelingKindKwArg: +def _get_modeling_kind(element: etree.Element) -> model.ModelingKind: """ - A helper function that creates a dict containing the modeling kind or nothing for a given xml element. - - Since the modeling kind can only be set in the __init__ method of a class that inherits from model.HasKind, - the dict returned by this function can be passed directly to the classes __init__ method. - An alternative to this function would be returning the modeling kind directly and falling back to the default - value if no "kind" xml element is present, but in this case the default value would have to be defined here as well. - In my opinion defining what the default value is, should be the task of the __init__ method, not the task of any - function in the deserialization. + Returns the modeling kind of an element with the default value INSTANCE, if none specified. :param element: The xml element. - :return: A dict containing {"kind": }, if a kind element was found. - An empty dict if not. + :return: The modeling kind of the element. """ - kwargs: ModelingKindKwArg = ModelingKindKwArg() - kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), MODELING_KIND_INVERSE) - if kind is not None: - kwargs["kind"] = kind - return kwargs + modeling_kind = _get_text_mapped_or_none(element.find(NS_AAS + "kind"), MODELING_KIND_INVERSE) + return modeling_kind if modeling_kind is not None else model.ModelingKind.INSTANCE def _construct_key(element: etree.Element, _failsafe: bool, **_kwargs: Any) -> model.Key: @@ -635,7 +619,7 @@ def _construct_annotated_relationship_element(element: etree.Element, failsafe: _child_text_mandatory(element, NS_AAS + "idShort"), _child_construct_mandatory(element, NS_AAS + "first", _construct_referable_reference), _child_construct_mandatory(element, NS_AAS + "second", _construct_referable_reference), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) annotations = _get_child_mandatory(element, NS_AAS + "annotations") for data_element_ref in _failsafe_construct_multiple(annotations.findall(NS_AAS + "reference"), @@ -649,7 +633,7 @@ def _construct_basic_event(element: etree.Element, failsafe: bool, **_kwargs: An basic_event = model.BasicEvent( _child_text_mandatory(element, NS_AAS + "idShort"), _child_construct_mandatory(element, NS_AAS + "observed", _construct_referable_reference), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) _amend_abstract_attributes(basic_event, element, failsafe) return basic_event @@ -659,7 +643,7 @@ def _construct_blob(element: etree.Element, failsafe: bool, **_kwargs: Any) -> m blob = model.Blob( _child_text_mandatory(element, NS_AAS + "idShort"), _child_text_mandatory(element, NS_AAS + "mimeType"), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: @@ -671,7 +655,7 @@ def _construct_blob(element: etree.Element, failsafe: bool, **_kwargs: Any) -> m def _construct_capability(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Capability: capability = model.Capability( _child_text_mandatory(element, NS_AAS + "idShort"), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) _amend_abstract_attributes(capability, element, failsafe) return capability @@ -683,7 +667,7 @@ def _construct_entity(element: etree.Element, failsafe: bool, **_kwargs: Any) -> _child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), # pass the asset to the constructor, because self managed entities need asset references asset=_failsafe_construct(element.find(NS_AAS + "assetRef"), _construct_asset_reference, failsafe), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) for stmt in _failsafe_construct_multiple(_get_child_mandatory(element, NS_AAS + "statements"), _construct_submodel_element, failsafe): @@ -696,7 +680,7 @@ def _construct_file(element: etree.Element, failsafe: bool, **_kwargs: Any) -> m file = model.File( _child_text_mandatory(element, NS_AAS + "idShort"), _child_text_mandatory(element, NS_AAS + "mimeType"), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: @@ -709,7 +693,7 @@ def _construct_multi_language_property(element: etree.Element, failsafe: bool, * -> model.MultiLanguageProperty: multi_language_property = model.MultiLanguageProperty( _child_text_mandatory(element, NS_AAS + "idShort"), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) value = _failsafe_construct(element.find(NS_AAS + "value"), _construct_lang_string_set, failsafe) if value is not None: @@ -724,7 +708,7 @@ def _construct_multi_language_property(element: etree.Element, failsafe: bool, * def _construct_operation(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Operation: operation = model.Operation( _child_text_mandatory(element, NS_AAS + "idShort"), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) in_output_variable = element.find(NS_AAS + "inoutputVariable") if in_output_variable is not None: @@ -749,7 +733,7 @@ def _construct_property(element: etree.Element, failsafe: bool, **_kwargs: Any) property_ = model.Property( _child_text_mandatory(element, NS_AAS + "idShort"), value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) value = _get_text_or_none(element.find(NS_AAS + "value")) if value is not None: @@ -765,7 +749,7 @@ def _construct_range(element: etree.Element, failsafe: bool, **_kwargs: Any) -> range_ = model.Range( _child_text_mandatory(element, NS_AAS + "idShort"), value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) max_ = _get_text_or_none(element.find(NS_AAS + "max")) if max_ is not None: @@ -780,7 +764,7 @@ def _construct_range(element: etree.Element, failsafe: bool, **_kwargs: Any) -> def _construct_reference_element(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.ReferenceElement: reference_element = model.ReferenceElement( _child_text_mandatory(element, NS_AAS + "idShort"), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) value = _failsafe_construct(element.find(NS_AAS + "value"), _construct_referable_reference, failsafe) if value is not None: @@ -795,7 +779,7 @@ def _construct_relationship_element(element: etree.Element, failsafe: bool, **_k _child_text_mandatory(element, NS_AAS + "idShort"), _child_construct_mandatory(element, NS_AAS + "first", _construct_referable_reference), _child_construct_mandatory(element, NS_AAS + "second", _construct_referable_reference), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) _amend_abstract_attributes(relationship_element, element, failsafe) return relationship_element @@ -807,7 +791,7 @@ def _construct_submodel_element_collection(element: etree.Element, failsafe: boo collection_type = model.SubmodelElementCollectionOrdered if ordered else model.SubmodelElementCollectionUnordered collection = collection_type( _child_text_mandatory(element, NS_AAS + "idShort"), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) value = _get_child_mandatory(element, NS_AAS + "value") for se in _failsafe_construct_multiple(value, _construct_submodel_element, failsafe): @@ -867,7 +851,7 @@ def _construct_asset(element: etree.Element, failsafe: bool, **_kwargs: Any) -> def _construct_submodel(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.Submodel: submodel = model.Submodel( _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier), - **_get_modeling_kind_kwarg(element) + kind=_get_modeling_kind(element) ) for submodel_element in _get_child_mandatory(element, NS_AAS + "submodelElements"): constructed = _failsafe_construct(submodel_element, _construct_submodel_element, failsafe) @@ -956,9 +940,6 @@ def _construct_iec61360_concept_description(element: etree.Element, failsafe: bo def _construct_concept_description(element: etree.Element, failsafe: bool, **_kwargs: Any) -> model.ConceptDescription: - """ - TODO: our model doesn't support more than one embeddedDataSpecification (yet) - """ cd: Optional[model.ConceptDescription] = None identifier = _child_construct_mandatory(element, NS_AAS + "identification", _construct_identifier) # Hack to detect IEC61360ConceptDescriptions, which are represented using dataSpecification according to DotAAS diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index beb2cff..7949306 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -13,6 +13,7 @@ import io import logging import unittest +from aas import model from aas.adapter.xml import read_aas_xml_file from lxml import etree # type: ignore from typing import Iterable, Type, Union @@ -161,7 +162,11 @@ class XMLDeserializationTest(unittest.TestCase): """) # should get parsed successfully - read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + object_store = read_aas_xml_file(io.BytesIO(xml.encode("utf-8")), False) + # modeling kind should default to INSTANCE + submodel = object_store.pop() + self.assertIsInstance(submodel, model.Submodel) + self.assertEqual(submodel._kind, model.ModelingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: xml = _xml_wrap(""" -- GitLab From e271d8112de2545657606d3d26292a55a2b81c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 14 Apr 2020 15:55:18 +0200 Subject: [PATCH 27/27] test.xml: don't access protected property _kind --- test/adapter/xml/test_xml_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 7949306..533eb8f 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -166,7 +166,7 @@ class XMLDeserializationTest(unittest.TestCase): # modeling kind should default to INSTANCE submodel = object_store.pop() self.assertIsInstance(submodel, model.Submodel) - self.assertEqual(submodel._kind, model.ModelingKind.INSTANCE) + self.assertEqual(submodel.kind, model.ModelingKind.INSTANCE) def test_reference_kind_mismatch(self) -> None: xml = _xml_wrap(""" -- GitLab