Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f831016
Merge pull request #424 from eclipse-basyx/develop
s-heppner Dec 5, 2025
7ad6a01
update constraint AASd-002 (#62)
Frosty2500 Dec 5, 2025
e0fc000
update RelationshipElemet first and second to be optional (#64)
Frosty2500 Dec 5, 2025
eb1b877
update data type lengths (#65)
Frosty2500 Dec 5, 2025
368a6b7
update entityType to be optional (#67)
Frosty2500 Dec 5, 2025
e85a0ed
add Role asset (#69)
Frosty2500 Dec 5, 2025
0fb526d
Fix Python 3.9 EOL (#433)
moritzsommer Dec 2, 2025
2f353a1
Merge branch 'develop' into update/Metamodel_V3.1.2
ZANDx1 Jan 26, 2026
97072fc
Remove AASd-090 (#71)
ZANDx1 Jan 27, 2026
f6bc2ba
Add Role to AssetKind (#73)
ZANDx1 Jan 27, 2026
56e4fe7
Update constraint documentation (#74)
ZANDx1 Jan 27, 2026
7430a4a
Update/metamodel v3.1.2: AASd-014 (#70)
ZANDx1 Feb 4, 2026
81278b9
Update metamodel element IDs and links to V3.1 (#63)
Frosty2500 Feb 4, 2026
bdbfdcc
Improve CI.yml definition (#453)
s-heppner Feb 9, 2026
738337e
Make contentType optional for Blob and File (#66)
Frosty2500 Feb 25, 2026
93c7fdc
Remove AASd-120 (#72)
ZANDx1 Feb 25, 2026
55333f6
release.yml: Implement CI for publishing server image on DockerHub (…
Frosty2500 Apr 20, 2026
c128a52
Remove multiplatform support from DockerHub publishing for now (#481)
s-heppner Apr 20, 2026
beed3da
Merge branch 'eclipse-basyx:main' into update/Metamodel_V3.1.2
moritzsommer Apr 21, 2026
ff37f04
Merge branch 'develop' into update/Metamodel_V3.1.2
s-heppner Apr 28, 2026
f667a98
CI: Temporarily disable compliance-tool tests (#76)
s-heppner Apr 28, 2026
2d02190
Merge branch 'develop' into update/Metamodel_V3.1.2
s-heppner May 5, 2026
c5cefae
Merge branch 'develop' into update/Metamodel_V3.1.2
s-heppner May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 30 additions & 29 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,36 +203,37 @@ jobs:
chmod +x ./etc/scripts/set_copyright_year.sh
./etc/scripts/set_copyright_year.sh --check

# Todo: reset when the compliance-tool was updated (#485)

compliance-tool-test:
# This job runs the unittests on the python versions specified down at the matrix
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.12"]
defaults:
run:
working-directory: ./compliance_tool

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Python dependencies
# install the local sdk in editable mode so it does not get overwritten
run: |
python -m pip install --upgrade pip
python -m pip install ../sdk
python -m pip install .[dev]
- name: Test with coverage + unittest
run: |
python -m coverage run --source=aas_compliance_tool -m unittest
- name: Report test coverage
if: ${{ always() }}
run: |
python -m coverage report -m
# compliance-tool-test:
# # This job runs the unittests on the python versions specified down at the matrix
# runs-on: ubuntu-latest
# strategy:
# matrix:
# python-version: ["3.10", "3.12"]
# defaults:
# run:
# working-directory: ./compliance_tool
#
# steps:
# - uses: actions/checkout@v4
# - name: Set up Python ${{ matrix.python-version }}
# uses: actions/setup-python@v5
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install Python dependencies
# # install the local sdk in editable mode so it does not get overwritten
# run: |
# python -m pip install --upgrade pip
# python -m pip install ../sdk
# python -m pip install .[dev]
# - name: Test with coverage + unittest
# run: |
# python -m coverage run --source=aas_compliance_tool -m unittest
# - name: Report test coverage
# if: ${{ always() }}
# run: |
# python -m coverage report -m

compliance-tool-static-analysis:
# This job runs static code analysis, namely pycodestyle and mypy
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ These are the implemented AAS specifications of the [current SDK release](https:

| Specification | Version |
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Part 1: Metamodel | [v3.0.1 (01001)](https://industrialdigitaltwin.org/wp-content/uploads/2024/06/IDTA-01001-3-0-1_SpecificationAssetAdministrationShell_Part1_Metamodel.pdf) |
| Part 1: Metamodel | [v3.1.2 (01001-3-0-1)](https://industrialdigitaltwin.io/aas-specifications/IDTA-01001/v3.1.2/index.html) |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| Part 1: Metamodel | [v3.1.2 (01001-3-0-1)](https://industrialdigitaltwin.io/aas-specifications/IDTA-01001/v3.1.2/index.html) |
| Part 1: Metamodel | [v3.1.2 (01001-3-1-2)](https://industrialdigitaltwin.io/aas-specifications/IDTA-01001/v3.1.2/index.html) |

Must be 3-1-2 acc. to DOI: https://doi.org/10.62628/IDTA.01001-3-1-2

| Schemata (JSONSchema, XSD) | [v3.0.8 (IDTA-01001-3-0-1_schemasV3.0.8)](https://github.com/admin-shell-io/aas-specs/releases/tag/IDTA-01001-3-0-1_schemasV3.0.8) |
| Part 2: API | [v3.1.1 (01002)](https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2025/08/IDTA-01002-3-1-1_AAS-Specification_Part2_API.pdf) |
| Part 3a: Data Specification IEC 61360 | [v3.1.1 (01003-a)](https://industrialdigitaltwin.org/wp-content/uploads/2025/08/IDTA-01003-a-3-1-1_AAS-Specification_Part3a_DataSpecification.pdf) |
Expand Down
3 changes: 2 additions & 1 deletion sdk/basyx/aas/adapter/_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
ASSET_KIND: Dict[model.AssetKind, str] = {
model.AssetKind.TYPE: 'Type',
model.AssetKind.INSTANCE: 'Instance',
model.AssetKind.NOT_APPLICABLE: 'NotApplicable'}
model.AssetKind.NOT_APPLICABLE: 'NotApplicable',
model.AssetKind.ROLE: 'Role'}

QUALIFIER_KIND: Dict[model.QualifierKind, str] = {
model.QualifierKind.CONCEPT_QUALIFIER: 'ConceptQualifier',
Expand Down
7 changes: 5 additions & 2 deletions sdk/basyx/aas/adapter/json/json_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,9 +528,12 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) ->
if 'specificAssetIds' in dct:
for desc_data in _get_ts(dct, "specificAssetIds", list):
specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId))

if 'entityType' in dct:
entity_type = ENTITY_TYPES_INVERSE[_get_ts(dct, 'entityType', str)]
else:
entity_type = None
ret = object_class(id_short=None,
entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)],
entity_type=entity_type,
global_asset_id=global_asset_id,
specific_asset_id=specific_asset_id)
cls._amend_abstract_attributes(ret, dct)
Expand Down
9 changes: 6 additions & 3 deletions sdk/basyx/aas/adapter/json/json_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,8 @@ def _blob_to_json(cls, obj: model.Blob) -> Dict[str, object]:
:return: dict with the serialized attributes of this object
"""
data = cls._abstract_classes_to_json(obj)
data['contentType'] = obj.content_type
if obj.content_type is not None:
data['contentType'] = obj.content_type
if obj.value is not None:
data['value'] = base64.b64encode(obj.value).decode()
return data
Expand All @@ -490,7 +491,8 @@ def _file_to_json(cls, obj: model.File) -> Dict[str, object]:
:return: dict with the serialized attributes of this object
"""
data = cls._abstract_classes_to_json(obj)
data['contentType'] = obj.content_type
if obj.content_type is not None:
data['contentType'] = obj.content_type
if obj.value is not None:
data['value'] = obj.value
return data
Expand Down Expand Up @@ -635,7 +637,8 @@ def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]:
data = cls._abstract_classes_to_json(obj)
if not cls.stripped and obj.statement:
data['statements'] = list(obj.statement)
data['entityType'] = _generic.ENTITY_TYPES[obj.entity_type]
if obj.entity_type:
data['entityType'] = _generic.ENTITY_TYPES[obj.entity_type]
if obj.global_asset_id:
data['globalAssetId'] = obj.global_asset_id
if obj.specific_asset_id:
Expand Down
8 changes: 6 additions & 2 deletions sdk/basyx/aas/adapter/xml/xml_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,10 +827,14 @@ def construct_entity(cls, element: etree._Element, object_class=model.Entity, **
for id in _child_construct_multiple(specific_asset_ids, NS_AAS + "specificAssetId",
cls.construct_specific_asset_id, cls.failsafe):
specific_asset_id.add(id)

entity_type_text = _get_text_or_none(element.find(NS_AAS + "entityType"))
if entity_type_text is not None:
entity_type = ENTITY_TYPES_INVERSE[entity_type_text]
else:
entity_type = None
entity = object_class(
id_short=None,
entity_type=_child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE),
entity_type=entity_type,
global_asset_id=_get_text_or_none(element.find(NS_AAS + "globalAssetId")),
specific_asset_id=specific_asset_id)

Expand Down
15 changes: 10 additions & 5 deletions sdk/basyx/aas/adapter/xml/xml_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,8 @@ def blob_to_xml(obj: model.Blob,
if obj.value is not None:
et_value.text = base64.b64encode(obj.value).decode()
et_blob.append(et_value)
et_blob.append(_generate_element(NS_AAS + "contentType", text=obj.content_type))
if obj.content_type is not None:
et_blob.append(_generate_element(NS_AAS + "contentType", text=obj.content_type))
return et_blob


Expand All @@ -644,7 +645,8 @@ def file_to_xml(obj: model.File,
et_file = abstract_classes_to_xml(tag, obj)
if obj.value:
et_file.append(_generate_element(NS_AAS + "value", text=obj.value))
et_file.append(_generate_element(NS_AAS + "contentType", text=obj.content_type))
if obj.content_type is not None:
et_file.append(_generate_element(NS_AAS + "contentType", text=obj.content_type))
return et_file


Expand Down Expand Up @@ -734,8 +736,10 @@ def relationship_element_to_xml(obj: model.RelationshipElement,
:return: Serialized :class:`~lxml.etree._Element` object
"""
et_relationship_element = abstract_classes_to_xml(tag, obj)
et_relationship_element.append(reference_to_xml(obj.first, NS_AAS+"first"))
et_relationship_element.append(reference_to_xml(obj.second, NS_AAS+"second"))
if obj.first is not None:
et_relationship_element.append(reference_to_xml(obj.first, NS_AAS+"first"))
if obj.second is not None:
et_relationship_element.append(reference_to_xml(obj.second, NS_AAS+"second"))
return et_relationship_element


Expand Down Expand Up @@ -823,7 +827,8 @@ def entity_to_xml(obj: model.Entity,
for statement in obj.statement:
et_statements.append(submodel_element_to_xml(statement))
et_entity.append(et_statements)
et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type]))
if obj.entity_type:
et_entity.append(_generate_element(NS_AAS + "entityType", text=_generic.ENTITY_TYPES[obj.entity_type]))
if obj.global_asset_id:
et_entity.append(_generate_element(NS_AAS + "globalAssetId", text=obj.global_asset_id))
if obj.specific_asset_id:
Expand Down
2 changes: 1 addition & 1 deletion sdk/basyx/aas/examples/data/example_aas.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
_embedded_data_specification_iec61360 = model.EmbeddedDataSpecification(
data_specification=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='https://admin-shell.io/DataSpecificationTemplates/'
'DataSpecificationIEC61360/3'),)),
'DataSpecificationIEC61360/3/1'),)),
data_specification_content=model.DataSpecificationIEC61360(preferred_name=model.PreferredNameTypeIEC61360({
'de': 'Test Specification',
'en-US': 'TestSpecification'
Expand Down
6 changes: 3 additions & 3 deletions sdk/basyx/aas/model/_string_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ def check(value: str, type_name: str, min_length: int = 0, max_length: Optional[


def check_content_type(value: str, type_name: str = "ContentType") -> None:
return check(value, type_name, 1, 100)
return check(value, type_name, 1, 128)


def check_identifier(value: str, type_name: str = "Identifier") -> None:
return check(value, type_name, 1, 2000)
return check(value, type_name, 1, 2048)


def check_label_type(value: str, type_name: str = "LabelType") -> None:
Expand All @@ -84,7 +84,7 @@ def check_name_type(value: str, type_name: str = "NameType") -> None:


def check_path_type(value: str, type_name: str = "PathType") -> None:
return check(value, type_name, 1, 2000)
return check(value, type_name, 1, 2048)


def check_qualifier_type(value: str, type_name: str = "QualifierType") -> None:
Expand Down
41 changes: 24 additions & 17 deletions sdk/basyx/aas/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ class ModellingKind(Enum):
@unique
class AssetKind(Enum):
"""
Enumeration for denoting whether an asset is a type asset or an instance asset or whether this kind of
Enumeration for denoting whether an asset is a type asset or an instance asset or role asset or whether this kind of
classification is not applicable.

.. note::
Expand All @@ -235,12 +235,14 @@ class AssetKind(Enum):

:cvar TYPE: Type asset
:cvar INSTANCE: Instance asset
:cvar NOT_APPLICABLE: Neither a type asset nor an instance asset
:cvar ROLE: Role asset
:cvar NOT_APPLICABLE: Neither a type asset nor an instance asset nor a role asset
"""

TYPE = 0
INSTANCE = 1
NOT_APPLICABLE = 2
ROLE = 3


class QualifierKind(Enum):
Expand Down Expand Up @@ -574,7 +576,7 @@ class HasExtension(Namespace, metaclass=abc.ABCMeta):

<<abstract>>

**Constraint AASd-077:** The name of an Extension within HasExtensions needs to be unique.
**Constraint AASd-077:** The name of an Extension within HasExtensions shall be unique.

:ivar namespace_element_sets: List of :class:`NamespaceSets <basyx.aas.model.base.NamespaceSet>`
:ivar extension: A :class:`~.NamespaceSet` of :class:`Extensions <.Extension>` of the element.
Expand Down Expand Up @@ -622,8 +624,9 @@ class Referable(HasExtension, metaclass=abc.ABCMeta):
**Constraint AASd-001:** In case of a referable element not being an identifiable element the
idShort is mandatory and used for referring to the element in its name space.

**Constraint AASd-002:** idShort shall only feature letters, digits, underscore (``_``); starting
mandatory with a letter.
**Constraint AASd-002:** idShort shall only feature letters, digits, underscore (``_``), hyphen (``-``);
starting mandatory with a letter and not ending with a hyphen.
I.e. ``^[a-zA-Z]|[a-zA-Z][a-zA-Z0-9_-]*[a-zA-Z0-9_]$``

**Constraint AASd-004:** Add parent in case of non-identifiable elements.

Expand Down Expand Up @@ -784,8 +787,9 @@ def validate_id_short(cls, id_short: NameType) -> None:
"""
Validates an id_short against Constraint AASd-002 and :class:`NameType` restrictions.

**Constraint AASd-002:** idShort of Referables shall only feature letters, digits, underscore (``_``); starting
mandatory with a letter. I.e. ``[a-zA-Z][a-zA-Z0-9_]+``
**Constraint AASd-002:** idShort shall only feature letters, digits, underscore (``_``), hyphen (``-``);
starting mandatory with a letter and not ending with a hyphen.
I.e. ``^[a-zA-Z]|[a-zA-Z][a-zA-Z0-9_-]*[a-zA-Z0-9_]$``

:param id_short: The id_short to validate
:raises ValueError: If the id_short doesn't comply to the constraints imposed by :class:`NameType`
Expand All @@ -794,25 +798,31 @@ def validate_id_short(cls, id_short: NameType) -> None:
"""
_string_constraints.check_name_type(id_short)
test_id_short: NameType = str(id_short)
if not re.fullmatch("[a-zA-Z0-9_]*", test_id_short):
if not re.fullmatch("[A-Za-z0-9_-]*", test_id_short):
raise AASConstraintViolation(
2,
"The id_short must contain only letters, digits and underscore"
"The id_short must contain only letters, digits underscore and hyphen"
)
if not test_id_short[0].isalpha():
raise AASConstraintViolation(
2,
"The id_short must start with a letter"
)
if test_id_short.endswith("-"):
raise AASConstraintViolation(
2,
"The id_short must not end with a hyphen"
)

category = property(_get_category, _set_category)

def _set_id_short(self, id_short: Optional[NameType]):
"""
Check the input string

**Constraint AASd-002:** idShort of Referables shall only feature letters, digits, underscore (``_``); starting
mandatory with a letter. I.e. ``[a-zA-Z][a-zA-Z0-9_]+``
**Constraint AASd-002:** idShort shall only feature letters, digits, underscore (``_``), hyphen (``-``);
starting mandatory with a letter and not ending with a hyphen.
I.e. ``^[a-zA-Z]|[a-zA-Z][a-zA-Z0-9_-]*[a-zA-Z0-9_]$``

**Constraint AASd-022:** idShort of non-identifiable referables shall be unique in its namespace
(case-sensitive)
Expand All @@ -834,9 +844,6 @@ def _set_id_short(self, id_short: Optional[NameType]):
raise AASConstraintViolation(117, f"id_short of {self!r} cannot be unset, since it is already "
f"contained in {self.parent!r}")
from .submodel import SubmodelElementList
if isinstance(self.parent, SubmodelElementList):
raise AASConstraintViolation(120, f"id_short of {self!r} cannot be set, because it is "
f"contained in a {self.parent!r}")
for set_ in self.parent.namespace_element_sets:
if set_.contains_id("id_short", id_short):
raise AASConstraintViolation(22, "Object with id_short '{}' is already present in the parent "
Expand Down Expand Up @@ -1197,7 +1204,7 @@ class DataSpecificationContent:
**Constraint AASc-3a-050:** If the ``Data_specification_IEC_61360`` is used
for an element, the value of ``HasDataSpecification.embedded_data_specifications``
shall contain the external reference to the IRI of the corresponding data specification
template ``https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3``
template ``https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/1``
"""
@abc.abstractmethod
def __init__(self):
Expand Down Expand Up @@ -1656,8 +1663,8 @@ class Qualifier(HasSemantics):
"""
A qualifier is a type-value pair that makes additional statements w.r.t. the value of the element.

**Constraint AASd-006:** If both, the value and the valueId of a Qualifier are present, the value needs
to be identical to the value of the referenced coded value in Qualifier/valueId.
**Constraint AASd-006:** If both, the value and the valueId of a Qualifier are present, the value shall
be identical to the value of the referenced coded value in Qualifier/valueId.

**Constraint AASd-020:** The value of Qualifier/value shall be consistent with the
data type as defined in Qualifier/valueType.
Expand Down
Loading
Loading