- Proper support for event arguments

- Better algorithm for event matching
- Added send_event support to pusher
This commit is contained in:
Fabio Manganiello 2018-01-02 00:35:55 +01:00
parent 08189653bb
commit 5a92c0ac3b
8 changed files with 179 additions and 96 deletions

View file

@ -127,13 +127,5 @@ class Daemon(object):
self.stop_app() self.stop_app()
def main(args=sys.argv[1:]):
print('Starting platypush v.{}'.format(__version__))
app = Daemon.build_from_cmdline(args)
app.start()
if __name__ == '__main__':
main()
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,8 +1,9 @@
import json import json
import logging import logging
import re
from platypush.config import Config from platypush.config import Config
from platypush.message.event import Event from platypush.message.event import Event, EventMatchResult
from platypush.message.request import Request from platypush.message.request import Request
from platypush.utils import get_event_class_by_type from platypush.utils import get_event_class_by_type
@ -73,11 +74,31 @@ class EventAction(Request):
def execute(self, **context): def execute(self, **context):
for (key, value) in context.items(): event_args = context.pop('event').args if 'event' in context else {}
self.args[key] = value
for (argname, value) in self.args.items():
if isinstance(value, str):
parsed_value = ''
while value:
m = re.match('([^\\\]*)\$([\w\d_-]+)(.*)', value)
if m:
context_argname = m.group(2)
value = m.group(3)
if context_argname in context:
parsed_value += m.group(1) + context[context_argname]
else:
parsed_value += m.group(1) + '$' + m.group(2)
else:
parsed_value += value
value = ''
value = parsed_value
self.args[argname] = value
super().execute() super().execute()
@classmethod @classmethod
def build(cls, action): def build(cls, action):
action = super().parse(action) action = super().parse(action)
@ -137,7 +158,7 @@ class EventHook(object):
logging.info('Running hook {} triggered by an event'.format(self.name)) logging.info('Running hook {} triggered by an event'.format(self.name))
for action in self.actions: for action in self.actions:
action.execute(**result.parsed_args) action.execute(event=event, **result.parsed_args)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,4 +1,5 @@
import logging import logging
import sys
from ..hook import EventHook from ..hook import EventHook
@ -22,10 +23,10 @@ class EventProcessor(object):
def process_event(self, event): def process_event(self, event):
""" Processes an event and runs any matched hooks """ """ Processes an event and runs the matched hooks with the highest score """
matched_hooks = [] matched_hooks = []
max_score = 0 max_score = -sys.maxsize
for hook in self.hooks: for hook in self.hooks:
match = hook.matches_event(event) match = hook.matches_event(event)

View file

