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

Merge branch 'master' into legacy

parents 997fe62f 7aad6bef
No related branches found
No related tags found
No related merge requests found
# Python Unified Device Interface
Current stable version: 5.2.6
Current stable version: 5.2.7
## Installation
1. Install the *WZL-Utilities* dependency via pip
......@@ -53,6 +54,9 @@ Setup your server as follows:
2. If you are not using MQTT or HTTP leave out the respective lines.
## Recent changes
5.2.7
- improved error output for developers
5.2.6
- bug fix
- fixed serialization to RFC3339 time string
......
from setuptools import setup, find_packages
setup(name='wzl-udi',
version='5.2.6',
version='5.2.7',
url='',
author='Matthias Bodenbenner',
author_email='m.bodenbenner@wzl.rwth-aachen.de',
......
# -*- coding: utf-8 -*-
import traceback
import functools
from aiohttp import web
......@@ -89,6 +91,7 @@ class HTTPServer(object):
status = 200
logger.info('Response: {}'.format(response))
except (DeviceException, ServerException, UserException) as e:
logger.error(traceback.format_exc())
response = {'error': str(e)}
status = 500
logger.error('Response: {}'.format(response))
......@@ -108,6 +111,7 @@ class HTTPServer(object):
status = 200
logger.info('Response: {}'.format(response))
except (DeviceException, ServerException, UserException) as e:
logger.error(traceback.format_exc())
response = {'error': str(e)}
status = 500
logger.error('Response: {}'.format(response))
......@@ -131,10 +135,12 @@ class HTTPServer(object):
status = 200
logger.info('Response: {}'.format(response))
except ChildNotFoundException as e:
logger.error(traceback.format_exc())
response = {'error': str(e)}
status = 404
logger.error('Response: {}'.format(response))
except (DeviceException, ServerException, UserException) as e:
logger.error(traceback.format_exc())
response = {'error': str(e)}
status = 500
logger.error('Response: {}'.format(response))
......@@ -168,10 +174,12 @@ class HTTPServer(object):
status = 200
logger.info('Response: {}'.format(response))
except ReadOnlyException as e:
logger.error(traceback.format_exc())
response = {'error': str(e)}
status = 403
logger.error('Response: {}'.format(response))
except InvokationException as e:
logger.error(traceback.format_exc())
response = {'error': str(e)}
status = 500
logger.error('Response: {}'.format(response))
......@@ -196,6 +204,7 @@ class HTTPServer(object):
status = 200
logger.info('Response: {}'.format(response))
except (DeviceException, ServerException, UserException) as e:
logger.error(traceback.format_exc())
response = {'error': str(e)}
status = 500
logger.error('Response: {}'.format(response))
......
# from __future__ import annotations
import json
import os
import sys
from typing import List, Any, Union, Dict
from wzl.utilities import root_logger
from . import docstring_parser
from .element import Element
from .error import ChildNotFoundException
from .function import Function
from .measurement import Measurement
from .parameter import Parameter
from ..utils.constants import HTTP_GET
from ..utils.error import SerialisationException, DeviceException, UserException
logger = root_logger.get(__name__)
class Component(Element):
def __init__(self, uuid: str, name: str, description: str, functions: List[Function], measurements: List[Measurement],
parameters: List[Parameter], components: List['Component'], implementation: Dict, ontology: str = None):
Element.__init__(self, uuid, name, description, ontology)
if uuid[:3] != 'COM':
raise Exception('{}: The UUID must start with COM!'.format(uuid))
if not isinstance(functions, list):
raise Exception('{}: Given functions are not a list!'.format(uuid))
for f in functions:
if not isinstance(f, Function):
raise Exception('{}: Given function is not of type Function!'.format(uuid))
if not isinstance(measurements, list):
raise Exception('{}: Given measurements are not a list!'.format(uuid))
for v in measurements:
if not isinstance(v, Measurement):
raise Exception('{}: Given measurement is not of type Variables!'.format(uuid))
if not isinstance(parameters, list):
raise Exception('{}: Given measurements are not a list!'.format(uuid))
for p in parameters:
if not isinstance(p, Parameter):
raise Exception('{}: Given measurement is not of type Variables!'.format(uuid))
if not isinstance(components, list):
raise Exception('{}: Given components are not a list!'.format(uuid))
for o in components:
if not isinstance(o, Component):
raise Exception('{}: Given component is not of type Components!'.format(uuid))
self._functions = functions
self._measurements = measurements
self._components = components
self._parameters = parameters
self._implementation_add = implementation['add'] if 'add' in implementation else None
self._implementation_remove = implementation['remove'] if 'remove' in implementation else None
def __getitem__(self, item: Union[str, List[str]], method: int = HTTP_GET) -> Any:
attribute = False
if isinstance(item, str):
attribute = hasattr(self, item)
if item == "functions":
return self._functions
if item == "measurements":
return self._measurements
if item == "parameters":
return self._measurements
if item == "components":
return self._components
if item == "children":
ret = []
everything = self._components + self._measurements + self._parameters + self._functions
for o in everything:
ret += [o.uuid]
return ret
# if the item is a list, the list contains the uuid of the descendants
if isinstance(item, list):
if len(item) > 0 and super().__getitem__('uuid', method) == item[0]:
item = item[1:]
if len(item) == 0:
return self
everything = self._components + self._measurements + self._parameters + self._functions
for o in everything:
if o.uuid == item[0]:
if len(item) == 1:
return o
else:
return o.__getitem__(item[1:], method)
raise Exception("{}: Given uuid {} is not the id of a child of the current component!".format(self.uuid, item))
return super().__getitem__(item, method)
def __setitem__(self, key: str, value: Any):
if key == "functions":
if not isinstance(value, list):
raise Exception('{}: Given functions are not a list!'.format(self.uuid))
for f in value:
if not isinstance(f, Function):
raise Exception('{}: Given function is not of type Function!'.format(self.uuid))
self._functions = value
elif key == "measurements":
if not isinstance(value, list):
raise Exception('{}: Given measurements are not a list!'.format(self.uuid))
for v in value:
if not isinstance(v, Measurement):
raise Exception('{}: Given measurement is not of type Variable!'.format(self.uuid))
self._measurements = value
elif key == "parameters":
if not isinstance(value, list):
raise Exception('{}: Given parameters are not a list!'.format(self.uuid))
for v in value:
if not isinstance(v, Parameter):
raise Exception('{}: Given parameter is not of type Parameter!'.format(self.uuid))
self._measurements = value
elif key == "components":
if not isinstance(value, list):
raise Exception('{}: Given components are not a list!'.format(self.uuid))
for o in value:
if not isinstance(o, Component):
raise Exception('{}: Given component is not of type Component!'.format(self.uuid))
self._components = value
else:
super().__setitem__(key, value)
def serialize(self, keys: List[Any], method: int = HTTP_GET) -> Dict[str, Any]:
if not keys: # list is empty
keys = ['uuid', 'name', 'description', 'children', 'ontology']
if 'all' in keys: # serialize complete tree recursively (overrides all other keys)
dictionary = super().serialize([])
dictionary['measurements'] = list(map(lambda x: x.serialize([]), self._measurements))
dictionary['functions'] = list(map(lambda x: x.serialize(['all']), self._functions))
dictionary['components'] = list(map(lambda x: x.serialize(['all']), self._components))
dictionary['parameters'] = list(map(lambda x: x.serialize(['all']), self._parameters))
return dictionary
dictionary = super().serialize(keys, method)
if 'children' in keys:
everything = self._components + self._measurements + self._parameters + self._functions
dictionary['children'] = list(map(lambda x: x.serialize(['name', 'uuid']), everything))
# if 'update' in keys:
# # TODO implement (update all children)
# pass
# # if self._implementation is not None:
# # dict['update'] = self._implementation()
# else:
# dict['children'] = list(map(lambda x: x.serialize(['value', 'uuid']), self._measurements))
return dictionary
@staticmethod
def merge_dictionaries(parent_dict, component_dict):
def merge_measurements(parent_list, component_list):
for measurement in parent_list:
if 'uuid' not in measurement:
raise Exception('UUID {} not given for measurement to be overwritten.'.format(measurement['uuid']))
idx = [i for i, v in enumerate(component_list) if v['uuid'] == measurement['uuid']]
if len(idx) != 1:
raise Exception('Mismatching UUID: {}.'.format(measurement['uuid']))
idx = idx[0]
component_list[idx].update(measurement)
return component_list
def merge_functions(parent_list, component_list):
for function in parent_list:
if 'uuid' not in function:
raise Exception('UUID {} not given for function to be overwritten.'.format(function['uuid']))
idx = [i for i, v in enumerate(component_list) if v['uuid'] == function['uuid']]
if len(idx) != 1:
raise Exception('Mismatching UUID: {}.'.format(function['uuid']))
idx = idx[0]
if 'name' in function:
component_list[idx]['name'] = function['name']
if 'description' in function:
component_list[idx]['description'] = function['description']
if 'arguments' in function:
component_list[idx]['arguments'] = merge_measurements(function['arguments'], component_list[idx]['arguments'])
if 'returns' in function:
component_list[idx]['returns'] = merge_measurements(function['returns'], component_list[idx]['returns'])
return component_list
# merge components, i.e. overwrite fields of "static" children dictionary with the "dynamic" fields of the parents dictionary
uuid = parent_dict['uuid']
component_dict['uuid'] = uuid
if 'name' in parent_dict:
component_dict['name'] = parent_dict['name']
if 'description' in parent_dict:
component_dict['description'] = parent_dict['description']
if 'measurements' in parent_dict:
component_dict['measurements'] = merge_measurements(parent_dict['measurements'], component_dict['measurements'])
if 'parameters' in parent_dict:
component_dict['paramters'] = merge_measurements(parent_dict['parameters'], component_dict['parameters'])
if 'functions' in parent_dict:
component_dict['functions'] = merge_functions(parent_dict['functions'], component_dict['functions'])
if 'components' in parent_dict:
for obj in parent_dict['components']:
index = [i for i, o in enumerate(component_dict['components']) if o['uuid'] == obj['uuid']]
if len(index) != 1:
raise Exception('Mismatching UUID: {}.'.format(obj['uuid']))
index = index[0]
component_dict['components'][index] = Component.merge_dictionaries(obj, component_dict['components'][index])
return component_dict
@staticmethod
def deserialize(dictionary, mapping=None, filepath=''):
if 'uuid' not in dictionary:
raise SerialisationException('The component can not be deserialized. UUID is missing!')
uuid = dictionary['uuid']
if uuid[:3] != 'COM':
raise SerialisationException(
'The component can not be deserialized. The UUID must start with COM, but actually starts with {}!'.format(uuid[:3]))
if 'file' in dictionary:
try:
with open(os.path.normpath(os.path.join(filepath, dictionary['file']))) as file:
component_dict = json.load(file)
dictionary = Component.merge_dictionaries(dictionary, component_dict)
except Exception as e:
raise SerialisationException('{}: The component can not be deserialized. Provided JSON-file can not be parsed! {}'.format(uuid, e))
if 'name' not in dictionary:
raise SerialisationException('{}: The component can not be deserialized. Name is missing!'.format(uuid))
if 'description' not in dictionary:
raise SerialisationException('{}: The component can not be deserialized. Description is missing!'.format(uuid))
if 'measurements' not in dictionary:
raise SerialisationException('{}: The component can not be deserialized. List of measurements is missing!'.format(uuid))
if 'parameters' not in dictionary:
raise SerialisationException('{}: The component can not be deserialized. List of parameters is missing!'.format(uuid))
if 'functions' not in dictionary:
raise SerialisationException('{}: The component can not be deserialized. List of functions is missing!'.format(uuid))
if 'components' not in dictionary:
raise SerialisationException('{}: The component can not be deserialized. List of components is missing!'.format(uuid))
try:
measurements = []
for var in dictionary['measurements']:
if mapping is not None:
submapping = mapping[var["uuid"]] if var['uuid'] in mapping else None
measurements += [Measurement.deserialize(var, submapping)]
else:
measurements += [Measurement.deserialize(var)]
except Exception as e:
raise SerialisationException('{}: A measurement of the component can not be deserialized. {}'.format(uuid, e))
try:
parameters = []
for par in dictionary['parameters']:
if mapping is not None:
submapping = mapping[par["uuid"]] if par['uuid'] in mapping else None
parameters += [Parameter.deserialize(par, submapping)]
else:
parameters += [Parameter.deserialize(par)]
except Exception as e:
raise SerialisationException('{}: A parameter of the component can not be deserialized. {}'.format(uuid, e))
try:
functions = []
for func in dictionary['functions']:
if mapping is not None:
submapping = mapping[func["uuid"]] if func['uuid'] in mapping else None
functions += [Function.deserialize(func, submapping)]
else:
functions += [Function.deserialize(func)]
except Exception as e:
raise SerialisationException('{}: A function of the component can not be deserialized. {}'.format(uuid, e))
try:
components = []
for obj in dictionary['components']:
if mapping is not None:
submapping = mapping[obj["uuid"]] if obj['uuid'] in mapping else None
components += [Component.deserialize(obj, submapping, filepath)]
else:
components += [Component.deserialize(obj, filepath=filepath)]
except Exception as e:
raise SerialisationException('{}: An component of the component can not be deserialized. {}'.format(uuid, e))
try:
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
return Component(dictionary['uuid'], dictionary['name'], dictionary['description'], functions, measurements, parameters, components, mapping,
ontology)
except Exception as e:
raise SerialisationException('{}: The component can not be deserialized. {}'.format(uuid, e))
def write(self, filename: str):
if filename[-5:] != ".json":
raise Exception('{} is not a json file!'.format(filename))
model_dict = self.serialize(['all'])
f = open(filename, "w")
f.write(json.dumps(model_dict))
f.close()
def update(self, element: Union['Component', Function, Measurement, Parameter]):
if isinstance(element, Component):
for i, o in enumerate(self._components):
if o.uuid == element.uuid:
self._components[i] = element
return
# self._components.append(element)
else:
raise Exception("Wrong type updating element on existing model!")
def add(self, uuid: str, class_name: str, json_file: str, *args, **kwargs):
if uuid[:3] == 'COM':
if uuid not in [o.uuid for o in self._components]:
try:
__import__(class_name)
implementation = getattr(sys.modules[class_name], class_name)(*args, **kwargs)
mapping = docstring_parser.parse_docstrings_for_soil(implementation)
self._components += [Component.load(json_file, mapping)]
if self._implementation_add is not None:
self._implementation_add(implementation)
except Exception as e:
raise DeviceException('Can not add component with UUID {}. {}'.format(uuid, e), predecessor=e)
else:
raise UserException('Component has already a child with UUID {}.'.format(uuid))
else:
raise UserException('UUID {} is not of the UUID of an component.'.format(uuid))
def remove(self, uuid: str, *args, **kwargs):
for o in self._components:
if o.uuid == uuid:
if self._implementation_remove is not None:
try:
self._implementation_remove(*args, **kwargs)
except Exception as e:
raise DeviceException(str(e), predecessor=e)
self._components.remove(o)
return
raise ChildNotFoundException('{}: Child {} not found!'.format(self.uuid, uuid))
@staticmethod
def load(file: Union[str, dict], implementation: Any) -> 'Component':
if isinstance(file, str):
if not os.path.isfile(file):
raise Exception('There is no file named {}!'.format(file))
if file[-5:] != ".json":
raise Exception('{} is not a json file!'.format(file))
with open(file, 'r') as f:
model_dict = json.load(f)
return Component.deserialize(model_dict, implementation, os.path.dirname(file))
elif isinstance(file, dict):
return Component.deserialize(file, implementation)
else:
raise Exception('Given file is not a name of a json-file nor a json-like dictionary.')
......@@ -60,7 +60,6 @@ def parse_docstrings_for_mqtt(implementation, parent_url="", *args, **kwargs):
child_schedule = parse_children(parse_docstrings_for_mqtt, parent_url, implementation)
for key in child_schedule:
schedule += [*child_schedule[key]]
print(schedule)
# parse functions and setters/getters of variables and parameters
object_dict = implementation.__class__.__dict__
......
......@@ -4,25 +4,43 @@ from ..utils.error import BasicException, DeviceException, UserException
class TypeException(DeviceException):
def __init__(self, description):
BasicException.__init__(self, description)
DeviceException.__init__(self, description)
class RangeException(DeviceException):
def __init__(self, description):
BasicException.__init__(self, description)
DeviceException.__init__(self, description)
class DimensionException(DeviceException):
def __init__(self, description):
BasicException.__init__(self, description)
DeviceException.__init__(self, description)
class InvalidModelException(UserException):
def __init__(self, description):
UserException.__init__(self, description)
class InvalidMappingException(UserException):
def __init__(self, description):
UserException.__init__(self, description)
class ChildNotFoundException(DeviceException):
def __init__(self, description):
BasicException.__init__(self, description)
DeviceException.__init__(self, description)
class AmbiguousUUIDException(UserException):
def __init__(self, description):
UserException.__init__(self, description)
class ReadOnlyException(UserException):
......@@ -30,18 +48,18 @@ class ReadOnlyException(UserException):
def __init__(self, uuid, name, description=None):
if description is None:
description = 'The parameter "{}" with UUID "{}" is a read-only parameter.'.format(name, uuid)
BasicException.__init__(self, description)
UserException.__init__(self, description)
class NotImplementedException(UserException):
def __init__(self, uuid, name, description=None):
if description is None:
description = 'The function "{}" with UUID "{}" is not implemented.'.format(name, uuid)
BasicException.__init__(self, description)
UserException.__init__(self, description)
class InvokationException(DeviceException):
def __init__(self, uuid, name, description=None):
if description is None:
description = 'Error when invoking "{}" with UUID "{}".'.format(name, uuid)
BasicException.__init__(self, description)
\ No newline at end of file
DeviceException.__init__(self, description)
from deprecated import deprecated
from typing import Dict
from wzl.utilities import root_logger
from ..utils.constants import HTTP_GET
from ..utils.error import SerialisationException
from .figure import Figure
logger = root_logger.get(__name__)
class Measurement(Figure):
def __init__(self, uuid, name, description, datatype, dimension, range, getter, unit, nonce=None, ontology: str = None):
Figure.__init__(self, uuid, name, description, datatype, dimension, range, None, getter, ontology)
if uuid[:3] != 'MEA':
raise Exception('{}: The UUID must start with MEA!'.format(uuid))
self._unit = unit
self._covariance = None # TODO init
self._uncertainty = None # TODO init
self._timestamp = None
self._nonce = nonce
@property
def unit(self):
return self._unit
@property
def covariance(self):
return self._covariance
@property
def uncertainty(self):
return self._uncertainty
@property
def timestamp(self):
return self._timestamp
@property
def nonce(self):
return self._nonce
def __getitem__(self, item: str, method=HTTP_GET):
"""
Getter-Method.
According to the given key the method returns the value of the corresponding attribute.
:param item: name of the attribute. Provided as string without leading underscores.
:param method: ???
:return: the value of the attribute indicated by 'item'.
"""
if item == "unit":
return self._unit
if item == 'nonce':
return self._nonce
if item == 'covariance':
return self._covariance
if item == 'uncertainty':
return self._uncertainty
if item == 'timestamp':
return self._timestamp
if item == []:
return self
return super().__getitem__(item, method)
def __setitem__(self, key: str, value):
"""
Setter - Method
If key is "value" datatype, dimension and range is checked for correctness.
:param key: sets the value of the attribute with name 'item' to the provided value.
:param value: value to be set
"""
if key in ['value', 'timestamp', 'covariance', 'uncertainty']:
raise KeyError('The {} attribute of a measurement can not be set manually!'.format(key))
elif key == "nonce":
self._nonce = self._nonce
elif key == 'unit':
self._unit = self._unit
else:
super().__setitem__(key, value)
def serialize(self, keys: [str], method=HTTP_GET):
"""
Serializes an object of type Measurement into a JSON-like dictionary.
:param keys: All attributes given in the "keys" array are serialized.
:param method: ???
:return: a dictionary having all "keys" as keys and the values of the corresponding attributes as value.
"""
# list is empty provide all attributes of the default-serialization
if not keys:
keys = ['uuid', 'name', 'description', 'datatype', 'value', 'dimension', 'range', 'timestamp', 'nonce', 'covariance', 'uncertainty',
'unit', 'ontology']
# TODO temporary workaround, forces the server to always return the timestamp if the value is queried
if 'value' in keys and 'timestamp' not in keys:
keys += ['timestamp']
dictionary = {}
# get all attribute values
for key in keys:
value = self.__getitem__(key, method)
# 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 ""
dictionary[key] = value
return dictionary
@staticmethod
def deserialize(dictionary: Dict, implementation=None):
"""
Takes a JSON-like dictionary, parses it, performs a complete correctness check and returns an object of type Figure with the
values provided in the dictionary, if dictionary is a valid serialization of a Figure.
:param dictionary: serialized measurement
:param implementation: implementation wrapper object,
:return: an object of type Figure
"""
# check if all required attributes are present
if 'uuid' not in dictionary:
raise SerialisationException('The measurement can not be deserialized. UUID is missing!')
uuid = dictionary['uuid']
if uuid[:3] != 'MEA':
raise SerialisationException(
'The Measurement can not be deserialized. The UUID must start with MEA, but actually starts with {}!'.format(uuid[:3]))
if 'name' not in dictionary:
raise SerialisationException('{}: The measurement can not be deserialized. Name is missing!'.format(uuid))
if 'description' not in dictionary:
raise SerialisationException('{}: The measurement can not be deserialized. Description is missing!'.format(uuid))
if 'datatype' not in dictionary:
raise SerialisationException('{}: The measurement can not be deserialized. Datatype is missing!'.format(uuid))
if 'dimension' not in dictionary:
raise SerialisationException('{}: The measurement can not be deserialized. Dimension is missing!'.format(uuid))
if 'value' not in dictionary:
raise SerialisationException('{}: The measurement can not be deserialized. Value is missing!'.format(uuid))
if 'range' not in dictionary:
raise SerialisationException('{}: The measurement can not be deserialized. Range is missing!'.format(uuid))
if 'unit' not in dictionary:
raise SerialisationException('{}: The measurement can not be deserialized. Unit is missing!'.format(uuid))
try:
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
return Measurement(dictionary['uuid'], dictionary['name'], dictionary['description'], 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))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment