2018-01-08 02:41:24 +01:00
|
|
|
import copy
|
2018-01-15 22:36:24 +01:00
|
|
|
import datetime
|
2017-12-17 16:15:44 +01:00
|
|
|
import json
|
2017-12-22 18:04:18 +01:00
|
|
|
import logging
|
2017-12-18 03:09:38 +01:00
|
|
|
import random
|
2018-01-05 23:20:39 +01:00
|
|
|
import re
|
2017-12-24 01:03:26 +01:00
|
|
|
import traceback
|
|
|
|
|
|
|
|
from threading import Thread
|
2017-12-17 16:15:44 +01:00
|
|
|
|
2017-12-27 10:18:51 +01:00
|
|
|
from platypush.context import get_plugin
|
2017-12-17 16:15:44 +01:00
|
|
|
from platypush.message import Message
|
2017-12-22 18:04:18 +01:00
|
|
|
from platypush.message.response import Response
|
2017-12-27 10:18:51 +01:00
|
|
|
from platypush.utils import get_module_and_method_from_action
|
2017-12-17 16:15:44 +01:00
|
|
|
|
|
|
|
class Request(Message):
|
|
|
|
""" Request message class """
|
|
|
|
|
2018-01-02 00:35:55 +01:00
|
|
|
def __init__(self, target, action, origin=None, id=None, backend=None, args=None):
|
2017-12-17 16:15:44 +01:00
|
|
|
"""
|
|
|
|
Params:
|
|
|
|
target -- Target node [String]
|
|
|
|
action -- Action to be executed (e.g. music.mpd.play) [String]
|
|
|
|
origin -- Origin node [String]
|
2017-12-18 03:09:38 +01:00
|
|
|
id -- Message ID, or None to get it auto-generated
|
2017-12-25 17:23:09 +01:00
|
|
|
backend -- Backend connected to the request, where the response will be delivered
|
2017-12-17 16:15:44 +01:00
|
|
|
args -- Additional arguments for the action [Dict]
|
|
|
|
"""
|
|
|
|
|
2017-12-22 18:04:18 +01:00
|
|
|
self.id = id if id else self._generate_id()
|
|
|
|
self.target = target
|
|
|
|
self.action = action
|
|
|
|
self.origin = origin
|
2018-01-02 00:35:55 +01:00
|
|
|
self.args = args if args else {}
|
2017-12-25 17:23:09 +01:00
|
|
|
self.backend = backend
|
2017-12-17 16:15:44 +01:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def build(cls, msg):
|
|
|
|
msg = super().parse(msg)
|
|
|
|
args = {
|
|
|
|
'target' : msg['target'],
|
|
|
|
'action' : msg['action'],
|
2017-12-18 01:10:51 +01:00
|
|
|
'args' : msg['args'] if 'args' in msg else {},
|
2017-12-17 16:15:44 +01:00
|
|
|
}
|
|
|
|
|
2017-12-18 22:40:56 +01:00
|
|
|
args['id'] = msg['id'] if 'id' in msg else cls._generate_id()
|
2017-12-17 16:15:44 +01:00
|
|
|
if 'origin' in msg: args['origin'] = msg['origin']
|
2017-12-20 20:25:08 +01:00
|
|
|
return cls(**args)
|
2017-12-17 16:15:44 +01:00
|
|
|
|
2017-12-18 03:09:38 +01:00
|
|
|
@staticmethod
|
|
|
|
def _generate_id():
|
|
|
|
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-01-04 16:11:54 +01:00
|
|
|
logging.info('Executing procedure request: {}'.format(self.action))
|
2018-01-04 02:45:23 +01:00
|
|
|
proc_name = self.action.split('.')[-1]
|
|
|
|
proc_config = Config.get_procedures()[proc_name]
|
2018-01-06 00:21:25 +01:00
|
|
|
proc = Procedure.build(name=proc_name, requests=proc_config['actions'],
|
|
|
|
async=proc_config['async'],
|
|
|
|
backend=self.backend, id=self.id)
|
|
|
|
|
2018-01-05 23:20:39 +01:00
|
|
|
return proc.execute(*args, **kwargs)
|
|
|
|
|
|
|
|
|
2018-01-06 12:40:22 +01:00
|
|
|
def _expand_context(self, event_args=None, **context):
|
2018-01-10 18:47:25 +01:00
|
|
|
from platypush.config import Config
|
|
|
|
|
2018-01-08 02:41:24 +01:00
|
|
|
if event_args is None: event_args = copy.deepcopy(self.args)
|
2018-01-06 12:40:22 +01:00
|
|
|
|
2018-01-10 18:47:25 +01:00
|
|
|
constants = Config.get_constants()
|
|
|
|
context['constants'] = {}
|
|
|
|
for (name,value) in constants.items():
|
|
|
|
context['constants'][name] = value
|
|
|
|
|
2018-01-06 12:40:22 +01:00
|
|
|
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
|
|
|
|
2018-01-06 12:40:22 +01:00
|
|
|
for key in keys:
|
|
|
|
value = event_args[key]
|
2018-01-08 02:41:24 +01:00
|
|
|
|
2018-01-06 12:40:22 +01:00
|
|
|
if isinstance(value, str):
|
2018-01-08 02:41:24 +01:00
|
|
|
value = self.expand_value_from_context(value, **context)
|
2018-01-06 12:40:22 +01:00
|
|
|
elif isinstance(value, dict) or isinstance(value, list):
|
|
|
|
self._expand_context(event_args=value, **context)
|
|
|
|
|
|
|
|
event_args[key] = value
|
|
|
|
|
|
|
|
return event_args
|
|
|
|
|
|
|
|
|
2018-01-07 23:31:19 +01:00
|
|
|
@classmethod
|
2018-01-08 02:41:24 +01:00
|
|
|
def expand_value_from_context(cls, value, **context):
|
2018-01-06 12:40:22 +01:00
|
|
|
parsed_value = ''
|
|
|
|
while value:
|
2018-01-07 23:31:19 +01:00
|
|
|
m = re.match('([^\$]*)(\${\s*(.+?)\s*})(.*)', value)
|
|
|
|
if m and not m.group(1).endswith('\\'):
|
|
|
|
prefix = m.group(1); expr = m.group(2);
|
|
|
|
inner_expr = m.group(3); value = m.group(4)
|
|
|
|
|
|
|
|
m = re.match('([^.\[\]()]+)(.*)', inner_expr)
|
|
|
|
context_argname = m.group(1)
|
|
|
|
path = m.group(2)
|
|
|
|
|
2018-01-06 12:40:22 +01:00
|
|
|
if context_argname in context:
|
2018-01-07 23:31:19 +01:00
|
|
|
try:
|
2018-01-17 03:16:59 +01:00
|
|
|
try:
|
|
|
|
context_value = eval("context['{}']{}".format(
|
|
|
|
context_argname, path if path else ''))
|
|
|
|
except:
|
|
|
|
context_value = eval(inner_expr)
|
2018-01-15 22:36:24 +01:00
|
|
|
|
|
|
|
if callable(context_value):
|
|
|
|
context_value = context_value()
|
|
|
|
if isinstance(context_value, datetime.date):
|
|
|
|
context_value = context_value.isoformat()
|
2018-01-17 03:16:59 +01:00
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
context_value = expr
|
2018-01-07 23:31:19 +01:00
|
|
|
|
2018-01-08 02:41:24 +01:00
|
|
|
parsed_value += prefix + (
|
|
|
|
json.dumps(context_value)
|
|
|
|
if isinstance(context_value, list)
|
|
|
|
or isinstance(context_value, dict)
|
|
|
|
else str(context_value))
|
|
|
|
|
2018-01-17 03:16:59 +01:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
parsed_value += prefix + eval(inner_expr)
|
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
parsed_value += prefix + expr
|
2018-01-06 12:40:22 +01:00
|
|
|
else:
|
|
|
|
parsed_value += value
|
|
|
|
value = ''
|
|
|
|
|
2018-01-07 23:31:19 +01:00
|
|
|
try: return json.loads(parsed_value)
|
|
|
|
except ValueError as e: return parsed_value
|
2018-01-04 02:45:23 +01:00
|
|
|
|
2018-01-05 23:20:39 +01:00
|
|
|
|
|
|
|
def _send_response(self, response):
|
|
|
|
if self.backend and self.origin:
|
|
|
|
self.backend.send_response(response=response, request=self)
|
|
|
|
else:
|
|
|
|
logging.info('Response whose request has no ' +
|
|
|
|
'origin attached: {}'.format(response))
|
|
|
|
|
|
|
|
|
|
|
|
def execute(self, n_tries=1, async=True, **context):
|
2017-12-22 18:04:18 +01:00
|
|
|
"""
|
|
|
|
Execute this request and returns a Response object
|
|
|
|
Params:
|
|
|
|
n_tries -- Number of tries in case of failure before raising a RuntimeError
|
2018-01-04 02:45:23 +01:00
|
|
|
async -- If True, the request will be run asynchronously and the
|
|
|
|
response posted on the bus when available (default),
|
|
|
|
otherwise the current thread will wait for the response
|
|
|
|
to be returned synchronously.
|
2018-01-05 23:20:39 +01:00
|
|
|
context -- Key-valued context. Example:
|
|
|
|
context = (group_name='Kitchen lights')
|
|
|
|
request.args:
|
2018-01-07 23:31:19 +01:00
|
|
|
- group: ${group_name} # will be expanded as "Kitchen lights")
|
2017-12-22 18:04:18 +01:00
|
|
|
"""
|
2018-01-04 02:45:23 +01:00
|
|
|
|
2017-12-24 01:03:26 +01:00
|
|
|
def _thread_func(n_tries):
|
2018-01-04 02:45:23 +01:00
|
|
|
if self.action.startswith('procedure.'):
|
2018-01-17 03:16:59 +01:00
|
|
|
context['n_tries'] = n_tries
|
|
|
|
response = self._execute_procedure(**context)
|
2018-01-07 23:31:19 +01:00
|
|
|
self._send_response(response)
|
|
|
|
return response
|
2018-01-05 23:20:39 +01:00
|
|
|
else:
|
|
|
|
(module_name, method_name) = get_module_and_method_from_action(self.action)
|
|
|
|
plugin = get_plugin(module_name)
|
2017-12-22 18:04:18 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
# Run the action
|
2018-01-05 23:20:39 +01:00
|
|
|
args = self._expand_context(**context)
|
|
|
|
response = plugin.run(method=method_name, **args)
|
|
|
|
|
2017-12-22 18:04:18 +01:00
|
|
|
if response and response.is_error():
|
|
|
|
raise RuntimeError('Response processed with errors: {}'.format(response))
|
|
|
|
|
|
|
|
logging.info('Processed response from plugin {}: {}'.
|
|
|
|
format(plugin, response))
|
|
|
|
except Exception as e:
|
|
|
|
# Retry mechanism
|
|
|
|
response = Response(output=None, errors=[str(e), traceback.format_exc()])
|
|
|
|
logging.exception(e)
|
|
|
|
if n_tries:
|
|
|
|
logging.info('Reloading plugin {} and retrying'.format(module_name))
|
2017-12-27 10:18:51 +01:00
|
|
|
get_plugin(module_name, reload=True)
|
2017-12-24 01:03:26 +01:00
|
|
|
_thread_func(n_tries-1)
|
2017-12-22 18:09:11 +01:00
|
|
|
return
|
2017-12-22 18:04:18 +01:00
|
|
|
finally:
|
2018-01-05 23:20:39 +01:00
|
|
|
self._send_response(response)
|
|
|
|
return response
|
2017-12-22 18:04:18 +01:00
|
|
|
|
2018-01-04 02:45:23 +01:00
|
|
|
if async:
|
|
|
|
Thread(target=_thread_func, args=(n_tries,)).start()
|
|
|
|
else:
|
|
|
|
return _thread_func(n_tries)
|
2017-12-22 18:04:18 +01:00
|
|
|
|
|
|
|
|
2017-12-17 16:15:44 +01:00
|
|
|
def __str__(self):
|
|
|
|
"""
|
|
|
|
Overrides the str() operator and converts
|
|
|
|
the message into a UTF-8 JSON string
|
|
|
|
"""
|
|
|
|
|
|
|
|
return json.dumps({
|
|
|
|
'type' : 'request',
|
|
|
|
'target' : self.target,
|
|
|
|
'action' : self.action,
|
|
|
|
'args' : self.args,
|
|
|
|
'origin' : self.origin if hasattr(self, 'origin') else None,
|
2017-12-18 03:09:38 +01:00
|
|
|
'id' : self.id if hasattr(self, 'id') else None,
|
2017-12-17 16:15:44 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|
|
|
|
|