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/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/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 @@ - + + + + + + + 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/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) diff --git a/basyx/aas/examples/data/example_aas.py b/basyx/aas/examples/data/example_aas.py index 7b0a98541..6d2a39326 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 ' @@ -276,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/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/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)} ) diff --git a/basyx/aas/model/aas.py b/basyx/aas/model/aas.py index c99efc594..9edf891ea 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,30 +54,65 @@ 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: 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._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.asset_type: Optional[base.Identifier] = asset_type self.default_thumbnail: Optional[base.Resource] = default_thumbnail + # 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_global_asset_id(global_asset_id) + self._validate_aasd_131(global_asset_id, bool(specific_asset_id)) - def _get_global_asset_id(self): + @property + def global_asset_id(self) -> Optional[base.Identifier]: 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") - else: - _string_constraints.check_identifier(global_asset_id) + @global_asset_id.setter + def global_asset_id(self, global_asset_id: Optional[base.Identifier]) -> None: + 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 - global_asset_id = property(_get_global_asset_id, _set_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 + + 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_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") + 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/base.py b/basyx/aas/model/base.py index a39f48fe9..2994a91b9 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -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. @@ -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: ... @@ -1516,7 +1520,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 +1541,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] @@ -1765,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": 22, # Referable, + "type": 21, # Qualifier, + "name": 77, # Extension, + # "id_short": 134, # model.OperationVariable +} + class NamespaceSet(MutableSet[_NSO], Generic[_NSO]): """ @@ -1864,35 +1876,65 @@ 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(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: + 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, 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='{getattr(element, attr_name)}') " \ + f"is already present in this set of objects" + else: + 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) + + 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) @@ -1900,22 +1942,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: @@ -1924,19 +1960,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() @@ -2047,9 +2078,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): diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 7ca7d70f4..af9c074fe 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 @@ -1091,7 +1090,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: Iterable[base.SpecificAssetId] = (), display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, description: Optional[base.MultiLanguageTextType] = None, @@ -1107,28 +1106,77 @@ 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.global_asset_id: Optional[base.Identifier] = global_asset_id - self._entity_type: base.EntityType - self.entity_type = entity_type + # assign private attributes, bypassing setters, as constraints will be checked below + self._entity_type: base.EntityType = entity_type + self._global_asset_id: Optional[base.Identifier] = global_asset_id + self._specific_asset_id: base.ConstrainedList[base.SpecificAssetId] = base.ConstrainedList( + specific_asset_id, + 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_global_asset_id(global_asset_id) + self._validate_aasd_014(entity_type, global_asset_id, bool(specific_asset_id)) - 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: - if self.global_asset_id is None and self.specific_asset_id is None \ - and entity_type == base.EntityType.SELF_MANAGED_ENTITY: - 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: - raise base.AASConstraintViolation( - 14, - "A co-managed entity has to have neither a globalAssetId nor a specificAssetId") + @entity_type.setter + def entity_type(self, entity_type: base.EntityType) -> None: + self._validate_aasd_014(entity_type, self.global_asset_id, bool(self.specific_asset_id)) self._entity_type = entity_type - entity_type = property(_get_entity_type, _set_entity_type) + @property + 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]) -> None: + 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 + 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 + + 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_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( + 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 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") class EventElement(SubmodelElement, metaclass=abc.ABCMeta): 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_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_base.py b/test/model/test_base.py index ef894de50..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: @@ -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]) 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..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):