Skip to content

Commit

Permalink
Fix the implementation of DataSpecificationIEC61360
Browse files Browse the repository at this point in the history
- Update IEC61360_DATA_TYPES from the Spec
- Add checking of value_type string: 1<len(value_type)<2000
- Some Refactoring
- Fix typehint of DataSpecification according to the spec
- Simplify Value and ValueReferencePair, as value is just a string acc. to spec
- Fix value_format, as it not one of xsd types, but also a string (e.g. "X..17" or "M" or "NR2..3.3"), which describes how the value should look like. Look in IEC61360-1 for further info
  • Loading branch information
zrgt committed Oct 17, 2023
1 parent 7b71f3c commit 8d9eb8e
Show file tree
Hide file tree
Showing 17 changed files with 132 additions and 162 deletions.
42 changes: 21 additions & 21 deletions basyx/aas/adapter/_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,26 @@
model.EntityType.CO_MANAGED_ENTITY: 'CoManagedEntity',
model.EntityType.SELF_MANAGED_ENTITY: 'SelfManagedEntity'}

IEC61360_DATA_TYPES: Dict[model.base.IEC61360DataType, str] = {
model.base.IEC61360DataType.DATE: 'DATE',
model.base.IEC61360DataType.STRING: 'STRING',
model.base.IEC61360DataType.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE',
model.base.IEC61360DataType.INTEGER_MEASURE: 'INTEGER_MEASURE',
model.base.IEC61360DataType.INTEGER_COUNT: 'INTEGER_COUNT',
model.base.IEC61360DataType.INTEGER_CURRENCY: 'INTEGER_CURRENCY',
model.base.IEC61360DataType.REAL_MEASURE: 'REAL_MEASURE',
model.base.IEC61360DataType.REAL_COUNT: 'REAL_COUNT',
model.base.IEC61360DataType.REAL_CURRENCY: 'REAL_CURRENCY',
model.base.IEC61360DataType.BOOLEAN: 'BOOLEAN',
model.base.IEC61360DataType.IRI: 'IRI',
model.base.IEC61360DataType.IRDI: 'IRDI',
model.base.IEC61360DataType.RATIONAL: 'RATIONAL',
model.base.IEC61360DataType.RATIONAL_MEASURE: 'RATIONAL_MEASURE',
model.base.IEC61360DataType.TIME: 'TIME',
model.base.IEC61360DataType.TIMESTAMP: 'TIMESTAMP',
model.base.IEC61360DataType.HTML: 'HTML',
model.base.IEC61360DataType.BLOB: 'BLOB',
model.base.IEC61360DataType.FILE: 'FILE',
IEC61360_DATA_TYPES: Dict[model.base.DataTypeIEC61360, str] = {
model.base.DataTypeIEC61360.DATE: 'DATE',
model.base.DataTypeIEC61360.STRING: 'STRING',
model.base.DataTypeIEC61360.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE',
model.base.DataTypeIEC61360.INTEGER_MEASURE: 'INTEGER_MEASURE',
model.base.DataTypeIEC61360.INTEGER_COUNT: 'INTEGER_COUNT',
model.base.DataTypeIEC61360.INTEGER_CURRENCY: 'INTEGER_CURRENCY',
model.base.DataTypeIEC61360.REAL_MEASURE: 'REAL_MEASURE',
model.base.DataTypeIEC61360.REAL_COUNT: 'REAL_COUNT',
model.base.DataTypeIEC61360.REAL_CURRENCY: 'REAL_CURRENCY',
model.base.DataTypeIEC61360.BOOLEAN: 'BOOLEAN',
model.base.DataTypeIEC61360.IRI: 'IRI',
model.base.DataTypeIEC61360.IRDI: 'IRDI',
model.base.DataTypeIEC61360.RATIONAL: 'RATIONAL',
model.base.DataTypeIEC61360.RATIONAL_MEASURE: 'RATIONAL_MEASURE',
model.base.DataTypeIEC61360.TIME: 'TIME',
model.base.DataTypeIEC61360.TIMESTAMP: 'TIMESTAMP',
model.base.DataTypeIEC61360.HTML: 'HTML',
model.base.DataTypeIEC61360.BLOB: 'BLOB',
model.base.DataTypeIEC61360.FILE: 'FILE',
}

IEC61360_LEVEL_TYPES: Dict[model.base.IEC61360LevelType, str] = {
Expand All @@ -107,7 +107,7 @@
REFERENCE_TYPES_INVERSE: Dict[str, Type[model.Reference]] = {v: k for k, v in REFERENCE_TYPES.items()}
KEY_TYPES_INVERSE: Dict[str, model.KeyTypes] = {v: k for k, v in KEY_TYPES.items()}
ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()}
IEC61360_DATA_TYPES_INVERSE: Dict[str, model.base.IEC61360DataType] = {v: k for k, v in IEC61360_DATA_TYPES.items()}
IEC61360_DATA_TYPES_INVERSE: Dict[str, model.base.DataTypeIEC61360] = {v: k for k, v in IEC61360_DATA_TYPES.items()}
IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.base.IEC61360LevelType] = \
{v: k for k, v in IEC61360_LEVEL_TYPES.items()}

Expand Down
17 changes: 8 additions & 9 deletions basyx/aas/adapter/json/json_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,11 @@ def _construct_lang_string_set(cls, lst: List[Dict[str, object]], object_class:
return object_class(ret)

@classmethod
def _construct_value_list(cls, dct: Dict[str, object], value_format: model.DataTypeDefXsd) -> model.ValueList:
def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList:
ret: model.ValueList = set()
for element in _get_ts(dct, 'valueReferencePairs', list):
try:
ret.add(cls._construct_value_reference_pair(element, value_format=value_format))
ret.add(cls._construct_value_reference_pair(element))
except (KeyError, TypeError) as e:
error_message = "Error while trying to convert JSON object into ValueReferencePair: {} >>> {}".format(
e, pprint.pformat(element, depth=2, width=2 ** 14, compact=True))
Expand All @@ -402,11 +402,10 @@ def _construct_value_list(cls, dct: Dict[str, object], value_format: model.DataT
return ret

@classmethod
def _construct_value_reference_pair(cls, dct: Dict[str, object], value_format: model.DataTypeDefXsd,
def _construct_value_reference_pair(cls, dct: Dict[str, object],
object_class=model.ValueReferencePair) -> model.ValueReferencePair:
return object_class(value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_format),
value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict)),
value_type=value_format)
return object_class(value=_get_ts(dct, 'value', str),
value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict)))

# #############################################################################
# Direct Constructor Methods (for classes with `modelType`) starting from here
Expand Down Expand Up @@ -481,11 +480,11 @@ def _construct_data_specification_iec61360(cls, dct: Dict[str, object],
if 'symbol' in dct:
ret.symbol = _get_ts(dct, 'symbol', str)
if 'valueFormat' in dct:
ret.value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueFormat', str)]
ret.value_format = _get_ts(dct, 'valueFormat', str)
if 'valueList' in dct:
ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict), value_format=ret.value_format)
ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict))
if 'value' in dct:
ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_format)
ret.value = _get_ts(dct, 'value', str)
if 'valueId' in dct:
ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict))
if 'levelType' in dct:
Expand Down
5 changes: 3 additions & 2 deletions basyx/aas/adapter/json/json_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,12 @@ def _data_specification_iec61360_to_json(
data_spec['sourceOfDefinition'] = obj.source_of_definition
if obj.symbol is not None:
data_spec['symbol'] = obj.symbol
data_spec['valueFormat'] = model.datatypes.XSD_TYPE_NAMES[obj.value_format]
if obj.value_format is not None:
data_spec['valueFormat'] = obj.value_format
if obj.value_list is not None:
data_spec['valueList'] = cls._value_list_to_json(obj.value_list)
if obj.value is not None:
data_spec['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None
data_spec['value'] = obj.value
if obj.level_types:
data_spec['levelType'] = {v: k in obj.level_types for k, v in _generic.IEC61360_LEVEL_TYPES.items()}
return data_spec
Expand Down
24 changes: 9 additions & 15 deletions basyx/aas/adapter/xml/xml_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,26 +1033,21 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel,
return submodel

@classmethod
def construct_value_reference_pair(cls, element: etree.Element, value_format: model.DataTypeDefXsd,
object_class=model.ValueReferencePair, **_kwargs: Any) \
-> model.ValueReferencePair:
return object_class(
model.datatypes.from_xsd(_child_text_mandatory(element, NS_AAS + "value"), value_format),
_child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference),
value_format
)
def construct_value_reference_pair(cls, element: etree.Element, object_class=model.ValueReferencePair
, **_kwargs: Any) -> model.ValueReferencePair:
return object_class(_child_text_mandatory(element, NS_AAS + "value"),
_child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference))

@classmethod
def construct_value_list(cls, element: etree.Element, value_format: model.DataTypeDefXsd, **_kwargs: Any) \
-> model.ValueList:
def construct_value_list(cls, element: etree.Element, **_kwargs: Any) -> model.ValueList:
"""
This function doesn't support the object_class parameter, because ValueList is just a generic type alias.
"""

return set(
_child_construct_multiple(_get_child_mandatory(element, NS_AAS + "valueReferencePairs"),
NS_AAS + "valueReferencePair", cls.construct_value_reference_pair,
cls.failsafe, value_format=value_format)
cls.failsafe)
)

@classmethod
Expand Down Expand Up @@ -1126,16 +1121,15 @@ def construct_data_specification_iec61360(cls, element: etree.Element, object_cl
cls.failsafe)
if definition is not None:
ds_iec.definition = definition
value_format = _get_text_mapped_or_none(element.find(NS_AAS + "valueFormat"), model.datatypes.XSD_TYPE_CLASSES)
value_format = _get_text_or_none(element.find(NS_AAS + "valueFormat"))
if value_format is not None:
ds_iec.value_format = value_format
value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe,
value_format=ds_iec.value_format)
value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe)
if value_list is not None:
ds_iec.value_list = value_list
value = _get_text_or_none(element.find(NS_AAS + "value"))
if value is not None and value_format is not None:
ds_iec.value = model.datatypes.from_xsd(value, value_format)
ds_iec.value = value
level_type = element.find(NS_AAS + "levelType")
if level_type is not None:
for child in level_type:
Expand Down
10 changes: 5 additions & 5 deletions basyx/aas/adapter/xml/xml_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def value_reference_pair_to_xml(obj: model.ValueReferencePair,
"""
et_vrp = _generate_element(tag)
# TODO: value_type isn't used at all by _value_to_xml(), thus we can ignore the type here for now
et_vrp.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore
et_vrp.append(_generate_element(NS_AAS+"value", text=obj.value)) # type: ignore
et_vrp.append(reference_to_xml(obj.value_id, NS_AAS+"valueId"))
return et_vrp

Expand Down Expand Up @@ -445,15 +445,15 @@ def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360,
text=_generic.IEC61360_DATA_TYPES[obj.data_type]))
if obj.definition is not None:
et_data_specification_iec61360.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition"))
et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat",
text=model.datatypes.XSD_TYPE_NAMES[obj.value_format]))

if obj.value_format is not None:
et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat", text=obj.value_format))
# this can be either None or an empty set, both of which are equivalent to the bool false
# thus we don't check 'is not None' for this property
if obj.value_list:
et_data_specification_iec61360.append(value_list_to_xml(obj.value_list))
if obj.value is not None:
et_data_specification_iec61360.append(_generate_element(NS_AAS + "value",
text=model.datatypes.xsd_repr(obj.value)))
et_data_specification_iec61360.append(_generate_element(NS_AAS + "value", text=obj.value))
if obj.level_types:
et_level_types = _generate_element(NS_AAS + "levelType")
for k, v in _generic.IEC61360_LEVEL_TYPES.items():
Expand Down
10 changes: 4 additions & 6 deletions basyx/aas/examples/data/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -850,13 +850,11 @@ def _check_value_list_equal(self, object_: model.ValueList, expected_value: mode
:return:
"""
for expected_pair in expected_value:
pair = self._find_element_by_attribute(expected_pair, object_, 'value', 'value_id', 'value_type')
self.check(pair is not None, 'ValueReferencePair[value={}, value_id={}, value_type={}] '
'must exist'.format(expected_pair.value, expected_pair.value_id,
expected_pair.value_type))
pair = self._find_element_by_attribute(expected_pair, object_, 'value', 'value_id')
self.check(pair is not None, 'ValueReferencePair[value={}, value_id={}] '
'must exist'.format(expected_pair.value, expected_pair.value_id))

found_elements = self._find_extra_elements_by_attribute(object_, expected_value,
'value', 'value_id', 'value_type')
found_elements = self._find_extra_elements_by_attribute(object_, expected_value, 'value', 'value_id')
self.check(found_elements == set(), 'ValueList must not have extra ValueReferencePairs',
value=found_elements)

Expand Down
6 changes: 2 additions & 4 deletions basyx/aas/examples/data/example_aas.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,19 @@
data_specification_content=model.DataSpecificationIEC61360(preferred_name=model.PreferredNameTypeIEC61360({
'de': 'Test Specification',
'en-US': 'TestSpecification'
}), data_type=model.IEC61360DataType.REAL_MEASURE,
}), data_type=model.DataTypeIEC61360.REAL_MEASURE,
definition=model.DefinitionTypeIEC61360({'de': 'Dies ist eine Data Specification für Testzwecke',
'en-US': 'This is a DataSpecification for testing purposes'}),
short_name=model.ShortNameTypeIEC61360({'de': 'Test Spec', 'en-US': 'TestSpec'}), unit='SpaceUnit',
unit_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='http://acplt.org/Units/SpaceUnit'),)),
source_of_definition='http://acplt.org/DataSpec/ExampleDef', symbol='SU', value_format=model.datatypes.String,
source_of_definition='http://acplt.org/DataSpec/ExampleDef', symbol='SU', value_format="M",
value_list={
model.ValueReferencePair(
value_type=model.datatypes.String,
value='exampleValue',
value_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='http://acplt.org/ValueId/ExampleValueId'),)), ),
model.ValueReferencePair(
value_type=model.datatypes.String,
value='exampleValue2',
value_id=model.ExternalReference((model.Key(type_=model.KeyTypes.GLOBAL_REFERENCE,
value='http://acplt.org/ValueId/ExampleValueId2'),)), )},
Expand Down
8 changes: 8 additions & 0 deletions basyx/aas/model/_string_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ def check_short_name_type(value: str, type_name: str = "ShortNameType") -> None:
return check(value, type_name, 1, 64)


def check_value_type_iec61360(value: str, type_name: str = "ValueTypeIEC61360") -> None:
return check(value, type_name, 1, 2000)


def check_version_type(value: str, type_name: str = "VersionType") -> None:
return check(value, type_name, 1, 4, re.compile(r"([0-9]|[1-9][0-9]*)"))

Expand Down Expand Up @@ -170,3 +174,7 @@ def constrain_short_name_type(pub_attr_name: str) -> Callable[[Type[_T]], Type[_

def constrain_version_type(pub_attr_name: str) -> Callable[[Type[_T]], Type[_T]]:
return constrain_attr(pub_attr_name, check_version_type)


def constrain_value_type_iec61360(pub_attr_name: str) -> Callable[[Type[_T]], Type[_T]]:
return constrain_attr(pub_attr_name, check_value_type_iec61360)
Loading

0 comments on commit 8d9eb8e

Please sign in to comment.