From 5a7068501a63ceb3e89b28b5313fbc55ff5fa9e7 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 10 Sep 2024 19:49:16 +0200 Subject: [PATCH 1/3] [request] The action name can be specified either on `action` or `name`. This is for UI compatibility purposes. --- platypush/message/request/__init__.py | 3 ++- platypush/plugins/procedures/__init__.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/platypush/message/request/__init__.py b/platypush/message/request/__init__.py index 7a55e65429..8dda18eb7d 100644 --- a/platypush/message/request/__init__.py +++ b/platypush/message/request/__init__.py @@ -64,12 +64,13 @@ class Request(Message): msg = super().parse(msg) args = { 'target': msg.get('target', Config.get('device_id')), - 'action': msg['action'], + 'action': msg.get('action', msg.get('name')), 'args': msg.get('args', {}), 'id': msg['id'] if 'id' in msg else cls._generate_id(), 'timestamp': msg['_timestamp'] if '_timestamp' in msg else time.time(), } + assert args.get('action'), 'No action specified in the request' if 'origin' in msg: args['origin'] = msg['origin'] if 'token' in msg: diff --git a/platypush/plugins/procedures/__init__.py b/platypush/plugins/procedures/__init__.py index 3e66f7631e..cffd2f65f1 100644 --- a/platypush/plugins/procedures/__init__.py +++ b/platypush/plugins/procedures/__init__.py @@ -286,9 +286,10 @@ class ProceduresPlugin(RunnablePlugin, ProcedureEntityManager): @classmethod def _serialize_action(cls, data: Union[Iterable, Dict]) -> Union[Dict, List]: if isinstance(data, dict): - if data.get('action'): + name = data.get('action', data.get('name')) + if name: return { - 'action': data['action'], + 'action': name, **({'args': data['args']} if data.get('args') else {}), } From 946c7b1783f9c594c685cb9680102c015ff54552 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 10 Sep 2024 19:53:14 +0200 Subject: [PATCH 2/3] [procedure] Ignore `id` field in `Procedure.build`. The reason is that an `id` specified on procedure level will be applied to all the child requests. This means that the first response from the first completed request will be sent to Redis and mistakenly interpreted by HTTP listeners as the return value of the whole procedure. `Procedure.build` should instead calculate its own ID for the procedure, and apply different IDs to the child requests. --- platypush/procedure/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/platypush/procedure/__init__.py b/platypush/procedure/__init__.py index 56cdc8f130..50ac5bb49c 100644 --- a/platypush/procedure/__init__.py +++ b/platypush/procedure/__init__.py @@ -55,7 +55,6 @@ class Procedure: requests, args=None, backend=None, - id=None, # pylint: disable=redefined-builtin procedure_class=None, **kwargs, ): @@ -66,6 +65,7 @@ class Procedure: if_config = LifoQueue() procedure_class = procedure_class or cls key = None + kwargs.pop('id', None) for request_config in requests: # Check if it's a break/continue/return statement @@ -91,7 +91,6 @@ class Procedure: 'condition': condition, 'else_branch': [], 'backend': backend, - 'id': id, } ) @@ -132,7 +131,6 @@ class Procedure: _async=_async, requests=request_config[key], backend=backend, - id=id, iterator_name=iterator_name, iterable=iterable, ) @@ -156,14 +154,12 @@ class Procedure: requests=request_config[key], condition=condition, backend=backend, - id=id, ) reqs.append(loop) continue request_config['origin'] = Config.get('device_id') - request_config['id'] = id if 'target' not in request_config: request_config['target'] = request_config['origin'] From 1e9f7fb2c68cfb78e38c992af0de6faf1eb15515 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 10 Sep 2024 19:55:26 +0200 Subject: [PATCH 3/3] [procedure] Added support for custom values on the return statement. This enables constructs like this in procedures: ```yaml - return - return 1 - return: ${output} ``` --- platypush/procedure/__init__.py | 61 +++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/platypush/procedure/__init__.py b/platypush/procedure/__init__.py index 50ac5bb49c..36c57c4dfa 100644 --- a/platypush/procedure/__init__.py +++ b/platypush/procedure/__init__.py @@ -1,10 +1,11 @@ import enum import logging import re +from dataclasses import dataclass from functools import wraps from queue import LifoQueue -from typing import Optional +from typing import Any, Optional from ..common import exec_wrapper from ..config import Config @@ -14,7 +15,7 @@ from ..message.response import Response logger = logging.getLogger('platypush') -class Statement(enum.Enum): +class StatementType(enum.Enum): """ Enumerates the possible statements in a procedure. """ @@ -24,6 +25,45 @@ class Statement(enum.Enum): RETURN = 'return' +@dataclass +class Statement: + """ + Models a statement in a procedure. + """ + + type: StatementType + argument: Optional[str] = None + + @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 + + def run(self, *_, **context): + return Request.expand_value_from_context(self.argument, **context) + + class Procedure: """Procedure class. A procedure is a pre-configured list of requests""" @@ -70,7 +110,15 @@ class Procedure: for request_config in requests: # Check if it's a break/continue/return statement if isinstance(request_config, str): - reqs.append(Statement(request_config)) + reqs.append(Statement.build(request_config)) + continue + + # Check if it's a return statement with a value + if ( + len(request_config.keys()) == 1 + and list(request_config.keys())[0] == 'return' + ): + reqs.append(ReturnStatement(argument=request_config['return'])) continue # Check if this request is an if-else @@ -218,15 +266,16 @@ class Procedure: continue if isinstance(request, Statement): - if request == Statement.RETURN: + if isinstance(request, ReturnStatement): + response = Response(output=request.run(**context)) self._should_return = True for proc in __stack__: proc._should_return = True # pylint: disable=protected-access break - if request in [Statement.BREAK, Statement.CONTINUE]: + if request.type in [StatementType.BREAK, StatementType.CONTINUE]: loop = self._find_nearest_loop(__stack__) - if request == Statement.BREAK: + if request == StatementType.BREAK: loop._should_break = True # pylint: disable=protected-access else: loop._should_continue = True # pylint: disable=protected-access