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 #146 #148
  • Loading branch information
jkhsjdhjs committed Nov 5, 2023
1 parent 9d01620 commit d0cf3d5
Show file tree
Hide file tree
Showing 16 changed files with 349 additions and 276 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 @@ -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_variable_to_json(cls, obj: model.SubmodelElement) -> Dict[str, object]:
"""
serialization of an object from class OperationVariable to json
serialization of an object from class SubmodelElement to a json OperationVariable representation
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: `OperationVariable` wrapper containing the serialized `SubmodelElement`
"""
data = cls._abstract_classes_to_json(obj)
data['value'] = obj.value
return data
return {'value': 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_variable_to_json(obj) for obj in 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
35 changes: 15 additions & 20 deletions basyx/aas/adapter/xml/xml_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,18 +726,20 @@ 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_variable_to_xml(obj: model.SubmodelElement, tag: str = NS_AAS+"operationVariable") -> etree.Element:
"""
Serialization of objects of class :class:`~aas.model.submodel.OperationVariable` to XML
Serialization of :class:`~aas.model.submodel.SubmodelElement` to the XML OperationVariable representation
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 obj: Object of class :class:`~aas.model.submodel.SubmodelElement`
:param tag: Namespace+Tag of the serialized element (optional). Default is "aas:operationVariable"
: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_value.append(submodel_element_to_xml(obj))
et_operation_variable.append(et_value)
return et_operation_variable

Expand All @@ -752,21 +754,14 @@ 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_variables = _generate_element(tag)
for submodel_element in nss:
et_variables.append(operation_variable_to_xml(submodel_element))
et_operation.append(et_variables)
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
57 changes: 44 additions & 13 deletions basyx/aas/examples/data/example_aas.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,8 @@ def create_example_submodel() -> model.Submodel:
embedded_data_specifications=()
)

operation_variable_property = model.Property(
id_short='ExampleProperty',
input_variable_property = model.Property(
id_short='ExamplePropertyInput',
value_type=model.datatypes.String,
value='exampleValue',
value_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
Expand All @@ -570,27 +570,58 @@ def create_example_submodel() -> model.Submodel:
'de': 'Beispiel Property Element'}),
parent=None,
semantic_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='http://acplt.org/Properties/ExampleProperty'),)),
value='http://acplt.org/Properties/ExamplePropertyInput'),)),
qualifier=(),
extension=(),
supplemental_semantic_id=(),
embedded_data_specifications=()
)

submodel_element_operation_variable_input = model.OperationVariable(
value=operation_variable_property)

submodel_element_operation_variable_output = model.OperationVariable(
value=operation_variable_property)
output_variable_property = model.Property(
id_short='ExamplePropertyOutput',
value_type=model.datatypes.String,
value='exampleValue',
value_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='http://acplt.org/ValueId/ExampleValueId'),)),
display_name=model.MultiLanguageNameType({'en-US': 'ExampleProperty',
'de': 'BeispielProperty'}),
category='CONSTANT',
description=model.MultiLanguageTextType({'en-US': 'Example Property object',
'de': 'Beispiel Property Element'}),
parent=None,
semantic_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='http://acplt.org/Properties/ExamplePropertyOutput'),)),
qualifier=(),
extension=(),
supplemental_semantic_id=(),
embedded_data_specifications=()
)

submodel_element_operation_variable_in_output = model.OperationVariable(
value=operation_variable_property)
in_output_variable_property = model.Property(
id_short='ExamplePropertyInOutput',
value_type=model.datatypes.String,
value='exampleValue',
value_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='http://acplt.org/ValueId/ExampleValueId'),)),
display_name=model.MultiLanguageNameType({'en-US': 'ExampleProperty',
'de': 'BeispielProperty'}),
category='CONSTANT',
description=model.MultiLanguageTextType({'en-US': 'Example Property object',
'de': 'Beispiel Property Element'}),
parent=None,
semantic_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='http://acplt.org/Properties/ExamplePropertyInOutput'),)),
qualifier=(),
extension=(),
supplemental_semantic_id=(),
embedded_data_specifications=()
)

submodel_element_operation = model.Operation(
id_short='ExampleOperation',
input_variable=[submodel_element_operation_variable_input],
output_variable=[submodel_element_operation_variable_output],
in_output_variable=[submodel_element_operation_variable_in_output],
input_variable=[input_variable_property],
output_variable=[output_variable_property],
in_output_variable=[in_output_variable_property],
category='PARAMETER',
description=model.MultiLanguageTextType({'en-US': 'Example Operation object',
'de': 'Beispiel Operation Element'}),
Expand Down
Loading

0 comments on commit d0cf3d5

Please sign in to comment.