@ -1,5 +1,6 @@
import json import json
import random import random
import re
import threading import threading
from platypush.config import Config from platypush.config import Config
@ -34,36 +35,12 @@ class Event(Message):
event_type = msg['args'].pop('type') event_type = msg['args'].pop('type')
event_class = get_event_class_by_type(event_type) event_class = get_event_class_by_type(event_type)
args = { args = msg['args'] if 'args' in msg else {}
'target' : msg['target'],
'origin' : msg['origin'],
**(msg['args'] if 'args' in msg else {}),
}
args['id'] = msg['id'] if 'id' in msg else cls._generate_id() args['id'] = msg['id'] if 'id' in msg else cls._generate_id()
args['target'] = msg['target'] if 'target' in msg else Config.get('device_id')
args['origin'] = msg['origin'] if 'origin' in msg else Config.get('device_id')
return event_class(**args) return event_class(**args)
def matches_condition(self, condition):
"""
If the event matches an event condition, it will return an EventMatchResult
Params:
-- condition -- The platypush.event.hook.EventCondition object
"""
result = EventMatchResult(is_match=False)
if not isinstance(self, condition.type): return result
for (attr, value) in condition.args.items():
if not hasattr(self.args, attr):
return result
if isinstance(self.args[attr], str) and not value in self.args[attr]:
return result
elif self.args[attr] != value:
return result
result.is_match = True
return result
@staticmethod @staticmethod
def _generate_id(): def _generate_id():
@ -73,6 +50,107 @@ class Event(Message):
id += '%.2x' % random.randint(0, 255) id += '%.2x' % random.randint(0, 255)
return id return id
def matches_condition(self, condition):
"""
If the event matches an event condition, it will return an EventMatchResult
Params:
-- condition -- The platypush.event.hook.EventCondition object
"""
result = EventMatchResult(is_match=False, parsed_args=self.args)
match_scores = []
if not isinstance(self, condition.type): return result
for (attr, value) in condition.args.items():
if attr not in self.args:
return result
if isinstance(self.args[attr], str):
arg_result = self._matches_argument(argname=attr, condition_value=value)
if arg_result.is_match:
match_scores.append(arg_result.score)
for (parsed_arg, parsed_value) in arg_result.parsed_args.items():
result.parsed_args[parsed_arg] = parsed_value
else:
return result
elif self.args[attr] != value:
# TODO proper support for list and dictionary matches
return result
result.is_match = True
if match_scores:
result.score = sum(match_scores) / float(len(match_scores))
return result
def _matches_argument(self, argname, condition_value):
"""
Returns an EventMatchResult if the event argument [argname] matches
[condition_value].
- Example:
- self.args = {
'phrase': 'Hey dude turn on the living room lights'
}
- self._matches_argument(argname='phrase', condition_value='Turn on the $lights lights')
will return EventMatchResult(is_match=True, parsed_args={ 'lights': 'living room' })
- self._matches_argument(argname='phrase', condition_value='Turn off the $lights lights')
will return EventMatchResult(is_match=False, parsed_args={})
"""
result = EventMatchResult(is_match=False)
event_tokens = re.split('\s+', self.args[argname].strip().lower())
condition_tokens = re.split('\s+', condition_value.strip().lower())
while event_tokens and condition_tokens:
event_token = event_tokens[0]
condition_token = condition_tokens[0]
if event_token == condition_token:
event_tokens.pop(0)
condition_tokens.pop(0)
result.score += 1
elif re.search(condition_token, event_token):
# The only supported regex-match as of now is the equivalent of
# the maybe operator.
# e.g. "turn on (the)? lights" would match both "turn on the lights"
# and "turn on lights". In such a case, we just consume the
# condition token and proceed forward. TODO add a more
# sophisticated regex-match handling
condition_tokens.pop(0)
else:
m = re.match('[^\\\]*\$([\w\d_-]+)', condition_token)
if m:
argname = m.group(1)
if argname not in result.parsed_args:
result.parsed_args[argname] = event_token
result.score += 1
else:
result.parsed_args[argname] += ' ' + event_token
if len(event_tokens) > 1 and len(condition_tokens) > 1 \
and event_tokens[1] == condition_tokens[1]:
# Stop appending tokens to this argument, as the next
# condition will be satisfied as well
condition_tokens.pop(0)
event_tokens.pop(0)
else:
result.score -= 1
event_tokens.pop(0)
# It's a match if all the tokens in the condition string have been satisfied
result.is_match = len(condition_tokens) == 0
return result
def __str__(self): def __str__(self):
""" """
Overrides the str() operator and converts Overrides the str() operator and converts
@ -98,10 +176,10 @@ class EventMatchResult(object):
the match is - in case of multiple event matches, the ones with the the match is - in case of multiple event matches, the ones with the
highest score will win """ highest score will win """
def __init__(self, is_match, score=1, parsed_args = {}): def __init__(self, is_match, score=0, parsed_args=None):
self.is_match = is_match self.is_match = is_match
self.score = score self.score = score
self.parsed_args = parsed_args self.parsed_args = {} if not parsed_args else parsed_args
# XXX Should be a stop Request, not an Event # XXX Should be a stop Request, not an Event

View file

@ -27,35 +27,9 @@ class SpeechRecognizedEvent(AssistantEvent):
self.recognized_phrase = phrase.strip().lower() self.recognized_phrase = phrase.strip().lower()
def matches_condition(self, condition): def matches_condition(self, condition):
result = EventMatchResult(is_match=False, score=0) result = super().matches_condition(condition)
if result.is_match and self._assistant:
if not isinstance(self, condition.type): return result self._assistant.stop_conversation()
recognized_tokens = re.split('\s+', self.recognized_phrase.strip().lower())
condition_tokens = re.split('\s+', condition.args['phrase'].strip().lower())
while recognized_tokens and condition_tokens:
rec_token = recognized_tokens[0]
cond_token = condition_tokens[0]
if rec_token == cond_token:
recognized_tokens.pop(0)
condition_tokens.pop(0)
result.score += 1
elif re.search(cond_token, rec_token):
condition_tokens.pop(0)
else:
m = re.match('^\$([\w\d_])', cond_token)
if m:
result.parsed_args[cond_token[1:]] = rec_token
recognized_tokens.pop(0)
condition_tokens.pop(0)
result.score += 1
else:
recognized_tokens.pop(0)
result.is_match = len(condition_tokens) == 0
if result.is_match and self._assistant: self._assistant.stop_conversation()
return result return result

View file

