Merge "Implement a variant of the Demo protocol from Markus"

This commit is contained in:
Enrico Faulhaber 2016-08-31 08:34:06 +02:00 committed by Gerrit Code Review
commit 69b979cdd0
20 changed files with 1093 additions and 178 deletions

View File

@ -40,12 +40,11 @@ for dirpath, dirnames, filenames in os.walk(DOC_SRC):
except OSError:
pass
for fn in filenames:
full_name = path.join(dirpath, fn)
sub_name = path.relpath(full_name, DOC_SRC)
final_name = path.join(DOC_DST, sub_name)
if not fn.endswith('md'):
# just copy everything else
with open(full_name, 'rb') as fi:

View File

@ -67,7 +67,7 @@ def main(argv=None):
args = parseArgv(argv[1:])
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
loggers.initLogging('secop', loglevel, path.join(log_path))
loggers.initLogging('secop', loglevel, log_path)
srv = Server(args.name, basepath)

View File

@ -21,18 +21,18 @@
## A Server ##
* get daemonizing working
* handle -d (nodaemon) and -D (default, daemonize) cmd line args
* support Async data units
* support feature publishing and selection
* rewrite MessageHandler to be agnostic of server
* move encoding to interface
* allow multiple interfaces per server
* fix error handling an make it consistent
## Device framework ##
* unify PARAMS and CONFIG (if no default value is given,
it needs to be specified in cfgfile, otherwise its optional)
* supply properties for PARAMS to auto-generate async data units
* self-polling support
* generic devicethreads
* proxydevice
* make get_device uri-aware
## Testsuite ##
@ -45,7 +45,6 @@ it needs to be specified in cfgfile, otherwise its optional)
* mabe use sphinx to generate docu: a pdf can then be auto-generated....
* transfer build docu into wiki via automated jobfile
Problem: wiki does not understand .md or .html

39
etc/demo.cfg Normal file
View File

@ -0,0 +1,39 @@
[server]
bindto=0.0.0.0
bindport=10767
interface = tcp
framing=demo
encoding=demo
[device heatswitch]
class=devices.demo.Switch
switch_on_time=3
switch_off_time=5
[device mf]
class=devices.demo.MagneticField
heatswitch = heatswitch
[device ts]
class=devices.demo.SampleTemp
sensor = 'Q1329V7R3'
ramp = 4
target = 10
default = 10
[device tc1]
class=devices.demo.CoilTemp
sensor="X34598T7"
[device tc2]
class=devices.demo.CoilTemp
sensor="X39284Q8'
[device label]
class=devices.demo.Label
system=Cryomagnet MX15
subdev_mf=mf
subdev_ts=ts
[device vt]
class=devices.demo.ValidatorTest

View File

@ -2,7 +2,7 @@
markdown>=2.6
# general stuff
psutil
python-daemon
python-daemon >=2.0
# for zmq
#pyzmq>=13.1.0

View File

@ -27,45 +27,66 @@
# all others MUST derive from those, the 'interface'-class is still derived
# from these base classes (how to do this?)
import time
import types
import inspect
from errors import ConfigError, ProgrammingError
from protocol import status
from validators import mapping
EVENT_ONLY_ON_CHANGED_VALUES = True
# storage for PARAMeter settings:
# if readonly is False, the value can be changed (by code, or remte)
# if readonly is False, the value can be changed (by code, or remote)
# if no default is given, the parameter MUST be specified in the configfile
# during startup, currentvalue is initialized with the default value or
# from the config file
# during startup, value is initialized with the default value or
# from the config file if specified there
class PARAM(object):
def __init__(self, description, validator=None, default=Ellipsis,
unit=None, readonly=False, export=True):
def __init__(self, description, validator=float, default=Ellipsis,
unit=None, readonly=True, export=True):
if isinstance(description, PARAM):
# make a copy of a PARAM object
self.__dict__.update(description.__dict__)
return
self.description = description
self.validator = validator
self.default = default
self.unit = unit
self.readonly = readonly
self.export = export
# internal caching...
self.currentvalue = default
# internal caching: value and timestamp of last change...
self.value = default
self.timestamp = 0
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
# storage for CMDs settings (description + call signature...)
class CMD(object):
def __init__(self, description, arguments, result):
# descriptive text for humans
self.description = description
# list of validators for arguments
self.argumenttype = arguments
# validator for results
self.arguments = arguments
# validator for result
self.resulttype = result
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
# Meta class
# warning: MAGIC!
class DeviceMeta(type):
def __new__(mcs, name, bases, attrs):
newtype = type.__new__(mcs, name, bases, attrs)
if '__constructed__' in attrs:
@ -81,22 +102,56 @@ class DeviceMeta(type):
setattr(newtype, entry, newentry)
# check validity of PARAM entries
for pname, info in newtype.PARAMS.items():
if not isinstance(info, PARAM):
for pname, pobj in newtype.PARAMS.items():
# XXX: allow dicts for overriding certain aspects only.
if not isinstance(pobj, PARAM):
raise ProgrammingError('%r: device PARAM %r should be a '
'PARAM object!' % (name, pname))
#XXX: greate getters and setters, setters should send async updates
# XXX: create getters for the units of params ??
# wrap of reading/writing funcs
rfunc = attrs.get('read_' + pname, None)
def getter():
return self.PARAMS[pname].currentvalue
def wrapped_rfunc(self, maxage=0, pname=pname, rfunc=rfunc):
if rfunc:
value = rfunc(self, maxage)
setattr(self, pname, value)
return value
else:
# return cached value
return self.PARAMS[pname].value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
setattr(newtype, 'read_' + pname, wrapped_rfunc)
def setter(value):
p = self.PARAMS[pname]
p.currentvalue = p.validator(value) if p.validator else value
# also send notification
self.DISPATCHER.announce_update(self, pname, value)
if not pobj.readonly:
wfunc = attrs.get('write_' + pname, None)
attrs[pname] = property(getter, setter)
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("setter: set %s to %r" % (pname, value))
if wfunc:
value = wfunc(self, value) or value
# XXX: use setattr or direct manipulation
# of self.PARAMS[pname]?
setattr(self, pname, value)
return value
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
setattr(newtype, 'write_' + pname, wrapped_wfunc)
def getter(self, pname=pname):
return self.PARAMS[pname].value
def setter(self, value, pname=pname):
pobj = self.PARAMS[pname]
value = pobj.validator(value) if pobj.validator else value
pobj.timestamp = time.time()
if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value):
pobj.value = value
# also send notification
self.log.debug('%s is now %r' % (pname, value))
self.DISPATCHER.announce_update(self, pname, pobj)
setattr(newtype, pname, property(getter, setter))
# also collect/update information about CMD's
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
@ -114,6 +169,15 @@ class DeviceMeta(type):
# Basic device class
#
# within devices, parameters should only be addressed as self.<pname>
# i.e. self.value, self.target etc...
# these are accesses to the cached version.
# they can also be written to
# (which auto-calls self.write_<pname> and generate an async update)
# if you want to 'update from the hardware', call self.read_<pname>
# the return value of this method will be used as the new cached value and
# be returned.
class Device(object):
"""Basic Device, doesn't do much"""
__metaclass__ = DeviceMeta
@ -123,7 +187,7 @@ class Device(object):
DISPATCHER = None
def __init__(self, logger, cfgdict, devname, dispatcher):
# remember the server object (for the async callbacks)
# remember the dispatcher object (for the async callbacks)
self.DISPATCHER = dispatcher
self.log = logger
self.name = devname
@ -137,13 +201,17 @@ class Device(object):
# is not specified in cfgdict
for k, v in self.PARAMS.items():
if k not in cfgdict:
if v.default is Ellipsis:
if v.default is Ellipsis and k != 'value':
# Ellipsis is the one single value you can not specify....
raise ConfigError('Device %s: Parameter %r has no default '
'value and was not given in config!'
% (self.name, k))
# assume default value was given
cfgdict[k] = v.default
# replace CLASS level PARAM objects with INSTANCE level ones
self.PARAMS[k] = PARAM(self.PARAMS[k])
# now 'apply' config:
# pass values through the validators and store as attributes
for k, v in cfgdict.items():
@ -156,7 +224,6 @@ class Device(object):
except ValueError as e:
raise ConfigError('Device %s: config parameter %r:\n%r'
% (self.name, k, e))
# XXX: with or without prefix?
setattr(self, k, v)
def init(self):
@ -172,15 +239,15 @@ class Readable(Device):
PARAMS = {
'value': PARAM('current value of the device', readonly=True, default=0.),
'status': PARAM('current status of the device', default=status.OK,
validator=mapping(**{'idle': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN}),
readonly=True),
}
def read_value(self, maxage=0):
raise NotImplementedError
def read_status(self):
return status.OK
class Driveable(Readable):
"""Basic Driveable device
@ -188,8 +255,6 @@ class Driveable(Readable):
providing a settable 'target' parameter to those of a Readable
"""
PARAMS = {
'target': PARAM('target value of the device', default=0.),
'target': PARAM('target value of the device', default=0.,
readonly=False),
}
def write_target(self, value):
raise NotImplementedError

View File

@ -146,7 +146,7 @@ class Cryostat(Driveable):
def __heatLink(self, coolertemp, sampletemp):
"""heatflow from sample to cooler. may be negative..."""
flow = (sampletemp - coolertemp) * \
((coolertemp + sampletemp) ** 2)/400.
((coolertemp + sampletemp) ** 2) / 400.
cp = clamp(self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp),
1, 10)
return clamp(flow, -cp, cp)
@ -156,7 +156,7 @@ class Cryostat(Driveable):
12 * temp / ((temp - 12.)**2 + 10) + 0.5
def __sampleLeak(self, temp):
return 0.02/temp
return 0.02 / temp
def thread(self):
self.sampletemp = self.config_T_start
@ -304,8 +304,8 @@ class Cryostat(Driveable):
# obtain min/max
deviation = 0
for _, T in window:
if abs(T-self.target) > deviation:
deviation = abs(T-self.target)
if abs(T - self.target) > deviation:
deviation = abs(T - self.target)
if (len(window) < 3) or deviation > self.tolerance:
self.status = status.BUSY, 'unstable'
elif self.setpoint == self.target:

265
src/devices/demo.py Normal file
View File

@ -0,0 +1,265 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# *****************************************************************************
"""testing devices"""
import time
import random
import threading
from devices.core import Readable, Driveable, PARAM
from validators import *
from protocol import status
class Switch(Driveable):
"""switch it on or off....
"""
PARAMS = {
'value': PARAM('current state (on or off)',
validator=mapping(on=1, off=0), default=0),
'target': PARAM('wanted state (on or off)',
validator=mapping(on=1, off=0), default=0,
readonly=False),
'switch_on_time': PARAM('how long to wait after switching the switch on', validator=floatrange(0, 60), unit='s', default=10, export=False),
'switch_off_time': PARAM('how long to wait after switching the switch off', validator=floatrange(0, 60), unit='s', default=10, export=False),
}
def init(self):
self._started = 0
def read_value(self, maxage=0):
# could ask HW
# we just return the value of the target here.
self._update()
return self.value
def read_target(self, maxage=0):
# could ask HW
return self.target
def write_target(self, value):
# could tell HW
pass
# note: setting self.target to the new value is done after this....
# note: we may also return the read-back value from the hw here
def read_status(self, maxage=0):
self.log.info("read status")
self._update()
if self.target == self.value:
return status.OK
return status.BUSY
def _update(self):
started = self.PARAMS['target'].timestamp
if self.target > self.value:
if time.time() > started + self.switch_on_time:
self.log.debug('is switched ON')
self.value = self.target
elif self.target < self.value:
if time.time() > started + self.switch_off_time:
self.log.debug('is switched OFF')
self.value = self.target
class MagneticField(Driveable):
"""a liquid magnet
"""
PARAMS = {
'value': PARAM('current field in T', unit='T',
validator=floatrange(-15, 15), default=0),
'ramp': PARAM('moving speed in T/min', unit='T/min',
validator=floatrange(0, 1), default=0.1, readonly=False),
'mode': PARAM('what to do after changing field', default=0,
validator=mapping(persistent=1, hold=0), readonly=False),
'heatswitch': PARAM('heat switch device',
validator=str, export=False),
}
def init(self):
self._state = 'idle'
self._heatswitch = self.DISPATCHER.get_device(self.heatswitch)
_thread = threading.Thread(target=self._thread)
_thread.daemon = True
_thread.start()
def read_value(self, maxage=0):
return self.value
def write_target(self, value):
# could tell HW
return round(value, 2)
# note: setting self.target to the new value is done after this....
# note: we may also return the read-back value from the hw here
def read_status(self, maxage=0):
return status.OK if self._state == 'idle' else status.BUSY
def _thread(self):
loopdelay = 1
while True:
ts = time.time()
if self._state == 'idle':
if self.target != self.value:
self.log.debug('got new target -> switching heater on')
self._state = 'switch_on'
self._heatswitch.write_target('on')
if self._state == 'switch_on':
# wait until switch is on
if self._heatswitch.read_value() == 'on':
self.log.debug(
'heatswitch is on -> ramp to %.3f' %
self.target)
self._state = 'ramp'
if self._state == 'ramp':
if self.target == self.value:
self.log.debug('at field! mode is %r' % self.mode)
if self.mode:
self.log.debug('at field -> switching heater off')
self._state = 'switch_off'
self._heatswitch.write_target('off')
else:
self.log.debug('at field -> hold')
self._state = 'idle'
self.status = self.read_status() # push async
else:
step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step)
self.value += step
if self._state == 'switch_off':
# wait until switch is off
if self._heatswitch.read_value() == 'off':
self.log.debug('heatswitch is off at %.3f' % self.value)
self._state = 'idle'
self.read_status() # update async
time.sleep(max(0.01, ts + loopdelay - time.time()))
self.log.error(self, 'main thread exited unexpectedly!')
class CoilTemp(Readable):
"""a coil temperature
"""
PARAMS = {
'value': PARAM('Coil temperatur in K', unit='K',
validator=float, default=0),
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
}
def read_value(self, maxage=0):
return round(2.3 + random.random(), 3)
class SampleTemp(Driveable):
"""a sample temperature
"""
PARAMS = {
'value': PARAM('Sample temperatur in K', unit='K',
validator=float, default=10),
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
'ramp': PARAM('moving speed in K/min',
validator=floatrange(0, 100), unit='K/min', default=0.1, readonly=False),
}
def init(self):
_thread = threading.Thread(target=self._thread)
_thread.daemon = True
_thread.start()
def write_target(self, value):
# could tell HW
return round(value, 2)
# note: setting self.target to the new value is done after this....
# note: we may also return the read-back value from the hw here
def _thread(self):
loopdelay = 1
while True:
ts = time.time()
if self.value == self.target:
if self.status != status.OK:
self.status = status.OK
else:
self.status = status.BUSY
step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step)
self.value += step
time.sleep(max(0.01, ts + loopdelay - time.time()))
self.log.error(self, 'main thread exited unexpectedly!')
class Label(Readable):
"""
"""
PARAMS = {
'system': PARAM("Name of the magnet system",
validator=str, export=False),
'subdev_mf': PARAM("name of subdevice for magnet status",
validator=str, export=False),
'subdev_ts': PARAM("name of subdevice for sample temp",
validator=str, export=False),
'value': PARAM("Value of out label string",
validator=str)
}
def read_value(self, maxage=0):
strings = [self.system]
dev_ts = self.DISPATCHER.get_device(self.subdev_ts)
if dev_ts:
strings.append('at %.3f %s' %
(dev_ts.read_value(), dev_ts.PARAMS['value'].unit))
else:
strings.append('No connection to sample temp!')
dev_mf = self.DISPATCHER.get_device(self.subdev_mf)
if dev_mf:
mf_stat = dev_mf.read_status()
mf_mode = dev_mf.mode
mf_val = dev_mf.value
mf_unit = dev_mf.PARAMS['value'].unit
if mf_stat == status.OK:
state = 'Persistent' if mf_mode else 'Non-persistent'
else:
state = 'ramping'
strings.append('%s at %.1f %s' % (state, mf_val, mf_unit))
else:
strings.append('No connection to magnetic field!')
return '; '.join(strings)
class ValidatorTest(Readable):
"""
"""
PARAMS = {
'oneof': PARAM('oneof', validator=oneof(int, 'X', 2.718), readonly=False, default=4.0),
'mapping': PARAM('mapping', validator=mapping('boo', 'faar', z=9), readonly=False, default=1),
'vector': PARAM('vector of int, float and str', validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')),
'array': PARAM('array: 2..3 time oneof(0,1)', validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]),
'nonnegative': PARAM('nonnegative', validator=nonnegative(), readonly=False, default=0),
'positive': PARAM('positive', validator=positive(), readonly=False, default=1),
'intrange': PARAM('intrange', validator=intrange(2, 9), readonly=False, default=4),
'floatrange': PARAM('floatrange', validator=floatrange(-1, 1), readonly=False, default=0,),
}

