- 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()
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:

View File

@ -1,8 +1,9 @@
import json
import logging
import re
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.utils import get_event_class_by_type
@ -73,11 +74,31 @@ class EventAction(Request):
def execute(self, **context):
for (key, value) in context.items():
self.args[key] = value
event_args = context.pop('event').args if 'event' in context else {}
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()
@classmethod
def build(cls, action):
action = super().parse(action)
@ -137,7 +158,7 @@ class EventHook(object):
logging.info('Running hook {} triggered by an event'.format(self.name))
for action in self.actions:
action.execute(**result.parsed_args)
action.execute(event=event, **result.parsed_args)
# vim:sw=4:ts=4:et:

View File

@ -1,4 +1,5 @@
import logging
import sys
from ..hook import EventHook
@ -22,10 +23,10 @@ class EventProcessor(object):
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 = []
max_score = 0
max_score = -sys.maxsize
for hook in self.hooks:
match = hook.matches_event(event)

View File

@ -1,5 +1,6 @@
import json
import random
import re
import threading
from platypush.config import Config
@ -34,36 +35,12 @@ class Event(Message):
event_type = msg['args'].pop('type')
event_class = get_event_class_by_type(event_type)
args = {
'target' : msg['target'],
'origin' : msg['origin'],
**(msg['args'] if 'args' in msg else {}),
}
args = msg['args'] if 'args' in msg else {}
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)
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
def _generate_id():
@ -73,6 +50,107 @@ class Event(Message):
id += '%.2x' % random.randint(0, 255)
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):
"""
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
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.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

View File

@ -27,35 +27,9 @@ class SpeechRecognizedEvent(AssistantEvent):
self.recognized_phrase = phrase.strip().lower()
def matches_condition(self, condition):
result = EventMatchResult(is_match=False, score=0)
if not isinstance(self, condition.type): return result
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()
result = super().matches_condition(condition)
if result.is_match and self._assistant:
self._assistant.stop_conversation()
return result

View File

@ -13,7 +13,7 @@ from platypush.utils import get_module_and_method_from_action
class Request(Message):
""" 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:
target -- Target node [String]
@ -28,7 +28,7 @@ class Request(Message):
self.target = target
self.action = action
self.origin = origin
self.args = args
self.args = args if args else {}
self.backend = backend
@classmethod

View File

@ -65,11 +65,21 @@ class Pusher(object):
"~/.config/platypush/config.yaml or " +
"/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")
parser.add_argument('--action', '-a', dest='action', required=True,
help="Action to execute, as package.method")
parser.add_argument('--action', '-a', dest='action', required=False,
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,
default=None, help="Backend to deliver the message " +
@ -86,8 +96,17 @@ class Pusher(object):
opts, args = parser.parse_known_args(args)
if len(args) % 2 != 0:
parser.print_help()
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 = {}
for i in range(0, len(args), 2):
opts.args[re.sub('^-+', '', args[i])] = args[i+1]
@ -109,7 +128,21 @@ class Pusher(object):
# self.backend_instance.stop()
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):
"""
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
- Pusher Default: 5 seconds
- 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
(e.g. cmd='echo ping' or groups="['Living Room']")
"""
@ -146,18 +176,5 @@ class Pusher(object):
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:

View File

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