@ -13,7 +13,7 @@ from platypush.utils import get_module_and_method_from_action
class Request(Message): class Request(Message):
""" Request message class """ """ Request message class """
def __init__(self, target, action, origin=None, id=None, backend=None, args={}): def __init__(self, target, action, origin=None, id=None, backend=None, args=None):
""" """
Params: Params:
target -- Target node [String] target -- Target node [String]
@ -28,7 +28,7 @@ class Request(Message):
self.target = target self.target = target
self.action = action self.action = action
self.origin = origin self.origin = origin
self.args = args self.args = args if args else {}
self.backend = backend self.backend = backend
@classmethod @classmethod

View file

@ -65,11 +65,21 @@ class Pusher(object):
"~/.config/platypush/config.yaml or " + "~/.config/platypush/config.yaml or " +
"/etc/platypush/config.yaml") "/etc/platypush/config.yaml")
parser.add_argument('--target', '-t', dest='target', required=True, parser.add_argument('--type', '-p', dest='type', required=False,
default='request', help="Type of message to send, request or event")
parser.add_argument('--target', '-t', dest='target', required=False,
default=Config.get('device_id'),
help="Destination of the command") help="Destination of the command")
parser.add_argument('--action', '-a', dest='action', required=True, parser.add_argument('--action', '-a', dest='action', required=False,
help="Action to execute, as package.method") default=None, help="Action to execute, as " +
"package.method (e.g. music.mpd.play), if this is a request")
parser.add_argument('--event', '-e', dest='event', required=False,
default=None, help="Event type, as full " +
"package.class (e.g. " +
"platypush.message.event.ping.PingEvent), if this is an event")
parser.add_argument('--backend', '-b', dest='backend', required=False, parser.add_argument('--backend', '-b', dest='backend', required=False,
default=None, help="Backend to deliver the message " + default=None, help="Backend to deliver the message " +
@ -86,8 +96,17 @@ class Pusher(object):
opts, args = parser.parse_known_args(args) opts, args = parser.parse_known_args(args)
if len(args) % 2 != 0: if len(args) % 2 != 0:
parser.print_help()
raise RuntimeError('Odd number of key-value options passed: {}'.format(args)) raise RuntimeError('Odd number of key-value options passed: {}'.format(args))
if opts.type == 'request' and not opts.action:
parser.print_help()
raise RuntimeError('No action provided for the request'.format(args))
if opts.type == 'event' and not opts.event:
parser.print_help()
raise RuntimeError('No type provided for the event'.format(args))
opts.args = {} opts.args = {}
for i in range(0, len(args), 2): for i in range(0, len(args), 2):
opts.args[re.sub('^-+', '', args[i])] = args[i+1] opts.args[re.sub('^-+', '', args[i])] = args[i+1]
@ -109,7 +128,21 @@ class Pusher(object):
# self.backend_instance.stop() # self.backend_instance.stop()
return _f return _f
def push(self, target, action, backend=None, config_file=None, def send_event(self, target=Config.get('device_id'),
type='platypush.message.event.Event', backend=None, **kwargs):
if not backend: backend = self.backend
self.backend_instance = self.get_backend(backend)
self.backend_instance.send_event({
'target': target,
'args': {
'type': type,
**kwargs
}
})
def push(self, target, action, backend=None,
timeout=default_response_wait_timeout, **kwargs): timeout=default_response_wait_timeout, **kwargs):
""" """
Sends a message on a backend and optionally waits for an answer. Sends a message on a backend and optionally waits for an answer.
@ -123,9 +156,6 @@ class Pusher(object):
timeout -- Response receive timeout in seconds timeout -- Response receive timeout in seconds
- Pusher Default: 5 seconds - Pusher Default: 5 seconds
- If timeout == 0 or None: Pusher exits without waiting for a response - If timeout == 0 or None: Pusher exits without waiting for a response
config_file -- Path to the configuration file to be used (default:
~/.config/platypush/config.yaml or
/etc/platypush/config.yaml)
**kwargs -- Optional key-valued arguments for the action method **kwargs -- Optional key-valued arguments for the action method
(e.g. cmd='echo ping' or groups="['Living Room']") (e.g. cmd='echo ping' or groups="['Living Room']")
""" """
@ -146,18 +176,5 @@ class Pusher(object):
response_timeout=timeout) response_timeout=timeout)
def main(args=sys.argv[1:]):
opts = Pusher.parse_build_args(args)
pusher = Pusher(config_file=opts.config, backend=opts.backend)
pusher.push(opts.target, action=opts.action, timeout=opts.timeout,
**opts.args)
if __name__ == '__main__':
main()
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -43,8 +43,8 @@ setup(
packages = find_packages(), packages = find_packages(),
entry_points = { entry_points = {
'console_scripts': [ 'console_scripts': [
'platypush=platypush:main', 'platypush=platypush.__main__:main',
'pusher=platypush.pusher:main', 'pusher=platypush.pusher.__main__:main',
], ],
}, },
data_files = [ data_files = [