View File

@ -28,14 +28,16 @@ from validators import floatrange
from epics import PV
class LN2(Readable):
"""Just a readable.
class name indicates it to be a sensor for LN2,
but the implementation may do anything
"""
def read_value(self, maxage=0):
return round(100*random.random(), 1)
return round(100 * random.random(), 1)
class Heater(Driveable):
@ -46,11 +48,11 @@ class Heater(Driveable):
"""
PARAMS = {
'maxheaterpower': PARAM('maximum allowed heater power',
validator=floatrange(0, 100), unit='W'),
validator=floatrange(0, 100), unit='W'),
}
def read_value(self, maxage=0):
return round(100*random.random(), 1)
return round(100 * random.random(), 1)
def write_target(self, target):
pass
@ -64,23 +66,22 @@ class Temp(Driveable):
"""
PARAMS = {
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
validator=str, readonly=True),
}
def read_value(self, maxage=0):
return round(100*random.random(), 1)
return round(100 * random.random(), 1)
def write_target(self, target):
pass
class EPICS_PV(Driveable):
"""pyepics test device."""
PARAMS = {
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
validator=str, readonly=True),
'max_rpm': PARAM("Maximum allowed rpm",
validator=str, readonly=True),
}

View File

@ -35,14 +35,13 @@ from logging import Logger, Formatter, Handler, DEBUG, INFO, WARNING, ERROR, \
from . import colors
LOGFMT = '%(asctime)s : %(levelname)-7s : %(name)-15s: %(message)s'
DATEFMT = '%H:%M:%S'
DATESTAMP_FMT = '%Y-%m-%d'
SECONDS_PER_DAY = 60 * 60 * 24
LOGLEVELS = {'debug': DEBUG, 'info': INFO, 'warning': WARNING, 'error': ERROR}
INVLOGLEVELS = {value : key for key, value in LOGLEVELS.items()}
INVLOGLEVELS = {value: key for key, value in LOGLEVELS.items()}
log = None
@ -58,7 +57,10 @@ def initLogging(rootname='secop', rootlevel='info', logdir='/tmp/log'):
log.addHandler(ColoredConsoleHandler())
# logfile for fg and bg process
log.addHandler(LogfileHandler(logdir, rootname))
if logdir.startswith('/var/log'):
log.addHandler(LogfileHandler(logdir, rootname))
else:
log.addHandler(LogfileHandler(logdir, ''))
def getLogger(name, subdir=False):
@ -73,7 +75,6 @@ class SecopLogger(Logger):
Logger.__init__(self, *args, **kwargs)
SecopLogger._storeLoggerNameLength(self)
def getChild(self, suffix, ownDir=False):
child = Logger.getChild(self, suffix)
child.setLevel(self.getEffectiveLevel())
@ -111,7 +112,6 @@ class SecopLogger(Logger):
SecopLogger.maxLogNameLength = len(logObj.name)
class ConsoleFormatter(Formatter):
"""
A lightweight formatter for the interactive console, with optional

View File

@ -27,12 +27,12 @@
_codes = {}
_attrs = {
'reset': '39;49;00m',
'bold': '01m',
'faint': '02m',
'standout': '03m',
'reset': '39;49;00m',
'bold': '01m',
'faint': '02m',
'standout': '03m',
'underline': '04m',
'blink': '05m',
'blink': '05m',
}
for _name, _value in _attrs.items():

View File

@ -70,6 +70,7 @@ class Driveable(Writeable):
"""A Moveable which may take a while to reach its target,
hence stopping it may be desired"""
def do_stop(self):
raise NotImplementedError('A Driveable MUST implement the STOP() '
'command')

View File

@ -37,17 +37,19 @@ Interface to the devices:
- remove_device(devname_or_obj): removes the device (during shutdown)
internal stuff which may be called
- get_devices(): return a list of devices + descriptive data as dict
- get_device_params():
- list_devices(): return a list of devices + descriptive data as dict
- list_device_params():
return a list of paramnames for this device + descriptive data
"""
import time
import threading
from messages import *
class Dispatcher(object):
def __init__(self, logger, options):
self.log = logger
# XXX: move framing and encoding to interface!
@ -73,7 +75,12 @@ class Dispatcher(object):
with self._dispatcher_lock:
# de-frame data
frames = self.framing.decode(data)
if frames is None:
# not enough data (yet) -> return and come back with more
return None
self.log.debug('Dispatcher: frames=%r' % frames)
if not frames:
conn.queue_reply(self._format_reply(HelpReply()))
for frame in frames:
reply = None
# decode frame
@ -82,45 +89,53 @@ class Dispatcher(object):
# act upon requestobj
msgtype = msg.TYPE
msgname = msg.NAME
msgargs = msg
# generate reply (coded and framed)
if msgtype != 'request':
reply = ProtocolErrorReply(msg)
reply = ProtocolError(msg)
else:
self.log.debug('Looking for handle_%s' % msgname)
handler = getattr(self, 'handle_%s' % msgname, None)
if handler:
reply = handler(msgargs)
reply = handler(conn, msg)
else:
self.log.debug('Can not handle msg %r' % msg)
reply = self.unhandled(msgname, msgargs)
reply = self.unhandled(msgname, msg)
if reply:
conn.queue_reply(self._format_reply(reply))
# queue reply viy conn.queue_reply(data)
# queue reply via conn.queue_reply(data)
def _format_reply(self, reply):
self.log.debug('formatting reply %r' % reply)
msg = self.encoding.encode(reply)
self.log.debug('encoded is %r' % msg)
frame = self.framing.encode(msg)
self.log.debug('frame is %r' % frame)
return frame
def announce_update(self, device, pname, value):
def announce_update(self, devobj, pname, pobj):
"""called by devices param setters to notify subscribers of new values
"""
eventname = '%s/%s' % (self.get_device(device).name, pname)
devname = devobj.name
eventname = '%s/%s' % (devname, pname)
subscriber = self._dispatcher_subscriptions.get(eventname, None)
if subscriber:
reply = AsyncDataUnit(device=self.get_device(device).name,
param=pname,
value=str(value),
timestamp=time.time(),
reply = AsyncDataUnit(devname=devname,
pname=pname,
value=str(pobj.value),
timestamp=pobj.timestamp,
)
data = self._format_reply(reply)
for conn in subscriber:
conn.queue_async_reply(data)
def subscribe(self, conn, device, pname):
eventname = '%s/%s' % (self.get_device(device).name, pname)
self._dispatcher_subscriptions.getdefault(eventname, set()).add(conn)
def subscribe(self, conn, devname, pname):
eventname = '%s/%s' % (devname, pname)
self._dispatcher_subscriptions.setdefault(eventname, set()).add(conn)
def unsubscribe(self, conn, devname, pname):
eventname = '%s/%s' % (devname, pname)
if eventname in self._dispatcher_subscriptions:
self._dispatcher_subscriptions.remove(conn)
def add_connection(self, conn):
"""registers new connection"""
@ -133,6 +148,8 @@ class Dispatcher(object):
# XXX: also clean _dispatcher_subscriptions !
def register_device(self, devobj, devname, export=True):
self.log.debug('registering Device %r as %s (export=%r)' %
(devobj, devname, export))
self._dispatcher_devices[devname] = devobj
if export:
self._dispatcher_export.append(devname)
@ -171,100 +188,295 @@ class Dispatcher(object):
return dn, dd
def list_device_params(self, devname):
self.log.debug('list_device_params(%r)' % devname)
if devname in self._dispatcher_export:
# XXX: omit export=False params!
return self.get_device(devname).PARAMS
res = {}
for paramname, param in self.get_device(devname).PARAMS.items():
if param.export == True:
res[paramname] = param
self.log.debug('list params for device %s -> %r' %
(devname, res))
return res
self.log.debug('-> device is not to be exported!')
return {}
# demo stuff
def _setDeviceValue(self, devobj, value):
# set the device value. return readback value
# if return == None -> Ellispis (readonly!)
if self._getDeviceParam(devobj, 'target') != Ellipsis:
return self._setDeviceParam(devobj, 'target', value)
return Ellipsis
def _getDeviceValue(self, devobj):
# get the device value
# if return == None -> Ellipsis
return self._getDeviceParam(devobj, 'value')
def _setDeviceParam(self, devobj, pname, value):
# set the device param. return readback value
# if return == None -> Ellipsis (readonly!)
pobj = devobj.PARAMS.get(pname, Ellipsis)
if pobj == Ellipsis:
return pobj
if pobj.readonly:
return self._getDeviceParam(devobj, pname)
writefunc = getattr(devobj, 'write_%s' % pname, None)
validator = pobj.validator
value = validator(value)
if writefunc:
value = writefunc(value) or value
else:
setattr(devobj, pname, value)
return self._getDeviceParam(devobj, pname)
def _getDeviceParam(self, devobj, pname):
# get the device value
# if return == None -> Ellipsis
readfunc = getattr(devobj, 'read_%s' % pname, None)
if readfunc:
# should also update the pobj (via the setter from the metaclass)
readfunc()
pobj = devobj.PARAMS.get(pname, None)
if pobj:
return (pobj.value, pobj.timestamp)
return getattr(devobj, pname, Ellipsis)
def handle_Demo(self, conn, msg):
novalue = msg.novalue
devname = msg.devname
paramname = msg.paramname
propname = msg.propname
assign = msg.assign
res = []
if novalue in ('+', '-'):
# XXX: handling of subscriptions: propname is ignored
if devname is None:
# list all subscriptions for this connection
for evname, conns in self._dispatcher_subscriptions.items():
if conn in conns:
res.append('+%s:%s' % evname.split('/'))
devices = self._dispatcher_export if devname == '*' else [devname]
for devname in devices:
devobj = self.get_device(devname)
if devname != '*' and devobj is None:
return NoSuchDeviceError(devname)
if paramname is None:
pnames = ['value', 'status']
elif paramname == '*':
pnames = devobj.PARAMS.keys()
else:
pnames = [paramname]
for pname in pnames:
pobj = devobj.PARAMS.get(pname, None)
if pobj and not pobj.export:
continue
if paramname != '*' and pobj is None:
return NoSuchParamError(devname, paramname)
if novalue == '+':
# subscribe
self.subscribe(conn, devname, pname)
res.append('+%s:%s' % (devname, pname))
elif novalue == '-':
# unsubscribe
self.unsubscribe(conn, devname, pname)
res.append('-%s:%s' % (devname, pname))
return DemoReply(res)
if devname is None:
return Error('no devname given!')
devices = self._dispatcher_export if devname == '*' else [devname]
for devname in devices:
devobj = self.get_device(devname)
if devname != '*' and devobj is None:
return NoSuchDeviceError(devname)
if paramname is None:
# Access Devices
val = self._setDeviceValue(
devobj, assign) if assign else self._getDeviceValue(devobj)
if val == Ellipsis:
if assign:
return ParamReadonlyError(devname, 'target')
return NoSuchDevice(devname)
formatfunc = lambda x: '' if novalue else ('=%r;t=%r' % x)
res.append(devname + formatfunc(val))
else:
pnames = devobj.PARAMS.keys(
) if paramname == '*' else [paramname]
for pname in pnames:
pobj = devobj.PARAMS.get(pname, None)
if pobj and not pobj.export:
continue
if paramname != '*' and pobj is None:
return NoSuchParamError(devname, paramname)
if propname is None:
# access params
callfunc = lambda x, y: self._setDeviceParam(x, y, assign) \
if assign else self._getDeviceParam(x, y)
formatfunc = lambda x: '' if novalue else (
'=%r;t=%r' % x)
try:
res.append(('%s:%s' % (devname, pname)) +
formatfunc(callfunc(devobj, pname)))
except TypeError as e:
return InternalError(e)
else:
props = pobj.__dict__.keys(
) if propname == '*' else [propname]
for prop in props:
# read props
try:
if novalue:
res.append(
'%s:%s:%s' %
(devname, pname, prop))
else:
res.append(
'%s:%s:%s=%r' %
(devname, pname, prop, getattr(
pobj, prop)))
except TypeError as e:
return InternalError(e)
# now clean responce a little
res = [
e.replace(
'/v=',
'=') for e in sorted(
(e.replace(
':value=',
'/v=') for e in res))]
return DemoReply(res)
# now the (defined) handlers for the different requests
def handle_Help(self, msg):
def handle_Help(self, conn, msg):
return HelpReply()
def handle_ListDevices(self, msgargs):
def handle_ListDevices(self, conn, msg):
# XXX: What about the descriptive data????
# XXX: choose!
#return ListDevicesReply(self.list_device_names())
return ListDevicesReply(*self.list_devices())
return ListDevicesReply(self.list_device_names())
# return ListDevicesReply(*self.list_devices())
def handle_ListDeviceParams(self, msgargs):
devobj = self.get_device(msgargs.device)
if devobj:
return ListDeviceParamsReply(msgargs.device,
self.get_device_params(devobj))
def handle_ListDeviceParams(self, conn, msg):
# reply with a list of the parameter names for a given device
self.log.error('Keep: ListDeviceParams')
if msg.device in self._dispatcher_export:
params = self.list_device_params(msg.device)
return ListDeviceParamsReply(msg.device, params.keys())
else:
return NoSuchDeviceErrorReply(msgargs.device)
return NoSuchDeviceError(msg.device)
def handle_ReadValue(self, msgargs):
devobj = self.get_device(msgargs.device)
if devobj:
return ReadValueReply(msgargs.device, devobj.read_value(),
def handle_ReadAllDevices(self, conn, msg):
# reply with a bunch of ReadValueReplies, reading ALL devices
result = []
for devname in sorted(self.list_device_names()):
devobj = self.get_device(devname)
value = self._getdeviceValue(devobj)
if value is not Ellipsis:
result.append(ReadValueReply(devname, value,
timestamp=time.time()))
return ReadAllDevicesReply(readValueReplies=result)
def handle_ReadValue(self, conn, msg):
devname = msg.device
devobj = self.get_device(devname)
if devobj is None:
return NoSuchDeviceError(devname)
value = self._getdeviceValue(devname)
if value is not Ellipsis:
return ReadValueReply(devname, value,
timestamp=time.time())
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_ReadParam(self, msgargs):
devobj = self.get_device(msgargs.device)
if devobj:
readfunc = getattr(devobj, 'read_%s' % msgargs.param, None)
if readfunc:
return ReadParamReply(msgargs.device, msgargs.param,
readfunc(), timestamp=time.time())
else:
return NoSuchParamErrorReply(msgargs.device, msgargs.param)
else:
return NoSuchDeviceErrorReply(msgargs.device)
return InternalError('undefined device value')
def handle_WriteParam(self, msgargs):
devobj = self.get_device(msgargs.device)
if devobj:
writefunc = getattr(devobj, 'write_%s' % msgargs.param, None)
if writefunc:
readbackvalue = writefunc(msgargs.value) or msgargs.value
# trigger async updates
setattr(devobj, msgargs.param, readbackvalue)
return WriteParamReply(msgargs.device, msgargs.param,
readbackvalue,
timestamp=time.time())
else:
if getattr(devobj, 'read_%s' % msgargs.param, None):
return ParamReadonlyErrorReply(msgargs.device,
msgargs.param)
else:
return NoSuchParamErrorReply(msgargs.device,
msgargs.param)
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_WriteValue(self, conn, msg):
value = msg.value
devname = msg.device
devobj = self.get_device(devname)
if devobj is None:
return NoSuchDeviceError(devname)
def handle_RequestAsyncData(self, msgargs):
return ErrorReply('AsyncData is not (yet) supported')
pobj = getattr(devobj.PARAMS, 'target', None)
if pobj is None:
return NoSuchParamError(devname, 'target')
def handle_ListOfFeatures(self, msgargs):
if pobj.readonly:
return ParamReadonlyError(devname, 'target')
validator = pobj.validator
try:
value = validator(value)
except Exception as e:
return InvalidParamValueError(devname, 'target', value, e)
value = self._setDeviceValue(devobj, value) or value
WriteValueReply(devname, value, timestamp=time.time())
def handle_ReadParam(self, conn, msg):
devname = msg.device
pname = msg.param
devobj = self.get_device(devname)
if devobj is None:
return NoSuchDeviceError(devname)
pobj = getattr(devobj.PARAMS, pname, None)
if pobj is None:
return NoSuchParamError(devname, pname)
value = self._getdeviceParam(devobj, pname)
if value is not Ellipsis:
return ReadParamReply(devname, pname, value,
timestamp=time.time())
return InternalError('undefined device value')
def handle_WriteParam(self, conn, msg):
value = msg.value
pname = msg.param
devname = msg.device
devobj = self.get_device(devname)
if devobj is None:
return NoSuchDeviceError(devname)
pobj = getattr(devobj.PARAMS, pname, None)
if pobj is None:
return NoSuchParamError(devname, pname)
if pobj.readonly:
return ParamReadonlyError(devname, pname)
validator = pobj.validator
try:
value = validator(value)
except Exception as e:
return InvalidParamValueError(devname, pname, value, e)
value = self._setDeviceParam(devobj, pname, value) or value
WriteParamReply(devname, pname, value, timestamp=time.time())
# XXX: !!!
def handle_RequestAsyncData(self, conn, msg):
return Error('AsyncData is not (yet) supported')
def handle_ListOfFeatures(self, conn, msg):
# no features supported (yet)
return ListOfFeaturesReply([])
def handle_ActivateFeature(self, msgargs):
return ErrorReply('Features are not (yet) supported')
def handle_ActivateFeature(self, conn, msg):
return Error('Features are not (yet) supported')
def unhandled(self, msgname, msgargs):
def unhandled(self, msgname, conn, msg):
"""handler for unhandled Messages
(no handle_<messagename> method was defined)
"""
self.log.error('IGN: got unhandled request %s' % msgname)
return ErrorReply('Got Unhandled Request')
def parse_message(self, message):
# parses a message and returns
# msgtype, msgname and parameters of message (as dict)
msgtype = 'unknown'
msgname = 'unknown'
if isinstance(message, ErrorReply):
msgtype = message.TYPE
msgname = message.__class__.__name__[:-len('Reply')]
elif isinstance(message, Request):
msgtype = message.TYPE
msgname = message.__class__.__name__[:-len('Request')]
elif isinstance(message, Reply):
msgtype = message.TYPE
msgname = message.__class__.__name__[:-len('Reply')]
return msgtype, msgname, \
attrdict([(k, getattr(message, k)) for k in message.ARGS])
return Error('Got Unhandled Request')

View File

@ -32,6 +32,7 @@ MAX_MESSAGE_SIZE = 1024
class TCPRequestHandler(SocketServer.BaseRequestHandler):
def setup(self):
self.log = self.server.log
self._queue = collections.deque(maxlen=100)

View File

@ -48,12 +48,19 @@ class Message(object):
'argument %r' % k)
names.remove(k)
self.__dict__[k] = v
if names:
raise TypeError('__init__() takes at least %d arguments (%d given)'
% len(self.ARGS), len(args)+len(kwds))
for name in names:
self.__dict__[name] = None
# if names:
# raise TypeError('__init__() takes at least %d arguments (%d given)'
# % (len(self.ARGS), len(args)+len(kwds)))
self.NAME = (self.__class__.__name__[:-len(self.TYPE)] or
self.__class__.__name__)
def __repr__(self):
return self.__class__.__name__ + '(' + \
', '.join('%s=%r' % (k, getattr(self, k))
for k in self.ARGS if getattr(self, k) is not None) + ')'
class Request(Message):
TYPE = REQUEST
@ -66,6 +73,16 @@ class Reply(Message):
class ErrorReply(Message):
TYPE = ERROR
# for DEMO
class DemoRequest(Request):
ARGS = ['novalue', 'devname', 'paramname', 'propname', 'assign']
class DemoReply(Reply):
ARGS = ['lines']
# actuall message objects
class ListDevicesRequest(Request):
@ -92,6 +109,30 @@ class ReadValueReply(Reply):
ARGS = ['device', 'value', 'timestamp', 'error', 'unit']
class WriteValueRequest(Request):
ARGS = ['device', 'value', 'unit'] # unit???
class WriteValueReply(Reply):
ARGS = ['device', 'value', 'timestamp', 'error', 'unit']
class ReadAllDevicesRequest(Request):
ARGS = ['maxage']
class ReadAllDevicesReply(Reply):
ARGS = ['readValueReplies']
class ListParamPropsRequest(Request):
ARGS = ['device', 'param']
class ListParamPropsReply(Request):
ARGS = ['device', 'param', 'props']
class ReadParamRequest(Request):
ARGS = ['device', 'param', 'maxage']
@ -117,7 +158,7 @@ class RequestAsyncDataReply(Reply):
class AsyncDataUnit(ReadParamReply):
ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
ARGS = ['devname', 'pname', 'value', 'timestamp', 'error', 'unit']
class ListOfFeaturesRequest(Request):
@ -138,40 +179,47 @@ class ActivateFeatureReply(Reply):
pass
class ProtocollError(ErrorReply):
ARGS = ['msgtype', 'msgname', 'msgargs']
# ERRORS
########
class ErrorReply(Reply):
ARGS = ['error']
class NoSuchDeviceErrorReply(ErrorReply):
class InternalError(ErrorReply):
ARGS = ['error']
class ProtocollError(ErrorReply):
ARGS = ['error']
class NoSuchDeviceError(ErrorReply):
ARGS = ['device']
class NoSuchParamErrorReply(ErrorReply):
class NoSuchParamError(ErrorReply):
ARGS = ['device', 'param']
class ParamReadonlyErrorReply(ErrorReply):
class ParamReadonlyError(ErrorReply):
ARGS = ['device', 'param']
class UnsupportedFeatureErrorReply(ErrorReply):
class UnsupportedFeatureError(ErrorReply):
ARGS = ['feature']
class NoSuchCommandErrorReply(ErrorReply):
class NoSuchCommandError(ErrorReply):
ARGS = ['device', 'command']
class CommandFailedErrorReply(ErrorReply):
class CommandFailedError(ErrorReply):
ARGS = ['device', 'command']
class InvalidParamValueErrorReply(ErrorReply):
ARGS = ['device', 'param', 'value']
class InvalidParamValueError(ErrorReply):
ARGS = ['device', 'param', 'value', 'error']
# Fun!

View File

@ -28,3 +28,10 @@ WARN = 300
UNSTABLE = 350
ERROR = 400
UNKNOWN = -1
#OK = 'idle'
#BUSY = 'busy'
#WARN = 'alarm'
#UNSTABLE = 'unstable'
#ERROR = 'ERROR'
#UNKNOWN = 'unknown'

View File

@ -61,6 +61,7 @@ class PickleEncoder(MessageEncoder):
class TextEncoder(MessageEncoder):
def __init__(self):
# build safe namespace
ns = dict()
@ -94,9 +95,109 @@ class TextEncoder(MessageEncoder):
return messages.HelpRequest()
def format_time(ts):
return float(ts) # XXX: switch to iso!
import re
DEMO_RE = re.compile(
r'^([!+-])?(\*|[a-z_][a-z_0-9]*)?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\=(.*))?')
def parse_str(s):
# QnD Hack! try to parse lists/tuples/ints/floats, ignore dicts, specials
# XXX: replace by proper parsing. use ast?
s = s.strip()
if s.startswith('[') and s.endswith(']'):
# evaluate inner
return [parse_str(part) for part in s[1:-1].split(',')]
if s.startswith('(') and s.endswith(')'):
# evaluate inner
return [parse_str(part) for part in s[1:-1].split(',')]
if s.startswith('"') and s.endswith('"'):
# evaluate inner
return s[1:-1]
if s.startswith("'") and s.endswith("'"):
# evaluate inner
return s[1:-1]
for conv in (int, float, lambda x: x):
try:
return conv(s)
except ValueError:
pass
class DemoEncoder(MessageEncoder):
def decode(sef, encoded):
# match [!][*|devicename][: *|paramname [: *|propname]] [=value]
match = DEMO_RE.match(encoded)
if match:
novalue, devname, pname, propname, assign = match.groups()
if assign:
print "parsing", assign,
assign = parse_str(assign)
print "->", assign
return messages.DemoRequest(
novalue, devname, pname, propname, assign)
return messages.HelpRequest()
def encode(self, msg):
if isinstance(msg, messages.DemoReply):
return msg.lines
handler_name = '_encode_' + msg.__class__.__name__
handler = getattr(self, handler_name, None)
if handler is None:
print "Handler %s not yet implemented!" % handler_name
try:
args = dict((k, msg.__dict__[k]) for k in msg.ARGS)
result = handler(**args)
except Exception as e:
print "Error encoding %r with %r!" % (msg, handler)
print e
return '~InternalError~'
return result
def _encode_AsyncDataUnit(self, devname, pname, value, timestamp,
error=None, unit=''):
return '#%s:%s=%s;t=%.3f' % (devname, pname, value, timestamp)
def _encode_Error(self, error):
return '~Error~ %r' % error
def _encode_InternalError(self, error):
return '~InternalError~ %r' % error
def _encode_ProtocollError(self, msgtype, msgname, msgargs):
return '~ProtocolError~ %s.%s.%r' % (msgtype, msgname, msgargs)
def _encode_NoSuchDeviceError(self, device):
return '~NoSuchDeviceError~ %s' % device
def _encode_NoSuchParamError(self, device, param):
return '~NoSuchParameterError~ %s:%s' % (device, param)
def _encode_ParamReadonlyError(self, device, param):
return '~ParamReadOnlyError~ %s:%s' % (device, param)
def _encode_NoSuchCommandError(self, device, command):
return '~NoSuchCommandError~ %s.%s' % (device, command)
def _encode_CommandFailedError(self, device, command):
return '~CommandFailedError~ %s.%s' % (device, command)
def _encode_InvalidParamValueError(self, device, param, value):
return '~InvalidValueForParamError~ %s:%s=%r' % (device, param, value)
def _encode_HelpReply(self):
return ['Help not yet implemented!',
'ask Markus Zolliker about the protocol']
ENCODERS = {
'pickle': PickleEncoder,
'text': TextEncoder,
'demo': DemoEncoder,
}

View File

@ -32,6 +32,7 @@ class Framer(object):
note: not all MessageEncoders can use all Framers,
but the intention is to have this for as many as possible.
"""
def encode(self, *frames):
"""return the wire-data for the given messageframes"""
raise NotImplemented
@ -126,9 +127,67 @@ class RLEFramer(Framer):
self.frames_to_go = 0
class DemoFramer(Framer):
"""Text based message framer
frmes are delimited by '\n'
messages are delimited by '\n\n'
'\r' is ignored
"""
def __init__(self):
self.data = b''
self.decoded = []
def encode(self, frames):
"""add transport layer encapsulation/framing of messages"""
if isinstance(frames, (tuple, list)):
return b'\n'.join(frames) + b'\n\n'
return b'%s\n\n' % frames
def decode(self, data):
"""remove transport layer encapsulation/framing of messages
returns a list of messageframes which got decoded from data!
"""
self.data += data
res = []
while b'\n' in self.data:
frame, self.data = self.data.split(b'\n', 1)
if frame.endswith('\r'):
frame = frame[:-1]
if self.data.startswith('\r'):
self.data = self.data[1:]
res.append(frame)
return res
def decode2(self, data):
"""remove transport layer encapsulation/framing of messages
returns a _list_ of messageframes which got decoded from data!
"""
self.data += data.replace(b'\r', '')
while b'\n' in self.data:
frame, self.data = self.data.split(b'\n', 1)
if frame:
# not an empty line -> belongs to this set of messages
self.decoded.append(frame)
else:
# empty line -> our set of messages is finished decoding
res = self.decoded
self.decoded = []
return res
return None
def reset(self):
self.data = b''
self.decoded = []
FRAMERS = {
'eol': EOLFramer,
'rle': RLEFramer,
'demo': DemoFramer,
}
__ALL__ = ['FRAMERS']

View File

@ -38,6 +38,7 @@ from errors import ConfigError
class Server(object):
def __init__(self, name, workdir, parentLogger=None):
self._name = name
self._workdir = workdir
@ -115,14 +116,23 @@ class Server(object):
devclass = devopts.pop('class')
# create device
self.log.debug('Creating Device %r' % devname)
export = devopts.pop('export', '1')
export = export.lower() in ('1', 'on', 'true', 'yes')
if 'default' in devopts:
devopts['value'] = devopts.pop('default')
# strip '"
for k, v in devopts.items():
for d in ("'", '"'):
if v.startswith(d) and v.endswith(d):
devopts[k] = v[1:-1]
devobj = devclass(self.log.getChild(devname), devopts, devname,
self._dispatcher)
devs.append([devname, devobj])
devs.append([devname, devobj, export])
# connect devices with dispatcher
for devname, devobj in devs:
for devname, devobj, export in devs:
self.log.info('registering device %r' % devname)
self._dispatcher.register_device(devobj, devname)
self._dispatcher.register_device(devobj, devname, export)
# also call init on the devices
devobj.init()
@ -152,5 +162,3 @@ class Server(object):
cls.__name__,
', '.join(options.keys())))
return obj

View File

@ -27,6 +27,13 @@
# also validators should have a __repr__ returning a 'python' string
# which recreates them
# if a validator does a mapping, it normally maps to the external representation (used for print/log/protocol/...)
# to get the internal representation (for the code), call method convert
class ProgrammingError(Exception):
pass
class Validator(object):
# list of tuples: (name, converter)
params = []
@ -61,12 +68,17 @@ class Validator(object):
', '.join(list(kwds.keys()))))
def __repr__(self):
params = ['%s=%r' % (pn, self.__dict__[pn]) for pn in self.params]
params = ['%s=%r' % (pn[0], self.__dict__[pn[0]])
for pn in self.params]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
def __call__(self, value):
return self.check(self.valuetype(value))
def convert(self, value):
# transforms the 'internal' representation into the 'external'
return self.valuetype(value)
class floatrange(Validator):
params = [('lower', float), ('upper', float)]
@ -78,22 +90,110 @@ class floatrange(Validator):
(value, self.lower, self.upper))
class intrange(Validator):
params = [('lower', int), ('upper', int)]
valuetype = int
def check(self, value):
if self.lower <= value <= self.upper:
return value
raise ValueError('Intrange: value %r must be within %f and %f' %
(value, self.lower, self.upper))
class positive(Validator):
def check(self, value):
if value > 0:
return value
raise ValueError('Value %r must be positive!' % obj)
raise ValueError('Value %r must be > 0!' % value)
class nonnegative(Validator):
def check(self, value):
if value >= 0:
return value
raise ValueError('Value %r must be positive!' % obj)
raise ValueError('Value %r must be >= 0!' % value)
class array(Validator):
"""integral amount of data-elements which are described by the SAME validator
The size of the array can also be described by an validator
"""
valuetype = list
params = [('size', lambda x: x),
('datatype', lambda x: x)]
def check(self, values):
requested_size = len(values)
try:
allowed_size = self.size(requested_size)
except ValueError as e:
raise ValueError(
'illegal number of elements %d, need %r: (%s)' %
(requested_size, self.size, e))
if requested_size != allowed_size:
raise ValueError(
'need %d elements (got %d)' %
(allowed_size, requested_size))
# apply data-type validator to all elements and return
res = []
for idx, el in enumerate(values):
try:
res.append(self.datatype(el))
except ValueError as e:
raise ValueError(
'Array Element %s (=%r) not conforming to %r: (%s)' %
(idx, el, self.datatype, e))
return res
# more complicated validator may not be able to use validator base class
class vector(object):
"""fixed length, eache element has its own validator"""
def __init__(self, *args):
self.validators = args
self.argstr = ', '.join([repr(e) for e in args])
def __call__(self, args):
if len(args) != len(self.validators):
raise ValueError('Vector: need exactly %d elementes (got %d)' %
len(self.validators), len(args))
return [v(e) for v, e in zip(self.validators, args)]
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class oneof(object):
"""needs to comply with one of the given validators/values"""
def __init__(self, *args):
self.oneof = args
self.argstr = ', '.join([repr(e) for e in args])
def __call__(self, arg):
for v in self.oneof:
if callable(v):
try:
if (v == int) and (float(arg) != int(arg)):
continue
return v(arg)
except ValueError:
pass # try next validator
elif v == arg:
return v
raise ValueError('Oneof: %r should be one of: %s' % (arg, self.argstr))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class mapping(object):
def __init__(self, *args, **kwds):
self.mapping = {}
# use given kwds directly
@ -112,11 +212,21 @@ class mapping(object):
self.revmapping[v] = k
def __call__(self, obj):
try:
obj = int(obj)
except ValueError:
pass
if obj in self.mapping:
return obj
if obj in self.revmapping:
return self.revmapping[obj]
raise ValueError("%r should be one of %r" %
(obj, list(self.mapping.keys())))
def __repr__(self):
params = ['%s=%r' % (mname, mval) for mname, mval in self.mapping]
params = ['%s=%r' % (mname, mval)
for mname, mval in self.mapping.items()]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
def convert(self, arg):
return self.mapping.get(arg, arg)