Skip to content

Commit 498c78f

Browse files
committed
Merge branch 'aas_manager/branch_for_update' into aas_manager/v30
2 parents 60612d2 + 04e06d6 commit 498c78f

14 files changed

+460
-159
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@ from basyx.aas.adapter.xml import write_aas_xml_file
130130

131131
data: model.DictObjectStore[model.Identifiable] = model.DictObjectStore()
132132
data.add(submodel)
133-
with open('Simple_Submodel.xml', 'wb') as f:
134-
write_aas_xml_file(file=f, data=data)
133+
write_aas_xml_file(file='Simple_Submodel.xml', data=data)
135134
```
136135

137136

basyx/aas/adapter/_generic.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@
88
The dicts defined in this module are used in the json and xml modules to translate enum members of our
99
implementation to the respective string and vice versa.
1010
"""
11-
from typing import Dict, Type
11+
import os
12+
from typing import BinaryIO, Dict, IO, Type, Union
1213

1314
from basyx.aas import model
1415

16+
# type aliases for path-like objects and IO
17+
# used by write_aas_xml_file, read_aas_xml_file, write_aas_json_file, read_aas_json_file
18+
Path = Union[str, bytes, os.PathLike]
19+
PathOrBinaryIO = Union[Path, BinaryIO]
20+
PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO
21+
1522
# XML Namespace definition
1623
XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"}
1724
XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}"

basyx/aas/adapter/aasx.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,11 @@ class AASXWriter:
285285
file_store)
286286
writer.write_core_properties(cp)
287287
288-
**Attention:** The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context
289-
manager functionality (as shown above). Otherwise the resulting AASX file will lack important data structures
290-
and will not be readable.
288+
.. attention::
289+
290+
The AASXWriter must always be closed using the :meth:`~.AASXWriter.close` method or its context manager
291+
functionality (as shown above). Otherwise, the resulting AASX file will lack important data structures
292+
and will not be readable.
291293
"""
292294
AASX_ORIGIN_PART_NAME = "/aasx/aasx-origin"
293295

basyx/aas/adapter/json/json_deserialization.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@
3030
Other embedded objects are converted using a number of helper constructor methods.
3131
"""
3232
import base64
33+
import contextlib
3334
import json
3435
import logging
3536
import pprint
36-
from typing import Dict, Callable, TypeVar, Type, List, IO, Optional, Set
37+
from typing import Dict, Callable, ContextManager, TypeVar, Type, List, IO, Optional, Set, get_args
3738

3839
from basyx.aas import model
3940
from .._generic import MODELLING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_TYPES_INVERSE, ENTITY_TYPES_INVERSE, \
4041
IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, KEY_TYPES_CLASSES_INVERSE, REFERENCE_TYPES_INVERSE, \
41-
DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE
42+
DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE, PathOrIO, Path
4243

4344
logger = logging.getLogger(__name__)
4445

@@ -794,7 +795,7 @@ def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFr
794795
return StrictAASFromJsonDecoder
795796

796797

797-
def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, replace_existing: bool = False,
798+
def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathOrIO, replace_existing: bool = False,
798799
ignore_existing: bool = False, failsafe: bool = True, stripped: bool = False,
799800
decoder: Optional[Type[AASFromJsonDecoder]] = None) -> Set[model.Identifier]:
800801
"""
@@ -803,7 +804,7 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r
803804
804805
:param object_store: The :class:`ObjectStore <basyx.aas.model.provider.AbstractObjectStore>` in which the
805806
identifiable objects should be stored
806-
:param file: A file-like object to read the JSON-serialized data from
807+
:param file: A filename or file-like object to read the JSON-serialized data from
807808
:param replace_existing: Whether to replace existing objects with the same identifier in the object store or not
808809
:param ignore_existing: Whether to ignore existing objects (e.g. log a message) or raise an error.
809810
This parameter is ignored if replace_existing is ``True``.
@@ -814,13 +815,31 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r
814815
See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91
815816
This parameter is ignored if a decoder class is specified.
816817
:param decoder: The decoder class used to decode the JSON objects
818+
:raises KeyError: **Non-failsafe**: Encountered a duplicate identifier
819+
:raises KeyError: Encountered an identifier that already exists in the given ``object_store`` with both
820+
``replace_existing`` and ``ignore_existing`` set to ``False``
821+
:raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError, TypeError): **Non-failsafe**:
822+
Errors during construction of the objects
823+
:raises TypeError: **Non-failsafe**: Encountered an element in the wrong list
824+
(e.g. an AssetAdministrationShell in ``submodels``)
817825
:return: A set of :class:`Identifiers <basyx.aas.model.base.Identifier>` that were added to object_store
818826
"""
819827
ret: Set[model.Identifier] = set()
820828
decoder_ = _select_decoder(failsafe, stripped, decoder)
821829

830+
# json.load() accepts TextIO and BinaryIO
831+
cm: ContextManager[IO]
832+
if isinstance(file, get_args(Path)):
833+
# 'file' is a path, needs to be opened first
834+
cm = open(file, "r", encoding="utf-8-sig")
835+
else:
836+
# 'file' is not a path, thus it must already be IO
837+
# mypy seems to have issues narrowing the type due to get_args()
838+
cm = contextlib.nullcontext(file) # type: ignore[arg-type]
839+
822840
# read, parse and convert JSON file
823-
data = json.load(file, cls=decoder_)
841+
with cm as fp:
842+
data = json.load(fp, cls=decoder_)
824843

825844
for name, expected_type in (('assetAdministrationShells', model.AssetAdministrationShell),
826845
('submodels', model.Submodel),
@@ -864,14 +883,19 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: IO, r
864883
return ret
865884

866885

867-
def read_aas_json_file(file: IO, **kwargs) -> model.DictObjectStore[model.Identifiable]:
886+
def read_aas_json_file(file: PathOrIO, **kwargs) -> model.DictObjectStore[model.Identifiable]:
868887
"""
869888
A wrapper of :meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into`, that reads all objects
870889
in an empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports the same keyword arguments as
871890
:meth:`~basyx.aas.adapter.json.json_deserialization.read_aas_json_file_into`.
872891
873892
:param file: A filename or file-like object to read the JSON-serialized data from
874893
:param kwargs: Keyword arguments passed to :meth:`read_aas_json_file_into`
894+
:raises KeyError: **Non-failsafe**: Encountered a duplicate identifier
895+
:raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError, TypeError): **Non-failsafe**:
896+
Errors during construction of the objects
897+
:raises TypeError: **Non-failsafe**: Encountered an element in the wrong list
898+
(e.g. an AssetAdministrationShell in ``submodels``)
875899
:return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the JSON file
876900
"""
877901
object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore()

basyx/aas/adapter/json/json_serialization.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
conversion functions to handle all the attributes of abstract base classes.
2828
"""
2929
import base64
30+
import contextlib
3031
import inspect
31-
from typing import List, Dict, IO, Optional, Type, Callable
32+
import io
33+
from typing import ContextManager, List, Dict, Optional, TextIO, Type, Callable, get_args
3234
import json
3335

3436
from basyx.aas import model
@@ -732,13 +734,21 @@ def object_store_to_json(data: model.AbstractObjectStore, stripped: bool = False
732734
return json.dumps(_create_dict(data), cls=encoder_, **kwargs)
733735

734736

735-
def write_aas_json_file(file: IO, data: model.AbstractObjectStore, stripped: bool = False,
737+
class _DetachingTextIOWrapper(io.TextIOWrapper):
738+
"""
739+
Like :class:`io.TextIOWrapper`, but detaches on context exit instead of closing the wrapped buffer.
740+
"""
741+
def __exit__(self, exc_type, exc_val, exc_tb):
742+
self.detach()
743+
744+
745+
def write_aas_json_file(file: _generic.PathOrIO, data: model.AbstractObjectStore, stripped: bool = False,
736746
encoder: Optional[Type[AASToJsonEncoder]] = None, **kwargs) -> None:
737747
"""
738748
Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset
739749
Administration Shell', chapter 5.5
740750
741-
:param file: A file-like object to write the JSON-serialized data to
751+
:param file: A filename or file-like object to write the JSON-serialized data to
742752
:param data: :class:`ObjectStore <basyx.aas.model.provider.AbstractObjectStore>` which contains different objects of
743753
the AAS meta model which should be serialized to a JSON file
744754
:param stripped: If `True`, objects are serialized to stripped json objects.
@@ -748,5 +758,21 @@ def write_aas_json_file(file: IO, data: model.AbstractObjectStore, stripped: boo
748758
:param kwargs: Additional keyword arguments to be passed to `json.dump()`
749759
"""
750760
encoder_ = _select_encoder(stripped, encoder)
761+
762+
# json.dump() only accepts TextIO
763+
cm: ContextManager[TextIO]
764+
if isinstance(file, get_args(_generic.Path)):
765+
# 'file' is a path, needs to be opened first
766+
cm = open(file, "w", encoding="utf-8")
767+
elif not hasattr(file, "encoding"):
768+
# only TextIO has this attribute, so this must be BinaryIO, which needs to be wrapped
769+
# mypy seems to have issues narrowing the type due to get_args()
770+
cm = _DetachingTextIOWrapper(file, "utf-8", write_through=True) # type: ignore[arg-type]
771+
else:
772+
# we already got TextIO, nothing needs to be done
773+
# mypy seems to have issues narrowing the type due to get_args()
774+
cm = contextlib.nullcontext(file) # type: ignore[arg-type]
775+
751776
# serialize object to json
752-
json.dump(_create_dict(data), file, cls=encoder_, **kwargs)
777+
with cm as fp:
778+
json.dump(_create_dict(data), fp, cls=encoder_, **kwargs)

basyx/aas/adapter/xml/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"""
1212
import os.path
1313

14-
from .xml_serialization import write_aas_xml_file
14+
from .xml_serialization import object_store_to_xml_element, write_aas_xml_file, object_to_xml_element, \
15+
write_aas_xml_element
1516
from .xml_deserialization import AASFromXmlDecoder, StrictAASFromXmlDecoder, StrippedAASFromXmlDecoder, \
1617
StrictStrippedAASFromXmlDecoder, XMLConstructables, read_aas_xml_file, read_aas_xml_file_into, read_aas_xml_element
1718

basyx/aas/adapter/xml/xml_deserialization.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@
4747
import base64
4848
import enum
4949

50-
from typing import Any, Callable, Dict, IO, Iterable, Optional, Set, Tuple, Type, TypeVar
50+
from typing import Any, Callable, Dict, Iterable, Optional, Set, Tuple, Type, TypeVar
5151
from .._generic import XML_NS_MAP, XML_NS_AAS, MODELLING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_TYPES_INVERSE, \
5252
ENTITY_TYPES_INVERSE, IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, KEY_TYPES_CLASSES_INVERSE, \
53-
REFERENCE_TYPES_INVERSE, DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE
53+
REFERENCE_TYPES_INVERSE, DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE, PathOrIO
5454

5555
NS_AAS = XML_NS_AAS
5656
REQUIRED_NAMESPACES: Set[str] = {XML_NS_MAP["aas"]}
@@ -315,8 +315,8 @@ def _failsafe_construct_mandatory(element: etree.Element, constructor: Callable[
315315
"""
316316
constructed = _failsafe_construct(element, constructor, False, **kwargs)
317317
if constructed is None:
318-
raise TypeError("The result of a non-failsafe _failsafe_construct() call was None! "
319-
"This is a bug in the Eclipse BaSyx Python SDK XML deserialization, please report it!")
318+
raise AssertionError("The result of a non-failsafe _failsafe_construct() call was None! "
319+
"This is a bug in the Eclipse BaSyx Python SDK XML deserialization, please report it!")
320320
return constructed
321321

322322

@@ -1186,14 +1186,16 @@ class StrictStrippedAASFromXmlDecoder(StrictAASFromXmlDecoder, StrippedAASFromXm
11861186
pass
11871187

11881188

1189-
def _parse_xml_document(file: IO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree.Element]:
1189+
def _parse_xml_document(file: PathOrIO, failsafe: bool = True, **parser_kwargs: Any) -> Optional[etree.Element]:
11901190
"""
11911191
Parse an XML document into an element tree
11921192
11931193
:param file: A filename or file-like object to read the XML-serialized data from
11941194
:param failsafe: If True, the file is parsed in a failsafe way: Instead of raising an Exception if the document
11951195
is malformed, parsing is aborted, an error is logged and None is returned
11961196
:param parser_kwargs: Keyword arguments passed to the XMLParser constructor
1197+
:raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML
1198+
:raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document
11971199
:return: The root element of the element tree
11981200
"""
11991201

@@ -1249,7 +1251,7 @@ class XMLConstructables(enum.Enum):
12491251
KEY = enum.auto()
12501252
REFERENCE = enum.auto()
12511253
MODEL_REFERENCE = enum.auto()
1252-
GLOBAL_REFERENCE = enum.auto()
1254+
EXTERNAL_REFERENCE = enum.auto()
12531255
ADMINISTRATIVE_INFORMATION = enum.auto()
12541256
QUALIFIER = enum.auto()
12551257
SECURITY = enum.auto()
@@ -1289,11 +1291,11 @@ class XMLConstructables(enum.Enum):
12891291
DATA_SPECIFICATION_IEC61360 = enum.auto()
12901292

12911293

1292-
def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool = True, stripped: bool = False,
1294+
def read_aas_xml_element(file: PathOrIO, construct: XMLConstructables, failsafe: bool = True, stripped: bool = False,
12931295
decoder: Optional[Type[AASFromXmlDecoder]] = None, **constructor_kwargs) -> Optional[object]:
12941296
"""
12951297
Construct a single object from an XML string. The namespaces have to be declared on the object itself, since there
1296-
is no surrounding aasenv element.
1298+
is no surrounding environment element.
12971299
12981300
:param file: A filename or file-like object to read the XML-serialized data from
12991301
:param construct: A member of the enum :class:`~.XMLConstructables`, specifying which type to construct.
@@ -1305,6 +1307,10 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool
13051307
This parameter is ignored if a decoder class is specified.
13061308
:param decoder: The decoder class used to decode the XML elements
13071309
:param constructor_kwargs: Keyword arguments passed to the constructor function
1310+
:raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML
1311+
:raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document
1312+
:raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during
1313+
construction of the objects
13081314
:return: The constructed object or None, if an error occurred in failsafe mode.
13091315
"""
13101316
decoder_ = _select_decoder(failsafe, stripped, decoder)
@@ -1316,7 +1322,7 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool
13161322
constructor = decoder_.construct_reference
13171323
elif construct == XMLConstructables.MODEL_REFERENCE:
13181324
constructor = decoder_.construct_model_reference
1319-
elif construct == XMLConstructables.GLOBAL_REFERENCE:
1325+
elif construct == XMLConstructables.EXTERNAL_REFERENCE:
13201326
constructor = decoder_.construct_external_reference
13211327
elif construct == XMLConstructables.ADMINISTRATIVE_INFORMATION:
13221328
constructor = decoder_.construct_administrative_information
@@ -1397,7 +1403,7 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool
13971403
return _failsafe_construct(element, constructor, decoder_.failsafe, **constructor_kwargs)
13981404

13991405

1400-
def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identifiable], file: IO,
1406+
def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identifiable], file: PathOrIO,
14011407
replace_existing: bool = False, ignore_existing: bool = False, failsafe: bool = True,
14021408
stripped: bool = False, decoder: Optional[Type[AASFromXmlDecoder]] = None,
14031409
**parser_kwargs: Any) -> Set[model.Identifier]:
@@ -1419,6 +1425,14 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif
14191425
This parameter is ignored if a decoder class is specified.
14201426
:param decoder: The decoder class used to decode the XML elements
14211427
:param parser_kwargs: Keyword arguments passed to the XMLParser constructor
1428+
:raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML
1429+
:raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document
1430+
:raises KeyError: **Non-failsafe**: Encountered a duplicate identifier
1431+
:raises KeyError: Encountered an identifier that already exists in the given ``object_store`` with both
1432+
``replace_existing`` and ``ignore_existing`` set to ``False``
1433+
:raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during
1434+
construction of the objects
1435+
:raises TypeError: **Non-failsafe**: Encountered an undefined top-level list (e.g. ``<aas:submodels1>``)
14221436
:return: A set of :class:`Identifiers <basyx.aas.model.base.Identifier>` that were added to object_store
14231437
"""
14241438
ret: Set[model.Identifier] = set()
@@ -1470,14 +1484,20 @@ def read_aas_xml_file_into(object_store: model.AbstractObjectStore[model.Identif
14701484
return ret
14711485

14721486

1473-
def read_aas_xml_file(file: IO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]:
1487+
def read_aas_xml_file(file: PathOrIO, **kwargs: Any) -> model.DictObjectStore[model.Identifiable]:
14741488
"""
14751489
A wrapper of :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`, that reads all objects in an
14761490
empty :class:`~basyx.aas.model.provider.DictObjectStore`. This function supports
14771491
the same keyword arguments as :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`.
14781492
14791493
:param file: A filename or file-like object to read the XML-serialized data from
14801494
:param kwargs: Keyword arguments passed to :meth:`~basyx.aas.adapter.xml.xml_deserialization.read_aas_xml_file_into`
1495+
:raises ~lxml.etree.XMLSyntaxError: **Non-failsafe**: If the given file(-handle) has invalid XML
1496+
:raises KeyError: **Non-failsafe**: If a required namespace has not been declared on the XML document
1497+
:raises KeyError: **Non-failsafe**: Encountered a duplicate identifier
1498+
:raises (~basyx.aas.model.base.AASConstraintViolation, KeyError, ValueError): **Non-failsafe**: Errors during
1499+
construction of the objects
1500+
:raises TypeError: **Non-failsafe**: Encountered an undefined top-level list (e.g. ``<aas:submodels1>``)
14811501
:return: A :class:`~basyx.aas.model.provider.DictObjectStore` containing all AAS objects from the XML file
14821502
"""
14831503
object_store: model.DictObjectStore[model.Identifiable] = model.DictObjectStore()

0 commit comments

Comments
 (0)