forked from platypush/platypush
Several improvements for request/procedure execution.
- Fixed regression introduced by incorrect format string in `exec`. - LINT for the `procedure` module. - Apply `Message.Encoder` when dumping values from the context.
This commit is contained in:
parent
b72fb83d18
commit
9c3da7a2a9
3 changed files with 78 additions and 52 deletions
|
@ -58,6 +58,8 @@ class Message:
|
||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
|
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
|
from platypush.procedure import Procedure
|
||||||
|
|
||||||
value = self.parse_datetime(obj)
|
value = self.parse_datetime(obj)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return value
|
return value
|
||||||
|
@ -75,6 +77,9 @@ class Message:
|
||||||
if isinstance(obj, JSONAble):
|
if isinstance(obj, JSONAble):
|
||||||
return obj.to_json()
|
return obj.to_json()
|
||||||
|
|
||||||
|
if isinstance(obj, Procedure):
|
||||||
|
return obj.to_dict()
|
||||||
|
|
||||||
if isinstance(obj, Enum):
|
if isinstance(obj, Enum):
|
||||||
return obj.value
|
return obj.value
|
||||||
|
|
||||||
|
|
|
@ -181,7 +181,7 @@ class Request(Message):
|
||||||
context_value = expr
|
context_value = expr
|
||||||
|
|
||||||
parsed_value += prefix + (
|
parsed_value += prefix + (
|
||||||
json.dumps(context_value)
|
json.dumps(context_value, cls=cls.Encoder)
|
||||||
if isinstance(context_value, (list, dict))
|
if isinstance(context_value, (list, dict))
|
||||||
else str(context_value)
|
else str(context_value)
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,6 +14,10 @@ logger = logging.getLogger('platypush')
|
||||||
|
|
||||||
|
|
||||||
class Statement(enum.Enum):
|
class Statement(enum.Enum):
|
||||||
|
"""
|
||||||
|
Enumerates the possible statements in a procedure.
|
||||||
|
"""
|
||||||
|
|
||||||
BREAK = 'break'
|
BREAK = 'break'
|
||||||
CONTINUE = 'continue'
|
CONTINUE = 'continue'
|
||||||
RETURN = 'return'
|
RETURN = 'return'
|
||||||
|
@ -42,6 +46,7 @@ class Procedure:
|
||||||
req.backend = self.backend
|
req.backend = self.backend
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
# pylint: disable=too-many-branches,too-many-statements
|
||||||
def build(
|
def build(
|
||||||
cls,
|
cls,
|
||||||
name,
|
name,
|
||||||
|
@ -49,7 +54,7 @@ class Procedure:
|
||||||
requests,
|
requests,
|
||||||
args=None,
|
args=None,
|
||||||
backend=None,
|
backend=None,
|
||||||
id=None,
|
id=None, # pylint: disable=redefined-builtin
|
||||||
procedure_class=None,
|
procedure_class=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
@ -185,11 +190,12 @@ class Procedure:
|
||||||
|
|
||||||
raise AssertionError('break/continue statement found outside of a loop')
|
raise AssertionError('break/continue statement found outside of a loop')
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches,too-many-statements
|
||||||
def execute(self, n_tries=1, __stack__=None, **context):
|
def execute(self, n_tries=1, __stack__=None, **context):
|
||||||
"""
|
"""
|
||||||
Execute the requests in the procedure
|
Execute the requests in the procedure.
|
||||||
Params:
|
|
||||||
n_tries -- Number of tries in case of failure before raising a RuntimeError
|
:param n_tries: Number of tries in case of failure before raising a RuntimeError.
|
||||||
"""
|
"""
|
||||||
if not __stack__:
|
if not __stack__:
|
||||||
__stack__ = [self]
|
__stack__ = [self]
|
||||||
|
@ -218,31 +224,35 @@ class Procedure:
|
||||||
if request == Statement.RETURN:
|
if request == Statement.RETURN:
|
||||||
self._should_return = True
|
self._should_return = True
|
||||||
for proc in __stack__:
|
for proc in __stack__:
|
||||||
proc._should_return = True
|
proc._should_return = True # pylint: disable=protected-access
|
||||||
break
|
break
|
||||||
|
|
||||||
if request in [Statement.BREAK, Statement.CONTINUE]:
|
if request in [Statement.BREAK, Statement.CONTINUE]:
|
||||||
loop = self._find_nearest_loop(__stack__)
|
loop = self._find_nearest_loop(__stack__)
|
||||||
if request == Statement.BREAK:
|
if request == Statement.BREAK:
|
||||||
loop._should_break = True
|
loop._should_break = True # pylint: disable=protected-access
|
||||||
else:
|
else:
|
||||||
loop._should_continue = True
|
loop._should_continue = True # pylint: disable=protected-access
|
||||||
break
|
break
|
||||||
|
|
||||||
if isinstance(self, LoopProcedure) and (
|
should_continue = getattr(self, '_should_continue', False)
|
||||||
self._should_continue or self._should_break
|
should_break = getattr(self, '_should_break', False)
|
||||||
):
|
if isinstance(self, LoopProcedure) and (should_continue or should_break):
|
||||||
if self._should_continue:
|
if should_continue:
|
||||||
self._should_continue = False
|
self._should_continue = ( # pylint: disable=attribute-defined-outside-init
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
if token:
|
if token and not isinstance(request, Statement):
|
||||||
request.token = token
|
request.token = token
|
||||||
|
|
||||||
context['_async'] = self._async
|
context['_async'] = self._async
|
||||||
context['n_tries'] = n_tries
|
context['n_tries'] = n_tries
|
||||||
response = request.execute(__stack__=__stack__, **context)
|
exec_ = getattr(request, 'execute', None)
|
||||||
|
if callable(exec_):
|
||||||
|
response = exec_(__stack__=__stack__, **context)
|
||||||
|
|
||||||
if not self._async and response:
|
if not self._async and response:
|
||||||
if isinstance(response.output, dict):
|
if isinstance(response.output, dict):
|
||||||
|
@ -317,14 +327,15 @@ class ForProcedure(LoopProcedure):
|
||||||
self.iterator_name = iterator_name
|
self.iterator_name = iterator_name
|
||||||
self.iterable = iterable
|
self.iterable = iterable
|
||||||
|
|
||||||
def execute(self, _async=None, **context):
|
# pylint: disable=eval-used
|
||||||
|
def execute(self, *_, **context):
|
||||||
try:
|
try:
|
||||||
iterable = eval(self.iterable)
|
iterable = eval(self.iterable)
|
||||||
assert hasattr(
|
assert hasattr(
|
||||||
iterable, '__iter__'
|
iterable, '__iter__'
|
||||||
), f'Object of type {type(iterable)} is not iterable: {iterable}'
|
), f'Object of type {type(iterable)} is not iterable: {iterable}'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f'Iterable {self.iterable} expansion error: {e}')
|
logger.debug('Iterable %s expansion error: %s', self.iterable, e)
|
||||||
iterable = Request.expand_value_from_context(self.iterable, **context)
|
iterable = Request.expand_value_from_context(self.iterable, **context)
|
||||||
|
|
||||||
response = Response()
|
response = Response()
|
||||||
|
@ -387,32 +398,37 @@ class WhileProcedure(LoopProcedure):
|
||||||
def _get_context(**context):
|
def _get_context(**context):
|
||||||
for k, v in context.items():
|
for k, v in context.items():
|
||||||
try:
|
try:
|
||||||
context[k] = eval(v)
|
context[k] = eval(v) # pylint: disable=eval-used
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f'Evaluation error for {v}: {e}')
|
logger.debug('Evaluation error for %s=%s: %s', k, v, e)
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
try:
|
try:
|
||||||
context[k] = eval('"' + re.sub(r'(^|[^\\])"', '\1\\"', v) + '"')
|
context[k] = eval( # pylint: disable=eval-used
|
||||||
except Exception as e:
|
'"' + re.sub(r'(^|[^\\])"', '\1\\"', v) + '"'
|
||||||
|
)
|
||||||
|
except Exception as ee:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Could not parse value for context variable %s=%s', k, v
|
'Could not parse value for context variable %s=%s: %s',
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
ee,
|
||||||
)
|
)
|
||||||
logger.warning('Context: %s', context)
|
logger.warning('Context: %s', context)
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def execute(self, _async=None, **context):
|
def execute(self, *_, **context):
|
||||||
response = Response()
|
response = Response()
|
||||||
context = self._get_context(**context)
|
context = self._get_context(**context)
|
||||||
for k, v in context.items():
|
for k, v in context.items():
|
||||||
try:
|
try:
|
||||||
exec(f'{k}={v}')
|
exec(f'{k}={v}') # pylint: disable=exec-used
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug('Evaluation error: %s=%s: %s', k, v, e)
|
logger.debug('Evaluation error: %s=%s: %s', k, v, e)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
condition_true = eval(self.condition)
|
condition_true = eval(self.condition) # pylint: disable=eval-used
|
||||||
if not condition_true:
|
if not condition_true:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -436,9 +452,9 @@ class WhileProcedure(LoopProcedure):
|
||||||
new_context = self._get_context(**response.output)
|
new_context = self._get_context(**response.output)
|
||||||
for k, v in new_context.items():
|
for k, v in new_context.items():
|
||||||
try:
|
try:
|
||||||
exec(f'{k}={v}')
|
exec(f'{k}={v}') # pylint: disable=exec-used
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f'Evaluation error: {k}={v}: {e}')
|
logger.debug('Evaluation error: %s=%s: %s', k, v, e)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -447,25 +463,23 @@ class IfProcedure(Procedure):
|
||||||
"""
|
"""
|
||||||
Models an if-else construct.
|
Models an if-else construct.
|
||||||
|
|
||||||
Example:
|
Example::
|
||||||
|
|
||||||
procedure.sync.process_results:
|
procedure.sync.process_results:
|
||||||
-
|
- action: http.get
|
||||||
action: http.get
|
args:
|
||||||
|
url: https://some-service/some/json/endpoint
|
||||||
|
# Example response: { "sensors": [ {"temperature": 18 } ] }
|
||||||
|
|
||||||
|
- if ${sensors['temperature'] < 20}:
|
||||||
|
- action: shell.exec
|
||||||
args:
|
args:
|
||||||
url: https://some-service/some/json/endpoint
|
cmd: '/path/turn_on_heating.sh'
|
||||||
# Example response: { "sensors": [ {"temperature": 18 } ] }
|
- else:
|
||||||
-
|
- action: shell.exec
|
||||||
if ${sensors['temperature'] < 20}:
|
args:
|
||||||
-
|
cmd: '/path/turn_off_heating.sh'
|
||||||
action: shell.exec
|
|
||||||
args:
|
|
||||||
cmd: '/path/turn_on_heating.sh'
|
|
||||||
else:
|
|
||||||
-
|
|
||||||
action: shell.exec
|
|
||||||
args:
|
|
||||||
cmd: '/path/turn_off_heating.sh'
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -476,7 +490,7 @@ class IfProcedure(Procedure):
|
||||||
else_branch=None,
|
else_branch=None,
|
||||||
args=None,
|
args=None,
|
||||||
backend=None,
|
backend=None,
|
||||||
id=None,
|
id=None, # pylint: disable=redefined-builtin
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
kwargs['_async'] = False
|
kwargs['_async'] = False
|
||||||
|
@ -498,15 +512,17 @@ class IfProcedure(Procedure):
|
||||||
super().__init__(name=name, requests=reqs, args=args, backend=backend, **kwargs)
|
super().__init__(name=name, requests=reqs, args=args, backend=backend, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
def build(
|
def build(
|
||||||
cls,
|
cls,
|
||||||
name,
|
name,
|
||||||
|
*_,
|
||||||
condition,
|
condition,
|
||||||
requests,
|
requests,
|
||||||
else_branch=None,
|
|
||||||
args=None,
|
args=None,
|
||||||
backend=None,
|
backend=None,
|
||||||
id=None,
|
id=None, # pylint: disable=redefined-builtin
|
||||||
|
else_branch=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
kwargs['_async'] = False
|
kwargs['_async'] = False
|
||||||
|
@ -532,22 +548,23 @@ class IfProcedure(Procedure):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, **context):
|
def execute(self, *_, **context):
|
||||||
for k, v in context.items():
|
for k, v in context.items():
|
||||||
try:
|
try:
|
||||||
exec(f'{k}={v}')
|
exec(f'{k}={v}') # pylint: disable=exec-used
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.debug(f'Evaluation error: {k}={v}: {e}')
|
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
try:
|
try:
|
||||||
exec('{k}="' + re.sub(r'(^|[^\\])"', '\1\\"', v) + '"')
|
exec( # pylint: disable=exec-used
|
||||||
|
f'{k}="' + re.sub(r'(^|[^\\])"', '\1\\"', v) + '"'
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Could not set context variable %s=%s: %s', k, v, e
|
'Could not set context variable %s=%s: %s', k, v, e
|
||||||
)
|
)
|
||||||
logger.debug('Context: %s', context)
|
logger.debug('Context: %s', context)
|
||||||
|
|
||||||
condition_true = eval(self.condition)
|
condition_true = eval(self.condition) # pylint: disable=eval-used
|
||||||
response = Response()
|
response = Response()
|
||||||
|
|
||||||
if condition_true:
|
if condition_true:
|
||||||
|
@ -559,6 +576,10 @@ class IfProcedure(Procedure):
|
||||||
|
|
||||||
|
|
||||||
def procedure(f):
|
def procedure(f):
|
||||||
|
"""
|
||||||
|
Public decorator to mark a function as a procedure.
|
||||||
|
"""
|
||||||
|
|
||||||
f.procedure = True
|
f.procedure = True
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
|
|
Loading…
Reference in a new issue