diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 939a9147..a8f22657 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index fa0512d9..05e0066e 100644 --- a/README.md +++ b/README.md @@ -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) | | 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) | diff --git a/sdk/basyx/aas/adapter/_generic.py b/sdk/basyx/aas/adapter/_generic.py index 65791526..aa4f9d69 100644 --- a/sdk/basyx/aas/adapter/_generic.py +++ b/sdk/basyx/aas/adapter/_generic.py @@ -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', diff --git a/sdk/basyx/aas/adapter/json/json_deserialization.py b/sdk/basyx/aas/adapter/json/json_deserialization.py index 0114504f..01ef253f 100644 --- a/sdk/basyx/aas/adapter/json/json_deserialization.py +++ b/sdk/basyx/aas/adapter/json/json_deserialization.py @@ -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) diff --git a/sdk/basyx/aas/adapter/json/json_serialization.py b/sdk/basyx/aas/adapter/json/json_serialization.py index 46c276aa..65fb0f8b 100644 --- a/sdk/basyx/aas/adapter/json/json_serialization.py +++ b/sdk/basyx/aas/adapter/json/json_serialization.py @@ -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 @@ -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 @@ -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: diff --git a/sdk/basyx/aas/adapter/xml/xml_deserialization.py b/sdk/basyx/aas/adapter/xml/xml_deserialization.py index d1376b9f..b36dddb9 100644 --- a/sdk/basyx/aas/adapter/xml/xml_deserialization.py +++ b/sdk/basyx/aas/adapter/xml/xml_deserialization.py @@ -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) diff --git a/sdk/basyx/aas/adapter/xml/xml_serialization.py b/sdk/basyx/aas/adapter/xml/xml_serialization.py index b74a993c..4b80e595 100644 --- a/sdk/basyx/aas/adapter/xml/xml_serialization.py +++ b/sdk/basyx/aas/adapter/xml/xml_serialization.py @@ -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 @@ -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 @@ -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 @@ -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: diff --git a/sdk/basyx/aas/examples/data/example_aas.py b/sdk/basyx/aas/examples/data/example_aas.py index d8b7e037..bb080756 100644 --- a/sdk/basyx/aas/examples/data/example_aas.py +++ b/sdk/basyx/aas/examples/data/example_aas.py @@ -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' diff --git a/sdk/basyx/aas/model/_string_constraints.py b/sdk/basyx/aas/model/_string_constraints.py index e382b825..41e86933 100644 --- a/sdk/basyx/aas/model/_string_constraints.py +++ b/sdk/basyx/aas/model/_string_constraints.py @@ -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: @@ -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: diff --git a/sdk/basyx/aas/model/base.py b/sdk/basyx/aas/model/base.py index ba631bd1..6c6eb25e 100644 --- a/sdk/basyx/aas/model/base.py +++ b/sdk/basyx/aas/model/base.py @@ -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:: @@ -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): @@ -574,7 +576,7 @@ class HasExtension(Namespace, metaclass=abc.ABCMeta): <> - **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 ` :ivar extension: A :class:`~.NamespaceSet` of :class:`Extensions <.Extension>` of the element. @@ -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. @@ -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` @@ -794,16 +798,21 @@ 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) @@ -811,8 +820,9 @@ 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) @@ -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 " @@ -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): @@ -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. diff --git a/sdk/basyx/aas/model/submodel.py b/sdk/basyx/aas/model/submodel.py index 733eaf58..62a4e5cb 100644 --- a/sdk/basyx/aas/model/submodel.py +++ b/sdk/basyx/aas/model/submodel.py @@ -150,13 +150,6 @@ def __init__(self, self.embedded_data_specifications: List[base.EmbeddedDataSpecification] = list(embedded_data_specifications) -ALLOWED_DATA_ELEMENT_CATEGORIES: Set[str] = { - "CONSTANT", - "PARAMETER", - "VARIABLE" -} - - class DataElement(SubmodelElement, metaclass=abc.ABCMeta): """ A data element is a :class:`~.SubmodelElement` that is not further composed out of other @@ -203,28 +196,13 @@ def __init__(self, super().__init__(id_short, display_name, category, description, parent, semantic_id, qualifier, extension, supplemental_semantic_id, embedded_data_specifications) - def _set_category(self, category: Optional[str]): - if category == "": - raise base.AASConstraintViolation(100, - "category is not allowed to be an empty string") - if category is None: - self._category = None - else: - if category not in ALLOWED_DATA_ELEMENT_CATEGORIES: - if not (isinstance(self, File) or isinstance(self, Blob)): - raise base.AASConstraintViolation( - 90, - "DataElement.category must be one of the following: " + - ", ".join(ALLOWED_DATA_ELEMENT_CATEGORIES)) - self._category = category - class Property(DataElement): """ A property is a :class:`DataElement` that has a single value. **Constraint AASd-007:** 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. + the value shall be identical to the value of the referenced coded value in Qualifier/valueId. :ivar id_short: Identifying string of the element within its name space. (inherited from :class:`~basyx.aas.model.base.Referable`) @@ -474,7 +452,7 @@ class Blob(DataElement): def __init__(self, id_short: Optional[base.NameType], - content_type: base.ContentType, + content_type: Optional[base.ContentType] = None, value: Optional[base.BlobType] = None, display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, @@ -492,7 +470,7 @@ def __init__(self, super().__init__(id_short, display_name, category, description, parent, semantic_id, qualifier, extension, supplemental_semantic_id, embedded_data_specifications) self.value: Optional[base.BlobType] = value - self.content_type: base.ContentType = content_type + self.content_type: Optional[base.ContentType] = content_type @_string_constraints.constrain_content_type("content_type") @@ -528,7 +506,7 @@ class File(DataElement): def __init__(self, id_short: Optional[base.NameType], - content_type: base.ContentType, + content_type: Optional[base.ContentType] = None, value: Optional[base.PathType] = None, display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, @@ -546,7 +524,7 @@ def __init__(self, super().__init__(id_short, display_name, category, description, parent, semantic_id, qualifier, extension, supplemental_semantic_id, embedded_data_specifications) self.value: Optional[base.PathType] = value - self.content_type: base.ContentType = content_type + self.content_type: Optional[base.ContentType] = content_type class ReferenceElement(DataElement): @@ -750,9 +728,6 @@ def __init__(self, raise def _generate_id_short(self, new: _SE) -> None: - if new.id_short is not None: - raise base.AASConstraintViolation(120, "Objects with an id_short may not be added to a " - f"SubmodelElementList, got {new!r} with id_short={new.id_short}") # Generate a unique id_short when a SubmodelElement is added, because children of a SubmodelElementList may not # have an id_short. The alternative would be making SubmodelElementList a special kind of base.Namespace without # a unique attribute for child-elements (which contradicts the definition of a Namespace). @@ -870,8 +845,8 @@ class RelationshipElement(SubmodelElement): def __init__(self, id_short: Optional[base.NameType], - first: base.Reference, - second: base.Reference, + first: Optional[base.Reference] = None, + second: Optional[base.Reference] = None, display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, description: Optional[base.MultiLanguageTextType] = None, @@ -887,8 +862,8 @@ def __init__(self, super().__init__(id_short, display_name, category, description, parent, semantic_id, qualifier, extension, supplemental_semantic_id, embedded_data_specifications) - self.first: base.Reference = first - self.second: base.Reference = second + self.first: Optional[base.Reference] = first + self.second: Optional[base.Reference] = second class AnnotatedRelationshipElement(RelationshipElement, base.UniqueIdShortNamespace): @@ -1063,8 +1038,8 @@ class Entity(SubmodelElement, base.UniqueIdShortNamespace): """ An entity is a :class:`~.SubmodelElement` that is used to model entities - **Constraint AASd-014:** global_asset_id or specific_asset_id must be set if ``entity_type`` is set to - :attr:`~basyx.aas.model.base.EntityType.SELF_MANAGED_ENTITY`. They must be empty otherwise. + **Constraint AASd-014:** Either the attribute ``globalAssetId`` or ``specificAssetId`` of an ``Entity`` + must be set if ``Entity/entityType`` is set to ``SelfManagedEntity``. :ivar id_short: Identifying string of the element within its name space. (inherited from :class:`~basyx.aas.model.base.Referable`) @@ -1098,7 +1073,7 @@ class Entity(SubmodelElement, base.UniqueIdShortNamespace): def __init__(self, id_short: Optional[base.NameType], - entity_type: base.EntityType, + entity_type: Optional[base.EntityType], statement: Iterable[SubmodelElement] = (), global_asset_id: Optional[base.Identifier] = None, specific_asset_id: Iterable[base.SpecificAssetId] = (), @@ -1118,7 +1093,7 @@ def __init__(self, supplemental_semantic_id, embedded_data_specifications) self.statement = base.NamespaceSet(self, [("id_short", True)], statement) # assign private attributes, bypassing setters, as constraints will be checked below - self._entity_type: base.EntityType = entity_type + self._entity_type: Optional[base.EntityType] = entity_type self._global_asset_id: Optional[base.Identifier] = global_asset_id self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = base.ConstrainedList( specific_asset_id, @@ -1130,11 +1105,11 @@ def __init__(self, self._validate_aasd_014(entity_type, global_asset_id, bool(specific_asset_id)) @property - def entity_type(self) -> base.EntityType: + def entity_type(self) -> Optional[base.EntityType]: return self._entity_type @entity_type.setter - def entity_type(self, entity_type: base.EntityType) -> None: + def entity_type(self, entity_type: Optional[base.EntityType]) -> None: self._validate_aasd_014(entity_type, self.global_asset_id, bool(self.specific_asset_id)) self._entity_type = entity_type @@ -1177,9 +1152,11 @@ def _validate_global_asset_id(global_asset_id: Optional[base.Identifier]) -> Non _string_constraints.check_identifier(global_asset_id) @staticmethod - def _validate_aasd_014(entity_type: base.EntityType, + def _validate_aasd_014(entity_type: Optional[base.EntityType], global_asset_id: Optional[base.Identifier], specific_asset_id_nonempty: bool) -> None: + if entity_type is None: + return if entity_type == base.EntityType.SELF_MANAGED_ENTITY and global_asset_id is None \ and not specific_asset_id_nonempty: raise base.AASConstraintViolation( diff --git a/sdk/docs/source/constraints.rst b/sdk/docs/source/constraints.rst index 6b037b08..24157c56 100644 --- a/sdk/docs/source/constraints.rst +++ b/sdk/docs/source/constraints.rst @@ -14,29 +14,27 @@ The status information means the following: In most cases, if a constraint violation is detected, an :class:`~basyx.aas.model.base.AASConstraintViolation` will be raised -.. |aasd002| replace:: ``idShort`` of ``Referable`` s shall only feature letters, digits, underscore (``_``); starting mandatory with a letter, i.e. ``[a-zA-Z][a-zA-Z0-9_]*``. +.. |aasd002| replace:: ``idShort`` of ``Referable`` s 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_]$``. .. |aasd005| replace:: If ``AdministrativeInformation/version`` is not specified, ``AdministrativeInformation/revision`` shall also be unspecified. This means that a revision requires a version. If there is no version, there is no revision. Revision is optional. -.. |aasd006| replace:: 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``. -.. |aasd007| replace:: If both the ``Property/value`` and the ``Property/valueId`` are present, the value of ``Property/value`` needs to be identical to the value of the referenced coded value in ``Property/valueId``. +.. |aasd006| replace:: 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``. +.. |aasd007| replace:: If both the ``Property/value`` and the ``Property/valueId`` are present, the value of ``Property/value`` shall be identical to the value of the referenced coded value in ``Property/valueId``. .. |aasd012| replace:: if both the ``MultiLanguageProperty/value`` and the ``MultiLanguageProperty/valueId`` are present, the meaning must be the same for each string in a specific language, as specified in ``MultiLanguageProperty/valueId``. -.. |aasd014| replace:: Either the attribute ``globalAssetId`` or ``specificAssetId`` of an ``Entity`` must be set if ``Entity/entityType`` is set to ``SelfManagedEntity``. Otherwise, they do not exist. +.. |aasd014| replace:: Either the attribute ``globalAssetId`` or ``specificAssetId`` of an ``Entity`` must be set if ``Entity/entityType`` is set to ``SelfManagedEntity``. .. |aasd020| replace:: The value of ``Qualifier/value`` shall be consistent with the data type as defined in ``Qualifier/valueType``. -.. |aasd021| replace:: Every qualifiable can only have one qualifier with the same ``Qualifier/type``. +.. |aasd021| replace:: Every qualifiable shall only have one qualifier with the same ``Qualifier/type``. .. |aasd022| replace:: ``idShort`` of non-identifiable referables within the same name space shall be unique (case-sensitive). -.. |aasd077| replace:: The name of an extension (``Extension/name``) within ``HasExtensions`` needs to be unique. +.. |aasd077| replace:: The name of an extension (``Extension/name``) within ``HasExtensions`` shall be unique. .. |aasd080| replace:: In case ``Key/type`` == ``GlobalReference`` ``idType`` shall not be any LocalKeyType (``IdShort, FragmentId``). .. |aasd081| replace:: In case ``Key/type`` == ``AssetAdministrationShell`` ``Key/idType`` shall not be any LocalKeyType (``IdShort``, ``FragmentId``). -.. |aasd090| replace:: for data elements, ``category`` (inherited by ``Referable``) shall be one of the following values: CONSTANT, PARAMETER or VARIABLE. Default: VARIABLE .. |aasd107| replace:: If a first level child element in a ``SubmodelElementList`` has a semanticId, it shall be identical to ``SubmodelElementList/semanticIdListElement``. .. |aasd108| replace:: All first level child elements in a ``SubmodelElementList`` shall have the same submodel element type as specified in ``SubmodelElementList/typeValueListElement``. .. |aasd109| replace:: If ``SubmodelElementList/typeValueListElement`` is equal to ``Property`` or ``Range,`` ``SubmodelElementList/valueTypeListElement`` shall be set and all first level child elements in the ``SubmodelElementList`` shall have the value type as specified in ``SubmodelElementList/valueTypeListElement``. .. |aasd114| replace:: If two first level child elements in a ``SubmodelElementList`` have a ``semanticId``, they shall be identical. .. |aasd115| replace:: If a first level child element in a ``SubmodelElementList`` does not specify a ``semanticId``, the value is assumed to be identical to ``SubmodelElementList/semanticIdListElement``. -.. |aasd116| replace:: ``globalAssetId`` (case-insensitive) is a reserved key. If used as value for ``SpecificAssetId/name,`` ``SpecificAssetId/value`` shall be identical to ``AssetInformation/globalAssetId``. +.. |aasd116| replace:: ``globalAssetId`` (case-insensitive) is a reserved key for ``SpecificAssetId/name`` with the semantics as defined in ``:attr:`basyx.aas.model.aas.AssetInformation.global_asset_id``. .. |aasd117| replace:: ``idShort`` of non-identifiable ``Referables`` not being a direct child of a ``SubmodelElementList`` shall be specified. .. |aasd118| replace:: If a supplemental semantic ID (``HasSemantics/supplementalSemanticId``) is defined, there shall also be a main semantic ID (``HasSemantics/semanticId``). .. |aasd119| replace:: If any ``Qualifier/kind`` value of a ``Qualifiable/qualifier`` is equal to ``TemplateQualifier`` and the qualified element inherits from ``HasKind``, the qualified element shall be of kind ``Template`` (``HasKind/kind = Template``). -.. |aasd120| replace:: ``idShort`` of submodel elements being a direct child of a ``SubmodelElementList`` shall not be specified. .. |aasd121| replace:: For ``References``, the value of ``Key/type`` of the first ``key`` of ``Reference/keys`` shall be one of ``GloballyIdentifiables``. .. |aasd122| replace:: For external references, i.e. ``References`` with ``Reference/type = ExternalReference``, the value of ``Key/type`` of the first key of ``Reference/keys`` shall be one of ``GenericGloballyIdentifiables``. .. |aasd123| replace:: For model references, i.e. ``References`` with ``Reference/type = ModellReference``, the value of ``Key/type`` of the first ``key`` of ``Reference/keys`` shall be one of ``AasIdentifiables``. @@ -76,7 +74,6 @@ an :class:`~basyx.aas.model.base.AASConstraintViolation` will be raised AASd-077, |aasd077|, ✅, AASd-080, |aasd080|, ✅, AASd-081, |aasd081|, ✅, - AASd-090, |aasd090|, ✅, AASd-107, |aasd107|, ✅, AASd-108, |aasd108|, ✅, AASd-109, |aasd109|, ✅, @@ -86,7 +83,6 @@ an :class:`~basyx.aas.model.base.AASConstraintViolation` will be raised AASd-117, |aasd117|, ✅, AASd-118, |aasd118|, ✅, AASd-119, |aasd119|, ❌, See `#119 `__ - AASd-120, |aasd120|, ✅, AASd-121, |aasd121|, ✅, AASd-122, |aasd122|, ✅, AASd-123, |aasd123|, ✅, diff --git a/sdk/test/model/test_base.py b/sdk/test/model/test_base.py index e300cc1f..c5b0429d 100644 --- a/sdk/test/model/test_base.py +++ b/sdk/test/model/test_base.py @@ -100,10 +100,15 @@ def test_id_short_constraint_aasd_002(self): test_object = ExampleReferable() test_object.id_short = "Test" self.assertEqual("Test", test_object.id_short) - test_object.id_short = "asdASd123_" - self.assertEqual("asdASd123_", test_object.id_short) + test_object.id_short = "asdASd-123_" + self.assertEqual("asdASd-123_", test_object.id_short) test_object.id_short = "AAs12_" self.assertEqual("AAs12_", test_object.id_short) + test_object.id_short = "A" + self.assertEqual("A", test_object.id_short) + with self.assertRaises(model.AASConstraintViolation) as cm: + test_object.id_short = "Test-" + self.assertEqual("The id_short must not end with a hyphen (Constraint AASd-002)", str(cm.exception)) with self.assertRaises(model.AASConstraintViolation) as cm: test_object.id_short = "98sdsfdAS" self.assertEqual("The id_short must start with a letter (Constraint AASd-002)", str(cm.exception)) @@ -113,12 +118,12 @@ def test_id_short_constraint_aasd_002(self): with self.assertRaises(model.AASConstraintViolation) as cm: test_object.id_short = "asdlujSAD8348@S" self.assertEqual( - "The id_short must contain only letters, digits and underscore (Constraint AASd-002)", + "The id_short must contain only letters, digits underscore and hyphen (Constraint AASd-002)", str(cm.exception)) with self.assertRaises(model.AASConstraintViolation) as cm: test_object.id_short = "abc\n" self.assertEqual( - "The id_short must contain only letters, digits and underscore (Constraint AASd-002)", + "The id_short must contain only letters, digits underscore and hyphen (Constraint AASd-002)", str(cm.exception)) def test_representation(self): diff --git a/sdk/test/model/test_string_constraints.py b/sdk/test/model/test_string_constraints.py index 55d5789f..88214e77 100644 --- a/sdk/test/model/test_string_constraints.py +++ b/sdk/test/model/test_string_constraints.py @@ -17,11 +17,11 @@ def test_identifier(self) -> None: with self.assertRaises(ValueError) as cm: _string_constraints.check_identifier(identifier) self.assertEqual("Identifier has a minimum length of 1! (length: 0)", cm.exception.args[0]) - identifier = "a" * 2001 + identifier = "a" * 2049 with self.assertRaises(ValueError) as cm: _string_constraints.check_identifier(identifier) - self.assertEqual("Identifier has a maximum length of 2000! (length: 2001)", cm.exception.args[0]) - identifier = "a" * 2000 + self.assertEqual("Identifier has a maximum length of 2048! (length: 2049)", cm.exception.args[0]) + identifier = "a" * 2048 _string_constraints.check_identifier(identifier) def test_version_type(self) -> None: @@ -73,8 +73,8 @@ def test_path_type_decoration(self) -> None: self.assertEqual("PathType has a minimum length of 1! (length: 0)", cm.exception.args[0]) dc = self.DummyClass("a") with self.assertRaises(ValueError) as cm: - dc.some_attr = "a" * 2001 - self.assertEqual("PathType has a maximum length of 2000! (length: 2001)", cm.exception.args[0]) + dc.some_attr = "a" * 2049 + self.assertEqual("PathType has a maximum length of 2048! (length: 2049)", cm.exception.args[0]) self.assertEqual(dc.some_attr, "a") def test_ignore_none_values(self) -> None: diff --git a/sdk/test/model/test_submodel.py b/sdk/test/model/test_submodel.py index 603c42bd..b5ee0d7d 100644 --- a/sdk/test/model/test_submodel.py +++ b/sdk/test/model/test_submodel.py @@ -234,19 +234,6 @@ def test_constraints(self): mlp2.semantic_id = semantic_id1 model.SubmodelElementList("test_list", model.MultiLanguageProperty, [mlp1, mlp2]) - # AASd-120 - mlp = model.MultiLanguageProperty("mlp") - with self.assertRaises(model.AASConstraintViolation) as cm: - model.SubmodelElementList("test_list", model.MultiLanguageProperty, [mlp]) - self.assertEqual("Objects with an id_short may not be added to a SubmodelElementList, got " - "MultiLanguageProperty[mlp] with id_short=mlp (Constraint AASd-120)", str(cm.exception)) - mlp.id_short = None - model.SubmodelElementList("test_list", model.MultiLanguageProperty, [mlp]) - with self.assertRaises(model.AASConstraintViolation) as cm: - mlp.id_short = "mlp" - self.assertEqual("id_short of MultiLanguageProperty[test_list[0]] cannot be set, because it is " - "contained in a SubmodelElementList[test_list] (Constraint AASd-120)", str(cm.exception)) - def test_aasd_108_add_set(self): prop = model.Property(None, model.datatypes.Int) mlp1 = model.MultiLanguageProperty(None)