platypush/platypush/message/request/__init__.py

300 lines
11 KiB
Python
Raw Normal View History

import copy
2018-01-15 22:36:24 +01:00
import datetime
import json
import logging
import random
import re
import time
from threading import Thread
from platypush.config import Config
from platypush.context import get_plugin
from platypush.message import Message
from platypush.message.response import Response
from platypush.utils import get_hash, get_module_and_method_from_action, get_redis_queue_name_by_message, \
is_functional_procedure
2020-09-27 01:33:38 +02:00
logger = logging.getLogger('platypush')
2018-06-06 20:09:18 +02:00
class Request(Message):
""" Request message class """
def __init__(self, target, action, origin=None, id=None, backend=None,
args=None, token=None, timestamp=None):
"""
Params:
target -- Target node [Str]
action -- Action to be executed (e.g. music.mpd.play) [Str]
origin -- Origin node [Str]
id -- Message ID, or None to get it auto-generated
backend -- Backend connected to the request, where the response will be delivered
args -- Additional arguments for the action [Dict]
token -- Authorization token, if required on the server [Str]
timestamp -- Message creation timestamp [Float]
"""
2018-10-08 15:30:00 +02:00
super().__init__(timestamp=timestamp)
2019-07-09 01:44:31 +02:00
self.id = id if id else self._generate_id()
self.target = target
self.action = action
self.origin = origin
self.args = args if args else {}
self.backend = backend
2019-07-09 01:44:31 +02:00
self.token = token
@classmethod
def build(cls, msg):
msg = super().parse(msg)
2019-07-09 01:44:31 +02:00
args = {'target': msg.get('target', Config.get('device_id')), 'action': msg['action'],
'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()}
if 'origin' in msg:
args['origin'] = msg['origin']
if 'token' in msg:
args['token'] = msg['token']
return cls(**args)
@staticmethod
def _generate_id():
2019-07-09 01:44:31 +02:00
_id = ''
for i in range(0, 16):
_id += '%.2x' % random.randint(0, 255)
return _id
2018-01-04 02:45:23 +01:00
def _execute_procedure(self, *args, **kwargs):
2018-01-04 16:11:54 +01:00
from platypush.config import Config
from platypush.procedure import Procedure
2018-01-04 02:45:23 +01:00
2018-06-06 20:09:18 +02:00
logger.info('Executing procedure request: {}'.format(self.action))
procedures = Config.get_procedures()
proc_name = '.'.join(self.action.split('.')[1:])
if proc_name not in procedures:
proc_name = self.action.split('.')[-1]
proc_config = procedures[proc_name]
if is_functional_procedure(proc_config):
self._expand_context(self.args, **kwargs)
kwargs = {**self.args, **kwargs}
if 'n_tries' in kwargs:
del kwargs['n_tries']
return proc_config(*args, **kwargs)
proc = Procedure.build(name=proc_name, requests=proc_config['actions'],
_async=proc_config['_async'], args=self.args,
backend=self.backend, id=self.id)
return proc.execute(*args, **kwargs)
def _expand_context(self, event_args=None, **context):
from platypush.config import Config
if event_args is None:
event_args = copy.deepcopy(self.args)
constants = Config.get_constants()
context['constants'] = {}
for (name, value) in constants.items():
context['constants'][name] = value
keys = []
if isinstance(event_args, dict):
keys = event_args.keys()
elif isinstance(event_args, list):
keys = range(0, len(event_args))
2018-01-04 02:45:23 +01:00
for key in keys:
value = event_args[key]
if isinstance(value, str):
value = self.expand_value_from_context(value, **context)
elif isinstance(value, dict) or isinstance(value, list):
self._expand_context(event_args=value, **context)
event_args[key] = value
return event_args
# noinspection PyBroadException
@classmethod
def expand_value_from_context(cls, _value, **context):
for (k, v) in context.items():
if isinstance(v, Message):
v = json.loads(str(v))
2018-06-07 09:33:26 +02:00
try:
exec('{}={}'.format(k, v))
except:
if isinstance(v, str):
try:
exec('{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v)))
except Exception as e:
logger.warning('Could not set context variable {}={}'.format(k, v))
logger.warning('Context: {}'.format(json.dumps(context)))
logger.exception(e)
parsed_value = ''
if not isinstance(_value, str):
parsed_value = _value
while _value and isinstance(_value, str):
m = re.match(r'([^$]*)(\${\s*(.+?)\s*})(.*)', _value)
if m and not m.group(1).endswith('\\'):
2019-07-09 01:44:31 +02:00
prefix = m.group(1)
expr = m.group(2)
inner_expr = m.group(3)
_value = m.group(4)
try:
context_value = eval(inner_expr)
if callable(context_value):
context_value = context_value()
if isinstance(context_value, range) or isinstance(context_value, tuple):
context_value = [*context_value]
if isinstance(context_value, datetime.date):
context_value = context_value.isoformat()
except Exception as e:
2018-06-06 20:09:18 +02:00
logger.exception(e)
context_value = expr
parsed_value += prefix + (
json.dumps(context_value)
if isinstance(context_value, list) or isinstance(context_value, dict)
else str(context_value)
)
else:
parsed_value += _value
_value = ''
2019-07-09 01:44:31 +02:00
try:
return json.loads(parsed_value)
except:
return parsed_value
def _send_response(self, response):
2018-09-20 12:49:57 +02:00
response = Response.build(response)
response.id = self.id
response.target = self.origin
response.origin = Config.get('device_id')
if self.backend and self.origin:
self.backend.send_response(response=response, request=self)
else:
2018-09-20 12:49:57 +02:00
redis = get_plugin('redis')
if redis:
queue_name = get_redis_queue_name_by_message(self)
redis.send_message(queue_name, response)
redis.expire(queue_name, 60)
def execute(self, n_tries=1, _async=True, **context):
"""
Execute this request and returns a Response object
Params:
n_tries -- Number of tries in case of failure before raising a RuntimeError
_async -- If True, the request will be run asynchronously and the
2018-01-04 02:45:23 +01:00
response posted on the bus when available (default),
otherwise the current thread will wait for the response
to be returned synchronously.
context -- Key-valued context. Example:
context = (group_name='Kitchen lights')
request.args:
- group: ${group_name} # will be expanded as "Kitchen lights")
"""
2018-01-04 02:45:23 +01:00
2019-07-09 01:44:31 +02:00
def _thread_func(_n_tries, errors=None):
response = None
2018-01-04 02:45:23 +01:00
if self.action.startswith('procedure.'):
2019-07-09 01:44:31 +02:00
context['n_tries'] = _n_tries
response = self._execute_procedure(**context)
if response is not None:
self._send_response(response)
return response
# utils.get_context is a special action that simply returns the current context
elif self.action == 'utils.get_context':
response = Response(output=context)
self._send_response(response)
return response
else:
2019-07-09 01:44:31 +02:00
action = self.expand_value_from_context(self.action, **context)
(module_name, method_name) = get_module_and_method_from_action(action)
plugin = get_plugin(module_name)
try:
# Run the action
args = self._expand_context(**context)
2019-07-09 01:44:31 +02:00
args = self.expand_value_from_context(args, **context)
2020-10-28 23:18:55 +01:00
if isinstance(args, dict):
response = plugin.run(method_name, **args)
elif isinstance(args, list):
response = plugin.run(method_name, *args)
else:
response = plugin.run(method_name, args)
2019-12-08 16:25:03 +01:00
if not response:
logger.warning('Received null response from action {}'.format(action))
else:
if response.is_error():
logger.warning(('Response processed with errors from ' +
'action {}: {}').format(
action, str(response)))
elif not response.disable_logging:
logger.info('Processed response from action {}: {}'.
format(action, str(response)))
except (AssertionError, TimeoutError) as e:
plugin.logger.exception(e)
logger.warning('{} from action [{}]: {}'.format(type(e), action, str(e)))
response = Response(output=None, errors=[str(e)])
except Exception as e:
# Retry mechanism
plugin.logger.exception(e)
logger.warning(('Uncaught exception while processing response ' +
2019-07-09 01:44:31 +02:00
'from action [{}]: {}').format(action, str(e)))
errors = errors or []
if str(e) not in errors:
errors.append(str(e))
response = Response(output=None, errors=errors)
if _n_tries - 1 > 0:
2018-06-06 20:09:18 +02:00
logger.info('Reloading plugin {} and retrying'.format(module_name))
get_plugin(module_name, reload=True)
response = _thread_func(_n_tries=_n_tries-1, errors=errors)
finally:
self._send_response(response)
return response
token_hash = Config.get('token_hash')
if token_hash:
if self.token is None or get_hash(self.token) != token_hash:
raise PermissionError()
if _async:
2018-01-04 02:45:23 +01:00
Thread(target=_thread_func, args=(n_tries,)).start()
else:
return _thread_func(n_tries)
def __str__(self):
"""
Overrides the str() operator and converts
the message into a UTF-8 JSON string
"""
return json.dumps({
2019-07-09 01:44:31 +02:00
'type': 'request',
'target': self.target,
'action': self.action,
'args': self.args,
'origin': self.origin if hasattr(self, 'origin') else None,
'id': self.id if hasattr(self, 'id') else None,
'token': self.token if hasattr(self, 'token') else None,
'_timestamp': self.timestamp,
})
# vim:sw=4:ts=4:et: