Skip to content

Commit

Permalink
make OperationVariable a Namespace
Browse files Browse the repository at this point in the history
This commit makes `OperationVariable` inherit from
`UniqueIdShortNamespace`, to implement Constraint AASd-134:

For an Operation, the idShort of all inputVariable/value,
outputVariable/value, and inoutputVariable/value shall be unique.

In the DotAAS spec, the attributes `inputVariable`, `outputVariable`
and `inoutputVariable` of `Operation` are defined to be a collection of
`OperationVariable` instances, which themselves just contain a single
`SubmodelElement`. Thus, the `OperationVariable` isn't really required
for `Operation`, as the `Operation` can just contain the
`SubmodelElements` directly, without an unnecessary wrapper. This makes
`Operation` less tedious to use and also allows us to use normal
`NamespaceSets` for the 3 attributes, which together with the
`UniqueIdShortNamespace` ensure, that the `idShort` of all contained
`SubmodelElements` is unique across all 3 attributes.

Aside this, the examples are updated since `SubmodelElements` as
children of an `Operation` are now linked to the parent. This prevents
us from reusing other `SubmodelElements` as `OperationVariables` as it
was done previously, since each `SubmodelElement` can only have one
parent.

Fix eclipse-basyx#146 eclipse-basyx#148
  • Loading branch information
jkhsjdhjs committed Nov 3, 2023
1 parent 9d01620 commit d4a2c6e
Show file tree
Hide file tree
Showing 16 changed files with 355 additions and 282 deletions.
12 changes: 7 additions & 5 deletions basyx/aas/adapter/json/json_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,14 @@ def _construct_administrative_information(
return ret

@classmethod
def _construct_operation_variable(
cls, dct: Dict[str, object], object_class=model.OperationVariable) -> model.OperationVariable:
def _construct_operation_variable(cls, dct: Dict[str, object]) -> model.SubmodelElement:
"""
Since we don't implement `OperationVariable`, this constructor discards the wrapping `OperationVariable` object
and just returns the contained :class:`~aas.model.submodel.SubmodelElement`.
"""
# TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T]
# see https://github.com/python/mypy/issues/5374
ret = object_class(value=_get_ts(dct, 'value', model.SubmodelElement)) # type: ignore
return ret
return _get_ts(dct, 'value', model.SubmodelElement) # type: ignore

@classmethod
def _construct_lang_string_set(cls, lst: List[Dict[str, object]], object_class: Type[LSS]) -> LSS:
Expand Down Expand Up @@ -590,7 +592,7 @@ def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operati
if json_name in dct:
for variable_data in _get_ts(dct, json_name, list):
try:
target.append(cls._construct_operation_variable(variable_data))
target.add(cls._construct_operation_variable(variable_data))
except (KeyError, TypeError) as e:
error_message = "Error while trying to convert JSON object into {} of {}: {}".format(
json_name, ret, pprint.pformat(variable_data, depth=2, width=2 ** 14, compact=True))
Expand Down
27 changes: 13 additions & 14 deletions basyx/aas/adapter/json/json_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"""
import base64
import inspect
from typing import List, Dict, IO, Optional, Type, Callable
from typing import List, Dict, IO, Iterable, Optional, Type, Callable
import json

from basyx.aas import model
Expand Down Expand Up @@ -79,7 +79,6 @@ def default(self, obj: object) -> object:
model.LangStringSet: self._lang_string_set_to_json,
model.MultiLanguageProperty: self._multi_language_property_to_json,
model.Operation: self._operation_to_json,
model.OperationVariable: self._operation_variable_to_json,
model.Property: self._property_to_json,
model.Qualifier: self._qualifier_to_json,
model.Range: self._range_to_json,
Expand Down Expand Up @@ -576,16 +575,17 @@ def _annotated_relationship_element_to_json(cls, obj: model.AnnotatedRelationshi
return data

@classmethod
def _operation_variable_to_json(cls, obj: model.OperationVariable) -> Dict[str, object]:
def _operation_variables_to_json(cls, obj: Iterable[model.SubmodelElement]) -> List[Dict[str, object]]:
"""
serialization of an object from class OperationVariable to json
Since we don't implement the `OperationVariable` class, which is just a wrapper for a single
:class:`~aas.model.submodel.SubmodelElement`, elements are serialized as the `value` attribute of an
`operationVariable` object.
:param obj: object of class OperationVariable
:return: dict with the serialized attributes of this object
:param obj: object of class `SubmodelElement`
:return: list of `OperationVariable` wrappers containing the serialized `SubmodelElement`
"""
data = cls._abstract_classes_to_json(obj)
data['value'] = obj.value
return data
return [{'value': se} for se in obj]

@classmethod
def _operation_to_json(cls, obj: model.Operation) -> Dict[str, object]:
Expand All @@ -596,12 +596,11 @@ def _operation_to_json(cls, obj: model.Operation) -> Dict[str, object]:
:return: dict with the serialized attributes of this object
"""
data = cls._abstract_classes_to_json(obj)
if obj.input_variable:
data['inputVariables'] = list(obj.input_variable)
if obj.output_variable:
data['outputVariables'] = list(obj.output_variable)
if obj.in_output_variable:
data['inoutputVariables'] = list(obj.in_output_variable)
for tag, nss in (('inputVariables', obj.input_variable),
('outputVariables', obj.output_variable),
('inoutputVariables', obj.in_output_variable)):
if nss:
data[tag] = cls._operation_variables_to_json(nss)
return data

@classmethod
Expand Down
53 changes: 22 additions & 31 deletions basyx/aas/adapter/xml/xml_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,20 @@ def _construct_referable_reference(cls, element: etree.Element, **kwargs: Any) \
# see https://github.com/python/mypy/issues/5374
return cls.construct_model_reference_expect_type(element, model.Referable, **kwargs) # type: ignore

@classmethod
def _construct_operation_variable(cls, element: etree.Element, **kwargs: Any) -> model.SubmodelElement:
"""
Since we don't implement `OperationVariable`, this constructor discards the wrapping `aas:operationVariable`
and `aas:value` and just returns the contained :class:`~aas.model.submodel.SubmodelElement`.
"""
value = _get_child_mandatory(element, NS_AAS + "value")
if len(value) == 0:
raise KeyError(f"{_element_pretty_identifier(value)} has no submodel element!")
if len(value) > 1:
logger.warning(f"{_element_pretty_identifier(value)} has more than one submodel element, "
"using the first one...")
return cls.construct_submodel_element(value[0], **kwargs)

@classmethod
def construct_key(cls, element: etree.Element, object_class=model.Key, **_kwargs: Any) \
-> model.Key:
Expand Down Expand Up @@ -722,19 +736,6 @@ def construct_data_element(cls, element: etree.Element, abstract_class_name: str
raise KeyError(_element_pretty_identifier(element) + f" is not a valid {abstract_class_name}!")
return data_elements[element.tag](element, **kwargs)

@classmethod
def construct_operation_variable(cls, element: etree.Element, object_class=model.OperationVariable,
**_kwargs: Any) -> model.OperationVariable:
value = _get_child_mandatory(element, NS_AAS + "value")
if len(value) == 0:
raise KeyError(f"{_element_pretty_identifier(value)} has no submodel element!")
if len(value) > 1:
logger.warning(f"{_element_pretty_identifier(value)} has more than one submodel element, "
"using the first one...")
return object_class(
_failsafe_construct_mandatory(value[0], cls.construct_submodel_element)
)

@classmethod
def construct_annotated_relationship_element(cls, element: etree.Element,
object_class=model.AnnotatedRelationshipElement, **_kwargs: Any) \
Expand Down Expand Up @@ -856,21 +857,14 @@ def construct_multi_language_property(cls, element: etree.Element, object_class=
def construct_operation(cls, element: etree.Element, object_class=model.Operation, **_kwargs: Any) \
-> model.Operation:
operation = object_class(None)
input_variables = element.find(NS_AAS + "inputVariables")
if input_variables is not None:
for input_variable in _child_construct_multiple(input_variables, NS_AAS + "operationVariable",
cls.construct_operation_variable, cls.failsafe):
operation.input_variable.append(input_variable)
output_variables = element.find(NS_AAS + "outputVariables")
if output_variables is not None:
for output_variable in _child_construct_multiple(output_variables, NS_AAS + "operationVariable",
cls.construct_operation_variable, cls.failsafe):
operation.output_variable.append(output_variable)
in_output_variables = element.find(NS_AAS + "inoutputVariables")
if in_output_variables is not None:
for in_output_variable in _child_construct_multiple(in_output_variables, NS_AAS + "operationVariable",
cls.construct_operation_variable, cls.failsafe):
operation.in_output_variable.append(in_output_variable)
for tag, target in ((NS_AAS + "inputVariables", operation.input_variable),
(NS_AAS + "outputVariables", operation.output_variable),
(NS_AAS + "inoutputVariables", operation.in_output_variable)):
variables = element.find(tag)
if variables is not None:
for var in _child_construct_multiple(variables, NS_AAS + "operationVariable",
cls._construct_operation_variable, cls.failsafe):
target.add(var)
cls._amend_abstract_attributes(operation, element)
return operation

Expand Down Expand Up @@ -1236,7 +1230,6 @@ class XMLConstructables(enum.Enum):
ADMINISTRATIVE_INFORMATION = enum.auto()
QUALIFIER = enum.auto()
SECURITY = enum.auto()
OPERATION_VARIABLE = enum.auto()
ANNOTATED_RELATIONSHIP_ELEMENT = enum.auto()
BASIC_EVENT_ELEMENT = enum.auto()
BLOB = enum.auto()
Expand Down Expand Up @@ -1306,8 +1299,6 @@ def read_aas_xml_element(file: IO, construct: XMLConstructables, failsafe: bool
constructor = decoder_.construct_administrative_information
elif construct == XMLConstructables.QUALIFIER:
constructor = decoder_.construct_qualifier
elif construct == XMLConstructables.OPERATION_VARIABLE:
constructor = decoder_.construct_operation_variable
elif construct == XMLConstructables.ANNOTATED_RELATIONSHIP_ELEMENT:
constructor = decoder_.construct_annotated_relationship_element
elif construct == XMLConstructables.BASIC_EVENT_ELEMENT:
Expand Down
47 changes: 21 additions & 26 deletions basyx/aas/adapter/xml/xml_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"""

from lxml import etree # type: ignore
from typing import Dict, IO, Optional, Type
from typing import Dict, IO, Iterable, Optional, Type
import base64

from basyx.aas import model
Expand Down Expand Up @@ -726,20 +726,25 @@ def annotated_relationship_element_to_xml(obj: model.AnnotatedRelationshipElemen
return et_annotated_relationship_element


def operation_variable_to_xml(obj: model.OperationVariable,
tag: str = NS_AAS+"operationVariable") -> etree.Element:
def operation_variables_to_xml(obj: Iterable[model.SubmodelElement], tag: str) -> etree.Element:
"""
Serialization of objects of class :class:`~aas.model.submodel.OperationVariable` to XML
Serialization of multiple :class:`~aas.model.submodel.SubmodelElement` objects to OperationVariables
Since we don't implement the `OperationVariable` class, which is just a wrapper for a single
:class:`~aas.model.submodel.SubmodelElement`, elements are serialized as the `aas:value` child of an
`aas:operationVariable` element.
:param obj: Object of class :class:`~aas.model.submodel.OperationVariable`
:param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operationVariable"
:param obj: Object of class :class:`~aas.model.submodel.SubmodelElement`
:param tag: Namespace+Tag of the serialized element. Either `inputVariable`, `outputVariable` or `inoutputVariable`.
:return: Serialized ElementTree object
"""
et_operation_variable = _generate_element(tag)
et_value = _generate_element(NS_AAS+"value")
et_value.append(submodel_element_to_xml(obj.value))
et_operation_variable.append(et_value)
return et_operation_variable
et_variables = _generate_element(tag)
for se in obj:
et_operation_variable = _generate_element(NS_AAS+"operationVariable")
et_value = _generate_element(NS_AAS+"value")
et_value.append(submodel_element_to_xml(se))
et_operation_variable.append(et_value)
et_variables.append(et_operation_variable)
return et_variables


def operation_to_xml(obj: model.Operation,
Expand All @@ -752,21 +757,11 @@ def operation_to_xml(obj: model.Operation,
:return: Serialized ElementTree object
"""
et_operation = abstract_classes_to_xml(tag, obj)
if obj.input_variable:
et_input_variables = _generate_element(NS_AAS+"inputVariables")
for input_ov in obj.input_variable:
et_input_variables.append(operation_variable_to_xml(input_ov, NS_AAS+"operationVariable"))
et_operation.append(et_input_variables)
if obj.output_variable:
et_output_variables = _generate_element(NS_AAS+"outputVariables")
for output_ov in obj.output_variable:
et_output_variables.append(operation_variable_to_xml(output_ov, NS_AAS+"operationVariable"))
et_operation.append(et_output_variables)
if obj.in_output_variable:
et_inoutput_variables = _generate_element(NS_AAS+"inoutputVariables")
for in_out_ov in obj.in_output_variable:
et_inoutput_variables.append(operation_variable_to_xml(in_out_ov, NS_AAS+"operationVariable"))
et_operation.append(et_inoutput_variables)
for tag, nss in ((NS_AAS+"inputVariables", obj.input_variable),
(NS_AAS+"outputVariables", obj.output_variable),
(NS_AAS+"inoutputVariables", obj.in_output_variable)):
if nss:
et_operation.append(operation_variables_to_xml(nss, tag))
return et_operation


Expand Down
30 changes: 7 additions & 23 deletions basyx/aas/examples/data/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,17 +550,6 @@ def _find_extra_elements_by_id_short(self, object_list: model.NamespaceSet, sear
found_elements.add(object_list_element)
return found_elements

def _check_operation_variable_equal(self, object_: model.OperationVariable,
expected_value: model.OperationVariable):
"""
Checks if the given OperationVariable objects are equal
:param object_: Given OperationVariable object to check
:param expected_value: expected OperationVariable object
:return:
"""
self._check_submodel_element(object_.value, expected_value.value)

def check_operation_equal(self, object_: model.Operation, expected_value: model.Operation):
"""
Checks if the given Operation objects are equal
Expand All @@ -570,18 +559,13 @@ def check_operation_equal(self, object_: model.Operation, expected_value: model.
:return:
"""
self._check_abstract_attributes_submodel_element_equal(object_, expected_value)
self.check_contained_element_length(object_, 'input_variable', model.OperationVariable,
len(expected_value.input_variable))
self.check_contained_element_length(object_, 'output_variable', model.OperationVariable,
len(expected_value.output_variable))
self.check_contained_element_length(object_, 'in_output_variable', model.OperationVariable,
len(expected_value.in_output_variable))
for iv1, iv2 in zip(object_.input_variable, expected_value.input_variable):
self._check_operation_variable_equal(iv1, iv2)
for ov1, ov2 in zip(object_.output_variable, expected_value.output_variable):
self._check_operation_variable_equal(ov1, ov2)
for iov1, iov2 in zip(object_.in_output_variable, expected_value.in_output_variable):
self._check_operation_variable_equal(iov1, iov2)
for input_nss, expected_nss, attr_name in (
(object_.input_variable, expected_value.input_variable, 'input_variable'),
(object_.output_variable, expected_value.output_variable, 'output_variable'),
(object_.in_output_variable, expected_value.in_output_variable, 'in_output_variable')):
self.check_contained_element_length(object_, attr_name, model.SubmodelElement, len(expected_nss))
for var1, var2 in zip(input_nss, expected_nss):
self._check_submodel_element(var1, var2)

def check_capability_equal(self, object_: model.Capability, expected_value: model.Capability):
"""
Expand Down
Loading

0 comments on commit d4a2c6e

Please sign in to comment.