2020-01-09 10:28:02 +01:00
|
|
|
import enum
|
2017-12-25 17:23:09 +01:00
|
|
|
import logging
|
2018-01-08 02:41:24 +01:00
|
|
|
import re
|
2024-09-16 03:16:53 +02:00
|
|
|
from copy import deepcopy
|
|
|
|
from dataclasses import dataclass, field
|
2020-04-08 23:22:54 +02:00
|
|
|
from functools import wraps
|
2017-12-25 17:23:09 +01:00
|
|
|
|
2018-09-27 01:09:00 +02:00
|
|
|
from queue import LifoQueue
|
2024-09-16 03:16:53 +02:00
|
|
|
from typing import Any, Dict, Iterable, List, Optional
|
2021-02-27 20:27:36 +01:00
|
|
|
|
|
|
|
from ..common import exec_wrapper
|
2017-12-25 17:23:09 +01:00
|
|
|
from ..config import Config
|
|
|
|
from ..message.request import Request
|
2018-01-05 23:20:39 +01:00
|
|
|
from ..message.response import Response
|
2017-12-25 17:23:09 +01:00
|
|
|
|
2020-09-27 01:33:38 +02:00
|
|
|
logger = logging.getLogger('platypush')
|
2018-06-06 20:09:18 +02:00
|
|
|
|
|
|
|
|
2024-09-10 19:55:26 +02:00
|
|
|
class StatementType(enum.Enum):
|
2024-01-04 13:13:16 +01:00
|
|
|
"""
|
|
|
|
Enumerates the possible statements in a procedure.
|
|
|
|
"""
|
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
BREAK = 'break'
|
|
|
|
CONTINUE = 'continue'
|
|
|
|
RETURN = 'return'
|
2024-09-16 03:16:53 +02:00
|
|
|
SET = 'set'
|
2020-01-09 10:28:02 +01:00
|
|
|
|
|
|
|
|
2024-09-10 19:55:26 +02:00
|
|
|
@dataclass
|
|
|
|
class Statement:
|
|
|
|
"""
|
|
|
|
Models a statement in a procedure.
|
|
|
|
"""
|
|
|
|
|
|
|
|
type: StatementType
|
2024-09-16 03:16:53 +02:00
|
|
|
argument: Optional[Any] = None
|
2024-09-10 19:55:26 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def build(cls, statement: str):
|
|
|
|
"""
|
|
|
|
Builds a statement from a string.
|
|
|
|
"""
|
|
|
|
|
|
|
|
m = re.match(r'\s*return\s*(.*)\s*', statement, re.IGNORECASE)
|
|
|
|
if m:
|
|
|
|
return ReturnStatement(argument=m.group(1))
|
|
|
|
|
|
|
|
return cls(StatementType(statement.lower()))
|
|
|
|
|
|
|
|
def run(self, *_, **__) -> Optional[Any]:
|
|
|
|
"""
|
|
|
|
Executes the statement.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class ReturnStatement(Statement):
|
|
|
|
"""
|
|
|
|
Models a return statement in a procedure.
|
|
|
|
"""
|
|
|
|
|
|
|
|
type: StatementType = StatementType.RETURN
|
|
|
|
|
2024-09-16 03:16:53 +02:00
|
|
|
def run(self, *_, **context) -> Any:
|
|
|
|
return Response(
|
|
|
|
output=Request.expand_value_from_context(
|
|
|
|
self.argument, **_update_context(context)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class SetStatement(Statement):
|
|
|
|
"""
|
|
|
|
Models a set variable statement in a procedure.
|
|
|
|
"""
|
|
|
|
|
|
|
|
type: StatementType = StatementType.SET
|
|
|
|
vars: dict = field(default_factory=dict)
|
|
|
|
|
2024-09-10 19:55:26 +02:00
|
|
|
def run(self, *_, **context):
|
2024-09-16 03:16:53 +02:00
|
|
|
vars = deepcopy(self.vars) # pylint: disable=redefined-builtin
|
|
|
|
for k, v in vars.items():
|
|
|
|
vars[k] = Request.expand_value_from_context(v, **context)
|
|
|
|
|
|
|
|
context.update(vars)
|
|
|
|
return Response(output=vars)
|
2024-09-10 19:55:26 +02:00
|
|
|
|
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
class Procedure:
|
|
|
|
"""Procedure class. A procedure is a pre-configured list of requests"""
|
2017-12-25 17:23:09 +01:00
|
|
|
|
2018-09-25 19:20:34 +02:00
|
|
|
def __init__(self, name, _async, requests, args=None, backend=None):
|
2017-12-25 17:23:09 +01:00
|
|
|
"""
|
|
|
|
Params:
|
|
|
|
name -- Procedure name
|
2023-12-09 01:23:36 +01:00
|
|
|
_async -- Whether the actions in the procedure are supposed to
|
2018-01-06 00:21:25 +01:00
|
|
|
be executed sequentially or in parallel (True or False)
|
2020-01-09 10:28:02 +01:00
|
|
|
requests -- List of platypush.message.request.Request objects
|
2017-12-25 17:23:09 +01:00
|
|
|
"""
|
|
|
|
|
2019-07-13 15:49:38 +02:00
|
|
|
self.name = name
|
|
|
|
self._async = _async
|
2017-12-25 17:23:09 +01:00
|
|
|
self.requests = requests
|
2019-07-13 15:49:38 +02:00
|
|
|
self.backend = backend
|
|
|
|
self.args = args or {}
|
2020-01-09 10:28:02 +01:00
|
|
|
self._should_return = False
|
2017-12-25 17:23:09 +01:00
|
|
|
|
|
|
|
for req in requests:
|
|
|
|
req.backend = self.backend
|
|
|
|
|
|
|
|
@classmethod
|
2024-01-04 13:13:16 +01:00
|
|
|
# pylint: disable=too-many-branches,too-many-statements
|
2023-12-09 01:23:36 +01:00
|
|
|
def build(
|
|
|
|
cls,
|
|
|
|
name,
|
|
|
|
_async,
|
|
|
|
requests,
|
|
|
|
args=None,
|
|
|
|
backend=None,
|
|
|
|
procedure_class=None,
|
|
|
|
**kwargs,
|
|
|
|
):
|
2017-12-25 17:23:09 +01:00
|
|
|
reqs = []
|
2020-01-09 10:28:02 +01:00
|
|
|
for_count = 0
|
|
|
|
while_count = 0
|
2018-04-29 22:25:31 +02:00
|
|
|
if_count = 0
|
2018-09-27 01:09:00 +02:00
|
|
|
if_config = LifoQueue()
|
|
|
|
procedure_class = procedure_class or cls
|
2019-07-13 14:22:43 +02:00
|
|
|
key = None
|
2024-09-10 19:53:14 +02:00
|
|
|
kwargs.pop('id', None)
|
2018-01-08 02:41:24 +01:00
|
|
|
|
2017-12-25 17:23:09 +01:00
|
|
|
for request_config in requests:
|
2020-01-09 10:28:02 +01:00
|
|
|
# Check if it's a break/continue/return statement
|
|
|
|
if isinstance(request_config, str):
|
2024-09-12 02:14:40 +02:00
|
|
|
cls._flush_if_statements(reqs, if_config)
|
2024-09-10 19:55:26 +02:00
|
|
|
reqs.append(Statement.build(request_config))
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Check if it's a return statement with a value
|
|
|
|
if (
|
|
|
|
len(request_config.keys()) == 1
|
2024-09-16 03:16:53 +02:00
|
|
|
and list(request_config.keys())[0] == StatementType.RETURN.value
|
|
|
|
):
|
|
|
|
cls._flush_if_statements(reqs, if_config)
|
|
|
|
reqs.append(
|
|
|
|
ReturnStatement(argument=request_config[StatementType.RETURN.value])
|
|
|
|
)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Check if it's a variable set statement
|
|
|
|
if (len(request_config.keys()) == 1) and (
|
|
|
|
list(request_config.keys())[0] == StatementType.SET.value
|
2024-09-10 19:55:26 +02:00
|
|
|
):
|
2024-09-12 02:14:40 +02:00
|
|
|
cls._flush_if_statements(reqs, if_config)
|
2024-09-16 03:16:53 +02:00
|
|
|
reqs.append(SetStatement(vars=request_config[StatementType.SET.value]))
|
2020-01-09 10:28:02 +01:00
|
|
|
continue
|
2018-01-08 02:41:24 +01:00
|
|
|
|
2018-04-29 22:25:31 +02:00
|
|
|
# Check if this request is an if-else
|
|
|
|
if len(request_config.keys()) >= 1:
|
|
|
|
key = list(request_config.keys())[0]
|
2021-02-24 01:28:46 +01:00
|
|
|
m = re.match(r'\s*(if)\s+\${(.*)}\s*', key)
|
2018-09-27 01:09:00 +02:00
|
|
|
|
2018-04-29 22:25:31 +02:00
|
|
|
if m:
|
2024-09-16 03:16:53 +02:00
|
|
|
cls._flush_if_statements(reqs, if_config)
|
2018-04-29 22:25:31 +02:00
|
|
|
if_count += 1
|
2023-12-09 01:23:36 +01:00
|
|
|
if_name = f'{name}__if_{if_count}'
|
2018-04-29 22:25:31 +02:00
|
|
|
condition = m.group(2)
|
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
if_config.put(
|
|
|
|
{
|
|
|
|
'name': if_name,
|
|
|
|
'_async': False,
|
|
|
|
'requests': request_config[key],
|
|
|
|
'condition': condition,
|
|
|
|
'else_branch': [],
|
|
|
|
'backend': backend,
|
|
|
|
}
|
|
|
|
)
|
2018-04-29 22:25:31 +02:00
|
|
|
|
|
|
|
continue
|
|
|
|
|
2018-09-27 01:09:00 +02:00
|
|
|
if key == 'else':
|
|
|
|
if if_config.empty():
|
2023-12-09 01:23:36 +01:00
|
|
|
raise RuntimeError(
|
|
|
|
f'else statement with no associated if in {name}'
|
|
|
|
)
|
2018-09-27 01:09:00 +02:00
|
|
|
|
|
|
|
conf = if_config.get()
|
|
|
|
conf['else_branch'] = request_config[key]
|
|
|
|
if_config.put(conf)
|
|
|
|
|
2018-09-27 02:20:25 +02:00
|
|
|
if not if_config.empty():
|
|
|
|
reqs.append(IfProcedure.build(**(if_config.get())))
|
|
|
|
if key == 'else':
|
|
|
|
continue
|
2018-09-27 01:09:00 +02:00
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
# Check if this request is a for loop
|
|
|
|
if len(request_config.keys()) == 1:
|
|
|
|
key = list(request_config.keys())[0]
|
2021-02-24 01:28:46 +01:00
|
|
|
m = re.match(r'\s*(fork?)\s+([\w\d_]+)\s+in\s+(.*)\s*', key)
|
2020-01-09 10:28:02 +01:00
|
|
|
|
|
|
|
if m:
|
|
|
|
for_count += 1
|
2023-12-09 01:23:36 +01:00
|
|
|
loop_name = f'{name}__for_{for_count}'
|
2020-01-09 10:28:02 +01:00
|
|
|
|
|
|
|
# A 'for' loop is synchronous. Declare a 'fork' loop if you
|
|
|
|
# want to process the elements in the iterable in parallel
|
2023-12-09 01:23:36 +01:00
|
|
|
_async = m.group(1) == 'fork'
|
2020-01-09 10:28:02 +01:00
|
|
|
iterator_name = m.group(2)
|
|
|
|
iterable = m.group(3)
|
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
loop = ForProcedure.build(
|
|
|
|
name=loop_name,
|
|
|
|
_async=_async,
|
|
|
|
requests=request_config[key],
|
|
|
|
backend=backend,
|
|
|
|
iterator_name=iterator_name,
|
|
|
|
iterable=iterable,
|
|
|
|
)
|
2020-01-09 10:28:02 +01:00
|
|
|
|
|
|
|
reqs.append(loop)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Check if this request is a while loop
|
|
|
|
if len(request_config.keys()) == 1:
|
|
|
|
key = list(request_config.keys())[0]
|
2021-02-24 01:28:46 +01:00
|
|
|
m = re.match(r'\s*while\s+\${(.*)}\s*', key)
|
2020-01-09 10:28:02 +01:00
|
|
|
|
|
|
|
if m:
|
|
|
|
while_count += 1
|
2023-12-09 01:23:36 +01:00
|
|
|
loop_name = f'{name}__while_{while_count}'
|
2020-01-09 10:28:02 +01:00
|
|
|
condition = m.group(1).strip()
|
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
loop = WhileProcedure.build(
|
|
|
|
name=loop_name,
|
|
|
|
_async=False,
|
|
|
|
requests=request_config[key],
|
|
|
|
condition=condition,
|
|
|
|
backend=backend,
|
|
|
|
)
|
2020-01-09 10:28:02 +01:00
|
|
|
|
|
|
|
reqs.append(loop)
|
|
|
|
continue
|
|
|
|
|
2017-12-25 17:23:09 +01:00
|
|
|
request_config['origin'] = Config.get('device_id')
|
|
|
|
if 'target' not in request_config:
|
|
|
|
request_config['target'] = request_config['origin']
|
|
|
|
|
|
|
|
request = Request.build(request_config)
|
|
|
|
reqs.append(request)
|
|
|
|
|
2024-09-12 02:14:40 +02:00
|
|
|
cls._flush_if_statements(reqs, if_config)
|
2018-09-27 01:09:00 +02:00
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
return procedure_class(
|
|
|
|
name=name,
|
|
|
|
_async=_async,
|
|
|
|
requests=reqs,
|
|
|
|
args=args,
|
|
|
|
backend=backend,
|
|
|
|
**kwargs,
|
|
|
|
)
|
2017-12-25 17:23:09 +01:00
|
|
|
|
2024-09-12 02:14:40 +02:00
|
|
|
@staticmethod
|
|
|
|
def _flush_if_statements(requests: List, if_config: LifoQueue):
|
|
|
|
while not if_config.empty():
|
|
|
|
pending_if = if_config.get()
|
|
|
|
requests.append(IfProcedure.build(**pending_if))
|
|
|
|
|
2024-01-04 13:13:16 +01:00
|
|
|
# pylint: disable=too-many-branches,too-many-statements
|
2024-09-16 03:16:53 +02:00
|
|
|
def execute(
|
|
|
|
self,
|
|
|
|
n_tries: int = 1,
|
|
|
|
__stack__: Optional[Iterable] = None,
|
|
|
|
new_context: Optional[Dict[str, Any]] = None,
|
|
|
|
**context,
|
|
|
|
):
|
2017-12-25 17:23:09 +01:00
|
|
|
"""
|
2024-01-04 13:13:16 +01:00
|
|
|
Execute the requests in the procedure.
|
|
|
|
|
|
|
|
:param n_tries: Number of tries in case of failure before raising a RuntimeError.
|
2017-12-25 17:23:09 +01:00
|
|
|
"""
|
2024-09-16 03:16:53 +02:00
|
|
|
__stack__ = (self,) if not __stack__ else (self, *__stack__)
|
|
|
|
new_context = new_context or {}
|
2020-01-09 10:28:02 +01:00
|
|
|
|
2018-09-25 19:20:34 +02:00
|
|
|
if self.args:
|
2019-02-15 11:45:11 +01:00
|
|
|
args = self.args.copy()
|
2019-07-13 15:49:38 +02:00
|
|
|
for k, v in args.items():
|
2024-09-16 03:16:53 +02:00
|
|
|
args[k] = context[k] = Request.expand_value_from_context(v, **context)
|
2023-12-09 01:23:36 +01:00
|
|
|
logger.info('Executing procedure %s with arguments %s', self.name, args)
|
2018-09-25 19:20:34 +02:00
|
|
|
else:
|
2023-12-09 01:23:36 +01:00
|
|
|
logger.info('Executing procedure %s', self.name)
|
2018-09-25 19:20:34 +02:00
|
|
|
|
2018-01-05 23:20:39 +01:00
|
|
|
response = Response()
|
2018-07-09 16:51:17 +02:00
|
|
|
token = Config.get('token')
|
2024-09-16 03:16:53 +02:00
|
|
|
context = _update_context(context)
|
|
|
|
locals().update(context)
|
2018-01-05 23:20:39 +01:00
|
|
|
|
2024-09-16 03:16:53 +02:00
|
|
|
# pylint: disable=too-many-nested-blocks
|
2017-12-25 17:23:09 +01:00
|
|
|
for request in self.requests:
|
2020-04-08 23:22:54 +02:00
|
|
|
if callable(request):
|
|
|
|
response = request(**context)
|
|
|
|
continue
|
|
|
|
|
2024-09-16 03:16:53 +02:00
|
|
|
context['_async'] = self._async
|
|
|
|
context['n_tries'] = n_tries
|
|
|
|
context['__stack__'] = __stack__
|
|
|
|
context['new_context'] = new_context
|
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
if isinstance(request, Statement):
|
2024-09-10 19:55:26 +02:00
|
|
|
if isinstance(request, ReturnStatement):
|
2024-09-16 03:16:53 +02:00
|
|
|
response = request.run(**context)
|
2020-01-09 10:28:02 +01:00
|
|
|
self._should_return = True
|
|
|
|
for proc in __stack__:
|
2024-01-04 13:13:16 +01:00
|
|
|
proc._should_return = True # pylint: disable=protected-access
|
2024-09-16 03:16:53 +02:00
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
break
|
|
|
|
|
2024-09-16 03:16:53 +02:00
|
|
|
if isinstance(request, SetStatement):
|
|
|
|
rs: dict = request.run(**context).output # type: ignore
|
|
|
|
context.update(rs)
|
|
|
|
new_context.update(rs)
|
|
|
|
locals().update(rs)
|
|
|
|
continue
|
|
|
|
|
2024-09-10 19:55:26 +02:00
|
|
|
if request.type in [StatementType.BREAK, StatementType.CONTINUE]:
|
2024-09-16 03:16:53 +02:00
|
|
|
for proc in __stack__:
|
|
|
|
if isinstance(proc, LoopProcedure):
|
|
|
|
if request.type == StatementType.BREAK:
|
|
|
|
setattr(proc, '_should_break', True) # noqa: B010
|
|
|
|
else:
|
|
|
|
setattr(proc, '_should_continue', True) # noqa: B010
|
|
|
|
break
|
|
|
|
|
|
|
|
proc._should_return = True # pylint: disable=protected-access
|
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
break
|
|
|
|
|
2024-01-04 13:13:16 +01:00
|
|
|
should_continue = getattr(self, '_should_continue', False)
|
|
|
|
should_break = getattr(self, '_should_break', False)
|
2024-09-16 03:16:53 +02:00
|
|
|
if self._should_return or should_continue or should_break:
|
2023-12-09 01:23:36 +01:00
|
|
|
break
|
2020-01-09 10:28:02 +01:00
|
|
|
|
2024-01-04 13:13:16 +01:00
|
|
|
if token and not isinstance(request, Statement):
|
2018-07-09 16:51:17 +02:00
|
|
|
request.token = token
|
|
|
|
|
2024-01-04 13:13:16 +01:00
|
|
|
exec_ = getattr(request, 'execute', None)
|
|
|
|
if callable(exec_):
|
2024-09-16 03:16:53 +02:00
|
|
|
response = exec_(**context)
|
|
|
|
context.update(context.get('new_context', {}))
|
2018-01-07 23:31:19 +01:00
|
|
|
|
2019-01-07 00:07:45 +01:00
|
|
|
if not self._async and response:
|
2018-01-07 23:31:19 +01:00
|
|
|
if isinstance(response.output, dict):
|
2024-09-16 03:16:53 +02:00
|
|
|
context.update(response.output)
|
2018-01-06 00:21:25 +01:00
|
|
|
|
|
|
|
context['output'] = response.output
|
|
|
|
context['errors'] = response.errors
|
2024-09-16 03:16:53 +02:00
|
|
|
new_context.update(context)
|
|
|
|
locals().update(context)
|
2018-01-05 23:20:39 +01:00
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
if self._should_return:
|
|
|
|
break
|
|
|
|
|
2019-01-07 00:07:45 +01:00
|
|
|
return response or Response()
|
2017-12-25 17:23:09 +01:00
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
def to_dict(self):
|
|
|
|
return {
|
|
|
|
'name': self.name,
|
|
|
|
'requests': self.requests,
|
|
|
|
'args': self.args,
|
|
|
|
'_async': self._async,
|
|
|
|
}
|
|
|
|
|
2017-12-25 17:23:09 +01:00
|
|
|
|
2018-01-08 02:41:24 +01:00
|
|
|
class LoopProcedure(Procedure):
|
2020-01-09 10:28:02 +01:00
|
|
|
"""
|
|
|
|
Base class while and for/fork loops.
|
|
|
|
"""
|
|
|
|
|
2024-09-16 03:16:53 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2020-01-09 10:28:02 +01:00
|
|
|
self._should_break = False
|
|
|
|
self._should_continue = False
|
|
|
|
|
|
|
|
|
|
|
|
class ForProcedure(LoopProcedure):
|
2018-01-08 02:41:24 +01:00
|
|
|
"""
|
|
|
|
Models a loop procedure, which expresses a construct similar to a
|
|
|
|
for loop in a programming language. The 'for' keyword implies a synchronous
|
|
|
|
loop, i.e. the nested actions will be executed in sequence. Use 'fork'
|
|
|
|
instead of 'for' if you want to run the actions in parallel.
|
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
Example::
|
2018-01-08 02:41:24 +01:00
|
|
|
|
|
|
|
procedure.sync.process_results:
|
2020-01-09 10:28:02 +01:00
|
|
|
- action: http.get
|
|
|
|
args:
|
|
|
|
url: https://some-service/some/json/endpoint
|
|
|
|
# Example response: { "results": [ {"id":1, "name":"foo"}, {"id":2,"name":"bar"} ]}
|
|
|
|
- for result in ${results}:
|
|
|
|
- action: some.custom.action
|
|
|
|
args:
|
|
|
|
id: ${result['id']}
|
|
|
|
name: ${result['name']}
|
2018-01-08 02:41:24 +01:00
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
"""
|
2018-01-08 02:41:24 +01:00
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
name,
|
|
|
|
iterator_name,
|
|
|
|
iterable,
|
|
|
|
requests,
|
|
|
|
_async=False,
|
|
|
|
args=None,
|
|
|
|
backend=None,
|
|
|
|
):
|
|
|
|
super().__init__(
|
|
|
|
name=name, _async=_async, requests=requests, args=args, backend=backend
|
|
|
|
)
|
2018-01-08 02:41:24 +01:00
|
|
|
self.iterator_name = iterator_name
|
|
|
|
self.iterable = iterable
|
|
|
|
|
2024-01-04 13:13:16 +01:00
|
|
|
# pylint: disable=eval-used
|
|
|
|
def execute(self, *_, **context):
|
2024-09-16 03:16:53 +02:00
|
|
|
ctx = _update_context(context)
|
|
|
|
locals().update(ctx)
|
|
|
|
|
2019-08-16 19:04:15 +02:00
|
|
|
try:
|
|
|
|
iterable = eval(self.iterable)
|
2023-12-09 01:23:36 +01:00
|
|
|
assert hasattr(
|
|
|
|
iterable, '__iter__'
|
|
|
|
), f'Object of type {type(iterable)} is not iterable: {iterable}'
|
2021-04-05 00:58:44 +02:00
|
|
|
except Exception as e:
|
2024-01-04 13:13:16 +01:00
|
|
|
logger.debug('Iterable %s expansion error: %s', self.iterable, e)
|
2024-09-16 03:16:53 +02:00
|
|
|
iterable = Request.expand_value_from_context(self.iterable, **ctx)
|
2019-08-16 19:04:15 +02:00
|
|
|
|
2018-01-08 02:41:24 +01:00
|
|
|
response = Response()
|
|
|
|
|
|
|
|
for item in iterable:
|
2024-09-16 03:16:53 +02:00
|
|
|
ctx[self.iterator_name] = item
|
|
|
|
response = super().execute(**ctx)
|
|
|
|
ctx.update(ctx.get('new_context', {}))
|
|
|
|
|
|
|
|
if response.output and isinstance(response.output, dict):
|
|
|
|
ctx = _update_context(ctx, **response.output)
|
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
if self._should_return:
|
2023-12-09 01:23:36 +01:00
|
|
|
logger.info('Returning from %s', self.name)
|
2020-01-09 10:28:02 +01:00
|
|
|
break
|
|
|
|
|
|
|
|
if self._should_continue:
|
|
|
|
self._should_continue = False
|
2023-12-09 01:23:36 +01:00
|
|
|
logger.info('Continuing loop %s', self.name)
|
2020-01-09 10:28:02 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
if self._should_break:
|
|
|
|
self._should_break = False
|
2023-12-09 01:23:36 +01:00
|
|
|
logger.info('Breaking loop %s', self.name)
|
2020-01-09 10:28:02 +01:00
|
|
|
break
|
|
|
|
|
2018-01-08 02:41:24 +01:00
|
|
|
return response
|
|
|
|
|
2018-04-29 22:25:31 +02:00
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
class WhileProcedure(LoopProcedure):
|
|
|
|
"""
|
|
|
|
Models a while loop procedure.
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
procedure.process_results:
|
|
|
|
- action: http.get
|
|
|
|
args:
|
|
|
|
url: https://some-service/some/json/endpoint
|
|
|
|
# Example response: {"id":1, "name":"foo"}}
|
|
|
|
|
|
|
|
- while ${output}:
|
|
|
|
- action: some.custom.action
|
|
|
|
args:
|
|
|
|
id: ${id}
|
|
|
|
name: ${name}
|
|
|
|
|
|
|
|
- action: http.get
|
|
|
|
args:
|
|
|
|
url: https://some-service/some/json/endpoint
|
|
|
|
# Example response: {"id":1, "name":"foo"}}
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
def __init__(
|
|
|
|
self, name, condition, requests, _async=False, args=None, backend=None
|
|
|
|
):
|
|
|
|
super().__init__(
|
|
|
|
name=name, _async=_async, requests=requests, args=args, backend=backend
|
|
|
|
)
|
2020-01-09 10:28:02 +01:00
|
|
|
self.condition = condition
|
|
|
|
|
2024-01-04 13:13:16 +01:00
|
|
|
def execute(self, *_, **context):
|
2020-01-09 10:28:02 +01:00
|
|
|
response = Response()
|
2024-09-16 03:16:53 +02:00
|
|
|
ctx = _update_context(context)
|
|
|
|
locals().update(ctx)
|
2020-01-09 10:28:02 +01:00
|
|
|
|
|
|
|
while True:
|
2024-01-04 13:13:16 +01:00
|
|
|
condition_true = eval(self.condition) # pylint: disable=eval-used
|
2020-01-09 10:28:02 +01:00
|
|
|
if not condition_true:
|
|
|
|
break
|
|
|
|
|
2024-09-16 03:16:53 +02:00
|
|
|
response = super().execute(**ctx)
|
|
|
|
ctx.update(ctx.get('new_context', {}))
|
|
|
|
if response.output and isinstance(response.output, dict):
|
|
|
|
_update_context(ctx, **response.output)
|
|
|
|
|
|
|
|
locals().update(ctx)
|
|
|
|
|
2020-01-09 10:28:02 +01:00
|
|
|
if self._should_return:
|
2023-12-09 01:23:36 +01:00
|
|
|
logger.info('Returning from %s', self.name)
|
2020-01-09 10:28:02 +01:00
|
|
|
break
|
|
|
|
|
|
|
|
if self._should_continue:
|
|
|
|
self._should_continue = False
|
2023-12-09 01:23:36 +01:00
|
|
|
logger.info('Continuing loop %s', self.name)
|
2020-01-09 10:28:02 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
if self._should_break:
|
|
|
|
self._should_break = False
|
2023-12-09 01:23:36 +01:00
|
|
|
logger.info('Breaking loop %s', self.name)
|
2020-01-09 10:28:02 +01:00
|
|
|
break
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
2018-04-29 22:25:31 +02:00
|
|
|
class IfProcedure(Procedure):
|
|
|
|
"""
|
|
|
|
Models an if-else construct.
|
|
|
|
|
2024-01-04 13:13:16 +01:00
|
|
|
Example::
|
2018-04-29 22:25:31 +02:00
|
|
|
|
|
|
|
procedure.sync.process_results:
|
2024-01-04 13:13:16 +01:00
|
|
|
- action: http.get
|
|
|
|
args:
|
|
|
|
url: https://some-service/some/json/endpoint
|
|
|
|
# Example response: { "sensors": [ {"temperature": 18 } ] }
|
|
|
|
|
|
|
|
- if ${sensors['temperature'] < 20}:
|
|
|
|
- action: shell.exec
|
|
|
|
args:
|
|
|
|
cmd: '/path/turn_on_heating.sh'
|
|
|
|
- else:
|
|
|
|
- action: shell.exec
|
2018-04-29 22:25:31 +02:00
|
|
|
args:
|
2024-01-04 13:13:16 +01:00
|
|
|
cmd: '/path/turn_off_heating.sh'
|
|
|
|
|
2018-04-29 22:25:31 +02:00
|
|
|
"""
|
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
name,
|
|
|
|
condition,
|
|
|
|
requests,
|
|
|
|
else_branch=None,
|
|
|
|
args=None,
|
|
|
|
backend=None,
|
2024-01-04 13:13:16 +01:00
|
|
|
id=None, # pylint: disable=redefined-builtin
|
2023-12-09 01:23:36 +01:00
|
|
|
**kwargs,
|
|
|
|
):
|
2018-08-08 00:00:11 +02:00
|
|
|
kwargs['_async'] = False
|
2018-04-29 22:25:31 +02:00
|
|
|
self.condition = condition
|
2018-09-27 01:09:00 +02:00
|
|
|
self.else_branch = else_branch
|
2018-04-29 22:25:31 +02:00
|
|
|
reqs = []
|
|
|
|
|
|
|
|
for req in requests:
|
2018-09-26 22:31:27 +02:00
|
|
|
if isinstance(req, dict):
|
|
|
|
req['origin'] = Config.get('device_id')
|
|
|
|
req['id'] = id
|
|
|
|
if 'target' not in req:
|
|
|
|
req['target'] = req['origin']
|
|
|
|
|
|
|
|
req = Request.build(req)
|
2018-04-29 22:25:31 +02:00
|
|
|
|
2018-09-26 22:31:27 +02:00
|
|
|
reqs.append(req)
|
2018-04-29 22:25:31 +02:00
|
|
|
|
2023-12-09 01:23:36 +01:00
|
|
|
super().__init__(name=name, requests=reqs, args=args, backend=backend, **kwargs)
|
2018-04-29 22:25:31 +02:00
|
|
|
|
2018-09-27 01:09:00 +02:00
|
|
|
@classmethod
|
2024-01-04 13:13:16 +01:00
|
|
|
# pylint: disable=arguments-differ
|
2023-12-09 01:23:36 +01:00
|
|
|
def build(
|
|
|
|
cls,
|
|
|
|
name,
|
2024-01-04 13:13:16 +01:00
|
|
|
*_,
|
2023-12-09 01:23:36 +01:00
|
|
|
condition,
|
|
|
|
requests,
|
|
|
|
args=None,
|
|
|
|
backend=None,
|
2024-01-04 13:13:16 +01:00
|
|
|
id=None, # pylint: disable=redefined-builtin
|
|
|
|
else_branch=None,
|
2023-12-09 01:23:36 +01:00
|
|
|
**kwargs,
|
|
|
|
):
|
2018-09-27 01:09:00 +02:00
|
|
|
kwargs['_async'] = False
|
|
|
|
if else_branch:
|
2023-12-09 01:23:36 +01:00
|
|
|
else_branch = super().build(
|
|
|
|
name=name + '__else',
|
|
|
|
requests=else_branch,
|
|
|
|
args=args,
|
|
|
|
backend=backend,
|
|
|
|
id=id,
|
|
|
|
procedure_class=Procedure,
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
|
|
|
|
return super().build(
|
|
|
|
name=name,
|
|
|
|
condition=condition,
|
|
|
|
requests=requests,
|
|
|
|
else_branch=else_branch,
|
|
|
|
args=args,
|
|
|
|
backend=backend,
|
|
|
|
id=id,
|
|
|
|
**kwargs,
|
|
|
|
)
|
2018-04-29 22:25:31 +02:00
|
|
|
|
2024-01-04 13:13:16 +01:00
|
|
|
def execute(self, *_, **context):
|
2024-09-16 03:16:53 +02:00
|
|
|
ctx = _update_context(context)
|
|
|
|
locals().update(ctx)
|
2024-01-04 13:13:16 +01:00
|
|
|
condition_true = eval(self.condition) # pylint: disable=eval-used
|
2018-04-29 22:25:31 +02:00
|
|
|
response = Response()
|
|
|
|
|
|
|
|
if condition_true:
|
2024-09-16 03:16:53 +02:00
|
|
|
response = super().execute(**ctx)
|
2018-04-29 22:25:31 +02:00
|
|
|
elif self.else_branch:
|
2024-09-16 03:16:53 +02:00
|
|
|
response = self.else_branch.execute(**ctx)
|
2018-04-29 22:25:31 +02:00
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
2024-09-16 03:16:53 +02:00
|
|
|
def _update_context(context: Optional[Dict[str, Any]] = None, **kwargs):
|
|
|
|
ctx = context or {}
|
|
|
|
ctx = {**ctx.get('context', {}), **ctx, **kwargs}
|
|
|
|
for k, v in ctx.items():
|
|
|
|
ctx[k] = Request.expand_value_from_context(v, **ctx)
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
2024-05-26 11:02:19 +02:00
|
|
|
def procedure(name_or_func: Optional[str] = None, *upper_args, **upper_kwargs):
|
|
|
|
name = name_or_func if isinstance(name_or_func, str) else None
|
|
|
|
|
2024-05-24 20:07:24 +02:00
|
|
|
def func_wrapper(f):
|
|
|
|
"""
|
|
|
|
Public decorator to mark a function as a procedure.
|
|
|
|
"""
|
2024-08-13 22:27:10 +02:00
|
|
|
import inspect
|
2024-05-24 20:07:24 +02:00
|
|
|
|
|
|
|
f.procedure = True
|
|
|
|
f.procedure_name = name
|
2024-08-13 22:27:10 +02:00
|
|
|
f._source = inspect.getsourcefile(f) # pylint: disable=protected-access
|
|
|
|
f._line = inspect.getsourcelines(f)[1] # pylint: disable=protected-access
|
2024-01-04 13:13:16 +01:00
|
|
|
|
2024-05-24 20:07:24 +02:00
|
|
|
@wraps(f)
|
|
|
|
def _execute_procedure(*args, **kwargs):
|
2024-05-26 11:02:19 +02:00
|
|
|
args = [*upper_args, *args]
|
|
|
|
kwargs = {**upper_kwargs, **kwargs}
|
2024-05-24 20:07:24 +02:00
|
|
|
return exec_wrapper(f, *args, **kwargs)
|
2020-04-08 23:22:54 +02:00
|
|
|
|
2024-05-24 20:07:24 +02:00
|
|
|
return _execute_procedure
|
2020-04-08 23:22:54 +02:00
|
|
|
|
2024-05-26 11:02:19 +02:00
|
|
|
if callable(name_or_func):
|
|
|
|
return func_wrapper(name_or_func)
|
|
|
|
|
2024-05-24 20:07:24 +02:00
|
|
|
return func_wrapper
|
2020-04-08 23:22:54 +02:00
|
|
|
|
|
|
|
|
2017-12-25 17:23:09 +01:00
|
|
|
# vim:sw=4:ts=4:et:
|