From 77e0f862ea0d148b67d28571b71303b93ef9e4a2 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 27 Oct 2023 21:19:24 +0200 Subject: [PATCH 01/25] Fix Entity.specificAssetIds in schemas --- basyx/aas/adapter/json/aasJSONSchema.json | 8 ++++++-- basyx/aas/adapter/xml/AAS.xsd | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/json/aasJSONSchema.json b/basyx/aas/adapter/json/aasJSONSchema.json index feb41a2af..9aacb9098 100644 --- a/basyx/aas/adapter/json/aasJSONSchema.json +++ b/basyx/aas/adapter/json/aasJSONSchema.json @@ -444,8 +444,12 @@ "maxLength": 2000, "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" }, - "specificAssetId": { - "$ref": "#/definitions/SpecificAssetId" + "specificAssetIds": { + "type": "array", + "items": { + "$ref": "#/definitions/SpecificAssetId" + }, + "minItems": 1 } }, "required": [ diff --git a/basyx/aas/adapter/xml/AAS.xsd b/basyx/aas/adapter/xml/AAS.xsd index 1c9a6e209..76c1fd544 100644 --- a/basyx/aas/adapter/xml/AAS.xsd +++ b/basyx/aas/adapter/xml/AAS.xsd @@ -284,7 +284,13 @@ - + + + + + + + From e1fffece3712555435285572af08f098c450a477 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 27 Oct 2023 21:31:31 +0200 Subject: [PATCH 02/25] - Set `Entity.specific_asset_id` as `Iterable[SpecificAssetId]`, because the spec has changed - Add check of constraint AASd-014 for Entity, see https://rwth-iat.github.io/aas-specs/AASiD/AASiD_1_Metamodel/index.html#Entity - Add check of constraint AASd-131 for AssetInformation, see https://rwth-iat.github.io/aas-specs/AASiD/AASiD_1_Metamodel/index.html#AssetInformation - Refactor de-/serialization of Entity - Refactor deserialization of AssetInformation because of check of constraint AASd-131 --- .../aas/adapter/json/json_deserialization.py | 19 +++++--- basyx/aas/adapter/json/json_serialization.py | 2 +- basyx/aas/adapter/xml/xml_deserialization.py | 28 ++++++----- basyx/aas/adapter/xml/xml_serialization.py | 5 +- basyx/aas/model/aas.py | 20 ++++++-- basyx/aas/model/submodel.py | 46 +++++++++++++------ 6 files changed, 83 insertions(+), 37 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 636531564..d2618735e 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -417,13 +417,19 @@ def _construct_value_reference_pair(cls, dct: Dict[str, object], @classmethod def _construct_asset_information(cls, dct: Dict[str, object], object_class=model.AssetInformation)\ -> model.AssetInformation: - ret = object_class(asset_kind=ASSET_KIND_INVERSE[_get_ts(dct, 'assetKind', str)]) - cls._amend_abstract_attributes(ret, dct) + global_asset_id = None if 'globalAssetId' in dct: - ret.global_asset_id = _get_ts(dct, 'globalAssetId', str) + global_asset_id = _get_ts(dct, 'globalAssetId', str) + specific_asset_id = set() if 'specificAssetIds' in dct: for desc_data in _get_ts(dct, "specificAssetIds", list): - ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) + specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) + + ret = object_class(asset_kind=ASSET_KIND_INVERSE[_get_ts(dct, 'assetKind', str)], + global_asset_id=global_asset_id, + specific_asset_id=specific_asset_id) + cls._amend_abstract_attributes(ret, dct) + if 'assetType' in dct: ret.asset_type = _get_ts(dct, 'assetType', str) if 'defaultThumbnail' in dct: @@ -497,9 +503,10 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> global_asset_id = None if 'globalAssetId' in dct: global_asset_id = _get_ts(dct, 'globalAssetId', str) - specific_asset_id = None + specific_asset_id = set() if 'specificAssetIds' in dct: - specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'specificAssetIds', dict)) + for desc_data in _get_ts(dct, "specificAssetIds", list): + specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId)) ret = object_class(id_short=None, entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], diff --git a/basyx/aas/adapter/json/json_serialization.py b/basyx/aas/adapter/json/json_serialization.py index c2f6e11cd..0ab6cddcb 100644 --- a/basyx/aas/adapter/json/json_serialization.py +++ b/basyx/aas/adapter/json/json_serialization.py @@ -631,7 +631,7 @@ def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]: if obj.global_asset_id: data['globalAssetId'] = obj.global_asset_id if obj.specific_asset_id: - data['specificAssetIds'] = obj.specific_asset_id + data['specificAssetIds'] = list(obj.specific_asset_id) return data @classmethod diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index fdf346af5..98f4a9fa3 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -797,13 +797,17 @@ def construct_capability(cls, element: etree.Element, object_class=model.Capabil @classmethod def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_kwargs: Any) -> model.Entity: - global_asset_id = _get_text_or_none(element.find(NS_AAS + "globalAssetId")) - specific_asset_id = _failsafe_construct(element.find(NS_AAS + "specificAssetId"), - cls.construct_specific_asset_id, cls.failsafe) + specific_asset_id = set() + specific_assset_ids = element.find(NS_AAS + "specificAssetIds") + if specific_assset_ids is not None: + for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", + cls.construct_specific_asset_id, cls.failsafe): + specific_asset_id.add(id) + entity = object_class( id_short=None, entity_type=_child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), - global_asset_id=global_asset_id, + global_asset_id=_get_text_or_none(element.find(NS_AAS + "globalAssetId")), specific_asset_id=specific_asset_id) if not cls.stripped: @@ -994,17 +998,19 @@ def construct_specific_asset_id(cls, element: etree.Element, object_class=model. @classmethod def construct_asset_information(cls, element: etree.Element, object_class=model.AssetInformation, **_kwargs: Any) \ -> model.AssetInformation: - asset_information = object_class( - _child_text_mandatory_mapped(element, NS_AAS + "assetKind", ASSET_KIND_INVERSE), - ) - global_asset_id = _get_text_or_none(element.find(NS_AAS + "globalAssetId")) - if global_asset_id is not None: - asset_information.global_asset_id = global_asset_id + specific_asset_id = set() specific_assset_ids = element.find(NS_AAS + "specificAssetIds") if specific_assset_ids is not None: for id in _child_construct_multiple(specific_assset_ids, NS_AAS + "specificAssetId", cls.construct_specific_asset_id, cls.failsafe): - asset_information.specific_asset_id.add(id) + specific_asset_id.add(id) + + asset_information = object_class( + _child_text_mandatory_mapped(element, NS_AAS + "assetKind", ASSET_KIND_INVERSE), + global_asset_id=_get_text_or_none(element.find(NS_AAS + "globalAssetId")), + specific_asset_id=specific_asset_id, + ) + asset_type = _get_text_or_none(element.find(NS_AAS + "assetType")) if asset_type is not None: asset_information.asset_type = asset_type diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 55446dac9..0ba5c8629 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -801,7 +801,10 @@ def entity_to_xml(obj: model.Entity, if obj.global_asset_id: et_entity.append(_generate_element(NS_AAS + "globalAssetId", text=obj.global_asset_id)) if obj.specific_asset_id: - et_entity.append(specific_asset_id_to_xml(obj.specific_asset_id, NS_AAS + "specificAssetId")) + et_specific_asset_id = _generate_element(name=NS_AAS + "specificAssetIds") + for specific_asset_id in obj.specific_asset_id: + et_specific_asset_id.append(specific_asset_id_to_xml(specific_asset_id, NS_AAS + "specificAssetId")) + et_entity.append(et_specific_asset_id) return et_entity diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index c99efc594..943f1e25e 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -29,6 +29,8 @@ class AssetInformation: identifiers. However, to support the corner case of very first phase of lifecycle where a stabilised/constant global asset identifier does not already exist, the corresponding attribute “globalAssetId” is optional. + *Constraint AASd-131*: The globalAssetId or at least one specificAssetId shall be defined for AssetInformation. + :ivar asset_kind: Denotes whether the Asset is of :class:`~aas.model.base.AssetKind` "TYPE" or "INSTANCE". Default is "INSTANCE". :ivar global_asset_id: :class:`~aas.model.base.Identifier` modelling the identifier of the asset the AAS is @@ -52,25 +54,33 @@ class AssetInformation: def __init__(self, asset_kind: base.AssetKind = base.AssetKind.INSTANCE, global_asset_id: Optional[base.Identifier] = None, - specific_asset_id: Optional[Set[base.SpecificAssetId]] = None, + specific_asset_id: Optional[Iterable[base.SpecificAssetId]] = None, asset_type: Optional[base.Identifier] = None, default_thumbnail: Optional[base.Resource] = None): super().__init__() self.asset_kind: base.AssetKind = asset_kind - self._global_asset_id: Optional[base.Identifier] = global_asset_id - self.specific_asset_id: Set[base.SpecificAssetId] = set() if specific_asset_id is None \ - else specific_asset_id + self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = base.ConstrainedList( + [] if specific_asset_id is None else specific_asset_id, + item_del_hook=self._check_constraint_del_spec_asset_id) + self.global_asset_id: Optional[base.Identifier] = global_asset_id self.asset_type: Optional[base.Identifier] = asset_type self.default_thumbnail: Optional[base.Resource] = default_thumbnail + def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, + _list: List[base.SpecificAssetId]) -> None: + if self.global_asset_id is None and len(_list) == 1: + raise base.AASConstraintViolation( + 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") + def _get_global_asset_id(self): return self._global_asset_id def _set_global_asset_id(self, global_asset_id: Optional[base.Identifier]): if global_asset_id is None: if self.specific_asset_id is None or not self.specific_asset_id: - raise ValueError("either global or specific asset id must be set") + raise base.AASConstraintViolation( + 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") else: _string_constraints.check_identifier(global_asset_id) self._global_asset_id = global_asset_id diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 7ca7d70f4..160b63840 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1091,7 +1091,7 @@ def __init__(self, entity_type: base.EntityType, statement: Iterable[SubmodelElement] = (), global_asset_id: Optional[base.Identifier] = None, - specific_asset_id: Optional[base.SpecificAssetId] = None, + specific_asset_id: Optional[Iterable[base.SpecificAssetId]] = None, display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, description: Optional[base.MultiLanguageTextType] = None, @@ -1107,27 +1107,47 @@ def __init__(self, super().__init__(id_short, display_name, category, description, parent, semantic_id, qualifier, extension, supplemental_semantic_id, embedded_data_specifications) self.statement = base.NamespaceSet(self, [("id_short", True)], statement) - self.specific_asset_id: Optional[base.SpecificAssetId] = specific_asset_id + self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = base.ConstrainedList( + [] if specific_asset_id is None else specific_asset_id, + item_del_hook=self._check_constraint_del_spec_asset_id) self.global_asset_id: Optional[base.Identifier] = global_asset_id - self._entity_type: base.EntityType - self.entity_type = entity_type + self._entity_type: base.EntityType = entity_type + self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, self.specific_asset_id) + + def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, + _list: List[base.SpecificAssetId]) -> None: + if self.global_asset_id is None and len(_list) == 1: + raise base.AASConstraintViolation( + 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") def _get_entity_type(self) -> base.EntityType: return self._entity_type def _set_entity_type(self, entity_type: base.EntityType) -> None: - if self.global_asset_id is None and self.specific_asset_id is None \ - and entity_type == base.EntityType.SELF_MANAGED_ENTITY: + self._validate_asset_ids_for_entity_type(entity_type, self.global_asset_id, self.specific_asset_id) + self._entity_type = entity_type + + def _get_global_asset_id(self): + return self._global_asset_id + + def _set_global_asset_id(self, global_asset_id: Optional[base.Identifier]): + self._validate_asset_ids_for_entity_type(self.entity_type, global_asset_id, self.specific_asset_id) + self._global_asset_id = global_asset_id + + @staticmethod + def _validate_asset_ids_for_entity_type(entity_type: base.EntityType, + global_asset_id: Optional[base.Identifier], + specific_asset_id: Optional[base.ConstrainedList[base.SpecificAssetId]]): + if entity_type == base.EntityType.SELF_MANAGED_ENTITY and global_asset_id is None and not specific_asset_id: raise base.AASConstraintViolation( - 14, - "A self-managed entity has to have a globalAssetId or a specificAssetId" - ) - if (self.global_asset_id or self.specific_asset_id) and entity_type == base.EntityType.CO_MANAGED_ENTITY: + 14, "A self-managed entity has to have a globalAssetId or a specificAssetId") + elif entity_type == base.EntityType.CO_MANAGED_ENTITY and (global_asset_id or specific_asset_id): raise base.AASConstraintViolation( - 14, - "A co-managed entity has to have neither a globalAssetId nor a specificAssetId") - self._entity_type = entity_type + 14, "A co-managed entity has to have neither a globalAssetId nor a specificAssetId") + if global_asset_id: + _string_constraints.check_identifier(global_asset_id) + global_asset_id = property(_get_global_asset_id, _set_global_asset_id) entity_type = property(_get_entity_type, _set_entity_type) From 92e86181f9d991f4d88d7c4385b9da3b30c72916 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 27 Oct 2023 21:57:43 +0200 Subject: [PATCH 03/25] Refactor `check_entity_equal()`&`check_asset_information_equal()` Add function for checking `Iterable[SpecificAssetId]` --- basyx/aas/examples/data/_helper.py | 42 ++++++++++++------------------ 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/basyx/aas/examples/data/_helper.py b/basyx/aas/examples/data/_helper.py index 9bf53e8aa..01e8a6d66 100644 --- a/basyx/aas/examples/data/_helper.py +++ b/basyx/aas/examples/data/_helper.py @@ -454,7 +454,7 @@ def _find_reference(self, object_: model.Reference, search_list: Iterable) -> Un return element return None - def _find_specific_asset_id(self, object_: model.SpecificAssetId, search_list: Union[Set, List]) \ + def _find_specific_asset_id(self, object_: model.SpecificAssetId, search_list: Iterable) \ -> Union[model.SpecificAssetId, None]: """ Find a SpecificAssetId in an list @@ -604,25 +604,26 @@ def check_entity_equal(self, object_: model.Entity, expected_value: model.Entity self._check_abstract_attributes_submodel_element_equal(object_, expected_value) self.check_attribute_equal(object_, 'entity_type', expected_value.entity_type) self.check_attribute_equal(object_, 'global_asset_id', expected_value.global_asset_id) - if object_.specific_asset_id and expected_value.specific_asset_id: - self.check_specific_asset_id(object_.specific_asset_id, expected_value.specific_asset_id) - else: - if expected_value.specific_asset_id: - self.check(expected_value.specific_asset_id is not None, - 'SpecificAssetId {} must exist'.format(repr(expected_value.specific_asset_id)), - value=object_.specific_asset_id) - else: - if object_.specific_asset_id: - self.check(expected_value.specific_asset_id is None, 'Enity {} must not have a ' - 'specificAssetId'.format(repr(object_)), - value=expected_value.specific_asset_id) + self._check_specific_asset_ids_equal(object_.specific_asset_id, expected_value.specific_asset_id, object_) self.check_contained_element_length(object_, 'statement', model.SubmodelElement, len(expected_value.statement)) for expected_element in expected_value.statement: element = object_.get_referable(expected_element.id_short) - self.check(element is not None, 'Entity {} must exist'.format(repr(expected_element))) + self.check(element is not None, f'Entity {repr(expected_element)} must exist') found_elements = self._find_extra_elements_by_id_short(object_.statement, expected_value.statement) - self.check(found_elements == set(), 'Enity {} must not have extra statements'.format(repr(object_)), + self.check(found_elements == set(), f'Enity {repr(object_)} must not have extra statements', + value=found_elements) + + def _check_specific_asset_ids_equal(self, object_: Iterable[model.SpecificAssetId], + expected_value: Iterable[model.SpecificAssetId], + object_parent): + for expected_pair in expected_value: + pair = self._find_specific_asset_id(expected_pair, object_) + if self.check(pair is not None, f'SpecificAssetId {repr(expected_pair)} must exist'): + self.check_specific_asset_id(pair, expected_pair) # type: ignore + + found_elements = self._find_extra_object(object_, expected_value, model.SpecificAssetId) + self.check(found_elements == set(), f'{repr(object_parent)} must not have extra specificAssetIds', value=found_elements) def _check_event_element_equal(self, object_: model.EventElement, expected_value: model.EventElement): @@ -722,16 +723,7 @@ def check_asset_information_equal(self, object_: model.AssetInformation, expecte self.check_attribute_equal(object_, 'global_asset_id', expected_value.global_asset_id) self.check_contained_element_length(object_, 'specific_asset_id', model.SpecificAssetId, len(expected_value.specific_asset_id)) - for expected_pair in expected_value.specific_asset_id: - pair = self._find_specific_asset_id(expected_pair, object_.specific_asset_id) - if self.check(pair is not None, 'SpecificAssetId {} must exist'.format(repr(expected_pair))): - self.check_specific_asset_id(pair, expected_pair) # type: ignore - - found_elements = self._find_extra_object(object_.specific_asset_id, expected_value.specific_asset_id, - model.SpecificAssetId) - self.check(found_elements == set(), '{} must not have extra ' - 'specificAssetIds'.format(repr(object_)), - value=found_elements) + self._check_specific_asset_ids_equal(object_.specific_asset_id, expected_value.specific_asset_id, object_) self.check_attribute_equal(object_, 'asset_type', object_.asset_type) if object_.default_thumbnail and expected_value.default_thumbnail: self.check_resource_equal(object_.default_thumbnail, expected_value.default_thumbnail) From 0ed5d4f3c76b00c4fdec627db2b620547c8e1362 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 27 Oct 2023 22:57:39 +0200 Subject: [PATCH 04/25] Update test files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add globalAssetId for all `ÀssetInformation` - Fix Entity.specificAssetIds in test files --- basyx/aas/examples/data/example_aas.py | 11 +++---- .../data/example_aas_mandatory_attributes.py | 3 +- .../adapter/json/test_json_deserialization.py | 6 ++-- test/adapter/xml/test_xml_deserialization.py | 6 ++++ .../files/test_demo_full_example.json | 29 ++++++++++--------- .../files/test_demo_full_example.xml | 29 ++++++++++--------- .../aasx/data.json | 29 ++++++++++--------- ...est_demo_full_example_wrong_attribute.json | 29 ++++++++++--------- ...test_demo_full_example_wrong_attribute.xml | 29 ++++++++++--------- .../aasx/data.xml | 29 ++++++++++--------- .../aasx/data.xml | 29 ++++++++++--------- .../test_deserializable_aas_warning.json | 3 +- .../files/test_deserializable_aas_warning.xml | 1 + test/model/test_provider.py | 9 ++++-- test/model/test_submodel.py | 4 +-- 15 files changed, 141 insertions(+), 105 deletions(-) diff --git a/basyx/aas/examples/data/example_aas.py b/basyx/aas/examples/data/example_aas.py index 7b0a98541..a872821ac 100644 --- a/basyx/aas/examples/data/example_aas.py +++ b/basyx/aas/examples/data/example_aas.py @@ -244,11 +244,12 @@ def create_example_bill_of_material_submodel() -> model.Submodel: entity_type=model.EntityType.SELF_MANAGED_ENTITY, statement={submodel_element_property, submodel_element_property2}, global_asset_id='http://acplt.org/TestAsset/', - specific_asset_id=model.SpecificAssetId(name="TestKey", - value="TestValue", - external_subject_id=model.ExternalReference( - (model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE, - value='http://acplt.org/SpecificAssetId/'),))), + specific_asset_id={ + model.SpecificAssetId(name="TestKey", value="TestValue", + external_subject_id=model.ExternalReference( + (model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE, + value='http://acplt.org/SpecificAssetId/'),)) + )}, category="PARAMETER", description=model.MultiLanguageTextType({ 'en-US': 'Legally valid designation of the natural or judicial person which ' diff --git a/basyx/aas/examples/data/example_aas_mandatory_attributes.py b/basyx/aas/examples/data/example_aas_mandatory_attributes.py index ab41b4aef..aa83d3f93 100644 --- a/basyx/aas/examples/data/example_aas_mandatory_attributes.py +++ b/basyx/aas/examples/data/example_aas_mandatory_attributes.py @@ -201,7 +201,8 @@ def create_example_empty_asset_administration_shell() -> model.AssetAdministrati :return: example asset administration shell """ asset_administration_shell = model.AssetAdministrationShell( - asset_information=model.AssetInformation(), + asset_information=model.AssetInformation( + global_asset_id='http://acplt.org/TestAsset2_Mandatory/'), id_='https://acplt.org/Test_AssetAdministrationShell2_Mandatory') return asset_administration_shell diff --git a/test/adapter/json/test_json_deserialization.py b/test/adapter/json/test_json_deserialization.py index 3ee11cb0f..7f127be93 100644 --- a/test/adapter/json/test_json_deserialization.py +++ b/test/adapter/json/test_json_deserialization.py @@ -31,7 +31,8 @@ def test_file_format_wrong_list(self) -> None: "modelType": "AssetAdministrationShell", "id": "https://acplt.org/Test_Asset", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Instance", + "globalAssetId": "https://acplt.org/Test_AssetId" } } ] @@ -142,7 +143,8 @@ def test_duplicate_identifier(self) -> None: "modelType": "AssetAdministrationShell", "id": "http://acplt.org/test_aas", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Instance", + "globalAssetId": "https://acplt.org/Test_AssetId" } }], "submodels": [{ diff --git a/test/adapter/xml/test_xml_deserialization.py b/test/adapter/xml/test_xml_deserialization.py index 4a473d2d4..f562e02a6 100644 --- a/test/adapter/xml/test_xml_deserialization.py +++ b/test/adapter/xml/test_xml_deserialization.py @@ -81,6 +81,7 @@ def test_missing_asset_kind(self) -> None: http://acplt.org/test_aas + http://acplt.org/TestAsset/ @@ -94,6 +95,7 @@ def test_missing_asset_kind_text(self) -> None: http://acplt.org/test_aas + http://acplt.org/TestAsset/ @@ -107,6 +109,7 @@ def test_invalid_asset_kind_text(self) -> None: http://acplt.org/test_aas invalidKind + http://acplt.org/TestAsset/ @@ -153,6 +156,7 @@ def test_reference_kind_mismatch(self) -> None: http://acplt.org/test_aas Instance + http://acplt.org/TestAsset/ ModelReference @@ -260,6 +264,7 @@ def test_duplicate_identifier(self) -> None: http://acplt.org/test_aas Instance + http://acplt.org/TestAsset/ @@ -375,6 +380,7 @@ def test_stripped_asset_administration_shell(self) -> None: http://acplt.org/test_aas Instance + http://acplt.org/TestAsset/ diff --git a/test/compliance_tool/files/test_demo_full_example.json b/test/compliance_tool/files/test_demo_full_example.json index 6dbe924ea..05371e694 100644 --- a/test/compliance_tool/files/test_demo_full_example.json +++ b/test/compliance_tool/files/test_demo_full_example.json @@ -254,7 +254,8 @@ "modelType": "AssetAdministrationShell", "id": "https://acplt.org/Test_AssetAdministrationShell2_Mandatory", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Instance", + "globalAssetId": "http://acplt.org/TestAsset2_Mandatory/" } }, { @@ -720,19 +721,21 @@ ], "entityType": "SelfManagedEntity", "globalAssetId": "http://acplt.org/TestAsset/", - "specificAssetId": { - "name": "TestKey", - "value": "TestValue", - "externalSubjectId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "http://acplt.org/SpecificAssetId/" - } - ] + "specificAssetIds": [ + { + "name": "TestKey", + "value": "TestValue", + "externalSubjectId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "http://acplt.org/SpecificAssetId/" + } + ] + } } - } + ] }, { "idShort": "ExampleEntity2", diff --git a/test/compliance_tool/files/test_demo_full_example.xml b/test/compliance_tool/files/test_demo_full_example.xml index 3c1c4eafa..dccbd55b0 100644 --- a/test/compliance_tool/files/test_demo_full_example.xml +++ b/test/compliance_tool/files/test_demo_full_example.xml @@ -245,6 +245,7 @@ https://acplt.org/Test_AssetAdministrationShell2_Mandatory Instance + http://acplt.org/TestAsset2_Mandatory/ @@ -601,19 +602,21 @@ SelfManagedEntity http://acplt.org/TestAsset/ - - TestKey - TestValue - - ExternalReference - - - GlobalReference - http://acplt.org/SpecificAssetId/ - - - - + + + TestKey + TestValue + + ExternalReference + + + GlobalReference + http://acplt.org/SpecificAssetId/ + + + + + PARAMETER diff --git a/test/compliance_tool/files/test_demo_full_example_json_aasx/aasx/data.json b/test/compliance_tool/files/test_demo_full_example_json_aasx/aasx/data.json index d7669dd99..93d0e3eda 100644 --- a/test/compliance_tool/files/test_demo_full_example_json_aasx/aasx/data.json +++ b/test/compliance_tool/files/test_demo_full_example_json_aasx/aasx/data.json @@ -262,7 +262,8 @@ "modelType": "AssetAdministrationShell", "id": "https://acplt.org/Test_AssetAdministrationShell2_Mandatory", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Instance", + "globalAssetId": "http://acplt.org/TestAsset2_Mandatory/" } }, { @@ -728,19 +729,21 @@ ], "entityType": "SelfManagedEntity", "globalAssetId": "http://acplt.org/TestAsset/", - "specificAssetId": { - "name": "TestKey", - "value": "TestValue", - "externalSubjectId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "http://acplt.org/SpecificAssetId/" - } - ] + "specificAssetIds": [ + { + "name": "TestKey", + "value": "TestValue", + "externalSubjectId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "http://acplt.org/SpecificAssetId/" + } + ] + } } - } + ] }, { "idShort": "ExampleEntity2", diff --git a/test/compliance_tool/files/test_demo_full_example_wrong_attribute.json b/test/compliance_tool/files/test_demo_full_example_wrong_attribute.json index d10beb9a8..4d05b2f55 100644 --- a/test/compliance_tool/files/test_demo_full_example_wrong_attribute.json +++ b/test/compliance_tool/files/test_demo_full_example_wrong_attribute.json @@ -254,7 +254,8 @@ "modelType": "AssetAdministrationShell", "id": "https://acplt.org/Test_AssetAdministrationShell2_Mandatory", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Instance", + "globalAssetId": "http://acplt.org/TestAsset2_Mandatory/" } }, { @@ -718,19 +719,21 @@ ], "entityType": "SelfManagedEntity", "globalAssetId": "http://acplt.org/TestAsset/", - "specificAssetId": { - "name": "TestKey", - "value": "TestValue", - "externalSubjectId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "http://acplt.org/SpecificAssetId/" - } - ] + "specificAssetIds": [ + { + "name": "TestKey", + "value": "TestValue", + "externalSubjectId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "http://acplt.org/SpecificAssetId/" + } + ] + } } - } + ] }, { "idShort": "ExampleEntity2", diff --git a/test/compliance_tool/files/test_demo_full_example_wrong_attribute.xml b/test/compliance_tool/files/test_demo_full_example_wrong_attribute.xml index 52d39d0a6..4d3d5a561 100644 --- a/test/compliance_tool/files/test_demo_full_example_wrong_attribute.xml +++ b/test/compliance_tool/files/test_demo_full_example_wrong_attribute.xml @@ -245,6 +245,7 @@ https://acplt.org/Test_AssetAdministrationShell2_Mandatory Instance + http://acplt.org/TestAsset2_Mandatory/ @@ -601,19 +602,21 @@ SelfManagedEntity http://acplt.org/TestAsset/ - - TestKey - TestValue - - ExternalReference - - - GlobalReference - http://acplt.org/SpecificAssetId/ - - - - + + + TestKey + TestValue + + ExternalReference + + + GlobalReference + http://acplt.org/SpecificAssetId/ + + + + + PARAMETER diff --git a/test/compliance_tool/files/test_demo_full_example_xml_aasx/aasx/data.xml b/test/compliance_tool/files/test_demo_full_example_xml_aasx/aasx/data.xml index 6922d53d3..6ba3cc9c5 100644 --- a/test/compliance_tool/files/test_demo_full_example_xml_aasx/aasx/data.xml +++ b/test/compliance_tool/files/test_demo_full_example_xml_aasx/aasx/data.xml @@ -253,6 +253,7 @@ https://acplt.org/Test_AssetAdministrationShell2_Mandatory Instance + http://acplt.org/TestAsset2_Mandatory/ @@ -609,19 +610,21 @@ SelfManagedEntity http://acplt.org/TestAsset/ - - TestKey - TestValue - - ExternalReference - - - GlobalReference - http://acplt.org/SpecificAssetId/ - - - - + + + TestKey + TestValue + + ExternalReference + + + GlobalReference + http://acplt.org/SpecificAssetId/ + + + + + PARAMETER diff --git a/test/compliance_tool/files/test_demo_full_example_xml_wrong_attribute_aasx/aasx/data.xml b/test/compliance_tool/files/test_demo_full_example_xml_wrong_attribute_aasx/aasx/data.xml index 6fda1e2c5..1ce64e565 100644 --- a/test/compliance_tool/files/test_demo_full_example_xml_wrong_attribute_aasx/aasx/data.xml +++ b/test/compliance_tool/files/test_demo_full_example_xml_wrong_attribute_aasx/aasx/data.xml @@ -253,6 +253,7 @@ https://acplt.org/Test_AssetAdministrationShell2_Mandatory Instance + http://acplt.org/TestAsset2_Mandatory/ @@ -609,19 +610,21 @@ SelfManagedEntity http://acplt.org/TestAsset/ - - TestKey - TestValue - - ExternalReference - - - GlobalReference - http://acplt.org/SpecificAssetId/ - - - - + + + TestKey + TestValue + + ExternalReference + + + GlobalReference + http://acplt.org/SpecificAssetId/ + + + + + PARAMETER diff --git a/test/compliance_tool/files/test_deserializable_aas_warning.json b/test/compliance_tool/files/test_deserializable_aas_warning.json index 14d358d4e..35da52c24 100644 --- a/test/compliance_tool/files/test_deserializable_aas_warning.json +++ b/test/compliance_tool/files/test_deserializable_aas_warning.json @@ -8,7 +8,8 @@ }, "modelType": "AssetAdministrationShell", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Instance", + "globalAssetId": "http://acplt.org/TestAsset/" } } ] diff --git a/test/compliance_tool/files/test_deserializable_aas_warning.xml b/test/compliance_tool/files/test_deserializable_aas_warning.xml index 8fdda7354..53f72ab71 100644 --- a/test/compliance_tool/files/test_deserializable_aas_warning.xml +++ b/test/compliance_tool/files/test_deserializable_aas_warning.xml @@ -9,6 +9,7 @@ https://acplt.org/Test_AssetAdministrationShell Instance + http://acplt.org/TestAsset/ diff --git a/test/model/test_provider.py b/test/model/test_provider.py index 5a0f5ada4..55586ffc8 100644 --- a/test/model/test_provider.py +++ b/test/model/test_provider.py @@ -12,8 +12,10 @@ class ProvidersTest(unittest.TestCase): def setUp(self) -> None: - self.aas1 = model.AssetAdministrationShell(model.AssetInformation(), "urn:x-test:aas1") - self.aas2 = model.AssetAdministrationShell(model.AssetInformation(), "urn:x-test:aas2") + self.aas1 = model.AssetAdministrationShell( + model.AssetInformation(global_asset_id="http://acplt.org/TestAsset1/"), "urn:x-test:aas1") + self.aas2 = model.AssetAdministrationShell( + model.AssetInformation(global_asset_id="http://acplt.org/TestAsset2/"), "urn:x-test:aas2") self.submodel1 = model.Submodel("urn:x-test:submodel1") self.submodel2 = model.Submodel("urn:x-test:submodel2") @@ -24,7 +26,8 @@ def test_store_retrieve(self) -> None: self.assertIn(self.aas1, object_store) property = model.Property('test', model.datatypes.String) self.assertFalse(property in object_store) - aas3 = model.AssetAdministrationShell(model.AssetInformation(), "urn:x-test:aas1") + aas3 = model.AssetAdministrationShell(model.AssetInformation(global_asset_id="http://acplt.org/TestAsset/"), + "urn:x-test:aas1") with self.assertRaises(KeyError) as cm: object_store.add(aas3) self.assertEqual("'Identifiable object with same id urn:x-test:aas1 is already " diff --git a/test/model/test_submodel.py b/test/model/test_submodel.py index cdc539978..201cad444 100644 --- a/test/model/test_submodel.py +++ b/test/model/test_submodel.py @@ -29,11 +29,11 @@ def test_set_entity(self): str(cm.exception) ) - specific_asset_id = model.SpecificAssetId(name="TestKey", + specific_asset_id = {model.SpecificAssetId(name="TestKey", value="TestValue", external_subject_id=model.ExternalReference((model.Key( type_=model.KeyTypes.GLOBAL_REFERENCE, - value='http://acplt.org/SpecificAssetId/'),))) + value='http://acplt.org/SpecificAssetId/'),)))} with self.assertRaises(model.AASConstraintViolation) as cm: obj3 = model.Entity(id_short='Test', entity_type=model.EntityType.CO_MANAGED_ENTITY, specific_asset_id=specific_asset_id, statement=()) From bc66b2a9585b841eb70eda54f019a2b2b401b206 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 27 Oct 2023 23:12:30 +0200 Subject: [PATCH 05/25] Update tutorial_serialization_deserialization.py --- basyx/aas/examples/tutorial_serialization_deserialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/examples/tutorial_serialization_deserialization.py b/basyx/aas/examples/tutorial_serialization_deserialization.py index dd11043b3..8f7b36949 100755 --- a/basyx/aas/examples/tutorial_serialization_deserialization.py +++ b/basyx/aas/examples/tutorial_serialization_deserialization.py @@ -47,7 +47,7 @@ ) aashell = model.AssetAdministrationShell( id_='https://acplt.org/Simple_AAS', - asset_information=model.AssetInformation(), + asset_information=model.AssetInformation(global_asset_id="test"), submodel={model.ModelReference.from_referable(submodel)} ) From 91bfe92515485dff81ed07dddc02d9149cfe18ae Mon Sep 17 00:00:00 2001 From: zrgt Date: Sun, 29 Oct 2023 15:50:21 +0100 Subject: [PATCH 06/25] Use getter/setter decorators for global_asset_id - Use getter/setter decorators for global_asset_id - Refactor `Entity.__init__`: use setter for `entity_type` and remove `_validate_asset_ids_for_entity_type()` from init because it will be called in `entity_type` setter - Add return type to some init funcs of abstract classes to calm down MyPy --- basyx/aas/model/aas.py | 8 ++++---- basyx/aas/model/base.py | 8 ++++---- basyx/aas/model/submodel.py | 18 +++++++++--------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index 943f1e25e..09740034c 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -73,10 +73,12 @@ def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId raise base.AASConstraintViolation( 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") - def _get_global_asset_id(self): + @property + def global_asset_id(self): return self._global_asset_id - def _set_global_asset_id(self, global_asset_id: Optional[base.Identifier]): + @global_asset_id.setter + def global_asset_id(self, global_asset_id: Optional[base.Identifier]): if global_asset_id is None: if self.specific_asset_id is None or not self.specific_asset_id: raise base.AASConstraintViolation( @@ -85,8 +87,6 @@ def _set_global_asset_id(self, global_asset_id: Optional[base.Identifier]): _string_constraints.check_identifier(global_asset_id) self._global_asset_id = global_asset_id - global_asset_id = property(_get_global_asset_id, _set_global_asset_id) - def __repr__(self) -> str: return "AssetInformation(assetKind={}, globalAssetId={}, specificAssetId={}, assetType={}, " \ "defaultThumbnail={})".format(self.asset_kind, self._global_asset_id, str(self.specific_asset_id), diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index e96bf4a1d..e835c3262 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -599,7 +599,7 @@ class Referable(HasExtension, metaclass=abc.ABCMeta): Default is an empty string, making it use the source of its ancestor, if possible. """ @abc.abstractmethod - def __init__(self): + def __init__(self) -> None: super().__init__() self._id_short: Optional[NameType] = None self.display_name: Optional[MultiLanguageNameType] = dict() @@ -1258,7 +1258,7 @@ class Identifiable(Referable, metaclass=abc.ABCMeta): :ivar ~.id: The globally unique id of the element. """ @abc.abstractmethod - def __init__(self): + def __init__(self) -> None: super().__init__() self.administration: Optional[AdministrativeInformation] = None # The id attribute is set by all inheriting classes __init__ functions. @@ -1516,7 +1516,7 @@ class HasKind(metaclass=abc.ABCMeta): :ivar _kind: Kind of the element: either type or instance. Default = :attr:`~ModellingKind.INSTANCE`. """ @abc.abstractmethod - def __init__(self): + def __init__(self) -> None: super().__init__() self._kind: ModellingKind = ModellingKind.INSTANCE @@ -1537,7 +1537,7 @@ class Qualifiable(Namespace, metaclass=abc.ABCMeta): qualifiable element. """ @abc.abstractmethod - def __init__(self): + def __init__(self) -> None: super().__init__() self.namespace_element_sets: List[NamespaceSet] = [] self.qualifier: NamespaceSet[Qualifier] diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 160b63840..00608a29d 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1111,8 +1111,7 @@ def __init__(self, [] if specific_asset_id is None else specific_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) self.global_asset_id: Optional[base.Identifier] = global_asset_id - self._entity_type: base.EntityType = entity_type - self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, self.specific_asset_id) + self.entity_type: base.EntityType = entity_type def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, _list: List[base.SpecificAssetId]) -> None: @@ -1120,17 +1119,21 @@ def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId raise base.AASConstraintViolation( 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") - def _get_entity_type(self) -> base.EntityType: + @property + def entity_type(self) -> base.EntityType: return self._entity_type - def _set_entity_type(self, entity_type: base.EntityType) -> None: + @entity_type.setter + def entity_type(self, entity_type: base.EntityType) -> None: self._validate_asset_ids_for_entity_type(entity_type, self.global_asset_id, self.specific_asset_id) self._entity_type = entity_type - def _get_global_asset_id(self): + @property + def global_asset_id(self): return self._global_asset_id - def _set_global_asset_id(self, global_asset_id: Optional[base.Identifier]): + @global_asset_id.setter + def global_asset_id(self, global_asset_id: Optional[base.Identifier]): self._validate_asset_ids_for_entity_type(self.entity_type, global_asset_id, self.specific_asset_id) self._global_asset_id = global_asset_id @@ -1147,9 +1150,6 @@ def _validate_asset_ids_for_entity_type(entity_type: base.EntityType, if global_asset_id: _string_constraints.check_identifier(global_asset_id) - global_asset_id = property(_get_global_asset_id, _set_global_asset_id) - entity_type = property(_get_entity_type, _set_entity_type) - class EventElement(SubmodelElement, metaclass=abc.ABCMeta): """ From 038662885ed5056da0cee0c8bf615eff47b6a589 Mon Sep 17 00:00:00 2001 From: zrgt Date: Sun, 29 Oct 2023 15:58:01 +0100 Subject: [PATCH 07/25] Fix errors from MyPy --- basyx/aas/model/base.py | 2 +- basyx/aas/model/submodel.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index e835c3262..96cb5fca8 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -599,7 +599,7 @@ class Referable(HasExtension, metaclass=abc.ABCMeta): Default is an empty string, making it use the source of its ancestor, if possible. """ @abc.abstractmethod - def __init__(self) -> None: + def __init__(self): super().__init__() self._id_short: Optional[NameType] = None self.display_name: Optional[MultiLanguageNameType] = dict() diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 00608a29d..18ed0f26c 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1126,7 +1126,7 @@ def entity_type(self) -> base.EntityType: @entity_type.setter def entity_type(self, entity_type: base.EntityType) -> None: self._validate_asset_ids_for_entity_type(entity_type, self.global_asset_id, self.specific_asset_id) - self._entity_type = entity_type + self._entity_type: base.EntityType = entity_type @property def global_asset_id(self): From 2f8a7317197492293fdbb3dceb215b7d6291fb62 Mon Sep 17 00:00:00 2001 From: zrgt Date: Sun, 29 Oct 2023 17:22:44 +0100 Subject: [PATCH 08/25] Fix pycodestyle errors --- test/model/test_submodel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/model/test_submodel.py b/test/model/test_submodel.py index 201cad444..bbd672257 100644 --- a/test/model/test_submodel.py +++ b/test/model/test_submodel.py @@ -30,10 +30,10 @@ def test_set_entity(self): ) specific_asset_id = {model.SpecificAssetId(name="TestKey", - value="TestValue", - external_subject_id=model.ExternalReference((model.Key( - type_=model.KeyTypes.GLOBAL_REFERENCE, - value='http://acplt.org/SpecificAssetId/'),)))} + value="TestValue", + external_subject_id=model.ExternalReference((model.Key( + type_=model.KeyTypes.GLOBAL_REFERENCE, + value='http://acplt.org/SpecificAssetId/'),)))} with self.assertRaises(model.AASConstraintViolation) as cm: obj3 = model.Entity(id_short='Test', entity_type=model.EntityType.CO_MANAGED_ENTITY, specific_asset_id=specific_asset_id, statement=()) From f53765889a839b96d3d9e5532b5e1d03ee1dd581 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 3 Nov 2023 11:20:24 +0100 Subject: [PATCH 09/25] Fix typehint of specific_asset_id --- basyx/aas/model/aas.py | 7 +++---- basyx/aas/model/submodel.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index 09740034c..e6032d622 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -54,15 +54,14 @@ class AssetInformation: def __init__(self, asset_kind: base.AssetKind = base.AssetKind.INSTANCE, global_asset_id: Optional[base.Identifier] = None, - specific_asset_id: Optional[Iterable[base.SpecificAssetId]] = None, + specific_asset_id: Iterable[base.SpecificAssetId] = (), asset_type: Optional[base.Identifier] = None, default_thumbnail: Optional[base.Resource] = None): super().__init__() self.asset_kind: base.AssetKind = asset_kind - self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = base.ConstrainedList( - [] if specific_asset_id is None else specific_asset_id, - item_del_hook=self._check_constraint_del_spec_asset_id) + self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ + base.ConstrainedList(specific_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) self.global_asset_id: Optional[base.Identifier] = global_asset_id self.asset_type: Optional[base.Identifier] = asset_type self.default_thumbnail: Optional[base.Resource] = default_thumbnail diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 18ed0f26c..440547cce 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1091,7 +1091,7 @@ def __init__(self, entity_type: base.EntityType, statement: Iterable[SubmodelElement] = (), global_asset_id: Optional[base.Identifier] = None, - specific_asset_id: Optional[Iterable[base.SpecificAssetId]] = None, + specific_asset_id: Iterable[base.SpecificAssetId] = (), display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, description: Optional[base.MultiLanguageTextType] = None, @@ -1107,9 +1107,8 @@ def __init__(self, super().__init__(id_short, display_name, category, description, parent, semantic_id, qualifier, extension, supplemental_semantic_id, embedded_data_specifications) self.statement = base.NamespaceSet(self, [("id_short", True)], statement) - self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = base.ConstrainedList( - [] if specific_asset_id is None else specific_asset_id, - item_del_hook=self._check_constraint_del_spec_asset_id) + self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ + base.ConstrainedList(specific_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) self.global_asset_id: Optional[base.Identifier] = global_asset_id self.entity_type: base.EntityType = entity_type From 046ee1518528971407f700deec317812092f9bd9 Mon Sep 17 00:00:00 2001 From: zrgt Date: Fri, 3 Nov 2023 11:46:04 +0100 Subject: [PATCH 10/25] Fix wrong example value --- basyx/aas/examples/data/example_aas.py | 2 +- basyx/aas/model/aas.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/examples/data/example_aas.py b/basyx/aas/examples/data/example_aas.py index a872821ac..6d2a39326 100644 --- a/basyx/aas/examples/data/example_aas.py +++ b/basyx/aas/examples/data/example_aas.py @@ -277,7 +277,7 @@ def create_example_bill_of_material_submodel() -> model.Submodel: entity_type=model.EntityType.CO_MANAGED_ENTITY, statement=(), global_asset_id=None, - specific_asset_id=None, + specific_asset_id=(), category="PARAMETER", description=model.MultiLanguageTextType({ 'en-US': 'Legally valid designation of the natural or judicial person which ' diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index e6032d622..aa5cd2f91 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -79,7 +79,7 @@ def global_asset_id(self): @global_asset_id.setter def global_asset_id(self, global_asset_id: Optional[base.Identifier]): if global_asset_id is None: - if self.specific_asset_id is None or not self.specific_asset_id: + if not self.specific_asset_id: raise base.AASConstraintViolation( 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") else: From 7a2e341383743e61d349e4a2c76da404e35328ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 16:13:27 +0100 Subject: [PATCH 11/25] model.base: fix `ConstrainedList.clear()` atomicity The default inherited `clear()` implementation repeatedly deletes the last item of the list until the list is empty. If the last item can be deleted successfully, but an item in front of it that will be deleted later cannot, this makes `clear()` non-atomic. Thus, the `clear()` method is now overriden in an atomic way. Furthermore, the ConstrainedList atomicity test is fixed to correctly test for this as well. --- basyx/aas/model/base.py | 4 ++++ test/model/test_base.py | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index e96bf4a1d..8f58456d8 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -1311,6 +1311,10 @@ def extend(self, values: Iterable[_T]) -> None: self._item_add_hook(v, self._list + v_list[:idx]) self._list = self._list + v_list + def clear(self) -> None: + # clear() repeatedly deletes the last item by default, making it not atomic + del self[:] + @overload def __getitem__(self, index: int) -> _T: ... diff --git a/test/model/test_base.py b/test/model/test_base.py index ef894de50..aa40e0199 100644 --- a/test/model/test_base.py +++ b/test/model/test_base.py @@ -1213,7 +1213,15 @@ def hook(itm: int, _list: List[int]) -> None: self.assertEqual(c_list, [1, 2, 3]) with self.assertRaises(ValueError): c_list.clear() - self.assertEqual(c_list, [1, 2, 3]) + # the default clear() implementation seems to repeatedly delete the last item until the list is empty + # in this case, the last item is 3, which cannot be deleted because it is > 2, thus leaving it unclear whether + # clear() really is atomic. to work around this, the list is reversed, making 1 the last item, and attempting + # to clear again. + c_list.reverse() + with self.assertRaises(ValueError): + c_list.clear() + self.assertEqual(c_list, [3, 2, 1]) + c_list.reverse() del c_list[0:2] self.assertEqual(c_list, [3]) From ff31f45b76605a50963ff145c4f465ad54ae7109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 16:55:24 +0100 Subject: [PATCH 12/25] model.submodel: remove `_string_constraints` decorator from `Entity` This decorator silently overrides the `global_asset_id` property, resulting in the constraints not being checked properly. --- basyx/aas/model/submodel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 440547cce..371026263 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1048,7 +1048,6 @@ def __init__(self, supplemental_semantic_id, embedded_data_specifications) -@_string_constraints.constrain_identifier("global_asset_id") class Entity(SubmodelElement, base.UniqueIdShortNamespace): """ An entity is a :class:`~.SubmodelElement` that is used to model entities From 7ac9aa6a0e74b1ddfd759196b95bf00aa7b56ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 17:01:22 +0100 Subject: [PATCH 13/25] model.submodel: move `Entity.global_asset_id` string constraint check to setter This only needs to be checked if the `global_asset_id` changes. --- basyx/aas/model/submodel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 371026263..6c2c26c84 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1133,6 +1133,8 @@ def global_asset_id(self): @global_asset_id.setter def global_asset_id(self, global_asset_id: Optional[base.Identifier]): self._validate_asset_ids_for_entity_type(self.entity_type, global_asset_id, self.specific_asset_id) + if global_asset_id is not None: + _string_constraints.check_identifier(global_asset_id) self._global_asset_id = global_asset_id @staticmethod @@ -1145,8 +1147,6 @@ def _validate_asset_ids_for_entity_type(entity_type: base.EntityType, elif entity_type == base.EntityType.CO_MANAGED_ENTITY and (global_asset_id or specific_asset_id): raise base.AASConstraintViolation( 14, "A co-managed entity has to have neither a globalAssetId nor a specificAssetId") - if global_asset_id: - _string_constraints.check_identifier(global_asset_id) class EventElement(SubmodelElement, metaclass=abc.ABCMeta): From 4b8a7ae2c33ba73875910ea142a93d159244fe1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 17:19:50 +0100 Subject: [PATCH 14/25] model.submodel: remove duplicate code in `Entity` `_validate_asset_ids_for_entity_type()` only needs to know whether there are `specific_asset_ids` or not. This can be represented by a boolean, allowing the delete hook of the `ConstrainedList` to make use of this function as well. --- basyx/aas/model/submodel.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 6c2c26c84..955547f45 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1112,10 +1112,8 @@ def __init__(self, self.entity_type: base.EntityType = entity_type def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, - _list: List[base.SpecificAssetId]) -> None: - if self.global_asset_id is None and len(_list) == 1: - raise base.AASConstraintViolation( - 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") + list_: List[base.SpecificAssetId]) -> None: + self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, len(list_) > 1) @property def entity_type(self) -> base.EntityType: @@ -1123,7 +1121,7 @@ def entity_type(self) -> base.EntityType: @entity_type.setter def entity_type(self, entity_type: base.EntityType) -> None: - self._validate_asset_ids_for_entity_type(entity_type, self.global_asset_id, self.specific_asset_id) + self._validate_asset_ids_for_entity_type(entity_type, self.global_asset_id, bool(self.specific_asset_id)) self._entity_type: base.EntityType = entity_type @property @@ -1132,7 +1130,7 @@ def global_asset_id(self): @global_asset_id.setter def global_asset_id(self, global_asset_id: Optional[base.Identifier]): - self._validate_asset_ids_for_entity_type(self.entity_type, global_asset_id, self.specific_asset_id) + self._validate_asset_ids_for_entity_type(self.entity_type, global_asset_id, bool(self.specific_asset_id)) if global_asset_id is not None: _string_constraints.check_identifier(global_asset_id) self._global_asset_id = global_asset_id @@ -1140,11 +1138,13 @@ def global_asset_id(self, global_asset_id: Optional[base.Identifier]): @staticmethod def _validate_asset_ids_for_entity_type(entity_type: base.EntityType, global_asset_id: Optional[base.Identifier], - specific_asset_id: Optional[base.ConstrainedList[base.SpecificAssetId]]): - if entity_type == base.EntityType.SELF_MANAGED_ENTITY and global_asset_id is None and not specific_asset_id: + specific_asset_id_nonempty: bool) -> None: + if entity_type == base.EntityType.SELF_MANAGED_ENTITY and global_asset_id is None \ + and not specific_asset_id_nonempty: raise base.AASConstraintViolation( 14, "A self-managed entity has to have a globalAssetId or a specificAssetId") - elif entity_type == base.EntityType.CO_MANAGED_ENTITY and (global_asset_id or specific_asset_id): + elif entity_type == base.EntityType.CO_MANAGED_ENTITY and (global_asset_id is not None + or specific_asset_id_nonempty): raise base.AASConstraintViolation( 14, "A co-managed entity has to have neither a globalAssetId nor a specificAssetId") From 3dcf9fff5d2ade13e5a535bddad9ee0490dbeda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 17:29:57 +0100 Subject: [PATCH 15/25] model.ass: add `_validate_asset_ids()` function to `AssetInformation` Similar to `submodel.Entity`, this is done to reduce duplicate code. --- basyx/aas/model/aas.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index aa5cd2f91..43068d390 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -67,10 +67,8 @@ def __init__(self, self.default_thumbnail: Optional[base.Resource] = default_thumbnail def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, - _list: List[base.SpecificAssetId]) -> None: - if self.global_asset_id is None and len(_list) == 1: - raise base.AASConstraintViolation( - 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") + list_: List[base.SpecificAssetId]) -> None: + self._validate_asset_ids(self.global_asset_id, len(list_) > 1) @property def global_asset_id(self): @@ -78,14 +76,17 @@ def global_asset_id(self): @global_asset_id.setter def global_asset_id(self, global_asset_id: Optional[base.Identifier]): - if global_asset_id is None: - if not self.specific_asset_id: - raise base.AASConstraintViolation( - 131, "An AssetInformation has to have a globalAssetId or a specificAssetId") - else: + self._validate_asset_ids(global_asset_id, bool(self.specific_asset_id)) + if global_asset_id is not None: _string_constraints.check_identifier(global_asset_id) self._global_asset_id = global_asset_id + @staticmethod + def _validate_asset_ids(global_asset_id: Optional[base.Identifier], specific_asset_id_nonempty: bool) -> None: + if global_asset_id is None and not specific_asset_id_nonempty: + raise base.AASConstraintViolation(131, + "An AssetInformation has to have a globalAssetId or a specificAssetId") + def __repr__(self) -> str: return "AssetInformation(assetKind={}, globalAssetId={}, specificAssetId={}, assetType={}, " \ "defaultThumbnail={})".format(self.asset_kind, self._global_asset_id, str(self.specific_asset_id), From b85316029b6f59a1a5c916847860a980009c83f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 17:42:57 +0100 Subject: [PATCH 16/25] model.{aas, submodel}: add set hook to `{AssetInformation, Entity}.specific_asset_id` Since `__setitem__` can be used to clear the list as well (e.g. `list[:] = ()`), the constraints need to be verified in this case as well. --- basyx/aas/model/aas.py | 9 ++++++++- basyx/aas/model/submodel.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index 43068d390..bbd093447 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -61,11 +61,18 @@ def __init__(self, super().__init__() self.asset_kind: base.AssetKind = asset_kind self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ - base.ConstrainedList(specific_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) + base.ConstrainedList(specific_asset_id, item_set_hook=self._check_constraint_set_spec_asset_id, + item_del_hook=self._check_constraint_del_spec_asset_id) self.global_asset_id: Optional[base.Identifier] = global_asset_id self.asset_type: Optional[base.Identifier] = asset_type self.default_thumbnail: Optional[base.Resource] = default_thumbnail + def _check_constraint_set_spec_asset_id(self, old: List[base.SpecificAssetId], new: List[base.SpecificAssetId], + list_: List[base.SpecificAssetId]) -> None: + self._validate_asset_ids(self.global_asset_id, + # whether the list is nonempty after the set operation + len(old) < len(list_) or len(new) > 0) + def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, list_: List[base.SpecificAssetId]) -> None: self._validate_asset_ids(self.global_asset_id, len(list_) > 1) diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 955547f45..058da04c5 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1107,10 +1107,17 @@ def __init__(self, supplemental_semantic_id, embedded_data_specifications) self.statement = base.NamespaceSet(self, [("id_short", True)], statement) self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ - base.ConstrainedList(specific_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) + base.ConstrainedList(specific_asset_id, item_set_hook=self._check_constraint_set_spec_asset_id, + item_del_hook=self._check_constraint_del_spec_asset_id) self.global_asset_id: Optional[base.Identifier] = global_asset_id self.entity_type: base.EntityType = entity_type + def _check_constraint_set_spec_asset_id(self, old: List[base.SpecificAssetId], new: List[base.SpecificAssetId], + list_: List[base.SpecificAssetId]) -> None: + self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, + # whether the list is nonempty after the set operation + len(old) < len(list_) or len(new) > 0) + def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, list_: List[base.SpecificAssetId]) -> None: self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, len(list_) > 1) From 39a8c4820653bca78497fd6a222591c350925b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 18:00:23 +0100 Subject: [PATCH 17/25] model.submodel: assign values correctly in `Entity.__init__` The attributes need to be assigned bypassing the setters because the setters try to access attributes that haven't been set yet for constraint validation. Only `global_asset_id` is set via the setter as a final constraint validation, and because `global_asset_id` is also constrained via a string constraint. --- basyx/aas/model/submodel.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 058da04c5..756a0b78d 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1109,8 +1109,13 @@ def __init__(self, self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ base.ConstrainedList(specific_asset_id, item_set_hook=self._check_constraint_set_spec_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) - self.global_asset_id: Optional[base.Identifier] = global_asset_id - self.entity_type: base.EntityType = entity_type + # assign private attributes, bypassing setters, as constraints will be checked below + self._entity_type: base.EntityType = entity_type + # use setter for global_asset_id, as it also checks the string constraint, + # which hasn't been checked at this point + # furthermore, the setter also validates AASd-014 + self._global_asset_id: Optional[base.Identifier] + self.global_asset_id = global_asset_id def _check_constraint_set_spec_asset_id(self, old: List[base.SpecificAssetId], new: List[base.SpecificAssetId], list_: List[base.SpecificAssetId]) -> None: @@ -1129,7 +1134,7 @@ def entity_type(self) -> base.EntityType: @entity_type.setter def entity_type(self, entity_type: base.EntityType) -> None: self._validate_asset_ids_for_entity_type(entity_type, self.global_asset_id, bool(self.specific_asset_id)) - self._entity_type: base.EntityType = entity_type + self._entity_type = entity_type @property def global_asset_id(self): From 6b3de298cba2ac25019124999e9f8503827ab9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 18:11:09 +0100 Subject: [PATCH 18/25] model.{aas, submodel}: add getter/setter for `{AssetInformation, Entity}.specific_asset_id` This prevents setting the attributes without verification of the constraints. --- basyx/aas/model/aas.py | 11 ++++++++++- basyx/aas/model/submodel.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index bbd093447..81ceb7e07 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -60,7 +60,7 @@ def __init__(self, super().__init__() self.asset_kind: base.AssetKind = asset_kind - self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ + self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ base.ConstrainedList(specific_asset_id, item_set_hook=self._check_constraint_set_spec_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) self.global_asset_id: Optional[base.Identifier] = global_asset_id @@ -88,6 +88,15 @@ def global_asset_id(self, global_asset_id: Optional[base.Identifier]): _string_constraints.check_identifier(global_asset_id) self._global_asset_id = global_asset_id + @property + def specific_asset_id(self) -> base.ConstrainedList[base.SpecificAssetId]: + return self._specific_asset_id + + @specific_asset_id.setter + def specific_asset_id(self, specific_asset_id: Iterable[base.SpecificAssetId]) -> None: + # constraints are checked via _check_constraint_set_spec_asset_id() in this case + self._specific_asset_id[:] = specific_asset_id + @staticmethod def _validate_asset_ids(global_asset_id: Optional[base.Identifier], specific_asset_id_nonempty: bool) -> None: if global_asset_id is None and not specific_asset_id_nonempty: diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 756a0b78d..6433b91de 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1106,7 +1106,7 @@ def __init__(self, super().__init__(id_short, display_name, category, description, parent, semantic_id, qualifier, extension, supplemental_semantic_id, embedded_data_specifications) self.statement = base.NamespaceSet(self, [("id_short", True)], statement) - self.specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ + self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ base.ConstrainedList(specific_asset_id, item_set_hook=self._check_constraint_set_spec_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) # assign private attributes, bypassing setters, as constraints will be checked below @@ -1147,6 +1147,15 @@ def global_asset_id(self, global_asset_id: Optional[base.Identifier]): _string_constraints.check_identifier(global_asset_id) self._global_asset_id = global_asset_id + @property + def specific_asset_id(self) -> base.ConstrainedList[base.SpecificAssetId]: + return self._specific_asset_id + + @specific_asset_id.setter + def specific_asset_id(self, specific_asset_id: Iterable[base.SpecificAssetId]) -> None: + # constraints are checked via _check_constraint_set_spec_asset_id() in this case + self._specific_asset_id[:] = specific_asset_id + @staticmethod def _validate_asset_ids_for_entity_type(entity_type: base.EntityType, global_asset_id: Optional[base.Identifier], From 47b2e476ac8784e485cb6df12118b5e180650885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 18:13:57 +0100 Subject: [PATCH 19/25] model.submodel: verify constraints when `SpecificAssetIds` are added to `Entity` This adds an `item_add_hook` to the `specific_asset_id` `ConstrainedList`, which is called whenever a new item is added to the list. This is necessary because a co-managed `Entity` is not allowed to have specific asset ids, so it shouldn't be possible to add any in this case. --- basyx/aas/model/submodel.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 6433b91de..88b189ad7 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1111,12 +1111,19 @@ def __init__(self, item_del_hook=self._check_constraint_del_spec_asset_id) # assign private attributes, bypassing setters, as constraints will be checked below self._entity_type: base.EntityType = entity_type + # add item_add_hook after items have been added, because checking the constraints requires the global_asset_id + # to be set + self._specific_asset_id._item_add_hook = self._check_constraint_add_spec_asset_id # use setter for global_asset_id, as it also checks the string constraint, # which hasn't been checked at this point # furthermore, the setter also validates AASd-014 self._global_asset_id: Optional[base.Identifier] self.global_asset_id = global_asset_id + def _check_constraint_add_spec_asset_id(self, _new: base.SpecificAssetId, _list: List[base.SpecificAssetId]) \ + -> None: + self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, True) + def _check_constraint_set_spec_asset_id(self, old: List[base.SpecificAssetId], new: List[base.SpecificAssetId], list_: List[base.SpecificAssetId]) -> None: self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, From 5e0f160698f7ee83a87d943485de86cfef6f6996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 18:18:49 +0100 Subject: [PATCH 20/25] model.{aas, submodel}: improve `{AssetInformation, Entity}.global_asset_id` type hints --- basyx/aas/model/aas.py | 8 +++++--- basyx/aas/model/submodel.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index 81ceb7e07..bf742eef7 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -63,7 +63,9 @@ def __init__(self, self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ base.ConstrainedList(specific_asset_id, item_set_hook=self._check_constraint_set_spec_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id) - self.global_asset_id: Optional[base.Identifier] = global_asset_id + self._global_asset_id: Optional[base.Identifier] + # AASd-131 is validated via the global_asset_id setter + self.global_asset_id = global_asset_id self.asset_type: Optional[base.Identifier] = asset_type self.default_thumbnail: Optional[base.Resource] = default_thumbnail @@ -78,11 +80,11 @@ def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId self._validate_asset_ids(self.global_asset_id, len(list_) > 1) @property - def global_asset_id(self): + def global_asset_id(self) -> Optional[base.Identifier]: return self._global_asset_id @global_asset_id.setter - def global_asset_id(self, global_asset_id: Optional[base.Identifier]): + def global_asset_id(self, global_asset_id: Optional[base.Identifier]) -> None: self._validate_asset_ids(global_asset_id, bool(self.specific_asset_id)) if global_asset_id is not None: _string_constraints.check_identifier(global_asset_id) diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 88b189ad7..c228cf9e2 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1144,11 +1144,11 @@ def entity_type(self, entity_type: base.EntityType) -> None: self._entity_type = entity_type @property - def global_asset_id(self): + def global_asset_id(self) -> Optional[base.Identifier]: return self._global_asset_id @global_asset_id.setter - def global_asset_id(self, global_asset_id: Optional[base.Identifier]): + def global_asset_id(self, global_asset_id: Optional[base.Identifier]) -> None: self._validate_asset_ids_for_entity_type(self.entity_type, global_asset_id, bool(self.specific_asset_id)) if global_asset_id is not None: _string_constraints.check_identifier(global_asset_id) From 6fb375d71f196f57b6b39702ff9788972afe8533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 4 Nov 2023 18:20:53 +0100 Subject: [PATCH 21/25] test.model: add `AssetInformation` tests and improve `Entity` tests --- test/model/test_aas.py | 86 +++++++++++++++++++++ test/model/test_submodel.py | 147 ++++++++++++++++++++++++++++++------ 2 files changed, 209 insertions(+), 24 deletions(-) create mode 100644 test/model/test_aas.py diff --git a/test/model/test_aas.py b/test/model/test_aas.py new file mode 100644 index 000000000..27ce13b4d --- /dev/null +++ b/test/model/test_aas.py @@ -0,0 +1,86 @@ +# Copyright (c) 2023 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. +# +# SPDX-License-Identifier: MIT + +import unittest + +from basyx.aas import model + + +class AssetInformationTest(unittest.TestCase): + def test_aasd_131_init(self) -> None: + with self.assertRaises(model.AASConstraintViolation) as cm: + model.AssetInformation(model.AssetKind.INSTANCE) + self.assertEqual("An AssetInformation has to have a globalAssetId or a specificAssetId (Constraint AASd-131)", + str(cm.exception)) + model.AssetInformation(model.AssetKind.INSTANCE, global_asset_id="https://acplt.org/TestAsset") + model.AssetInformation(model.AssetKind.INSTANCE, specific_asset_id=(model.SpecificAssetId("test", "test"),)) + model.AssetInformation(model.AssetKind.INSTANCE, global_asset_id="https://acplt.org/TestAsset", + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + + def test_aasd_131_set(self) -> None: + asset_information = model.AssetInformation(model.AssetKind.INSTANCE, + global_asset_id="https://acplt.org/TestAsset", + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + asset_information.global_asset_id = None + with self.assertRaises(model.AASConstraintViolation) as cm: + asset_information.specific_asset_id = model.ConstrainedList(()) + self.assertEqual("An AssetInformation has to have a globalAssetId or a specificAssetId (Constraint AASd-131)", + str(cm.exception)) + + asset_information = model.AssetInformation(model.AssetKind.INSTANCE, + global_asset_id="https://acplt.org/TestAsset", + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + asset_information.specific_asset_id = model.ConstrainedList(()) + with self.assertRaises(model.AASConstraintViolation) as cm: + asset_information.global_asset_id = None + self.assertEqual("An AssetInformation has to have a globalAssetId or a specificAssetId (Constraint AASd-131)", + str(cm.exception)) + + def test_aasd_131_specific_asset_id_add(self) -> None: + asset_information = model.AssetInformation(model.AssetKind.INSTANCE, + global_asset_id="https://acplt.org/TestAsset") + specific_asset_id1 = model.SpecificAssetId("test", "test") + specific_asset_id2 = model.SpecificAssetId("test", "test") + asset_information.specific_asset_id.append(specific_asset_id1) + asset_information.specific_asset_id.extend((specific_asset_id2,)) + self.assertIs(asset_information.specific_asset_id[0], specific_asset_id1) + self.assertIs(asset_information.specific_asset_id[1], specific_asset_id2) + + def test_aasd_131_specific_asset_id_set(self) -> None: + asset_information = model.AssetInformation(model.AssetKind.INSTANCE, + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + with self.assertRaises(model.AASConstraintViolation) as cm: + asset_information.specific_asset_id[:] = () + self.assertEqual("An AssetInformation has to have a globalAssetId or a specificAssetId (Constraint AASd-131)", + str(cm.exception)) + specific_asset_id = model.SpecificAssetId("test", "test") + self.assertIsNot(asset_information.specific_asset_id[0], specific_asset_id) + asset_information.specific_asset_id[:] = (specific_asset_id,) + self.assertIs(asset_information.specific_asset_id[0], specific_asset_id) + asset_information.specific_asset_id[0] = model.SpecificAssetId("test", "test") + self.assertIsNot(asset_information.specific_asset_id[0], specific_asset_id) + + def test_aasd_131_specific_asset_id_del(self) -> None: + specific_asset_id = model.SpecificAssetId("test", "test") + asset_information = model.AssetInformation(model.AssetKind.INSTANCE, + specific_asset_id=(model.SpecificAssetId("test1", "test1"), + specific_asset_id)) + with self.assertRaises(model.AASConstraintViolation) as cm: + del asset_information.specific_asset_id[:] + self.assertEqual("An AssetInformation has to have a globalAssetId or a specificAssetId (Constraint AASd-131)", + str(cm.exception)) + with self.assertRaises(model.AASConstraintViolation) as cm: + asset_information.specific_asset_id.clear() + self.assertEqual("An AssetInformation has to have a globalAssetId or a specificAssetId (Constraint AASd-131)", + str(cm.exception)) + self.assertIsNot(asset_information.specific_asset_id[0], specific_asset_id) + del asset_information.specific_asset_id[0] + self.assertIs(asset_information.specific_asset_id[0], specific_asset_id) + with self.assertRaises(model.AASConstraintViolation) as cm: + del asset_information.specific_asset_id[0] + self.assertEqual("An AssetInformation has to have a globalAssetId or a specificAssetId (Constraint AASd-131)", + str(cm.exception)) diff --git a/test/model/test_submodel.py b/test/model/test_submodel.py index bbd672257..74c12328d 100644 --- a/test/model/test_submodel.py +++ b/test/model/test_submodel.py @@ -12,34 +12,133 @@ class EntityTest(unittest.TestCase): + def test_aasd_014_init_self_managed(self) -> None: + with self.assertRaises(model.AASConstraintViolation) as cm: + model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY) + self.assertEqual("A self-managed entity has to have a globalAssetId or a specificAssetId (Constraint AASd-014)", + str(cm.exception)) + model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY, global_asset_id="https://acplt.org/TestAsset") + model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY, + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY, global_asset_id="https://acplt.org/TestAsset", + specific_asset_id=(model.SpecificAssetId("test", "test"),)) - def test_set_entity(self): + def test_aasd_014_init_co_managed(self) -> None: + model.Entity("TestEntity", model.EntityType.CO_MANAGED_ENTITY) with self.assertRaises(model.AASConstraintViolation) as cm: - obj = model.Entity(id_short='Test', entity_type=model.EntityType.SELF_MANAGED_ENTITY, statement=()) - self.assertIn( - 'A self-managed entity has to have a globalAssetId or a specificAssetId', - str(cm.exception) - ) + model.Entity("TestEntity", model.EntityType.CO_MANAGED_ENTITY, + global_asset_id="https://acplt.org/TestAsset") + self.assertEqual("A co-managed entity has to have neither a globalAssetId nor a specificAssetId " + "(Constraint AASd-014)", str(cm.exception)) with self.assertRaises(model.AASConstraintViolation) as cm: - obj2 = model.Entity(id_short='Test', entity_type=model.EntityType.CO_MANAGED_ENTITY, - global_asset_id='http://acplt.org/TestAsset/', - statement=()) - self.assertIn( - 'A co-managed entity has to have neither a globalAssetId nor a specificAssetId', - str(cm.exception) - ) + model.Entity("TestEntity", model.EntityType.CO_MANAGED_ENTITY, + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + self.assertEqual("A co-managed entity has to have neither a globalAssetId nor a specificAssetId " + "(Constraint AASd-014)", str(cm.exception)) + with self.assertRaises(model.AASConstraintViolation) as cm: + model.Entity("TestEntity", model.EntityType.CO_MANAGED_ENTITY, + global_asset_id="https://acplt.org/TestAsset", + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + self.assertEqual("A co-managed entity has to have neither a globalAssetId nor a specificAssetId " + "(Constraint AASd-014)", str(cm.exception)) + + def test_aasd_014_set_self_managed(self) -> None: + entity = model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY, + global_asset_id="https://acplt.org/TestAsset", + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + entity.global_asset_id = None + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.specific_asset_id = model.ConstrainedList(()) + self.assertEqual("A self-managed entity has to have a globalAssetId or a specificAssetId (Constraint AASd-014)", + str(cm.exception)) + + entity = model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY, + global_asset_id="https://acplt.org/TestAsset", + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + entity.specific_asset_id = model.ConstrainedList(()) + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.global_asset_id = None + self.assertEqual("A self-managed entity has to have a globalAssetId or a specificAssetId (Constraint AASd-014)", + str(cm.exception)) + + def test_aasd_014_set_co_managed(self) -> None: + entity = model.Entity("TestEntity", model.EntityType.CO_MANAGED_ENTITY) + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.global_asset_id = "https://acplt.org/TestAsset" + self.assertEqual("A co-managed entity has to have neither a globalAssetId nor a specificAssetId " + "(Constraint AASd-014)", str(cm.exception)) + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.specific_asset_id = model.ConstrainedList((model.SpecificAssetId("test", "test"),)) + self.assertEqual("A co-managed entity has to have neither a globalAssetId nor a specificAssetId " + "(Constraint AASd-014)", str(cm.exception)) + + def test_aasd_014_specific_asset_id_add_self_managed(self) -> None: + entity = model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY, + global_asset_id="https://acplt.org/TestAsset") + specific_asset_id1 = model.SpecificAssetId("test", "test") + specific_asset_id2 = model.SpecificAssetId("test", "test") + entity.specific_asset_id.append(specific_asset_id1) + entity.specific_asset_id.extend((specific_asset_id2,)) + self.assertIs(entity.specific_asset_id[0], specific_asset_id1) + self.assertIs(entity.specific_asset_id[1], specific_asset_id2) + + def test_aasd_014_specific_asset_id_add_co_managed(self) -> None: + entity = model.Entity("TestEntity", model.EntityType.CO_MANAGED_ENTITY) + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.specific_asset_id.append(model.SpecificAssetId("test", "test")) + self.assertEqual("A co-managed entity has to have neither a globalAssetId nor a specificAssetId " + "(Constraint AASd-014)", str(cm.exception)) + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.specific_asset_id.extend((model.SpecificAssetId("test", "test"),)) + self.assertEqual("A co-managed entity has to have neither a globalAssetId nor a specificAssetId " + "(Constraint AASd-014)", str(cm.exception)) + + def test_assd_014_specific_asset_id_set_self_managed(self) -> None: + entity = model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY, + specific_asset_id=(model.SpecificAssetId("test", "test"),)) + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.specific_asset_id[:] = () + self.assertEqual("A self-managed entity has to have a globalAssetId or a specificAssetId (Constraint AASd-014)", + str(cm.exception)) + specific_asset_id = model.SpecificAssetId("test", "test") + self.assertIsNot(entity.specific_asset_id[0], specific_asset_id) + entity.specific_asset_id[:] = (specific_asset_id,) + self.assertIs(entity.specific_asset_id[0], specific_asset_id) + entity.specific_asset_id[0] = model.SpecificAssetId("test", "test") + self.assertIsNot(entity.specific_asset_id[0], specific_asset_id) + + def test_assd_014_specific_asset_id_set_co_managed(self) -> None: + entity = model.Entity("TestEntity", model.EntityType.CO_MANAGED_ENTITY) + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.specific_asset_id[:] = (model.SpecificAssetId("test", "test"),) + self.assertEqual("A co-managed entity has to have neither a globalAssetId nor a specificAssetId " + "(Constraint AASd-014)", str(cm.exception)) + entity.specific_asset_id[:] = () + + def test_aasd_014_specific_asset_id_del_self_managed(self) -> None: + specific_asset_id = model.SpecificAssetId("test", "test") + entity = model.Entity("TestEntity", model.EntityType.SELF_MANAGED_ENTITY, + specific_asset_id=(model.SpecificAssetId("test", "test"), + specific_asset_id)) + with self.assertRaises(model.AASConstraintViolation) as cm: + del entity.specific_asset_id[:] + self.assertEqual("A self-managed entity has to have a globalAssetId or a specificAssetId (Constraint AASd-014)", + str(cm.exception)) + with self.assertRaises(model.AASConstraintViolation) as cm: + entity.specific_asset_id.clear() + self.assertEqual("A self-managed entity has to have a globalAssetId or a specificAssetId (Constraint AASd-014)", + str(cm.exception)) + self.assertIsNot(entity.specific_asset_id[0], specific_asset_id) + del entity.specific_asset_id[0] + self.assertIs(entity.specific_asset_id[0], specific_asset_id) + with self.assertRaises(model.AASConstraintViolation) as cm: + del entity.specific_asset_id[0] + self.assertEqual("A self-managed entity has to have a globalAssetId or a specificAssetId (Constraint AASd-014)", + str(cm.exception)) - specific_asset_id = {model.SpecificAssetId(name="TestKey", - value="TestValue", - external_subject_id=model.ExternalReference((model.Key( - type_=model.KeyTypes.GLOBAL_REFERENCE, - value='http://acplt.org/SpecificAssetId/'),)))} - with self.assertRaises(model.AASConstraintViolation) as cm: - obj3 = model.Entity(id_short='Test', entity_type=model.EntityType.CO_MANAGED_ENTITY, - specific_asset_id=specific_asset_id, statement=()) - self.assertIn( - 'A co-managed entity has to have neither a globalAssetId nor a specificAssetId', - str(cm.exception)) + def test_aasd_014_specific_asset_id_del_co_managed(self) -> None: + entity = model.Entity("TestEntity", model.EntityType.CO_MANAGED_ENTITY) + del entity.specific_asset_id[:] class PropertyTest(unittest.TestCase): From 9ed57047de6e82b724ae08869ed95d39ed84e396 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 7 Nov 2023 17:39:20 +0100 Subject: [PATCH 22/25] Refactor `Entity` and `AssetInformation` - Refactor hook funcs param names - Set all private attributes, bypassing setters. - Place setting of `Entity._global_asset_id` ahead of `Entity._specific_asset_id` to set item_add_hook directly in the ConstrainedList initialization - Rename `Entity._validate_asset_ids_for_entity_type` to `Entity._validate_asset_ids` - Place check of `global_asset_id` value into `_validate_asset_ids` - Run `_validate_asset_ids` at the end of init --- basyx/aas/model/aas.py | 32 ++++++++++---------- basyx/aas/model/submodel.py | 58 ++++++++++++++++++------------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index bf742eef7..aa7b63f05 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -60,24 +60,26 @@ def __init__(self, super().__init__() self.asset_kind: base.AssetKind = asset_kind - self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ - base.ConstrainedList(specific_asset_id, item_set_hook=self._check_constraint_set_spec_asset_id, - item_del_hook=self._check_constraint_del_spec_asset_id) - self._global_asset_id: Optional[base.Identifier] - # AASd-131 is validated via the global_asset_id setter - self.global_asset_id = global_asset_id self.asset_type: Optional[base.Identifier] = asset_type self.default_thumbnail: Optional[base.Resource] = default_thumbnail - - def _check_constraint_set_spec_asset_id(self, old: List[base.SpecificAssetId], new: List[base.SpecificAssetId], - list_: List[base.SpecificAssetId]) -> None: + # assign private attributes, bypassing setters, as constraints will be checked below + self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = base.ConstrainedList( + specific_asset_id, + item_set_hook=self._check_constraint_set_spec_asset_id, + item_del_hook=self._check_constraint_del_spec_asset_id + ) + self._global_asset_id: Optional[base.Identifier] = global_asset_id + self._validate_asset_ids(global_asset_id, bool(specific_asset_id)) + + def _check_constraint_set_spec_asset_id(self, items_to_replace: List[base.SpecificAssetId], + new_items: List[base.SpecificAssetId], + old_list: List[base.SpecificAssetId]) -> None: self._validate_asset_ids(self.global_asset_id, - # whether the list is nonempty after the set operation - len(old) < len(list_) or len(new) > 0) + len(old_list) - len(items_to_replace) + len(new_items) > 0) def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, - list_: List[base.SpecificAssetId]) -> None: - self._validate_asset_ids(self.global_asset_id, len(list_) > 1) + old_list: List[base.SpecificAssetId]) -> None: + self._validate_asset_ids(self.global_asset_id, len(old_list) > 1) @property def global_asset_id(self) -> Optional[base.Identifier]: @@ -86,8 +88,6 @@ def global_asset_id(self) -> Optional[base.Identifier]: @global_asset_id.setter def global_asset_id(self, global_asset_id: Optional[base.Identifier]) -> None: self._validate_asset_ids(global_asset_id, bool(self.specific_asset_id)) - if global_asset_id is not None: - _string_constraints.check_identifier(global_asset_id) self._global_asset_id = global_asset_id @property @@ -104,6 +104,8 @@ def _validate_asset_ids(global_asset_id: Optional[base.Identifier], specific_ass if global_asset_id is None and not specific_asset_id_nonempty: raise base.AASConstraintViolation(131, "An AssetInformation has to have a globalAssetId or a specificAssetId") + if global_asset_id is not None: + _string_constraints.check_identifier(global_asset_id) def __repr__(self) -> str: return "AssetInformation(assetKind={}, globalAssetId={}, specificAssetId={}, assetType={}, " \ diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index c228cf9e2..36fdce48f 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1106,33 +1106,30 @@ def __init__(self, super().__init__(id_short, display_name, category, description, parent, semantic_id, qualifier, extension, supplemental_semantic_id, embedded_data_specifications) self.statement = base.NamespaceSet(self, [("id_short", True)], statement) - self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = \ - base.ConstrainedList(specific_asset_id, item_set_hook=self._check_constraint_set_spec_asset_id, - item_del_hook=self._check_constraint_del_spec_asset_id) # assign private attributes, bypassing setters, as constraints will be checked below self._entity_type: base.EntityType = entity_type - # add item_add_hook after items have been added, because checking the constraints requires the global_asset_id - # to be set - self._specific_asset_id._item_add_hook = self._check_constraint_add_spec_asset_id - # use setter for global_asset_id, as it also checks the string constraint, - # which hasn't been checked at this point - # furthermore, the setter also validates AASd-014 - self._global_asset_id: Optional[base.Identifier] - self.global_asset_id = global_asset_id - - def _check_constraint_add_spec_asset_id(self, _new: base.SpecificAssetId, _list: List[base.SpecificAssetId]) \ - -> None: - self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, True) - - def _check_constraint_set_spec_asset_id(self, old: List[base.SpecificAssetId], new: List[base.SpecificAssetId], - list_: List[base.SpecificAssetId]) -> None: - self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, - # whether the list is nonempty after the set operation - len(old) < len(list_) or len(new) > 0) + self._global_asset_id: Optional[base.Identifier] = global_asset_id + self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = base.ConstrainedList( + specific_asset_id, + item_add_hook=self._check_constraint_add_spec_asset_id, + item_set_hook=self._check_constraint_set_spec_asset_id, + item_del_hook=self._check_constraint_del_spec_asset_id + ) + self._validate_asset_ids(entity_type, global_asset_id, bool(specific_asset_id)) + + def _check_constraint_add_spec_asset_id(self, _new_item: base.SpecificAssetId, + _old_list: List[base.SpecificAssetId]) -> None: + self._validate_asset_ids(self.entity_type, self.global_asset_id, True) + + def _check_constraint_set_spec_asset_id(self, items_to_replace: List[base.SpecificAssetId], + new_items: List[base.SpecificAssetId], + old_list: List[base.SpecificAssetId]) -> None: + self._validate_asset_ids(self.entity_type, self.global_asset_id, + len(old_list) - len(items_to_replace) + len(new_items) > 0) def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, - list_: List[base.SpecificAssetId]) -> None: - self._validate_asset_ids_for_entity_type(self.entity_type, self.global_asset_id, len(list_) > 1) + old_list: List[base.SpecificAssetId]) -> None: + self._validate_asset_ids(self.entity_type, self.global_asset_id, len(old_list) > 1) @property def entity_type(self) -> base.EntityType: @@ -1140,7 +1137,7 @@ def entity_type(self) -> base.EntityType: @entity_type.setter def entity_type(self, entity_type: base.EntityType) -> None: - self._validate_asset_ids_for_entity_type(entity_type, self.global_asset_id, bool(self.specific_asset_id)) + self._validate_asset_ids(entity_type, self.global_asset_id, bool(self.specific_asset_id)) self._entity_type = entity_type @property @@ -1149,9 +1146,7 @@ def global_asset_id(self) -> Optional[base.Identifier]: @global_asset_id.setter def global_asset_id(self, global_asset_id: Optional[base.Identifier]) -> None: - self._validate_asset_ids_for_entity_type(self.entity_type, global_asset_id, bool(self.specific_asset_id)) - if global_asset_id is not None: - _string_constraints.check_identifier(global_asset_id) + self._validate_asset_ids(self.entity_type, global_asset_id, bool(self.specific_asset_id)) self._global_asset_id = global_asset_id @property @@ -1164,9 +1159,9 @@ def specific_asset_id(self, specific_asset_id: Iterable[base.SpecificAssetId]) - self._specific_asset_id[:] = specific_asset_id @staticmethod - def _validate_asset_ids_for_entity_type(entity_type: base.EntityType, - global_asset_id: Optional[base.Identifier], - specific_asset_id_nonempty: bool) -> None: + def _validate_asset_ids(entity_type: base.EntityType, + global_asset_id: Optional[base.Identifier], + specific_asset_id_nonempty: bool) -> None: if entity_type == base.EntityType.SELF_MANAGED_ENTITY and global_asset_id is None \ and not specific_asset_id_nonempty: raise base.AASConstraintViolation( @@ -1176,6 +1171,9 @@ def _validate_asset_ids_for_entity_type(entity_type: base.EntityType, raise base.AASConstraintViolation( 14, "A co-managed entity has to have neither a globalAssetId nor a specificAssetId") + if global_asset_id is not None: + _string_constraints.check_identifier(global_asset_id) + class EventElement(SubmodelElement, metaclass=abc.ABCMeta): """ From 0222237888f92cd003260d92a7a2bcca5352ca6e Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 7 Nov 2023 18:56:02 +0100 Subject: [PATCH 23/25] Refactor `Entity` and `AssetInformation` - Bundle check methods together at the end of classes - Extract validation of `global_asset_id` to `_validate_asset_ids()` - Rename `Entity._validate_asset_ids()` to `_validate_aasd_014` and `AssetInformation._validate_asset_ids()` to `_validate_aasd_131`, as the methods only validate these constraints and not all asset ids --- basyx/aas/model/aas.py | 33 ++++++++++++++---------- basyx/aas/model/submodel.py | 50 ++++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index aa7b63f05..9edf891ea 100644 --- a/basyx/aas/model/aas.py +++ b/basyx/aas/model/aas.py @@ -69,17 +69,8 @@ def __init__(self, item_del_hook=self._check_constraint_del_spec_asset_id ) self._global_asset_id: Optional[base.Identifier] = global_asset_id - self._validate_asset_ids(global_asset_id, bool(specific_asset_id)) - - def _check_constraint_set_spec_asset_id(self, items_to_replace: List[base.SpecificAssetId], - new_items: List[base.SpecificAssetId], - old_list: List[base.SpecificAssetId]) -> None: - self._validate_asset_ids(self.global_asset_id, - len(old_list) - len(items_to_replace) + len(new_items) > 0) - - def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, - old_list: List[base.SpecificAssetId]) -> None: - self._validate_asset_ids(self.global_asset_id, len(old_list) > 1) + self._validate_global_asset_id(global_asset_id) + self._validate_aasd_131(global_asset_id, bool(specific_asset_id)) @property def global_asset_id(self) -> Optional[base.Identifier]: @@ -87,7 +78,8 @@ def global_asset_id(self) -> Optional[base.Identifier]: @global_asset_id.setter def global_asset_id(self, global_asset_id: Optional[base.Identifier]) -> None: - self._validate_asset_ids(global_asset_id, bool(self.specific_asset_id)) + self._validate_global_asset_id(global_asset_id) + self._validate_aasd_131(global_asset_id, bool(self.specific_asset_id)) self._global_asset_id = global_asset_id @property @@ -99,8 +91,23 @@ def specific_asset_id(self, specific_asset_id: Iterable[base.SpecificAssetId]) - # constraints are checked via _check_constraint_set_spec_asset_id() in this case self._specific_asset_id[:] = specific_asset_id + def _check_constraint_set_spec_asset_id(self, items_to_replace: List[base.SpecificAssetId], + new_items: List[base.SpecificAssetId], + old_list: List[base.SpecificAssetId]) -> None: + self._validate_aasd_131(self.global_asset_id, + len(old_list) - len(items_to_replace) + len(new_items) > 0) + + def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, + old_list: List[base.SpecificAssetId]) -> None: + self._validate_aasd_131(self.global_asset_id, len(old_list) > 1) + + @staticmethod + def _validate_global_asset_id(global_asset_id: Optional[base.Identifier]) -> None: + if global_asset_id is not None: + _string_constraints.check_identifier(global_asset_id) + @staticmethod - def _validate_asset_ids(global_asset_id: Optional[base.Identifier], specific_asset_id_nonempty: bool) -> None: + def _validate_aasd_131(global_asset_id: Optional[base.Identifier], specific_asset_id_nonempty: bool) -> None: if global_asset_id is None and not specific_asset_id_nonempty: raise base.AASConstraintViolation(131, "An AssetInformation has to have a globalAssetId or a specificAssetId") diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 36fdce48f..af9c074fe 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -1115,21 +1115,8 @@ def __init__(self, item_set_hook=self._check_constraint_set_spec_asset_id, item_del_hook=self._check_constraint_del_spec_asset_id ) - self._validate_asset_ids(entity_type, global_asset_id, bool(specific_asset_id)) - - def _check_constraint_add_spec_asset_id(self, _new_item: base.SpecificAssetId, - _old_list: List[base.SpecificAssetId]) -> None: - self._validate_asset_ids(self.entity_type, self.global_asset_id, True) - - def _check_constraint_set_spec_asset_id(self, items_to_replace: List[base.SpecificAssetId], - new_items: List[base.SpecificAssetId], - old_list: List[base.SpecificAssetId]) -> None: - self._validate_asset_ids(self.entity_type, self.global_asset_id, - len(old_list) - len(items_to_replace) + len(new_items) > 0) - - def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, - old_list: List[base.SpecificAssetId]) -> None: - self._validate_asset_ids(self.entity_type, self.global_asset_id, len(old_list) > 1) + self._validate_global_asset_id(global_asset_id) + self._validate_aasd_014(entity_type, global_asset_id, bool(specific_asset_id)) @property def entity_type(self) -> base.EntityType: @@ -1137,7 +1124,7 @@ def entity_type(self) -> base.EntityType: @entity_type.setter def entity_type(self, entity_type: base.EntityType) -> None: - self._validate_asset_ids(entity_type, self.global_asset_id, bool(self.specific_asset_id)) + self._validate_aasd_014(entity_type, self.global_asset_id, bool(self.specific_asset_id)) self._entity_type = entity_type @property @@ -1146,7 +1133,8 @@ def global_asset_id(self) -> Optional[base.Identifier]: @global_asset_id.setter def global_asset_id(self, global_asset_id: Optional[base.Identifier]) -> None: - self._validate_asset_ids(self.entity_type, global_asset_id, bool(self.specific_asset_id)) + self._validate_global_asset_id(global_asset_id) + self._validate_aasd_014(self.entity_type, global_asset_id, bool(self.specific_asset_id)) self._global_asset_id = global_asset_id @property @@ -1158,10 +1146,29 @@ def specific_asset_id(self, specific_asset_id: Iterable[base.SpecificAssetId]) - # constraints are checked via _check_constraint_set_spec_asset_id() in this case self._specific_asset_id[:] = specific_asset_id + def _check_constraint_add_spec_asset_id(self, _new_item: base.SpecificAssetId, + _old_list: List[base.SpecificAssetId]) -> None: + self._validate_aasd_014(self.entity_type, self.global_asset_id, True) + + def _check_constraint_set_spec_asset_id(self, items_to_replace: List[base.SpecificAssetId], + new_items: List[base.SpecificAssetId], + old_list: List[base.SpecificAssetId]) -> None: + self._validate_aasd_014(self.entity_type, self.global_asset_id, + len(old_list) - len(items_to_replace) + len(new_items) > 0) + + def _check_constraint_del_spec_asset_id(self, _item_to_del: base.SpecificAssetId, + old_list: List[base.SpecificAssetId]) -> None: + self._validate_aasd_014(self.entity_type, self.global_asset_id, len(old_list) > 1) + @staticmethod - def _validate_asset_ids(entity_type: base.EntityType, - global_asset_id: Optional[base.Identifier], - specific_asset_id_nonempty: bool) -> None: + def _validate_global_asset_id(global_asset_id: Optional[base.Identifier]) -> None: + if global_asset_id is not None: + _string_constraints.check_identifier(global_asset_id) + + @staticmethod + def _validate_aasd_014(entity_type: base.EntityType, + global_asset_id: Optional[base.Identifier], + specific_asset_id_nonempty: bool) -> None: if entity_type == base.EntityType.SELF_MANAGED_ENTITY and global_asset_id is None \ and not specific_asset_id_nonempty: raise base.AASConstraintViolation( @@ -1171,9 +1178,6 @@ def _validate_asset_ids(entity_type: base.EntityType, raise base.AASConstraintViolation( 14, "A co-managed entity has to have neither a globalAssetId nor a specificAssetId") - if global_asset_id is not None: - _string_constraints.check_identifier(global_asset_id) - class EventElement(SubmodelElement, metaclass=abc.ABCMeta): """ From a0184e567667cf22a7b59045efcbea5c349dd7a7 Mon Sep 17 00:00:00 2001 From: zrgt Date: Wed, 8 Nov 2023 17:38:53 +0100 Subject: [PATCH 24/25] Refactor NamespaceSet - Refactored `NamespaceSet.add()` as too big - Extracted some methods, in particular `_execute_item_del_hook`. I used the method also in other places - As we check different constraints for uniqueness in the namespace, I defined ATTRIBUTES_CONSTRAINT_IDS. The dict will be used when throwing exception. The solution with the dict is temporary, we need other solution here. --- basyx/aas/model/base.py | 120 ++++++++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 47 deletions(-) diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index 5677fb19b..cfc6dd91f 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -1769,6 +1769,14 @@ def remove_object_by_semantic_id(self, semantic_id: Reference) -> None: ATTRIBUTE_TYPES = Union[NameType, Reference, QualifierType] +# TODO: Find a better solution for providing constraint ids +ATTRIBUTES_CONSTRAINT_IDS = { + "id_short": 117, # Referable, + "type": 21, # Qualifier, + "name": 77, # Extension, + "semantic_id": 134, # model.OperationVariable +} + class NamespaceSet(MutableSet[_NSO], Generic[_NSO]): """ @@ -1868,35 +1876,64 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[_NSO]: return iter(next(iter(self._backend.values()))[0].values()) - def add(self, value: _NSO): - if value.parent is not None and value.parent is not self.parent: - raise ValueError("Object has already a parent, but it must not be part of two namespaces.") + def add(self, element: _NSO): + if element.parent is not None and element.parent is not self.parent: + raise ValueError("Object has already a parent; it cannot belong to two namespaces.") # TODO remove from current parent instead (allow moving)? - if self._item_id_set_hook is not None: - self._item_id_set_hook(value) + + self._execute_item_id_set_hook(element) + self._validate_namespace_constraints(element) + self._execute_item_add_hook(element) + + element.parent = self.parent + for key_attr_name, (backend, case_sensitive) in self._backend.items(): + backend[self._get_attribute(element, key_attr_name, case_sensitive)] = element + + def _validate_namespace_constraints(self, element: _NSO): for set_ in self.parent.namespace_element_sets: - for attr_name, (backend, case_sensitive) in set_._backend.items(): - if hasattr(value, attr_name): - attr_value = self._get_attribute(value, attr_name, case_sensitive) - if attr_value is None: - raise AASConstraintViolation(117, f"{value!r} has attribute {attr_name}=None, which is not " - f"allowed within a {self.parent.__class__.__name__}!") - if attr_value in backend: - raise AASConstraintViolation(22, "Object with attribute (name='{}', value='{}') is already " - "present in {}" - .format(attr_name, str(getattr(value, attr_name)), - "this set of objects" - if set_ is self else "another set in the same namespace")) + for key_attr_name, (backend_dict, case_sensitive) in set_._backend.items(): + if hasattr(element, key_attr_name): + key_attr_value = self._get_attribute(element, key_attr_name, case_sensitive) + self._check_attr_is_not_none(element, key_attr_name, key_attr_value) + self._check_value_is_not_in_backend(key_attr_name, key_attr_value, backend_dict, set_) + + def _check_attr_is_not_none(self, element: _NSO, attr_name: str, attr): + if attr is None: + if attr_name == "id_short": + raise AASConstraintViolation(117, f"{element!r} has attribute {attr_name}=None, " + f"which is not allowed within a {self.parent.__class__.__name__}!") + else: + raise ValueError(f"{element!r} has attribute {attr_name}=None, which is not allowed!") + + def _check_value_is_not_in_backend(self, attr_name: str, attr, backend_dict: Dict, set_: "NamespaceSet"): + if attr in backend_dict: + if set_ is self: + text = f"Object with attribute (name='{attr_name}', value='{attr}') " \ + f"is already present in this set of objects" + else: + text = f"Object with attribute (name='{attr_name}', value='{attr}') " \ + f"is already present in another set in the same namespace" + raise AASConstraintViolation(ATTRIBUTES_CONSTRAINT_IDS.get(attr_name, 0), text) + + def _execute_item_id_set_hook(self, element: _NSO): + if self._item_id_set_hook is not None: + self._item_id_set_hook(element) + + def _execute_item_add_hook(self, element: _NSO): if self._item_add_hook is not None: try: - self._item_add_hook(value, self.__iter__()) - except Exception: - if self._item_id_del_hook is not None: - self._item_id_del_hook(value) + self._item_add_hook(element, self.__iter__()) + except Exception as e: + self._execute_item_del_hook(element) raise - value.parent = self.parent - for attr_name, (backend, case_sensitive) in self._backend.items(): - backend[self._get_attribute(value, attr_name, case_sensitive)] = value + + def _execute_item_del_hook(self, element: _NSO): + # parent needs to be unset first, otherwise generated id_shorts cannot be unset + # see SubmodelElementList + if hasattr(element, "parent"): + element.parent = None + if self._item_id_del_hook is not None: + self._item_id_del_hook(element) def remove_by_id(self, attribute_name: str, identifier: ATTRIBUTE_TYPES) -> None: item = self.get_object_by_attribute(attribute_name, identifier) @@ -1904,22 +1941,16 @@ def remove_by_id(self, attribute_name: str, identifier: ATTRIBUTE_TYPES) -> None def remove(self, item: _NSO) -> None: item_found = False - for attr_name, (backend, case_sensitive) in self._backend.items(): - item_in_dict = backend[self._get_attribute(item, attr_name, case_sensitive)] - if item_in_dict is item: + for key_attr_name, (backend_dict, case_sensitive) in self._backend.items(): + key_attr_value = self._get_attribute(item, key_attr_name, case_sensitive) + if backend_dict[key_attr_value] is item: + # item has to be removed from backend before _item_del_hook() is called, + # as the hook may unset the id_short, as in SubmodelElementLists + del backend_dict[key_attr_value] item_found = True - break if not item_found: raise KeyError("Object not found in NamespaceDict") - # parent needs to be unset first, otherwise generated id_shorts cannot be unset - # see SubmodelElementList - item.parent = None - # item has to be removed from backend before _item_del_hook() is called, as the hook may unset the id_short, - # as in SubmodelElementLists - for attr_name, (backend, case_sensitive) in self._backend.items(): - del backend[self._get_attribute(item, attr_name, case_sensitive)] - if self._item_id_del_hook is not None: - self._item_id_del_hook(item) + self._execute_item_del_hook(item) def discard(self, x: _NSO) -> None: if x not in self: @@ -1928,19 +1959,14 @@ def discard(self, x: _NSO) -> None: def pop(self) -> _NSO: _, value = next(iter(self._backend.values()))[0].popitem() - if self._item_id_del_hook is not None: - self._item_id_del_hook(value) + self._execute_item_del_hook(value) value.parent = None return value def clear(self) -> None: for attr_name, (backend, case_sensitive) in self._backend.items(): for value in backend.values(): - # parent needs to be unset first, otherwise generated id_shorts cannot be unset - # see SubmodelElementList - value.parent = None - if self._item_id_del_hook is not None: - self._item_id_del_hook(value) + self._execute_item_del_hook(value) for attr_name, (backend, case_sensitive) in self._backend.items(): backend.clear() @@ -2051,9 +2077,9 @@ def __init__(self, parent: Union[UniqueIdShortNamespace, UniqueSemanticIdNamespa def __iter__(self) -> Iterator[_NSO]: return iter(self._order) - def add(self, value: _NSO): - super().add(value) - self._order.append(value) + def add(self, element: _NSO): + super().add(element) + self._order.append(element) def remove(self, item: Union[Tuple[str, ATTRIBUTE_TYPES], _NSO]): if isinstance(item, tuple): From ddc425b15fce14c35055402ffc1339dc4e1156c2 Mon Sep 17 00:00:00 2001 From: zrgt Date: Wed, 8 Nov 2023 19:16:16 +0100 Subject: [PATCH 25/25] Fix of namespace and its tests - Use correct constraint ids for each NamespaceSet in tests, use 000 if not constraint id is suitable --- basyx/aas/model/base.py | 13 +++++++------ test/model/test_base.py | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index cfc6dd91f..2994a91b9 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -1771,10 +1771,10 @@ def remove_object_by_semantic_id(self, semantic_id: Reference) -> None: # TODO: Find a better solution for providing constraint ids ATTRIBUTES_CONSTRAINT_IDS = { - "id_short": 117, # Referable, + "id_short": 22, # Referable, "type": 21, # Qualifier, "name": 77, # Extension, - "semantic_id": 134, # model.OperationVariable + # "id_short": 134, # model.OperationVariable } @@ -1895,7 +1895,7 @@ def _validate_namespace_constraints(self, element: _NSO): if hasattr(element, key_attr_name): key_attr_value = self._get_attribute(element, key_attr_name, case_sensitive) self._check_attr_is_not_none(element, key_attr_name, key_attr_value) - self._check_value_is_not_in_backend(key_attr_name, key_attr_value, backend_dict, set_) + self._check_value_is_not_in_backend(element, key_attr_name, key_attr_value, backend_dict, set_) def _check_attr_is_not_none(self, element: _NSO, attr_name: str, attr): if attr is None: @@ -1905,13 +1905,14 @@ def _check_attr_is_not_none(self, element: _NSO, attr_name: str, attr): else: raise ValueError(f"{element!r} has attribute {attr_name}=None, which is not allowed!") - def _check_value_is_not_in_backend(self, attr_name: str, attr, backend_dict: Dict, set_: "NamespaceSet"): + def _check_value_is_not_in_backend(self, element: _NSO, attr_name: str, attr, + backend_dict: Dict[ATTRIBUTE_TYPES, _NSO], set_: "NamespaceSet"): if attr in backend_dict: if set_ is self: - text = f"Object with attribute (name='{attr_name}', value='{attr}') " \ + text = f"Object with attribute (name='{attr_name}', value='{getattr(element, attr_name)}') " \ f"is already present in this set of objects" else: - text = f"Object with attribute (name='{attr_name}', value='{attr}') " \ + text = f"Object with attribute (name='{attr_name}', value='{getattr(element, attr_name)}') " \ f"is already present in another set in the same namespace" raise AASConstraintViolation(ATTRIBUTES_CONSTRAINT_IDS.get(attr_name, 0), text) diff --git a/test/model/test_base.py b/test/model/test_base.py index aa40e0199..3fa8a64a3 100644 --- a/test/model/test_base.py +++ b/test/model/test_base.py @@ -374,7 +374,7 @@ def test_NamespaceSet(self) -> None: self.assertEqual( "Object with attribute (name='semantic_id', value='ExternalReference(key=(Key(" "type=GLOBAL_REFERENCE, value=http://acplt.org/Test1),))') is already present in this set of objects " - "(Constraint AASd-022)", + "(Constraint AASd-000)", str(cm.exception)) self.namespace.set2.add(self.prop5) self.namespace.set2.add(self.prop6) @@ -389,7 +389,7 @@ def test_NamespaceSet(self) -> None: self.assertEqual( "Object with attribute (name='semantic_id', value='" "ExternalReference(key=(Key(type=GLOBAL_REFERENCE, value=http://acplt.org/Test1),))')" - " is already present in another set in the same namespace (Constraint AASd-022)", + " is already present in another set in the same namespace (Constraint AASd-000)", str(cm.exception)) self.assertIs(self.prop1, self.namespace.set1.get("id_short", "Prop1")) @@ -456,7 +456,7 @@ def test_NamespaceSet(self) -> None: with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace3.set1.add(self.qualifier1alt) self.assertEqual("Object with attribute (name='type', value='type1') is already present in this set " - "of objects (Constraint AASd-022)", + "of objects (Constraint AASd-021)", str(cm.exception)) def test_namespaceset_hooks(self) -> None: