Skip to content
Snippets Groups Projects
Commit d7f14d19 authored by Matthias Stefan Bodenbenner's avatar Matthias Stefan Bodenbenner
Browse files

BREAKING CHANGES: aligned naming of datatypes with Textual-SOIL

parent bfb4b77f
No related branches found
No related tags found
No related merge requests found
[![Build](https://git-ce.rwth-aachen.de/wzl-mq-ms/forschung-lehre/lava/unified-device-interface/python/badges/master/pipeline.svg)](https://git-ce.rwth-aachen.de/wzl-mq-ms/forschung-lehre/lava/unified-device-interface/python/commits/master)
# Python Unified Device Interface
Current stable version: 6.3.1
Current stable version: 7.0.0
Stable legacy version: 5.2.7
## Installation
......@@ -65,6 +65,13 @@ Funded by the Deutsche Forschungsgemeinschaft (DFG, German Research Foundation)
## Recent changes
**7.0.0** - 2023-02-23
- aligned the naming of datatypes with *Textual SOIL*
- "bool" is replaced by "boolean"
- "double" is replaced by "float"
- old naming is still accepted when starting the server, but responses of the server use the SOIl-conform naming
**6.3.1** - 2023-02-21
- updated from Python 3.9 to Python 3.11
......
......
......@@ -73,7 +73,7 @@
null,
null
],
"datatype": "double",
"datatype": "float",
"dimension": [
3
],
......@@ -170,7 +170,7 @@
null,
null
],
"datatype": "int",
"datatype": "integer",
"dimension": [],
"value": 0,
"unit": "NONE",
......
......
from setuptools import setup, find_packages
setup(name='wzl-udi',
version='6.3.0',
version='7.0.0',
url='https://git-ce.rwth-aachen.de/wzl-mq-public/soil/python',
author='Matthias Bodenbenner',
author_email='m.bodenbenner@wzl.mq.rwth-aachen.de',
......
......
import enum
from src.soil.error import TypeException
class Datatype(enum.Enum):
BOOLEAN = 0
INTEGER = 1
FLOAT = 2
STRING = 3
TIME = 4
ENUM = 5
@classmethod
def from_string(cls, datatype: str):
if datatype in ["bool", "boolean"]:
return cls.BOOLEAN
if datatype in ["int", "integer"]:
return cls.INTEGER
if datatype in ["float", "double"]:
return cls.FLOAT
if datatype in ["string"]:
return cls.STRING
if datatype in ["time"]:
return cls.TIME
if datatype in ["enum"]:
return cls.ENUM
raise TypeException("Unknown type descriptor: {}".format(datatype))
def to_string(self) -> str:
return ["boolean", "int", "float", "string", "time", "enum"][self.value]
......@@ -2,6 +2,8 @@ from enum import auto, Flag, IntEnum
import datetime
from src.soil.datatype import Datatype
class EventSeverity(IntEnum):
DEBUG = 0
......@@ -79,10 +81,11 @@ class Event(object):
:param target: Value associated with the trigger, either interpreted as threshold or target value, depending on the type of trigger.
:param message: Optional message, containing a user-understandable description of the event, also send with the event.
"""
datatype = Datatype.from_string(datatype)
if not EventTrigger.is_valid(trigger):
raise ValueError("Combination of trigger values is not reasonable!")
# check compatibility of trigger and datatype
if datatype in ['enum', 'string', 'bool']:
if datatype in [Datatype.ENUM, Datatype.STRING, Datatype.BOOLEAN]:
if trigger not in [EventTrigger.EQUALS, EventTrigger.EQUALS | EventTrigger.NOT]:
raise AttributeError('Datatype and Trigger are not compatible.')
if not isinstance(message, str):
......
......
......@@ -11,6 +11,8 @@ import time
from typing import Any, List, Callable, Union
from wzl.utilities import root_logger
from .datatype import Datatype
nest_asyncio.apply()
from .element import Element
......@@ -38,18 +40,18 @@ def serialize_time(time):
class Figure(Element, ABC):
def __init__(self, uuid: str, name: str, description: str, datatype: str, dimension: List, range: List, value: Any, getter: Callable,
def __init__(self, uuid: str, name: str, description: str, datatype: Datatype, dimension: List, range: List, value: Any, getter: Callable,
ontology: str = None):
Element.__init__(self, uuid, name, description, ontology)
if type(datatype) is not str:
raise Exception('{}: Datatype must be passed as string.'.format(uuid))
# if type(datatype) is not str:
# raise Exception('{}: Datatype must be passed as string.'.format(uuid))
Figure.check_all(datatype, dimension, range, value)
if getter is not None and not callable(getter):
raise TypeError("{}: The getter of the Figure must be callable!".format(uuid))
self._datatype = datatype
self._dimension = dimension
self._range = range
if datatype == 'time':
if datatype == Datatype.TIME:
self._value = parse_time(value)
else:
self._value = value
......@@ -130,6 +132,10 @@ class Figure(Element, ABC):
dictionary = {}
for key in keys:
value = self.__getitem__(key, method)
if key == "datatype":
dictionary[key] = value.to_string()
else:
dictionary[key] = value
return dictionary
......@@ -165,7 +171,7 @@ class Figure(Element, ABC):
raise e
@staticmethod
def check_type(datatype, value):
def check_type(datatype: Datatype, value: any):
"""
Checks if the given value is of the correct datatype. If value is not a scale, it checks all "subvalues" for correct datatype.
:param datatype: datatype the value provided by "value" should have
......@@ -177,26 +183,24 @@ class Figure(Element, ABC):
# base case: value is a scalar
if Figure.is_scalar(value):
# check if the type of value corresponds to given datatype
if datatype == 'bool' and not isinstance(value, bool):
if datatype == Datatype.BOOLEAN and not isinstance(value, bool):
raise TypeException("Boolean field does not match non-boolean value {}!".format(value))
elif datatype == 'int' and not isinstance(value, int):
elif datatype == Datatype.INTEGER and not isinstance(value, int):
raise TypeException("Integer field does not match non-integer value {}!".format(value))
elif datatype == 'double' and not isinstance(value, float) and not isinstance(value, int):
raise TypeException("Double field does not match non-double value {}!".format(value))
elif datatype == 'string' and not isinstance(value, str):
elif datatype == Datatype.FLOAT and not isinstance(value, float) and not isinstance(value, int):
raise TypeException("Float field does not match non-float value {}!".format(value))
elif datatype == Datatype.STRING and not isinstance(value, str):
raise TypeException("String field does not match non-string value {}!".format(value))
elif datatype == 'enum' and not isinstance(value, str):
elif datatype == Datatype.ENUM and not isinstance(value, str):
raise TypeException(
"Enum field {} must be a string!".format(value))
elif datatype == 'time' and not isinstance(value, str):
elif datatype == Datatype.TIME and not isinstance(value, str):
raise TypeException(
"Time field {} must be string.".format(
value))
elif datatype == 'time' and isinstance(value, str):
elif datatype == Datatype.TIME and isinstance(value, str):
if value != "" and value is not None and not rfc3339.validate_rfc3339(value):
raise TypeException("Value is not a valid RFC3339-formatted timestring: {}".format(value))
elif datatype not in ["bool", "int", "double", "string", "enum", "time"]:
raise TypeException("Unknown type descriptor: {}".format(datatype))
else:
# recursion case: value is an array or matrix => check datatype of each "subvalue" recursively
for v in value:
......@@ -206,7 +210,7 @@ class Figure(Element, ABC):
raise e
@staticmethod
def check_range(datatype, range, value):
def check_range(datatype: Datatype, range, value):
"""
Checks if the given value is within provided range (depending on the given datatype)
......@@ -226,36 +230,36 @@ class Figure(Element, ABC):
"""
# if the list is empty, all values are possible
if not range:
if datatype == 'enum':
if datatype == Datatype.ENUM:
raise RangeException('A value of type enum must provide a range with possible values!')
else:
return
# base case: value is scalar => check if the value is in range
if Figure.is_scalar(value):
# bool is not checked, since there is only true and false
if datatype == 'bool':
if datatype == Datatype.BOOLEAN:
return
elif datatype == 'int' and value is not None:
elif datatype == Datatype.INTEGER and value is not None:
if range[0] is not None and value < range[0]:
raise RangeException("Integer value {} is smaller than lower bound {}!".format(value, range[0]))
elif range[1] is not None and value > range[1]:
raise RangeException("Integer value {} is higher than upper bound {}!".format(value, range[1]))
elif datatype == 'double' and value is not None:
elif datatype == Datatype.FLOAT and value is not None:
if range[0] is not None and value < range[0]:
raise RangeException("Double value {} is smaller than lower bound {}!".format(value, range[0]))
elif range[1] is not None and value > range[1]:
raise RangeException("Double value {} is higher than upper bound {}!".format(value, range[1]))
elif datatype == 'string' and value is not None:
elif datatype == Datatype.STRING and value is not None:
if range[0] is not None and len(value) < range[0]:
raise RangeException(
"String value {} is too short. Minimal required length is {}!".format(value, range[0]))
elif range[1] is not None and len(value) > range[1]:
raise RangeException(
"String value {} is too long. Maximal allowed length is {}!".format(value, range[1]))
elif datatype == 'enum' and value is not None:
elif datatype == Datatype.ENUM and value is not None:
if value not in range:
raise RangeException("Enum value {} is not within the set of allowed values!".format(value))
elif datatype == 'time' and value is not None and value != "":
elif datatype == Datatype.TIME and value is not None and value != "":
if range[0] is not None:
if not rfc3339.validate_rfc3339(range[0]):
raise TypeException(
......
......
......@@ -4,6 +4,7 @@ from typing import Dict
from deprecated import deprecated
from wzl.utilities import root_logger
from .datatype import Datatype
from .figure import Figure
from ..utils.constants import HTTP_GET
from ..utils.error import SerialisationException
......@@ -119,7 +120,11 @@ class Measurement(Figure):
# in case of timestamp convert into RFC3339 string
if key == 'timestamp' or (key == 'value' and self._datatype == 'time'):
value = value.isoformat() + 'Z' if value is not None else ""
if key == "datatype":
dictionary[key] = value.to_string()
else:
dictionary[key] = value
return dictionary
@staticmethod
......@@ -159,7 +164,7 @@ class Measurement(Figure):
try:
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
return Measurement(dictionary['uuid'], dictionary['name'], dictionary['description'],
dictionary['datatype'], dictionary['dimension'],
Datatype.from_string(dictionary['datatype']), dictionary['dimension'],
dictionary['range'], implementation, dictionary['unit'], ontology)
except Exception as e:
raise SerialisationException('{}: The measurement can not be deserialized. {}'.format(uuid, e))
import asyncio
import inspect
from typing import Dict
from wzl.utilities import root_logger
from ..utils.constants import HTTP_GET
from ..utils.error import DeviceException, SerialisationException
from .datatype import Datatype
from .error import ReadOnlyException
from .figure import Figure
from ..utils.constants import HTTP_GET
from ..utils.error import DeviceException, SerialisationException
logger = root_logger.get(__name__)
class Parameter(Figure):
def __init__(self, uuid, name, description, datatype, dimension, range, value, getter=None, setter=None, ontology: str = None):
def __init__(self, uuid, name, description, datatype, dimension, range, value, getter=None, setter=None,
ontology: str = None):
Figure.__init__(self, uuid, name, description, datatype, dimension, range, value, getter, ontology)
if uuid[:3] not in ['PAR', 'ARG', 'RET']:
raise Exception('{}: The UUID must start with PAR, ARG or RET!'.format(uuid))
......@@ -73,6 +76,9 @@ class Parameter(Figure):
dictionary = {}
for key in keys:
value = self.__getitem__(key, method)
if key == "datatype":
dictionary[key] = value.to_string()
else:
dictionary[key] = value
return dictionary
......@@ -91,15 +97,18 @@ class Parameter(Figure):
uuid = dictionary['uuid']
if uuid[:3] not in ['PAR', 'ARG', 'RET']:
raise SerialisationException(
'The Parameter can not be deserialized. The UUID must start with PAR, ARG or RET, but actually starts with {}!'.format(uuid[:3]))
'The Parameter can not be deserialized. The UUID must start with PAR, ARG or RET, but actually starts with {}!'.format(
uuid[:3]))
if 'name' not in dictionary:
raise SerialisationException('{}: The parameter can not be deserialized. Name is missing!'.format(uuid))
if 'description' not in dictionary:
raise SerialisationException('{}: The parameter can not be deserialized. Description is missing!'.format(uuid))
raise SerialisationException(
'{}: The parameter can not be deserialized. Description is missing!'.format(uuid))
if 'datatype' not in dictionary:
raise SerialisationException('{}: The parameter can not be deserialized. Datatype is missing!'.format(uuid))
if 'dimension' not in dictionary:
raise SerialisationException('{}: The parameter can not be deserialized. Dimension is missing!'.format(uuid))
raise SerialisationException(
'{}: The parameter can not be deserialized. Dimension is missing!'.format(uuid))
if 'value' not in dictionary:
raise SerialisationException('{}: The parameter can not be deserialized. Value is missing!'.format(uuid))
if 'range' not in dictionary:
......@@ -109,7 +118,8 @@ class Parameter(Figure):
getter = implementation['getter'] if implementation is not None else None
setter = implementation['setter'] if implementation is not None else None
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
return Parameter(dictionary['uuid'], dictionary['name'], dictionary['description'], dictionary['datatype'], dictionary['dimension'],
return Parameter(dictionary['uuid'], dictionary['name'], dictionary['description'],
Datatype.from_string(dictionary['datatype']), dictionary['dimension'],
dictionary['range'], dictionary['value'], getter, setter, ontology)
except Exception as e:
raise SerialisationException('{}: The variable can not be deserialized. {}'.format(uuid, e))
......
......
......@@ -2,6 +2,7 @@ from deprecated import deprecated
from typing import Dict
from wzl.utilities import root_logger
from .datatype import Datatype
from ..utils.constants import HTTP_GET
from ..utils.error import SerialisationException
from .figure import Figure
......@@ -98,6 +99,9 @@ class Variable(Figure):
# in case of timestamp convert into RFC3339 string
if key == 'timestamp' or (key == 'value' and self._datatype == 'time'):
value = value.isoformat() + 'Z' if value is not None else ""
if key == "datatype":
dictionary[key] = value.to_string()
else:
dictionary[key] = value
return dictionary
......@@ -130,7 +134,7 @@ class Variable(Figure):
raise SerialisationException('{}: The variable can not be deserialized. Unit is missing!'.format(uuid))
try:
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
return Variable(dictionary['uuid'], dictionary['name'], dictionary['description'], dictionary['datatype'], dictionary['dimension'],
return Variable(dictionary['uuid'], dictionary['name'], dictionary['description'], Datatype.from_string(dictionary['datatype']), dictionary['dimension'],
dictionary['range'], implementation, dictionary['unit'], ontology)
except Exception as e:
raise SerialisationException('{}: The variable can not be deserialized. {}'.format(uuid, e))
......@@ -121,7 +121,7 @@ def start(com_lasertracker: COMLasertracker, config: Dict, soil_model_file: str)
'signature': {'arguments': {}, 'returns': []}, 'mqtt_callback': mqtt.publish}
model = Component.load(soil_model_file, mapping['COM-Lasertracker'])
http = HTTPServer(loop, address, port, model, 'xml')
http = HTTPServer(loop, address, port, model, dataformat=config['dataformat'])
# start servers
main_logger.info("Starting main asynchronous loop")
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment