diff --git a/bin/secop-console b/bin/secop-console deleted file mode 100755 index faaaa08..0000000 --- a/bin/secop-console +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# pylint: disable=invalid-name -# -*- 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 -# -# ***************************************************************************** - -import sys -import argparse -from os import path - -# Path magic to make python find our stuff. -# also remember our basepath (for etc, pid lookup, etc) -basepath = path.abspath(path.join(sys.path[0], '..')) -etc_path = path.join(basepath, 'etc') -pid_path = path.join(basepath, 'pid') -log_path = path.join(basepath, 'log') -# sys.path[0] = path.join(basepath, 'src') -sys.path[0] = basepath - -# do not move above! -import mlzlog -from secop.client.console import ClientConsole - - -def parseArgv(argv): - parser = argparse.ArgumentParser(description="Connect to a SECoP server") - loggroup = parser.add_mutually_exclusive_group() - loggroup.add_argument("-v", "--verbose", - help="Output lots of diagnostic information", - action='store_true', default=False) - loggroup.add_argument("-q", "--quiet", help="suppress non-error messages", - action='store_true', default=False) - parser.add_argument("name", - type=str, - help="Name of the instance.\n" - " Uses etc/name.cfg for configuration\n",) - return parser.parse_args() - - -def main(argv=None): - if argv is None: - argv = sys.argv - - args = parseArgv(argv[1:]) - - loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info') - mlzlog.initLogging('console', loglevel, log_path) - - console = ClientConsole(args.name, basepath) - - try: - console.run() - except KeyboardInterrupt: - console.close() - - -if __name__ == '__main__': - sys.exit(main(sys.argv)) diff --git a/secop/basic_validators.py b/secop/basic_validators.py deleted file mode 100644 index 05a7548..0000000 --- a/secop/basic_validators.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- 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 -# -# ***************************************************************************** -"""basic validators (for properties)""" - -# TODO: remove, as not used anymore - - -import re - -from secop.errors import ProgrammingError - - -def FloatProperty(value): - return float(value) - - -def PositiveFloatProperty(value): - value = float(value) - if value > 0: - return value - raise ValueError('Value must be >0 !') - - -def NonNegativeFloatProperty(value): - value = float(value) - if value >= 0: - return value - raise ValueError('Value must be >=0 !') - - -def IntProperty(value): - if int(value) == float(value): - return int(value) - raise ValueError('Can\'t convert %r to int!' % value) - - -def PositiveIntProperty(value): - value = IntProperty(value) - if value > 0: - return value - raise ValueError('Value must be >0 !') - - -def NonNegativeIntProperty(value): - value = IntProperty(value) - if value >= 0: - return value - raise ValueError('Value must be >=0 !') - - -def BoolProperty(value): - try: - if value.lower() in ['0', 'false', 'no', 'off',]: - return False - if value.lower() in ['1', 'true', 'yes', 'on', ]: - return True - except AttributeError: # was no string - if bool(value) == value: - return value - raise ValueError('%r is no valid boolean: try one of True, False, "on", "off",...' % value) - - -def StringProperty(value): - return str(value) - - -def UnitProperty(value): - # probably too simple! - for s in str(value): - if s.lower() not in '°abcdefghijklmnopqrstuvwxyz': - raise ValueError('%r is not a valid unit!') - - -def FmtStrProperty(value, regexp=re.compile(r'^%\.?\d+[efg]$')): - value=str(value) - if regexp.match(value): - return value - raise ValueError('%r is not a valid fmtstr!' % value) - - -def OneOfProperty(*args): - # literally oneof! - if not args: - raise ProgrammingError('OneOfProperty needs some argumets to check against!') - def OneOfChecker(value): - if value not in args: - raise ValueError('Value must be one of %r' % list(args)) - return value - return OneOfChecker - - -def NoneOr(checker): - if not callable(checker): - raise ProgrammingError('NoneOr needs a basic validator as Argument!') - def NoneOrChecker(value): - if value is None: - return None - return checker(value) - return NoneOrChecker - - -def EnumProperty(**kwds): - if not kwds: - raise ProgrammingError('EnumProperty needs a mapping!') - def EnumChecker(value): - if value in kwds: - return kwds[value] - if value in kwds.values(): - return value - raise ValueError('Value must be one of %r' % list(kwds)) - return EnumChecker - -def TupleProperty(*checkers): - if not checkers: - checkers = [None] - for c in checkers: - if not callable(c): - raise ProgrammingError('TupleProperty needs basic validators as Arguments!') - def TupleChecker(values): - if len(values)==len(checkers): - return tuple(c(v) for c, v in zip(checkers, values)) - raise ValueError('Value needs %d elements!' % len(checkers)) - return TupleChecker - -def ListOfProperty(checker): - if not callable(checker): - raise ProgrammingError('ListOfProperty needs a basic validator as Argument!') - def ListOfChecker(values): - return [checker(v) for v in values] - return ListOfChecker diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py deleted file mode 100644 index 72bfe42..0000000 --- a/secop/client/baseclient.py +++ /dev/null @@ -1,587 +0,0 @@ -# -*- 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 -# -# ***************************************************************************** -"""Define Client side proxies""" - -# TODO: remove, as currently not used - - -import json -import queue -import socket -import threading -import time -from collections import OrderedDict -from select import select - -import serial - -from secop.datatypes import CommandType, EnumType, get_datatype -from secop.errors import EXCEPTIONS -from secop.lib import formatException, formatExtendedStack, mkthread -from secop.lib.parsing import format_time, parse_time -from secop.protocol.messages import BUFFERREQUEST, COMMANDREQUEST, \ - DESCRIPTIONREPLY, DESCRIPTIONREQUEST, DISABLEEVENTSREQUEST, \ - ENABLEEVENTSREQUEST, ERRORPREFIX, EVENTREPLY, \ - HEARTBEATREQUEST, HELPREQUEST, IDENTREQUEST, READREPLY, \ - READREQUEST, REQUEST2REPLY, WRITEREPLY, WRITEREQUEST - -try: - import mlzlog -except ImportError: - pass - - -class TCPConnection: - # disguise a TCP connection as serial one - - def __init__(self, host, port, getLogger=None): - if getLogger: - self.log = getLogger('TCPConnection') - else: - self.log = mlzlog.getLogger('TCPConnection') - self._host = host - self._port = int(port) - self._thread = None - self.callbacks = [] # called if SEC-node shuts down - self._io = None - self.connect() - - def connect(self): - self._readbuffer = queue.Queue(100) - time.sleep(1) - io = socket.create_connection((self._host, self._port)) - io.setblocking(False) - self.stopflag = False - self._io = io - if self._thread and self._thread.is_alive(): - return - self._thread = mkthread(self._run) - - def _run(self): - try: - data = b'' - while not self.stopflag: - rlist, _, xlist = select([self._io], [], [self._io], 1) - if xlist: - # on some strange systems, a closed connection is indicated by - # an exceptional condition instead of "read ready" + "empty recv" - newdata = b'' - else: - if not rlist: - continue # check stopflag every second - # self._io is now ready to read some bytes - try: - newdata = self._io.recv(1024) - except socket.error as err: - if err.args[0] == socket.EAGAIN: - # if we receive an EAGAIN error, just continue - continue - newdata = b'' - except Exception: - newdata = b'' - if not newdata: # no data on recv indicates a closed connection - raise IOError('%s:%d disconnected' % (self._host, self._port)) - lines = (data + newdata).split(b'\n') - for line in lines[:-1]: # last line is incomplete or empty - try: - self._readbuffer.put(line.strip(b'\r').decode('utf-8'), - block=True, timeout=1) - except queue.Full: - self.log.debug('rcv queue full! dropping line: %r' % line) - data = lines[-1] - except Exception as err: - self.log.error(err) - try: - self._io.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - try: - self._io.close() - except socket.error: - pass - for cb, args in self.callbacks: - cb(*args) - - def readline(self, timeout=None): - """blocks until a full line was read and returns it - - returns None when connection is stopped""" - if self.stopflag: - return None - return self._readbuffer.get(block=True, timeout=timeout) - - def stop(self): - self.stopflag = True - self._readbuffer.put(None) # terminate pending readline - - def readable(self): - return not self._readbuffer.empty() - - def write(self, data): - if self._io is None: - self.connect() - self._io.sendall(data.encode('latin-1')) - - def writeline(self, line): - self.write(line + '\n') - - def writelines(self, *lines): - for line in lines: - self.writeline(line) - - -class Value: - t = None # pylint: disable = C0103 - u = None - e = None - fmtstr = '%s' - - def __init__(self, value, qualifiers=None): - self.value = value - if qualifiers: - self.__dict__.update(qualifiers) - if 't' in qualifiers: - try: - self.t = float(qualifiers['t']) - except Exception: - self.t = parse_time(qualifiers['t']) - - def __repr__(self): - r = [] - if self.t is not None: - r.append("timestamp=%r" % format_time(self.t)) - if self.u is not None: - r.append('unit=%r' % self.u) - if self.e is not None: - r.append(('error=%s' % self.fmtstr) % self.e) - if r: - return (self.fmtstr + '(%s)') % (self.value, ', '.join(r)) - return self.fmtstr % self.value - - -class Client: - secop_id = 'unknown' - describing_data = {} - stopflag = False - connection_established = False - - def __init__(self, opts, autoconnect=True, getLogger=None): - if 'testing' not in opts: - if getLogger: - self.log = getLogger('client') - else: - self.log = mlzlog.getLogger('client', True) - else: - class logStub: - - def info(self, *args): - pass - debug = info - error = info - warning = info - exception = info - self.log = logStub() - self._cache = dict() - if 'module' in opts: - # serial port - devport = opts.pop('module') - baudrate = int(opts.pop('baudrate', 115200)) - self.contactPoint = "serial://%s:%s" % (devport, baudrate) - self.connection = serial.Serial( - devport, baudrate=baudrate, timeout=1) - self.connection.callbacks = [] - elif 'testing' not in opts: - host = opts.pop('host', 'localhost') - port = int(opts.pop('port', 10767)) - self.contactPoint = "tcp://%s:%d" % (host, port) - self.connection = TCPConnection(host, port, getLogger=getLogger) - else: - self.contactPoint = 'testing' - self.connection = opts.pop('testing') - - # maps an expected reply to a list containing a single Event() - # upon rcv of that reply, entry is appended with False and - # the data of the reply. - # if an error is received, the entry is appended with True and an - # appropriate Exception. - # Then the Event is set. - self.expected_replies = {} - - # maps spec to a set of callback functions (or single_shot callbacks) - self.callbacks = dict() - self.single_shots = dict() - - # mapping the modulename to a dict mapping the parameter names to their values - # note: the module value is stored as the value of the parameter value - # of the module - - self._syncLock = threading.RLock() - self._thread = threading.Thread(target=self._run) - self._thread.daemon = True - self._thread.start() - - if autoconnect: - self.startup() - - def _run(self): - while not self.stopflag: - try: - self._inner_run() - except Exception as err: - print(formatExtendedStack()) - self.log.exception(err) - raise - - def _inner_run(self): - data = '' - self.connection.writeline('*IDN?') - - while not self.stopflag: - line = self.connection.readline() - if line is None: # connection stopped - break - self.connection_established = True - self.log.debug('got answer %r' % line) - if line.startswith(('SECoP', 'SINE2020&ISSE,SECoP')): - self.log.info('connected to: ' + line.strip()) - self.secop_id = line - continue - msgtype, spec, data = self.decode_message(line) - if msgtype in (EVENTREPLY, READREPLY, WRITEREPLY): - # handle async stuff - self._handle_event(spec, data) - # handle sync stuff - self._handle_sync_reply(msgtype, spec, data) - - def _handle_sync_reply(self, msgtype, spec, data): - # handle sync stuff - if msgtype.startswith(ERRORPREFIX): - # find originating msgtype and map to expected_reply_type - # errormessages carry to offending request as the first - # result in the resultist - request = msgtype[len(ERRORPREFIX):] - reply = REQUEST2REPLY.get(request, request) - - entry = self.expected_replies.get((reply, spec), None) - if entry: - self.log.error("request %r resulted in Error %r" % - ("%s %s" % (request, spec), (data[0], data[1]))) - entry.extend([True, EXCEPTIONS[data[0]](*data[1:])]) - entry[0].set() - return - self.log.error("got an unexpected %s %r" % (msgtype,data[0:1])) - self.log.error(repr(data)) - return - if msgtype == DESCRIPTIONREPLY: - entry = self.expected_replies.get((msgtype, ''), None) - else: - entry = self.expected_replies.get((msgtype, spec), None) - - if entry: - self.log.debug("got expected reply '%s %s'" % (msgtype, spec) - if spec else "got expected reply '%s'" % msgtype) - entry.extend([False, msgtype, spec, data]) - entry[0].set() - - def encode_message(self, requesttype, spec='', data=None): - """encodes the given message to a string - """ - req = [str(requesttype)] - if spec: - req.append(str(spec)) - if data is not None: - req.append(json.dumps(data)) - req = ' '.join(req) - return req - - def decode_message(self, msg): - """return a decoded message triple""" - msg = msg.strip() - if ' ' not in msg: - return msg, '', None - msgtype, spec = msg.split(' ', 1) - data = None - if ' ' in spec: - spec, json_data = spec.split(' ', 1) - try: - data = json.loads(json_data) - except ValueError: - # keep as string - data = json_data - # print formatException() - return msgtype, spec, data - - def _handle_event(self, spec, data): - """handles event""" -# self.log.debug('handle_event %r %r' % (spec, data)) - if ':' not in spec: - self.log.warning("deprecated specifier %r" % spec) - spec = '%s:value' % spec - modname, pname = spec.split(':', 1) - - if data: - self._cache.setdefault(modname, {})[pname] = Value(*data) - else: - self.log.warning( - 'got malformed answer! (%s,%s)' % (spec, data)) - -# self.log.info('cache: %s:%s=%r (was: %s)', modname, pname, data, previous) - if spec in self.callbacks: - for func in self.callbacks[spec]: - try: - mkthread(func, modname, pname, data) - except Exception as err: - self.log.exception('Exception in Callback!', err) - run = set() - if spec in self.single_shots: - for func in self.single_shots[spec]: - try: - mkthread(func, data) - except Exception as err: - self.log.exception('Exception in Single-shot Callback!', - err) - run.add(func) - self.single_shots[spec].difference_update(run) - - def _getDescribingModuleData(self, module): - return self.describingModulesData[module] - - def _getDescribingParameterData(self, module, parameter): - return self._getDescribingModuleData(module)['accessibles'][parameter] - - def _decode_substruct(self, specialkeys=[], data={}): # pylint: disable=W0102 - # take a dict and move all keys which are not in specialkeys - # into a 'properties' subdict - # specialkeys entries are converted from list to ordereddict - try: - result = {} - for k in specialkeys: - result[k] = OrderedDict(data.pop(k, [])) - result['properties'] = data - return result - except Exception as err: - raise RuntimeError('Error decoding substruct of descriptive data: %r\n%r' % (err, data)) - - def _issueDescribe(self): - _, _, describing_data = self._communicate(DESCRIPTIONREQUEST) - try: - describing_data = self._decode_substruct( - ['modules'], describing_data) - for modname, module in list(describing_data['modules'].items()): - # convert old namings of interface_classes - if 'interface_class' in module: - module['interface_classes'] = module.pop('interface_class') - elif 'interfaces' in module: - module['interface_classes'] = module.pop('interfaces') - describing_data['modules'][modname] = self._decode_substruct( - ['accessibles'], module) - - self.describing_data = describing_data - - for module, moduleData in self.describing_data['modules'].items(): - for aname, adata in moduleData['accessibles'].items(): - datatype = get_datatype(adata.pop('datainfo')) - # *sigh* special handling for 'some' parameters.... - if isinstance(datatype, EnumType): - datatype._enum.name = aname - if aname == 'status': - datatype.members[0]._enum.name = 'Status' - self.describing_data['modules'][module]['accessibles'] \ - [aname]['datatype'] = datatype - except Exception as _exc: - print(formatException(verbose=True)) - raise - - def register_callback(self, module, parameter, cb): - self.log.debug('registering callback %r for %s:%s' % - (cb, module, parameter)) - self.callbacks.setdefault('%s:%s' % (module, parameter), set()).add(cb) - - def unregister_callback(self, module, parameter, cb): - self.log.debug('unregistering callback %r for %s:%s' % - (cb, module, parameter)) - self.callbacks.setdefault('%s:%s' % (module, parameter), - set()).discard(cb) - - def register_shutdown_callback(self, func, *args): - self.connection.callbacks.append((func, args)) - - def communicate(self, msgtype, spec='', data=None): - # only return the data portion.... - return self._communicate(msgtype, spec, data)[2] - - def _communicate(self, msgtype, spec='', data=None): - self.log.debug('communicate: %r %r %r' % (msgtype, spec, data)) - if self.stopflag: - raise RuntimeError('alreading stopping!') - if msgtype == IDENTREQUEST: - return self.secop_id - - # sanitize input - msgtype = str(msgtype) - spec = str(spec) - - if msgtype not in (DESCRIPTIONREQUEST, ENABLEEVENTSREQUEST, - DISABLEEVENTSREQUEST, COMMANDREQUEST, - WRITEREQUEST, BUFFERREQUEST, - READREQUEST, HEARTBEATREQUEST, HELPREQUEST): - raise EXCEPTIONS['Protocol'](args=[ - self.encode_message(msgtype, spec, data), - dict( - errorclass='Protocol', - errorinfo='%r: No Such Messagetype defined!' % msgtype, ), - ]) - - # handle syntactic sugar - if msgtype == WRITEREQUEST and ':' not in spec: - spec = spec + ':target' - if msgtype == READREQUEST and ':' not in spec: - spec = spec + ':value' - - # check if such a request is already out - rply = REQUEST2REPLY[msgtype] - if (rply, spec) in self.expected_replies: - raise RuntimeError( - "can not have more than one requests of the same type at the same time!" - ) - - # prepare sending request - event = threading.Event() - self.expected_replies[(rply, spec)] = [event] - self.log.debug('prepared reception of %r msg' % rply) - - # send request - msg = self.encode_message(msgtype, spec, data) - while not self.connection_established: - self.log.debug('connection not established yet, waiting ...') - time.sleep(0.1) - self.connection.writeline(msg) - self.log.debug('sent msg %r' % msg) - - # wait for reply. timeout after 10s - if event.wait(10): - self.log.debug('checking reply') - entry = self.expected_replies.pop((rply, spec)) - # entry is: event, is_error, exc_or_msgtype [,spec, date]<- if !err - is_error = entry[1] - if is_error: - # if error, entry[2] contains the rigth Exception to raise - raise entry[2] - # valid reply: entry[2:5] contain msgtype, spec, data - return tuple(entry[2:5]) - - # timed out - del self.expected_replies[(rply, spec)] - # XXX: raise a TimedOut ? - raise RuntimeError("timeout upon waiting for reply to %r!" % msgtype) - - def quit(self): - # after calling this the client is dysfunctional! - # self.communicate(DISABLEEVENTSREQUEST) - self.stopflag = True - self.connection.stop() - if self._thread and self._thread.is_alive(): - self._thread.join(10) - - def startup(self, _async=False): - self._issueDescribe() - # always fill our cache - self.communicate(ENABLEEVENTSREQUEST) - # deactivate updates if not wanted - if not _async: - self.communicate(DISABLEEVENTSREQUEST) - - def queryCache(self, module, parameter=None): - result = self._cache.get(module, {}) - - if parameter is not None: - result = result[parameter] - - return result - - def getParameter(self, module, parameter): - return self.communicate(READREQUEST, '%s:%s' % (module, parameter)) - - def setParameter(self, module, parameter, value): - datatype = self._getDescribingParameterData(module, - parameter)['datatype'] - - value = datatype.from_string(value) - value = datatype.export_value(value) - self.communicate(WRITEREQUEST, '%s:%s' % (module, parameter), value) - - @property - def describingData(self): - return self.describing_data - - @property - def describingModulesData(self): - return self.describingData['modules'] - - @property - def equipmentId(self): - if self.describingData: - return self.describingData['properties']['equipment_id'] - return 'Undetermined' - - @property - def protocolVersion(self): - return self.secop_id - - @property - def modules(self): - return list(self.describing_data['modules'].keys()) - - def getParameters(self, module): - params = filter(lambda item: not isinstance(item[1]['datatype'], CommandType), - self.describing_data['modules'][module]['accessibles'].items()) - return list(param[0] for param in params) - - def getModuleProperties(self, module): - return self.describing_data['modules'][module]['properties'] - - def getModuleBaseClass(self, module): - return self.getModuleProperties(module)['interface_classes'] - - def getCommands(self, module): - cmds = filter(lambda item: isinstance(item[1]['datatype'], CommandType), - self.describing_data['modules'][module]['accessibles'].items()) - return OrderedDict(cmds) - - def execCommand(self, module, command, args): - # ignore reply message + reply specifier, only return data - return self._communicate(COMMANDREQUEST, '%s:%s' % (module, command), list(args) if args else None)[2] - - def getProperties(self, module, parameter): - return self.describing_data['modules'][module]['accessibles'][parameter] - - def syncCommunicate(self, *msg): - res = self._communicate(*msg) # pylint: disable=E1120 - try: - res = self.encode_message(*res) - except Exception: - res = str(res) - return res - - def ping(self, pingctr=[0]): # pylint: disable=W0102 - pingctr[0] = pingctr[0] + 1 - self.communicate(HEARTBEATREQUEST, pingctr[0]) diff --git a/secop/client/console.py b/secop/client/console.py deleted file mode 100644 index b466a02..0000000 --- a/secop/client/console.py +++ /dev/null @@ -1,193 +0,0 @@ -# -*- 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 -# -# ***************************************************************************** -"""console client""" - -# TODO: remove, as currently not used - - -import code -import configparser -import socket -import threading -from collections import deque -from os import path - -import mlzlog - -from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg -from secop.protocol.messages import EVENTREPLY - - -class NameSpace(dict): - - def __init__(self): - dict.__init__(self) - self.__const = set() - - def setconst(self, name, value): - dict.__setitem__(self, name, value) - self.__const.add(name) - - def __setitem__(self, name, value): - if name in self.__const: - raise RuntimeError('%s cannot be assigned' % name) - dict.__setitem__(self, name, value) - - def __delitem__(self, name): - if name in self.__const: - raise RuntimeError('%s cannot be deleted' % name) - dict.__delitem__(self, name) - - - -def getClientOpts(cfgfile): - parser = configparser.SafeConfigParser() - if not parser.read([cfgfile + '.cfg']): - print("Error reading cfg file %r" % cfgfile) - return {} - if not parser.has_section('client'): - print("No Server section found!") - return dict(item for item in parser.items('client')) - - -class ClientConsole: - - def __init__(self, cfgname, basepath): - self.namespace = NameSpace() - self.namespace.setconst('help', self.helpCmd) - - cfgfile = path.join(basepath, 'etc', cfgname) - cfg = getClientOpts(cfgfile) - self.client = Client(cfg) - self.client.populateNamespace(self.namespace) - - def run(self): - console = code.InteractiveConsole(self.namespace) - console.interact("Welcome to the SECoP console") - - def close(self): - pass - - def helpCmd(self, arg=Ellipsis): - if arg is Ellipsis: - print("No help available yet") - else: - help(arg) - - -class TCPConnection: - - def __init__(self, connect, port, **kwds): - self.log = mlzlog.log.getChild('connection', False) - port = int(port) - self.connection = socket.create_connection((connect, port), 3) - self.queue = deque() - self._rcvdata = '' - self.callbacks = set() - self._thread = threading.Thread(target=self.thread) - self._thread.daemonize = True - self._thread.start() - - def send(self, msg): - self.log.debug("Sending msg %r" % msg) - data = encode_msg_frame(*msg.serialize()) - self.log.debug("raw data: %r" % data) - self.connection.sendall(data) - - def thread(self): - while True: - try: - self.thread_step() - except Exception as e: - self.log.exception("Exception in RCV thread: %r" % e) - - def thread_step(self): - data = b'' - while True: - newdata = self.connection.recv(1024) - self.log.debug("RCV: got raw data %r" % newdata) - data = data + newdata - while True: - origin, data = get_msg(data) - if origin is None: - break # no more messages to process - if not origin: # empty string - continue # ??? - _ = decode_msg(origin) - # construct msgObj from msg - try: - #msgObj = Message(*msg) - #msgObj.origin = origin.decode('latin-1') - #self.handle(msgObj) - pass - except Exception: - # ??? what to do here? - pass - - def handle(self, msg): - if msg.action == EVENTREPLY: - self.log.info("got Async: %r" % msg) - for cb in self.callbacks: - try: - cb(msg) - except Exception as e: - self.log.debug( - "handle_async: got exception %r" % e, exception=True) - else: - self.queue.append(msg) - - def read(self): - while not self.queue: - pass # XXX: remove BUSY polling - return self.queue.popleft() - - def register_callback(self, callback): - """registers callback for async data""" - self.callbacks.add(callback) - - def unregister_callback(self, callback): - """unregisters callback for async data""" - self.callbacks.discard(callback) - - -class Client: - - def __init__(self, opts): - self.log = mlzlog.log.getChild('client', True) - self._cache = dict() - self.connection = TCPConnection(**opts) - self.connection.register_callback(self.handle_async) - - def handle_async(self, msg): - self.log.info("Got async update %r" % msg) - module = msg.module - param = msg.param - value = msg.value - self._cache.getdefault(module, {})[param] = value - # XXX: further notification-callbacks needed ??? - - def populateNamespace(self, namespace): - #self.connection.send(Message(DESCRIPTIONREQUEST)) - # reply = self.connection.read() - # self.log.info("found modules %r" % reply) - # create proxies, populate cache.... - namespace.setconst('connection', self.connection) diff --git a/secop/lib/parsing.py b/secop/lib/parsing.py deleted file mode 100644 index 8bc69e5..0000000 --- a/secop/lib/parsing.py +++ /dev/null @@ -1,406 +0,0 @@ -# -*- 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 -# -# ***************************************************************************** -"""Define parsing helpers""" - -# TODO: remove, as currently not used - -import re -import time -from datetime import datetime, timedelta, tzinfo - -# format_time and parse_time could be simplified with external dateutil lib -# http://stackoverflow.com/a/15228038 - -# based on http://stackoverflow.com/a/39418771 - - -class LocalTimezone(tzinfo): - ZERO = timedelta(0) - STDOFFSET = timedelta(seconds=-time.timezone) - if time.daylight: - DSTOFFSET = timedelta(seconds=-time.altzone) - else: - DSTOFFSET = STDOFFSET - - DSTDIFF = DSTOFFSET - STDOFFSET - - def utcoffset(self, dt): - if self._isdst(dt): - return self.DSTOFFSET - return self.STDOFFSET - - def dst(self, dt): - if self._isdst(dt): - return self.DSTDIFF - return self.ZERO - - def tzname(self, dt): - return time.tzname[self._isdst(dt)] - - def _isdst(self, dt): - tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.weekday(), 0, 0) - stamp = time.mktime(tt) - tt = time.localtime(stamp) - return tt.tm_isdst > 0 - - -LocalTimezone = LocalTimezone() - - -def format_time(timestamp=None): - # get time in UTC - if timestamp is None: - d = datetime.now(LocalTimezone) - else: - d = datetime.fromtimestamp(timestamp, LocalTimezone) - return d.isoformat("T") - -# Solution based on -# https://bugs.python.org/review/15873/diff/16581/Lib/datetime.py#newcode1418Lib/datetime.py:1418 - - -class Timezone(tzinfo): - - def __init__(self, offset, name='unknown timezone'): # pylint: disable=W0231 - self.offset = offset - self.name = name - - def tzname(self, dt): - return self.name - - def utcoffset(self, dt): - return self.offset - - def dst(self, dt): - return timedelta(0) - - -datetime_re = re.compile( - r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' - r'[T ](?P\d{1,2}):(?P\d{1,2})' - r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d*)?)?' - r'(?PZ|[+-]\d{2}(?::?\d{2})?)?$') - - -def _parse_isostring(isostring): - """Parses a string and return a datetime.datetime. - This function supports time zone offsets. When the input contains one, - the output uses a timezone with a fixed offset from UTC. - """ - match = datetime_re.match(isostring) - if match: - kw = match.groupdict() - if kw['microsecond']: - kw['microsecond'] = kw['microsecond'].ljust(6, '0') - _tzinfo = kw.pop('tzinfo') - if _tzinfo == 'Z': - _tzinfo = timezone.utc # pylint: disable=E0602 - elif _tzinfo is not None: - offset_mins = int(_tzinfo[-2:]) if len(_tzinfo) > 3 else 0 - offset_hours = int(_tzinfo[1:3]) - offset = timedelta(hours=offset_hours, minutes=offset_mins) - if _tzinfo[0] == '-': - offset = -offset - _tzinfo = Timezone(offset) - kw = {k: int(v) for k, v in kw.items() if v is not None} - kw['tzinfo'] = _tzinfo - return datetime(**kw) - raise ValueError("%s is not a valid ISO8601 string I can parse!" % - isostring) - - -def parse_time(isostring): - try: - return float(isostring) - except ValueError: - dt = _parse_isostring(isostring) - return time.mktime(dt.timetuple()) + dt.microsecond * 1e-6 - -# possibly unusable stuff below! - - -def format_args(args): - if isinstance(args, list): - return ','.join(format_args(arg) for arg in args).join('[]') - if isinstance(args, tuple): - return ','.join(format_args(arg) for arg in args).join('()') - if isinstance(args, str): - # XXX: check for 'easy' strings only and omit the '' - return repr(args) - return repr(args) # for floats/ints/... - - -class ArgsParser: - """returns a pythonic object from the input expression - - grammar: - expr = number | string | array_expr | record_expr - number = int | float - string = '"' (chars - '"')* '"' | "'" (chars - "'")* "'" - array_expr = '[' (expr ',')* expr ']' - record_expr = '(' (name '=' expr ',')* ')' - int = '-' pos_int | pos_int - pos_int = [0..9]+ - float = int '.' pos_int ( [eE] int )? - name = [A-Za-z_] [A-Za-z0-9_]* - """ - - DIGITS_CHARS = '0123456789' - NAME_CHARS = '_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - NAME_CHARS2 = NAME_CHARS + DIGITS_CHARS - - def __init__(self, string=''): - self.string = string - self.idx = 0 - self.length = len(string) - - def setstring(self, string): - self.string = string - self.idx = 0 - self.length = len(string) - self.skip() - - def peek(self): - if self.idx >= self.length: - return None - return self.string[self.idx] - - def get(self): - res = self.peek() - self.idx += 1 - return res - - def skip(self): - """skips whitespace""" - while self.peek() in ('\t', ' '): - self.get() - - def match(self, what): - if self.peek() != what: - return False - self.get() - self.skip() - return True - - def parse(self, arg=None): - """parses given or constructed_with string""" - self.setstring(arg or self.string) - res = [] - while self.idx < self.length: - res.append(self.parse_exp()) - self.match(',') - if len(res) > 1: - return tuple(*res) - return res[0] - - def parse_exp(self): - """expr = array_expr | record_expr | string | number""" - idx = self.idx - res = self.parse_array() - if res: - return res - self.idx = idx - res = self.parse_record() - if res: - return res - self.idx = idx - res = self.parse_string() - if res: - return res - self.idx = idx - return self.parse_number() - - def parse_number(self): - """number = float | int """ - idx = self.idx - number = self.parse_float() - if number is not None: - return number - self.idx = idx # rewind - return self.parse_int() - - def parse_string(self): - """string = '"' (chars - '"')* '"' | "'" (chars - "'")* "'" """ - delim = self.peek() - if delim in ('"', "'"): - lastchar = self.get() - string = [] - while self.peek() != delim or lastchar == '\\': - lastchar = self.peek() - string.append(self.get()) - self.get() - self.skip() - return ''.join(string) - return self.parse_name() - - def parse_array(self): - """array_expr = '[' (expr ',')* expr ']' """ - if self.get() != '[': - return None - self.skip() - res = [] - while self.peek() != ']': - el = self.parse_exp() - if el is None: - return el - res.append(el) - if self.match(']'): - return res - if self.get() != ',': - return None - self.skip() - self.get() - self.skip() - return res - - def parse_record(self): - """record_expr = '(' (name '=' expr ',')* ')' """ - if self.get() != '(': - return None - self.skip() - res = {} - while self.peek() != ')': - name = self.parse_name() - if self.get() != '=': - return None - self.skip() - value = self.parse_exp() - res[name] = value - if self.peek() == ')': - self.get() - self.skip() - return res - if self.get() != ',': - return None - self.skip() - self.get() - self.skip() - return res - - def parse_int(self): - """int = '-' pos_int | pos_int""" - if self.peek() == '-': - self.get() - number = self.parse_pos_int() - if number is not None: - return -number # pylint: disable=invalid-unary-operand-type - return None - return self.parse_pos_int() - - def parse_pos_int(self): - """pos_int = [0..9]+""" - number = 0 - if self.peek() not in self.DIGITS_CHARS: - return None - while self.peek() in self.DIGITS_CHARS: - number = number * 10 + int(self.get()) - self.skip() - return number - - def parse_float(self): - """float = int '.' pos_int ( [eE] int )?""" - number = self.parse_int() - if self.get() != '.': - return None - idx = self.idx - fraction = self.parse_pos_int() - while idx < self.idx: - fraction /= 10. - idx += 1 - if number >= 0: - number = number + fraction - else: - number = number - fraction - exponent = 0 - if self.peek() in ('e', 'E'): - self.get() - exponent = self.parse_int() - if exponent is None: - return exponent - while exponent > 0: - number *= 10. - exponent -= 1 - while exponent < 0: - number /= 10. - exponent += 1 - self.skip() - return number - - def parse_name(self): - """name = [A-Za-z_] [A-Za-z0-9_]*""" - name = [] - if self.peek() in self.NAME_CHARS: - name.append(self.get()) - while self.peek() in self.NAME_CHARS2: - name.append(self.get()) - self.skip() - return ''.join(name) - return None - - -def parse_args(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_args(part) for part in s[1:-1].split(',')] - if s.startswith('(') and s.endswith(')'): - # evaluate inner - return tuple(parse_args(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] - if '.' in s: - return float(s) - return int(s) - - -__ALL__ = ['format_time', 'parse_time', 'parse_args'] - -# if __name__ == '__main__': -# print "minimal testing: lib/parsing:" -# print "time_formatting:", -# t = time.time() -# s = format_time(t) -# assert (abs(t - parse_time(s)) < 1e-6) -# print "OK"# -# -# print "ArgsParser:" -# a = ArgsParser() -# print a.parse('[ "\'\\\"A" , "<>\'", \'",C\', [1.23e1, 123.0e-001] , ]') - -# #import pdb -# #pdb.run('print a.parse()', globals(), locals()) - -# print "args_formatting:", -# for obj in [1, 2.3, 'X', (1, 2, 3), [1, (3, 4), 'X,y']]: -# s = format_args(obj) -# p = a.parse(s) -# print p, -# assert (parse_args(format_args(obj)) == obj) -# print "OK" -# print "OK" diff --git a/test/test_basic_validators.py b/test/test_basic_validators.py deleted file mode 100644 index 450f8b5..0000000 --- a/test/test_basic_validators.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- 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 -# -# ***************************************************************************** -"""test basic validators.""" - -# no fixtures needed -import pytest - -from secop.basic_validators import BoolProperty, EnumProperty, FloatProperty, \ - FmtStrProperty, IntProperty, NoneOr, NonNegativeFloatProperty, \ - NonNegativeIntProperty, OneOfProperty, PositiveFloatProperty, \ - PositiveIntProperty, StringProperty, TupleProperty, UnitProperty - - -class unprintable: - def __str__(self): - raise NotImplementedError - -@pytest.mark.parametrize('validators_args', [ - [FloatProperty, [None, 'a'], [1, 1.23, '1.23', '9e-12']], - [PositiveFloatProperty, ['x', -9, '-9', 0], [1, 1.23, '1.23', '9e-12']], - [NonNegativeFloatProperty, ['x', -9, '-9'], [0, 1.23, '1.23', '9e-12']], - [IntProperty, [None, 'a', 1.2, '1.2'], [1, '-1']], - [PositiveIntProperty, ['x', 1.9, '-9', '1e-4'], [1, '1']], - [NonNegativeIntProperty, ['x', 1.9, '-9', '1e-6'], [0, '1']], - [BoolProperty, ['x', 3], ['on', 'off', True, False]], - [StringProperty, [unprintable()], ['1', 1.2, [{}]]], - [UnitProperty, [unprintable(), '3', 9], ['mm', 'Gbarn', 'acre']], - [FmtStrProperty, [1, None, 'a', '%f'], ['%.0e', '%.3f','%.1g']], -]) -def test_validators(validators_args): - v, fails, oks = validators_args - for value in fails: - with pytest.raises(Exception): - v(value) - for value in oks: - v(value) - - -@pytest.mark.parametrize('checker_inits', [ - [OneOfProperty, lambda: OneOfProperty(a=3),], # pylint: disable=unexpected-keyword-arg - [NoneOr, lambda: NoneOr(None),], - [EnumProperty, lambda: EnumProperty(1),], # pylint: disable=too-many-function-args - [TupleProperty, lambda: TupleProperty(1,2,3),], -]) -def test_checker_fails(checker_inits): - empty, badargs = checker_inits - with pytest.raises(Exception): - empty() - with pytest.raises(Exception): - badargs() - - -@pytest.mark.parametrize('checker_args', [ - [OneOfProperty(1,2,3), ['x', None, 4], [1, 2, 3]], - [NoneOr(IntProperty), ['a', 1.2, '1.2'], [None, 1, '-1', '999999999999999']], - [EnumProperty(a=1, b=2), ['x', None, 3], ['a', 'b', 1, 2]], - [TupleProperty(IntProperty, StringProperty), [1, 'a', ('x', 2)], [(1,'x')]], -]) -def test_checkers(checker_args): - v, fails, oks = checker_args - for value in fails: - with pytest.raises(Exception): - v(value) - for value in oks: - v(value) diff --git a/test/test_client_baseclient.py b/test/test_client_baseclient.py deleted file mode 100644 index 040f9cf..0000000 --- a/test/test_client_baseclient.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- 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 -# -# ***************************************************************************** -"""test base client.""" - -from collections import OrderedDict - -import pytest - -from secop.client.baseclient import Client - -# define Test-only connection object - - -class TestConnect: - callbacks = [] - - def writeline(self, line): - pass - - def readline(self): - return '' - - -@pytest.fixture(scope="module") -def clientobj(request): - print (" SETUP ClientObj") - testconnect = TestConnect() - yield Client(dict(testing=testconnect), autoconnect=False) - for cb, arg in testconnect.callbacks: - cb(arg) - print (" TEARDOWN ClientObj") - - -# pylint: disable=redefined-outer-name -def test_describing_data_decode(clientobj): - assert {'modules': OrderedDict(), 'properties': {} - } == clientobj._decode_substruct(['modules'], {}) - describing_data = {'equipment_id': 'eid', - 'modules': [['LN2', {'commands': [], - 'interfaces': ['Readable', 'Module'], - 'parameters': [['value', {'datatype': ['double'], - 'description': 'current value', - 'readonly': True, - } - ]] - } - ]] - } - decoded_data = {'modules': OrderedDict([('LN2', {'commands': OrderedDict(), - 'parameters': OrderedDict([('value', {'datatype': ['double'], - 'description': 'current value', - 'readonly': True, - } - )]), - 'properties': {'interfaces': ['Readable', 'Module']} - } - )]), - 'properties': {'equipment_id': 'eid', - } - } - - a = clientobj._decode_substruct(['modules'], describing_data) - for modname, module in a['modules'].items(): - a['modules'][modname] = clientobj._decode_substruct( - ['parameters', 'commands'], module) - assert a == decoded_data