# -*- coding: utf-8 -*-
# (c) 2015 Tuomas Airaksinen
#
# This file is part of Automate.
#
# Automate 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 3 of the License, or
# (at your option) any later version.
#
# Automate 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 Automate. If not, see <http://www.gnu.org/licenses/>.
#
# ------------------------------------------------------------------
#
# If you like Automate, please take a look at this page:
# http://evankelista.net/automate/
import datetime
from builtins import bytes
import re
import threading
import xmlrpc.client
import socket
import subprocess
from http.client import HTTPException
from traits.api import (CList, Any, Property, Set, Bool, Event, CBool, on_trait_change, cached_property)
from automate.callable import AbstractCallable
from automate.common import deep_iterate, get_modules_all
from automate.statusobject import StatusObject
from automate.common import (threaded, thread_start, is_iterable)
[docs]class Empty(AbstractCallable):
"""
Do nothing but return None. Default action in Programs.
Usage::
Empty()
"""
test = Bool(False)
def call(self, caller, **kwargs):
return None
[docs]class AbstractAction(AbstractCallable):
"""
Abstract base class for actions (i.e. callables that do something but
do not necessarily return anything.
"""
status = Bool(True)
[docs]class Attrib(AbstractAction):
"""
Give specified attribute of a object.
:param no_eval bool: if True, evaluation of object is skipped -- use this to access attributes of SystemObjects
Usage & example::
Attrib(obj, 'attributename')
Attrib(sensor_name, 'status', no_eval=True)
"""
@property
def method(self):
return self._args[1]
def call(self, caller, **kwargs):
no_eval = self._kwargs.get('no_eval', False)
if not caller:
return
if no_eval:
obj = self.obj
else:
obj = self.call_eval(self.obj, caller, **kwargs)
attr = self.call_eval(self.method, caller, **kwargs)
return getattr(obj, attr, None)
[docs]class Method(AbstractAction):
"""
Call method in an object with specified args
Usage::
Method(obj, 'methodname')
"""
@property
def method(self):
return self._args[1]
@property
def args(self):
try:
return self._args[2:]
except IndexError:
return ()
def call(self, caller, **kwargs):
if not caller:
return
kwargs = self._kwargs.copy()
if not kwargs.pop('no_caller', False):
arglist = [caller] + list(self.args)
else:
arglist = self.args
return getattr(self.call_eval(self.obj, caller, **kwargs), self.call_eval(self.method, caller, **kwargs))(*arglist, **kwargs)
[docs]class Func(AbstractAction):
"""
Call function with given arguments.
Usage & example::
Func(function, *args, **kwargs)
Func(time.sleep, 2)
:param bool add_caller: if True, then caller program is passed as first argument.
"""
@property
def args(self):
return tuple(self._args[1:])
def call(self, caller, **kwargs):
if not caller:
return
_kwargs = {k: self.call_eval(v, caller, **kwargs) for k, v in list(self._kwargs.items())}
return_value = _kwargs.pop('return_value', True)
args = [self.call_eval(i, caller, return_value=return_value, **kwargs) for i in self.args]
if _kwargs.pop('add_caller', False):
arglist = [caller] + list(args)
else:
arglist = args
self.logger.debug("Func %s %s %s", self.obj, arglist, _kwargs)
try:
return self.call_eval(self.obj, caller, **kwargs)(*arglist, **_kwargs)
except Exception as e:
self.logger.exception('Exception occurred in %s: %s', self, e)
[docs]class OnlyTriggers(AbstractCallable):
"""
Baseclass for actions that do not have any targets (i.e. almost all actions).
"""
def _give_targets(self):
return None
[docs]class Log(OnlyTriggers):
"""
Print callable argument outputs / other arguments to the log.
Usage::
Log(object1, object2, 'string1'...)
:param str log_level: Log level (i.e. logging function name) (default 'info')
"""
default_log_level = 'info'
def call(self, caller, **kwargs):
log_level = self._kwargs.get('log_level', self.default_log_level)
l = []
for o in self.objects:
l.append(self.call_eval(o, caller, **kwargs))
getattr(self.logger, log_level)(*l)
return True
[docs]class Debug(Log):
"""
Same as :class:`.Log` but with debug logging level.
"""
default_log_level = 'debug'
[docs]class ToStr(OnlyTriggers):
"""
Return string representation of given arguments evaluated.
Usage::
ToStr('formatstring {} {}', callable1, statusobject1)
:param bool no_sub: if True, removes format string from argument list. Then usage is simply:
.. code-block:: python
ToStr(callable1, statusobject1, no_sub=True)
"""
def call(self, caller, **kwargs):
_kwargs = self._kwargs.copy()
no_sub = _kwargs.pop('no_sub', False)
if no_sub:
rv = ' '.join([str(self.call_eval(i, caller, **kwargs)) for i in self.objects])
else:
rv = str(self.call_eval(self.obj, caller, **kwargs)).format(
*[self.call_eval(i, caller, **kwargs) for i in self.objects[1:]], **_kwargs)
return rv
[docs]class Eval(AbstractAction):
"""Execute python command given as a string with eval (or exec).
Usage::
Eval("print time.{param}()", pre_exec="import time", param="time")
First argument: python command to be evaluated. If it can be evaluated by
eval() then return value is the evaluated value. Otherwise, exec() is used and True
is returned.
:param str pre_exec: pre-execution string. For example necessary import commands.
:param dict namespace: Namespace. Defaults to locals() in :mod:`.builtin_callables`.
Optionally, other keyword arguments can be given, and they are replaced in the first argument
by format().
See also (and prefer using): :class:`.Func`
"""
def call(self, caller, **kwargs):
if not caller:
return
_kwargs = self._kwargs.copy()
namespace = _kwargs.pop('namespace', locals())
pre_exec = _kwargs.pop('pre_exec', '')
if pre_exec:
exec(pre_exec.format(**self._kwargs), namespace)
try:
return eval(self.obj.format(**self._kwargs), namespace)
except SyntaxError:
exec(self.obj.format(**self._kwargs), namespace)
return True
[docs]class Exec(Eval):
"""Synonym to :class:`.Eval`"""
[docs]class GetService(AbstractAction):
"""
Get service by name and number.
Usage::
GetService(name)
GetService(name, number)
Usage examples::
GetService('WebService')
GetService('WebService', 1)
"""
def call(self, caller, **kwargs):
name = self._args[0]
num = self._args[1] if len(self._args) > 1 else 0
return self.system.services_by_name[name][num]
[docs]class ReloadService(AbstractAction):
"""
Reload given service.
Usage::
ReloadService(name, number)
ReloadService(name)
Usage examples::
ReloadService('WebService', 0)
ReloadService('ArduinoService')
"""
def call(self, caller, **kwargs):
name = self._args[0]
num = self._args[1] if len(self._args) > 1 else 0
return self.system.services_by_name[name][num].reload()
[docs]class Shell(AbstractAction):
"""
Execute shell command and return string value
:param bool no_wait: if True, execute shell command in new thread and return pid
:param bool output: if True, execute will return the output written to stdout by shell command. By default,
execution status (integer) is returned.
:param str input: if given, input is passed to stdin of the given shell command.
Usage examples::
Shell('/bin/echo test', output=True) # returns 'test'
Shell('mplayer something.mp3', no_wait=True) # returns PID of mplayer
# process that keeps running
Shell('/bin/cat', input='test', output=True) # returns 'test'.
"""
def call(self, caller, **kwargs):
cmd = self.call_eval(self.obj, caller, **kwargs)
input = self._kwargs.get('input', None)
if input:
input = bytes(self.call_eval(input, caller, **kwargs), 'utf-8')
if not caller:
return
try:
process = subprocess.Popen(
cmd, executable='bash', shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
self.logger.debug('Shell: cmd "%s", pid %s', cmd, process.pid)
if self._kwargs.get('no_wait', False):
thread_start(self.system, lambda: process.communicate(input))
return process.pid
else:
out, err = process.communicate(input)
retcode = process.poll()
if self._kwargs.get('output', False):
return out.decode('utf-8')
else:
return retcode
except Exception as e:
self.system.logger.exception('Error %s in %s, cmd: %s', e, self, cmd)
return -1
[docs]class SetStatus(AbstractAction):
"""
Set sensor or actuator value
Usage::
SetStatus(target, source)
# sets status of target to the status of source.
SetStatus(target, source, Force=True)
# sets status to hardware level even if it is not changed
SetStatus([actuator1, actuator2], [sensor1, sensor2])
# sets status of actuator 1 to status of sensor1 and
# status of actuator2 to status of sensor2.
"""
def call(self, caller=None, trigger=None, **kwargs):
if not caller:
return
force = self._kwargs.get('force', False)
values = self.call_eval(self.value, caller, **kwargs)
objs = self.name_to_system_object(self.obj)
if isinstance(objs, AbstractCallable):
objs = self.call_eval(objs, caller, return_value=False, **kwargs)
if not is_iterable(objs):
objs = [objs]
objs = list(objs)
if not is_iterable(values):
values = [values] * len(objs)
values = list(values)
if len(objs) != len(values):
raise RuntimeError('SetStatus: length of targets should be equal to '
'length of sources: %s != %s' % (len(objs), len(values)))
self.system.logger.debug('SetStatus: objs %s, rv: %s', objs, values)
for idx, obj in enumerate(objs):
value = self.call_eval(values[idx], caller, **kwargs)
obj = self.call_eval(obj, caller, return_value=False, trigger=trigger, **kwargs)
self.system.logger.debug('SetStatus(%s, %s) by %s', obj, value, caller)
try:
obj.set_status(value, origin=caller, force=force)
except ValueError as e:
self.system.logger.exception(
'Trying to set invalid status %s of type %s (by %s). Error: %s', value, type(value), caller, e)
except AttributeError as e:
self.system.logger.exception(
'Trying to set status of invalid object %s of type %s, by %s. Error: %s', obj, type(obj), caller, e)
return True
def _give_triggers(self):
return self.value
def _give_targets(self):
return self.obj
[docs]class SetAttr(AbstractAction):
"""Set object's attributes
Usage::
SetAttr(obj, attr=value, attr2=value2)
# performs setattr(obj, attr, value) and setattr(obj, attr2, value2).
"""
def call(self, caller, **kwargs):
if not caller:
return
obj = self.obj
for attr, val in list(self._kwargs.items()):
val = self.call_eval(val, caller, **kwargs)
setattr(obj, attr, val)
return True
def _give_triggers(self):
return list(self._kwargs.values())
def _give_targets(self):
return self.obj
[docs]class Changed(AbstractCallable):
"""
Is value changed since evaluated last time? If this is the first time this Callable
is called (i.e. comparison to last value cannot be made), return True.
Usage::
Changed(sensor1)
"""
_lastval = Any(transient=True)
def call(self, caller, **kwargs):
if not caller:
return
with self._lock:
newval = self.call_eval(self.obj, caller, **kwargs)
rval = self._lastval != newval
self.logger.debug("Changed(%s) last:%s new:%s: rval:%s", caller, self._lastval, newval, rval)
self._lastval = newval
return rval
def _give_targets(self):
return None
[docs]class Swap(AbstractAction):
"""
Swap sensor or BinaryActuator status (False to True and True to False)
Usage::
Swap(actuator1)
"""
def call(self, caller, trigger=None, **kwargs):
if not caller:
return
rv = not (self.call_eval(self.obj, caller, **kwargs))
self.logger.debug("Swap(%s): rv:%s", caller, rv)
if trigger == self.obj:
self.logger.debug('Swap: ignoring setting trigger value')
return False
self.obj.set_status(rv, caller)
return rv
def _give_triggers(self):
return None
[docs]class AbstractRunner(AbstractAction):
""" Abstract baseclass for Callables that are used primarily to run other Actions """
[docs]class Run(AbstractRunner):
"""Run specified Callables one at time. Return always True.
Usage::
Run(callable1, callable2, ...)
"""
def call(self, caller=None, **kwargs):
if not caller:
return
for i in self.objects:
self.call_eval(i, caller, **kwargs)
return True
[docs]class Delay(AbstractRunner):
"""Execute commands delayed by time (in seconds) in separate thread
Usage::
Delay(delay_in_seconds, action)
"""
@property
def delay(self):
return self._args[0]
@property
def objects(self):
return self._args[1:]
def call(self, caller, **kwargs):
if not caller:
return
with self._lock:
state = self.get_state(caller)
timers = state.get_or_create('timers', [])
self.logger.info("Scheduling %s", self)
delay = self.call_eval(self.delay, caller, **kwargs)
timer = threading.Timer(delay, None)
timer.function = threaded(self.system, self._run, caller, timer, **kwargs)
time_after_delay = datetime.datetime.now() + datetime.timedelta(seconds=delay)
timer.name = "Timer for %s timed at %s (%d sek)" % (self, time_after_delay, delay)
timer.start()
timers.append(timer)
def cancel(self, caller):
with self._lock:
state = self.get_state(caller)
timers = state.get('timers', [])
for timer in timers:
if timer.is_alive():
self.logger.info("Cancelling %s", self)
timer.cancel()
self.del_state(caller)
super(Delay, self).cancel(caller)
def _run(self, caller, timer, **kwargs):
self.logger.info("Time is up, running %s", self)
for i in self.objects:
if not caller in self.state:
# cancelled
return
self.call_eval(i, caller, **kwargs)
with self._lock:
if caller in self.state: # if not cancelled
self.get_state(caller).timers.remove(timer)
return True
[docs]class Threaded(Delay):
"""Execute commands in a single thread (in order)
Usage::
Threaded(action)
"""
def __init__(self, *args, **kwargs):
super(Threaded, self).__init__(0, *args, **kwargs)
[docs]class If(AbstractCallable):
"""Basic If statement
Usage::
If(x, y, z) # if x, then run y, z, where x, y, and z are Callables or StatusObjects
If(x, y)
"""
def call(self, caller=None, **kwargs):
if self.call_eval(self.objects[0], caller, **kwargs):
objs = self.objects[1:]
if len(objs) > 1:
for i in objs:
self.call_eval(i, caller, **kwargs)
return True
else:
return self.call_eval(self.objects[1], caller, **kwargs)
else:
return False
def _give_triggers(self):
return self.objects[1:]
def _give_targets(self):
return self.objects[1:]
[docs]class IfElse(AbstractCallable):
"""
Basic if - then - else statement
Usage::
IfElse(x, y, z) # if x, then run y, else run z, where x, y,
# and z are Callables or StatusObjects
IfElse(x, y)
"""
def call(self, caller=None, **kwargs):
if self.call_eval(self.objects[0], caller, **kwargs):
return self.call_eval(self.objects[1], caller, **kwargs)
if len(self.objects) > 2:
return self.call_eval(self.objects[2], caller, **kwargs)
else:
return False
def _give_triggers(self):
return self.objects[1:]
def _give_targets(self):
return self.objects[1:]
[docs]class Switch(AbstractCallable):
"""
Basic switch - case statement.
Two alternative usages:
* First argument switch criterion (integer-valued) and others are cases **OR**
* First argument is switch criterion and second argument is dictionary that contains all possible
cases as keys and related actions as their values.
Usage::
Switch(criterion, choice1, choice2...) # where criteria is integer-valued
# (Callable or StatusObject etc.)
# and choice1, 2... are Callables.
Switch(criterion, {'value1': callable1, 'value2': 'callable2'})
"""
def call(self, caller=None, **kwargs):
sel = self.call_eval(self.objects[0], caller, **kwargs)
if isinstance(self.value, dict):
return self.call_eval(self.value.get(sel, None), caller, **kwargs)
else:
return self.call_eval(self.objects[sel + 1], caller, **kwargs)
def _give_targets(self):
return deep_iterate(self.objects[1:])
def _give_triggers(self):
return deep_iterate(self.objects[1:])
[docs]class TryExcept(AbstractRunner):
"""Try returning `x`, but if exception occurs in the value evaluation, then return `y`.
Usage::
Try(x, y) # where x and y are Callables or StatusObjects etc.
"""
def call(self, caller=None, **kwargs):
try:
return self.call_eval(self.objects[0], caller, **kwargs)
except:
return self.call_eval(self.objects[1], caller, **kwargs)
class AbstractMathematical(AbstractCallable):
def _give_targets(self):
return None
[docs]class Integral(AbstractMathematical):
"""
Calculate integral of object status within time interval
Usage::
Integral(sensor, time1, time2)
"""
def call(self, caller=None, **kwargs):
obj = self.obj
if len(self._args) == 3:
t0 = self.call_eval(self._args[1], caller, **kwargs)
t1 = self.call_eval(self._args[2], caller, **kwargs)
return obj.integral(t0, t1)
elif len(self._args) == 1:
return obj.integral()
[docs]class Min(AbstractMathematical):
"""
Give minimum number of given objects.
Usage::
Min(x, y, z...)
# where x,y,z are anything that can be
# evaluated as number (Callables, Statusobjects etc).
"""
def call(self, caller=None, **kwargs):
val = float("inf")
for i in self.objects:
val = min(val, self.call_eval(i, caller, **kwargs))
return val
[docs]class Max(AbstractMathematical):
"""Give maximum number of given objects
Usage::
Max(x, y, z...)
# where x,y,z are anything that can be
# evaluated as number (Callables, Statusobjects etc).
"""
def call(self, caller=None, **kwargs):
val = -float("inf")
for i in self.objects:
val = max(val, self.call_eval(i, caller, **kwargs))
return val
[docs]class Sum(AbstractMathematical):
"""Give sum of given objects
Usage::
Sum(x, y, z...)
# where x,y,z are anything that can be
# evaluated as number (Callables, Statusobjects etc).
"""
def call(self, caller=None, **kwargs):
_sum = 0.0
for i in self.objects:
_sum += self.call_eval(i, caller, **kwargs)
return _sum
[docs]class Product(AbstractMathematical):
"""Give product of given objects
Usage::
Product(x, y, z...)
# where x,y,z are anything that can be
# evaluated as number (Callables, Statusobjects etc).
"""
def call(self, caller=None, **kwargs):
_sum = 1.0
for i in self.objects:
_sum *= self.call_eval(i, caller, **kwargs)
return _sum
[docs]class Mult(Product):
"""Synonym of Product"""
[docs]class Division(AbstractMathematical):
"""Give division of given objects
Usage::
Division(x, y)
# where x,y are anything that can be
# evaluated as number (Callables, Statusobjects etc).
"""
def call(self, caller=None, **kwargs):
if len(self.objects) != 2:
raise RuntimeError('Wrong amount of arguments')
obj1, obj2 = self.objects
_val1 = self.call_eval(obj1, caller, **kwargs)
_val2 = self.call_eval(obj2, caller, **kwargs)
return _val1 / _val2
[docs]class Div(Division):
""" Synonym of Division"""
[docs]class Add(Sum):
"""Synonym of Sum """
[docs]class AbstractLogical(AbstractMathematical):
"""Abstract class for logic operations (:class:`.And`, :class:`.Or` etc.) """
[docs]class Anything(AbstractLogical):
""" Condition which gives `True` always
Usage::
Anything(x,y,z...)
"""
def call(self, caller=None, **kwargs):
return True
[docs]class Or(AbstractLogical):
""" Or condition
Usage::
Or(x,y,z...) # gives truth value of x or y or z or ,,,
"""
def call(self, caller=None, **kwargs):
def _or(list):
for i in list:
val = self.call_eval(i, caller, **kwargs)
if is_iterable(val):
val = _or(val)
if val:
return True
return False
return _or(self.objects)
[docs]class And(AbstractLogical):
"""And condition
Usage::
And(x,y,z...) # gives truth value of x and y and z and ...
"""
def call(self, caller=None, **kwargs):
def _and(list):
for i in list:
val = self.call_eval(i, caller, **kwargs)
if is_iterable(val):
val = _and(val)
if not val:
return False
return True
return _and(self.objects)
[docs]class Neg(AbstractLogical):
"""Give negative of specified callable (minus sign)
Usage::
Neg(x) # returns -x
"""
def call(self, caller=None, **kwargs):
if len(self.objects) != 1:
raise RuntimeError('Too many arguments')
return -self.call_eval(self.obj, caller, **kwargs)
[docs]class Inverse(AbstractLogical):
"""Give inverse of specified callable (1/something)
Usage::
Inv(x) # returns 1/x
"""
def call(self, caller=None, **kwargs):
if len(self.objects) != 1:
raise RuntimeError('Too many arguments')
return 1./self.call_eval(self.obj, caller, **kwargs)
[docs]class Inv(Inverse):
""" Synonym of Inverse """
[docs]class Not(AbstractLogical):
"""Give negation of specified object
Usage::
Not(x) # returns not x
"""
def call(self, caller=None, **kwargs):
if len(self.objects) != 1:
raise RuntimeError('Too many arguments')
return not self.call_eval(self.obj, caller, **kwargs)
[docs]class Equal(AbstractLogical):
"""Equality condition, i.e. is x == y
Usage::
Equal(x, y) # returns truth value of x == y
"""
def call(self, caller=None, **kwargs):
return self.call_eval(self.obj, caller, **kwargs) == self.call_eval(self.value, caller, **kwargs)
[docs]class Less(AbstractLogical):
"""Condition: is x < y
Usage::
Less(x,y) # returns truth value of x < y
"""
def call(self, caller=None, **kwargs):
a = self.call_eval(self.obj, caller, **kwargs)
b = self.call_eval(self.value, caller, **kwargs)
try:
rv = a < b
except TypeError:
rv = False
return rv
[docs]class More(AbstractLogical):
"""Condition: is x > y
Usage::
More(x,y) # returns truth value of x > y
"""
def call(self, caller=None, **kwargs):
a = self.call_eval(self.obj, caller, **kwargs)
b = self.call_eval(self.value, caller, **kwargs)
try:
rv = a > b
except TypeError:
rv = False
return rv
[docs]class Value(AbstractLogical):
"""Give specified value
Usage::
Value(x) # returns value of x. Used to convert StatusObject into Callable,
# for example, if StatusObject status needs to be used directly
# as a condition of Program condition attributes.
"""
_args = CList
def call(self, caller=None, **kwargs):
return self.call_eval(self.obj, caller, **kwargs)
[docs]class AbstractQuery(AbstractCallable):
"""
Baseclass for query type of Callables, i.e. those that return
set of objects from system based on given conditions.
"""
class ReprObject(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
[docs]class OfType(AbstractQuery):
"""
Gives all objects of given type that are found in System
Usage & example::
OfType(type, **kwargs)
OfType(AbstractActuator, exclude=['actuator1', 'actuator2'])
# returns all actuators in system, except those named 'actuator1' and 'actuator2'.
:param list exclude: list of instances to be excluded from the returned list.
"""
system_objects_changed = Event
triggers = Property(trait=Set(trait=StatusObject),
depends_on='on_setup_callable, _kwargs_items, _args_items, system_objects_changed')
targets = Property(trait=Set(trait=StatusObject),
depends_on='on_setup_callable, _kwargs, _kwargs_items, _args, _args_items, '
'system_objects_changed')
setup_complete = CBool(transient=True)
@on_trait_change('system')
def set_setup_complete(self, name, new):
if name == 'system':
self.system.on_trait_change(self.set_setup_complete, 'post_init_trigger')
if name == 'post_init_trigger':
self.setup_complete = True
@on_trait_change('system.objects, system.objects_items, setup_complete')
def trigger_system_objects_changed(self):
if self.setup_complete:
self.system_objects_changed = 1
def _both(self):
if not self.system:
return set()
exclude = set(self._kwargs.get('exclude', [])) # exclude must be list type
return {i for i in self.system.objects if isinstance(i, tuple(self.objects))} - exclude
@cached_property
def _get_triggers(self):
if self._kwargs.get('type', 'both') in ['triggers', 'both']:
return self._both()
return set()
@cached_property
def _get_targets(self):
if self._kwargs.get('type', 'both') in ['targets', 'both']:
return self._both()
return set()
def call(self, caller=None, **kwargs):
return self.targets
def give_str(self):
args = [ReprObject(i.__name__) for i in self.objects]
return self._give_str(args, self._kwargs)
def give_str_indented(self, tags=False):
args = [ReprObject(i.__name__) for i in self.objects]
return self._give_str_indented(args, self._kwargs, tags)
[docs]class RegexSearch(AbstractCallable):
"""
Scan through string looking for a match to the pattern.
Return matched parts of string by :func:`re.search`.
:param int group: Match group can be chosen by group number.
Usage & examples::
RegexSearch(match_string, content_to_search, **kwargs)
RegexSearch(r'(\d*)(\w*)', '12test') # returns '12'
RegexSearch(r'(\d*)(\w*)', '12test', group=2) # returns 'test'
RegexSearch(r'testasfd', 'test') # returns ''
.. tip::
More examples in unit tests
"""
def call(self, caller=None, **kwargs):
pattern = str(self.call_eval(self.obj, caller, **kwargs))
searchstring = str(self.call_eval(self.value, caller, **kwargs))
match = re.search(pattern, searchstring, re.MULTILINE)
if match:
return match.group(self._kwargs.get('group', 1))
else:
return ''
[docs]class RegexMatch(AbstractCallable):
"""
Try to apply the pattern at the start of the string.
Return matched parts of string by :func:`re.match`.
:param int group: Match group can be chosen by group number.
Usage & examples from unit tests::
RegexMatch(match_string, content_to_search, **kwargs)
RegexMatch(r'heptest', 'heptest') # returns 'heptest'
RegexMatch(r'heptest1', 'heptest') # returns ''
RegexMatch(r'(hep)te(st1)', 'heptest1', group=1) # returns 'hep'
RegexMatch(r'(hep)te(st1)', 'heptest1', group=2) # returns 'st1'
.. tip::
More examples in unit tests
"""
def call(self, caller=None, **kwargs):
pattern = self.call_eval(self.obj, caller, **kwargs)
searchstring = self.call_eval(self.value, caller, **kwargs)
match = re.match(pattern, searchstring, re.MULTILINE)
if match:
return match.group(self._kwargs.get('group', 0))
else:
return ''
[docs]class RemoteFunc(AbstractCallable):
"""
Evaluate remote function via XMLRPC.
Usage::
RemoteFunc('host', 'funcname', *args, **kwargs)
"""
_cached_server = Any(transient=True)
def call(self, caller, **kwargs):
try:
if self._cached_server is None:
host = self.call_eval(self.obj, caller, **kwargs)
self._cached_server = server = xmlrpc.client.ServerProxy(host)
else:
server = self._cached_server
funcname = self.call_eval(self.value, caller, **kwargs)
args = [self.call_eval(o, caller, **kwargs) for o in self.objects[2:]]
_kwargs = {k: self.call_eval(v, caller, **kwargs) for k, v in list(self._kwargs.items())}
try:
return getattr(server, funcname)(*args, **_kwargs)
except (xmlrpc.client.Fault, HTTPException) as e:
self.logger.exception(
'Exception occurred in remote function call (%s,%s)(*%s, **%s), error: %s', server, funcname, args, _kwargs, e)
except (socket.gaierror, IOError, xmlrpc.client.Fault) as e:
self.logger.exception('Could not call remote function, error: %s', e)
[docs]class WaitUntil(AbstractRunner):
"""
Wait until sensor/actuator/callable status changes to True and then execute commands.
WaitUntil will return immediately and only execute specified actions
after criteria is fullfilled.
Usage::
WaitUntil(sensor_or_callable, Action1, Action2, etc)
.. note::
No triggers are collected from WaitUntil
"""
def call(self, caller, **kwargs):
obj = self.name_to_system_object(self.obj)
def callback(**kwargs2):
nocallback = kwargs2.get('nocallback', False)
callbacks = self.get_state(caller).callbacks if not nocallback else []
if obj.status and (nocallback or (callbacks and callback in callbacks)):
for i in self.objects[1:]:
self.call_eval(i, caller, **kwargs)
if not nocallback:
self.logger.debug('Removing executed callback')
with self._lock:
obj.on_trait_change(callback, 'status', remove=True)
callbacks.remove(callback)
if not obj.status:
self.logger.debug('Adding callback')
obj.on_trait_change(callback, 'status')
with self._lock:
callbacks = self.get_state(caller).get_or_create('callbacks', [])
callbacks.append(callback)
else:
self.logger.debug('Calling directly, no callback')
callback(nocallback=True)
def cancel(self, caller):
self.logger.debug('Cancelling callbacks')
with self._lock:
state = self.get_state(caller)
callbacks = state.get('callbacks', [])
for cb in callbacks:
self.obj.on_trait_change(cb, 'status', remove=True)
self.del_state(caller)
super(WaitUntil, self).cancel(caller)
def _give_triggers(self):
return None
[docs]class While(AbstractRunner):
"""
Executes commands (in thread) as long as criteria (sensor, actuator, callable status) remains true.
Flushes worker queue between each iteration such that criteria is updated, if executed
actions alter it.
:param Callable do_after: given Callable is executed after while loop is finished.
Usage & example::
While(criteria, action1, action2, do_after=action3)
# Example loop that runs actions 10 times. Assumes s=UserIntSensor()
Run(
SetStatus(s, 0),
While(s < 10,
SetStatus(s, s+1),
other_actions
)
)
.. note::
While execution is performed in separate thread
.. note::
No triggers are collected from While
"""
class ExitThread(Exception):
pass
def _run(self, caller, thread, **kwargs):
self.system.flush()
try:
while self.call_eval(self.obj, caller, **kwargs):
for i in self.objects[1:]:
if thread._cancel_while:
raise self.ExitThread
self.call_eval(i, caller, **kwargs)
self.system.flush()
do_after = self._kwargs.get('do_after')
if do_after:
self.call_eval(do_after, caller, **kwargs)
except self.ExitThread:
self.logger.debug('While exited via cancel')
with self._lock:
state = self.get_state(caller)
state.threads.remove(thread)
if not state.threads:
self.logger.debug('Last thread, removing state')
self.del_state(caller)
def call(self, caller, **kwargs):
with self._lock:
state = self.get_state(caller)
threads = state.get_or_create('threads', [])
t = threading.Timer(0., None)
t.function = threaded(self.system, self._run, caller, t, **kwargs)
t.name = 'Thread for %s' % self
t._cancel_while = False
threads.append(t)
t.start()
return True
def cancel(self, caller):
self.logger.debug('Canceling While')
with self._lock:
state = self.get_state(caller)
for t in state.get('threads', []):
t._cancel_while = True
super(While, self).cancel(caller)
def _give_triggers(self):
return None
[docs]class TriggeredBy(OnlyTriggers):
"""
Return whether action was triggered by one of specified triggers or not
If no arguments, return the trigger.
Usage::
TriggeredBy() # -> returns the trigger
TriggeredBy(trig1, trig2...) #-> Returns if trigger is one of arguments
"""
def call(self, caller=None, trigger=None, **kwargs):
if self.objects:
obj_eval = [self.name_to_system_object(obj) for obj in self.objects]
return trigger in obj_eval
else:
return trigger
__all__ = get_modules_all(AbstractCallable, locals())