Skip to content

Commit 2f353a1

Browse files
committed
Merge branch 'develop' into update/Metamodel_V3.1.2
Syncing with develop (copyright mismatch).
2 parents 0fb526d + a081c0c commit 2f353a1

File tree

6 files changed

+265
-77
lines changed

6 files changed

+265
-77
lines changed

sdk/basyx/aas/adapter/aasx.py

Lines changed: 78 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ def _read_aas_part_into(self, part_name: str,
231231
read_identifiables.add(obj.id)
232232
if isinstance(obj, model.Submodel):
233233
self._collect_supplementary_files(part_name, obj, file_store)
234+
elif isinstance(obj, model.AssetAdministrationShell):
235+
self._collect_supplementary_files(part_name, obj, file_store)
234236

235237
def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore:
236238
"""
@@ -261,33 +263,59 @@ def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore:
261263
raise ValueError(error_message)
262264
return model.DictObjectStore()
263265

264-
def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel,
266+
def _collect_supplementary_files(self, part_name: str,
267+
root_element: Union[model.AssetAdministrationShell, model.Submodel],
265268
file_store: "AbstractSupplementaryFileContainer") -> None:
266269
"""
267-
Helper function to search File objects within a single parsed Submodel, extract the referenced supplementary
268-
files and update the File object's values with the absolute path.
270+
Helper function to search File objects within a single parsed AssetAdministrationShell or Submodel.
271+
Resolve their absolute paths, and update the corresponding File/Thumbnail objects with the absolute path.
269272
270-
:param part_name: The OPC part name of the part the Submodel has been parsed from. This is used to resolve
273+
:param part_name: The OPC part name of the part the root_element has been parsed from. This is used to resolve
271274
relative file paths.
272-
:param submodel: The Submodel to process
275+
:param root_element: The AssetAdministrationShell or Submodel to process
273276
:param file_store: The SupplementaryFileContainer to add the extracted supplementary files to
274277
"""
275-
for element in traversal.walk_submodel(submodel):
276-
if isinstance(element, model.File):
277-
if element.value is None:
278-
continue
279-
# Only absolute-path references and relative-path URI references (see RFC 3986, sec. 4.2) are considered
280-
# to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute
281-
# URIs and network-path references)
282-
if element.value.startswith('//') or ':' in element.value.split('/')[0]:
283-
logger.info(f"Skipping supplementary file {element.value}, since it seems to be an absolute URI or "
284-
f"network-path URI reference")
285-
continue
286-
absolute_name = pyecma376_2.package_model.part_realpath(element.value, part_name)
287-
logger.debug(f"Reading supplementary file {absolute_name} from AASX package ...")
288-
with self.reader.open_part(absolute_name) as p:
289-
final_name = file_store.add_file(absolute_name, p, self.reader.get_content_type(absolute_name))
290-
element.value = final_name
278+
if isinstance(root_element, model.AssetAdministrationShell):
279+
if (root_element.asset_information.default_thumbnail and
280+
root_element.asset_information.default_thumbnail.path):
281+
file_name = self._add_supplementary_file(part_name,
282+
root_element.asset_information.default_thumbnail.path,
283+
file_store)
284+
if file_name:
285+
root_element.asset_information.default_thumbnail.path = file_name
286+
if isinstance(root_element, model.Submodel):
287+
for element in traversal.walk_submodel(root_element):
288+
if isinstance(element, model.File):
289+
if element.value is None:
290+
continue
291+
final_name = self._add_supplementary_file(part_name, element.value, file_store)
292+
if final_name:
293+
element.value = final_name
294+
295+
def _add_supplementary_file(self, part_name: str, file_path: str,
296+
file_store: "AbstractSupplementaryFileContainer") -> Optional[str]:
297+
"""
298+
Helper function to extract a single referenced supplementary file
299+
and return the absolute path within the AASX package.
300+
301+
:param part_name: The OPC part name of the part the root_element has been parsed from. This is used to resolve
302+
relative file paths.
303+
:param file_path: The file path or URI reference of the supplementary file to be extracted
304+
:param file_store: The SupplementaryFileContainer to add the extracted supplementary files to
305+
:return: The stored file name as returned by *file_store*, or ``None`` if the reference was skipped.
306+
"""
307+
# Only absolute-path references and relative-path URI references (see RFC 3986, sec. 4.2) are considered
308+
# to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute
309+
# URIs and network-path references)
310+
if file_path.startswith('//') or ':' in file_path.split('/')[0]:
311+
logger.info(f"Skipping supplementary file {file_path}, since it seems to be an absolute URI or "
312+
f"network-path URI reference")
313+
return None
314+
absolute_name = pyecma376_2.package_model.part_realpath(file_path, part_name)
315+
logger.debug(f"Reading supplementary file {absolute_name} from AASX package ...")
316+
with self.reader.open_part(absolute_name) as p:
317+
final_name = file_store.add_file(absolute_name, p, self.reader.get_content_type(absolute_name))
318+
return final_name
291319

292320

293321
class AASXWriter:
@@ -541,7 +569,8 @@ def write_all_aas_objects(self,
541569
contained objects into an ``aas_env`` part in the AASX package. If the ObjectStore includes
542570
:class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by
543571
:class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store``
544-
and added to the AASX package.
572+
and added to the AASX package. If the ObjectStore contains a thumbnail referenced by
573+
``default_thumbnail`` in :class:`~basyx.aas.model.aas.AssetInformation`, it is also added to the AASX package.
545574
546575
.. attention::
547576
@@ -563,17 +592,24 @@ def write_all_aas_objects(self,
563592
logger.debug(f"Writing AASX part {part_name} with AAS objects ...")
564593
supplementary_files: List[str] = []
565594

595+
def _collect_supplementary_file(file_name: str) -> None:
596+
# Skip File objects with empty value URI references that are considered to be no local file
597+
# (absolute URIs or network-path URI references)
598+
if file_name is None or file_name.startswith('//') or ':' in file_name.split('/')[0]:
599+
return
600+
supplementary_files.append(file_name)
601+
566602
# Retrieve objects and scan for referenced supplementary files
567603
for the_object in objects:
604+
if isinstance(the_object, model.AssetAdministrationShell):
605+
if (the_object.asset_information.default_thumbnail and
606+
the_object.asset_information.default_thumbnail.path):
607+
_collect_supplementary_file(the_object.asset_information.default_thumbnail.path)
568608
if isinstance(the_object, model.Submodel):
569609
for element in traversal.walk_submodel(the_object):
570610
if isinstance(element, model.File):
571-
file_name = element.value
572-
# Skip File objects with empty value URI references that are considered to be no local file
573-
# (absolute URIs or network-path URI references)
574-
if file_name is None or file_name.startswith('//') or ':' in file_name.split('/')[0]:
575-
continue
576-
supplementary_files.append(file_name)
611+
if element.value:
612+
_collect_supplementary_file(element.value)
577613

578614
# Add aas-spec relationship
579615
if not split_part:
@@ -824,15 +860,25 @@ def add_file(self, name: str, file: IO[bytes], content_type: str) -> str:
824860
if hash not in self._store:
825861
self._store[hash] = data
826862
self._store_refcount[hash] = 0
827-
name_map_data = (hash, content_type)
863+
return self._assign_unique_name(name, hash, content_type)
864+
865+
def rename_file(self, old_name: str, new_name: str) -> str:
866+
if old_name not in self._name_map:
867+
raise KeyError(f"File with name {old_name} not found in SupplementaryFileContainer.")
868+
if new_name == old_name:
869+
return new_name
870+
file_hash, file_content_type = self._name_map[old_name]
871+
del self._name_map[old_name]
872+
return self._assign_unique_name(new_name, file_hash, file_content_type)
873+
874+
def _assign_unique_name(self, name: str, sha: bytes, content_type: str) -> str:
828875
new_name = name
829876
i = 1
830877
while True:
831878
if new_name not in self._name_map:
832-
self._name_map[new_name] = name_map_data
833-
self._store_refcount[hash] += 1
879+
self._name_map[new_name] = (sha, content_type)
834880
return new_name
835-
elif self._name_map[new_name] == name_map_data:
881+
elif self._name_map[new_name] == (sha, content_type):
836882
return new_name
837883
new_name = self._append_counter(name, i)
838884
i += 1

sdk/basyx/aas/model/base.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2025 the Eclipse BaSyx Authors
1+
# Copyright (c) 2026 the Eclipse BaSyx Authors
22
#
33
# This program and the accompanying materials are made available under the terms of the MIT License, available in
44
# the LICENSE file of this project.
@@ -293,7 +293,8 @@ class LangStringSet(MutableMapping[str, str]):
293293
"""
294294
def __init__(self, dict_: Dict[str, str]):
295295
self._dict: Dict[str, str] = {}
296-
296+
if not isinstance(dict_, dict):
297+
raise TypeError(f"A {self.__class__.__name__} must be initialized with a dict!, got {type(dict_)}")
297298
if len(dict_) < 1:
298299
raise ValueError(f"A {self.__class__.__name__} must not be empty!")
299300
for ltag in dict_:
@@ -617,9 +618,9 @@ class Referable(HasExtension, metaclass=abc.ABCMeta):
617618
def __init__(self):
618619
super().__init__()
619620
self._id_short: Optional[NameType] = None
620-
self.display_name: Optional[MultiLanguageNameType] = dict()
621+
self._display_name: Optional[MultiLanguageNameType] = None
621622
self._category: Optional[NameType] = None
622-
self.description: Optional[MultiLanguageTextType] = dict()
623+
self._description: Optional[MultiLanguageTextType] = None
623624
# We use a Python reference to the parent Namespace instead of a Reference Object, as specified. This allows
624625
# simpler and faster navigation/checks and it has no effect in the serialized data formats anyway.
625626
self.parent: Optional[UniqueIdShortNamespace] = None
@@ -837,6 +838,28 @@ def _set_id_short(self, id_short: Optional[NameType]):
837838
# Redundant to the line above. However, this way, we make sure that we really update the _id_short
838839
self._id_short = id_short
839840

841+
@property
842+
def display_name(self) -> Optional[MultiLanguageNameType]:
843+
"""Display name of the element (MultiLanguageNameType)."""
844+
return self._display_name
845+
846+
@display_name.setter
847+
def display_name(self, value: Union[MultiLanguageNameType, dict, None]) -> None:
848+
if value is not None and not isinstance(value, MultiLanguageNameType):
849+
value = MultiLanguageNameType(value)
850+
self._display_name = value
851+
852+
@property
853+
def description(self) -> Optional[MultiLanguageTextType]:
854+
"""Description of the element (MultiLanguageTextType)."""
855+
return self._description
856+
857+
@description.setter
858+
def description(self, value: Union[MultiLanguageTextType, dict, None]) -> None:
859+
if value is not None and not isinstance(value, MultiLanguageTextType):
860+
value = MultiLanguageTextType(value)
861+
self._description = value
862+
840863
def update_from(self, other: "Referable"):
841864
"""
842865
Internal function to update the object's attributes from a different version of the exact same object.

sdk/basyx/aas/model/submodel.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2025 the Eclipse BaSyx Authors
1+
# Copyright (c) 2026 the Eclipse BaSyx Authors
22
#
33
# This program and the accompanying materials are made available under the terms of the MIT License, available in
44
# the LICENSE file of this project.
@@ -344,6 +344,16 @@ def __init__(self,
344344
self.value: Optional[base.MultiLanguageTextType] = value
345345
self.value_id: Optional[base.Reference] = value_id
346346

347+
@property
348+
def value(self) -> Optional[base.MultiLanguageTextType]:
349+
return self._value
350+
351+
@value.setter
352+
def value(self, value: Union[base.MultiLanguageTextType, dict, None]) -> None:
353+
if value is not None and not isinstance(value, base.MultiLanguageTextType):
354+
value = base.MultiLanguageTextType(value)
355+
self._value = value
356+
347357

348358
class Range(DataElement):
349359
"""

0 commit comments

Comments
 (0)