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:
Fabio Manganiello 2024-01-04 13:13:16 +01:00
parent b72fb83d18
commit 9c3da7a2a9
Signed by: blacklight
GPG key ID: D90FBA7F76362774
3 changed files with 78 additions and 52 deletions

View file

@ -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

View file

@ -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)
) )

View file

@ -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)