def test_executor_once(): """Test that ExecutorEventEmitters also emit events for once. """ ee = ExecutorEventEmitter() should_call = Mock() @ee.once('event') def event_handler(): should_call(True) ee.emit('event') sleep(1) should_call.assert_called_once()
def test_executor_error(): """Test that ExecutorEventEmitters handle errors. """ ee = ExecutorEventEmitter() should_call = Mock() @ee.on('event') def event_handler(): raise PyeeTestError() @ee.on('error') def handle_error(e): should_call(e) ee.emit('event') sleep(1) should_call.assert_called_once()
class MessageBusClient: def __init__(self, host=None, port=None, route=None, ssl=None): config_overrides = dict(host=host, port=port, route=route, ssl=ssl) self.config = load_message_bus_config(**config_overrides) self.emitter = ExecutorEventEmitter() self.client = self.create_client() self.retry = 5 self.connected_event = Event() self.started_running = False @staticmethod def build_url(host, port, route, ssl): return '{scheme}://{host}:{port}{route}'.format( scheme='wss' if ssl else 'ws', host=host, port=str(port), route=route) def create_client(self): url = MessageBusClient.build_url(ssl=self.config.ssl, host=self.config.host, port=self.config.port, route=self.config.route) return WebSocketApp(url, on_open=self.on_open, on_close=self.on_close, on_error=self.on_error, on_message=self.on_message) def on_open(self): LOG.info("Connected") self.connected_event.set() self.emitter.emit("open") # Restore reconnect timer to 5 seconds on sucessful connect self.retry = 5 def on_close(self): self.emitter.emit("close") def on_error(self, error): """ On error start trying to reconnect to the websocket. """ if isinstance(error, WebSocketConnectionClosedException): LOG.warning('Could not send message because connection has closed') else: LOG.exception('=== ' + repr(error) + ' ===') try: self.emitter.emit('error', error) if self.client.keep_running: self.client.close() except Exception as e: LOG.error('Exception closing websocket: ' + repr(e)) LOG.warning("Message Bus Client will reconnect in %d seconds." % self.retry) time.sleep(self.retry) self.retry = min(self.retry * 2, 60) try: self.emitter.emit('reconnecting') self.client = self.create_client() self.run_forever() except WebSocketException: pass def on_message(self, message): parsed_message = Message.deserialize(message) self.emitter.emit('message', message) self.emitter.emit(parsed_message.msg_type, parsed_message) def emit(self, message): if not self.connected_event.wait(10): if not self.started_running: raise ValueError('You must execute run_forever() ' 'before emitting messages') self.connected_event.wait() try: if hasattr(message, 'serialize'): self.client.send(message.serialize()) else: self.client.send(json.dumps(message.__dict__)) except WebSocketConnectionClosedException: LOG.warning('Could not send {} message because connection ' 'has been closed'.format(message.msg_type)) def wait_for_message(self, message_type, timeout=3.0): """Wait for a message of a specific type. Arguments: message_type (str): the message type of the expected message timeout: seconds to wait before timeout, defaults to 3 Returns: The received message or None if the response timed out """ return MessageWaiter(self, message_type).wait(timeout) def wait_for_response(self, message, reply_type=None, timeout=3.0): """Send a message and wait for a response. Arguments: message (Message): message to send reply_type (str): the message type of the expected reply. Defaults to "<message.msg_type>.response". timeout: seconds to wait before timeout, defaults to 3 Returns: The received message or None if the response timed out """ message_type = reply_type or message.msg_type + '.response' waiter = MessageWaiter(self, message_type) # Setup response handler # Send message and wait for it's response self.emit(message) return waiter.wait(timeout) def on(self, event_name, func): self.emitter.on(event_name, func) def once(self, event_name, func): self.emitter.once(event_name, func) def remove(self, event_name, func): try: if event_name in self.emitter._events: LOG.debug("Removing found '" + str(event_name) + "'") else: LOG.debug("Not able to find '" + str(event_name) + "'") self.emitter.remove_listener(event_name, func) except ValueError: LOG.warning('Failed to remove event {}: {}'.format( event_name, str(func))) for line in traceback.format_stack(): LOG.warning(line.strip()) if event_name in self.emitter._events: LOG.debug("Removing found '" + str(event_name) + "'") else: LOG.debug("Not able to find '" + str(event_name) + "'") LOG.warning("Existing events: " + str(self.emitter._events)) for evt in self.emitter._events: LOG.warning(" " + str(evt)) LOG.warning(" " + str(self.emitter._events[evt])) if event_name in self.emitter._events: LOG.debug("Removing found '" + str(event_name) + "'") else: LOG.debug("Not able to find '" + str(event_name) + "'") LOG.warning('----- End dump -----') def remove_all_listeners(self, event_name): ''' Remove all listeners connected to event_name. Args: event_name: event from which to remove listeners ''' if event_name is None: raise ValueError self.emitter.remove_all_listeners(event_name) def run_forever(self): self.started_running = True self.client.run_forever() def close(self): self.client.close() self.connected_event.clear()
class MessageBusClient: def __init__(self, host='0.0.0.0', port=8181, route='/core', ssl=False): self.config = MessageBusClientConf(host, port, route, ssl) self.emitter = ExecutorEventEmitter() self.client = self.create_client() self.retry = 5 self.connected_event = Event() self.started_running = False @staticmethod def build_url(host, port, route, ssl): return '{scheme}://{host}:{port}{route}'.format( scheme='wss' if ssl else 'ws', host=host, port=str(port), route=route) def create_client(self): url = MessageBusClient.build_url(ssl=self.config.ssl, host=self.config.host, port=self.config.port, route=self.config.route) return WebSocketApp(url, on_open=self.on_open, on_close=self.on_close, on_error=self.on_error, on_message=self.on_message) def on_open(self): LOG.info("Connected") self.connected_event.set() self.emitter.emit("open") # Restore reconnect timer to 5 seconds on sucessful connect self.retry = 5 def on_close(self): self.emitter.emit("close") def on_error(self, error): """ On error start trying to reconnect to the websocket. """ if isinstance(error, WebSocketConnectionClosedException): LOG.warning('Could not send message because connection has closed') else: LOG.exception('=== ' + repr(error) + ' ===') try: self.emitter.emit('error', error) if self.client.keep_running: self.client.close() except Exception as e: LOG.error('Exception closing websocket: ' + repr(e)) LOG.warning( "Message Bus Client will reconnect in %d seconds." % self.retry ) time.sleep(self.retry) self.retry = min(self.retry * 2, 60) try: self.emitter.emit('reconnecting') self.client = self.create_client() self.run_forever() except WebSocketException: pass def on_message(self, message): parsed_message = Message.deserialize(message) self.emitter.emit('message', message) self.emitter.emit(parsed_message.msg_type, parsed_message) def emit(self, message): if not self.connected_event.wait(10): if not self.started_running: raise ValueError('You must execute run_forever() ' 'before emitting messages') self.connected_event.wait() try: if hasattr(message, 'serialize'): self.client.send(message.serialize()) else: self.client.send(json.dumps(message.__dict__)) except WebSocketConnectionClosedException: LOG.warning('Could not send {} message because connection ' 'has been closed'.format(message.msg_type)) def wait_for_response(self, message, reply_type=None, timeout=None): """Send a message and wait for a response. Args: message (Message): message to send reply_type (str): the message type of the expected reply. Defaults to "<message.msg_type>.response". timeout: seconds to wait before timeout, defaults to 3 Returns: The received message or None if the response timed out """ response = [] def handler(message): """Receive response data.""" response.append(message) # Setup response handler self.once(reply_type or message.msg_type + '.response', handler) # Send request self.emit(message) # Wait for response start_time = time.monotonic() while len(response) == 0: time.sleep(0.2) if time.monotonic() - start_time > (timeout or 3.0): try: self.remove(reply_type, handler) except (ValueError, KeyError): # ValueError occurs on pyee 1.0.1 removing handlers # registered with once. # KeyError may theoretically occur if the event occurs as # the handler is removed pass return None return response[0] def on(self, event_name, func): self.emitter.on(event_name, func) def once(self, event_name, func): self.emitter.once(event_name, func) def remove(self, event_name, func): try: if not event_name in self.emitter._events: LOG.debug("Not able to find '"+str(event_name)+"'") self.emitter.remove_listener(event_name, func) except ValueError: LOG.warning('Failed to remove event {}: {}'.format(event_name, str(func))) for line in traceback.format_stack(): LOG.warning(line.strip()) if not event_name in self.emitter._events: LOG.debug("Not able to find '"+str(event_name)+"'") LOG.warning("Existing events: " + str(self.emitter._events)) for evt in self.emitter._events: LOG.warning(" "+str(evt)) LOG.warning(" "+str(self.emitter._events[evt])) if event_name in self.emitter._events: LOG.debug("Removing found '"+str(event_name)+"'") else: LOG.debug("Not able to find '"+str(event_name)+"'") LOG.warning('----- End dump -----') def remove_all_listeners(self, event_name): """Remove all listeners connected to event_name. Arguments: event_name: event from which to remove listeners """ if event_name is None: raise ValueError self.emitter.remove_all_listeners(event_name) def run_forever(self): self.started_running = True self.client.run_forever() def close(self): self.client.close() self.connected_event.clear() def run_in_thread(self): """Launches the run_forever in a separate daemon thread.""" t = Thread(target=self.run_forever) t.daemon = True t.start() return t
class Flack(Flask): def __init__(self, import_name, endpoint='/', events_endpoint='/slack/events', **kwargs): super().__init__(import_name, **kwargs) self.dispatcher = Dispatcher() self.before_request_funcs.setdefault(None, []).append( self._redirect_requests) self.emitter = ExecutorEventEmitter() self._endpoint = endpoint self._bind_main_entrypoint(endpoint) self._bind_events_entrypoint(events_endpoint) def shortcut(self, callback_id, **options): def decorate(func): command = callback_id self.add_url_rule(f'/{command}', command, func, **options) self.dispatcher.add_matcher(ShortcutMatcher(command)) return func return decorate def command(self, func=None, **options): """A decorator that is used to register a function as a command handler. It can be used as a plain decorator or as a parametrized decorator factory. This does the same as `add_command_handler` Usage: >>>@command >>>def hola(): >>> print('hola', kwargs) >>>@command(name='goodbye') >>>def chau(): >>> print('chau', kwargs) """ def decorate(func): command = options.pop('name', func.__name__) rule = f'/{command}' if not command.startswith('/') else command self.add_url_rule(rule, command, func, **options) self.dispatcher.add_matcher(Command(command)) return func used_as_plain_decorator = bool(func) if used_as_plain_decorator: return decorate(func) else: return decorate def action(self, action_id=None, **options): if action_id is None and options.get('block_id') is None or callable( action_id): raise TypeError( "action() missing 1 required positional argument: 'action_id'") def decorate(func): block_id = options.pop('block_id', None) command = action_id or block_id # TODO: Handle special characters that make url rule invalid? self.add_url_rule(f'/{command}', command, func, **options) self.dispatcher.add_matcher( ActionMatcher(command, block_id=block_id)) return func return decorate def view(self, view_callback_id, **options): def decorate(func): self.add_url_rule(f'/{view_callback_id}', view_callback_id, func, **options) self.dispatcher.add_matcher(ViewMatcher(view_callback_id)) return func return decorate def event(self, event, func=None): def add_listener(func): self.emitter.on(event, func) return add_listener(func) if func else add_listener def default(self, func): self._handle_unknown = func def _handle_unknown(self): """Ignore unknown commands by default.""" return None def error(self, func): self._handle_error = func def _handle_error(self, e): return self.make_response(('Oops..', 500)) def _redirect_requests(self): request = _request_ctx_stack.top.request if request.routing_exception is not None: self.raise_routing_exception(request) if request.method == 'GET' or request.path != self._endpoint: return try: endpoint = self.dispatcher.match(request) except StopIteration: logger.info('No handler matched this request') return self._handle_unknown() except Exception as e: logger.exception('Something bad happened.') return self._handle_error(e) rule = request.url_rule rule.endpoint = endpoint def _bind_main_entrypoint(self, endpoint): self.add_url_rule(endpoint, '_entrypoint', lambda: 'Home', methods=('GET', 'POST')) def _bind_events_entrypoint(self, endpoint): self.add_url_rule(endpoint, '_slack_events', self._handle_event, methods=('POST', )) def _handle_event(self): """Respond to event request sync and emit event for async event handling""" event_data = request.get_json() # Respond to slack challenge to enable our endpoint as an event receiver if "challenge" in event_data: return make_response(event_data.get("challenge"), 200, {"Content-Type": "application/json"}) if "event" in event_data: event_type = event_data["event"]["type"] self.emitter.emit(event_type, event_data) return make_response("", 200)
class LocalHive(FakeCroftMind): protocol = LocalHiveProtocol intent_messages = [ "recognizer_loop:utterance", "intent.service.intent.get", "intent.service.skills.get", "intent.service.active_skills.get", "intent.service.adapt.get", "intent.service.padatious.get", "intent.service.adapt.manifest.get", "intent.service.padatious.manifest.get", "intent.service.adapt.vocab.manifest.get", "intent.service.padatious.entities.manifest.get", "register_vocab", "register_intent", "detach_intent", "detach_skill", "add_context", "remove_context", "clear_context", "mycroft.skills.loaded", "active_skill_request", 'mycroft.speech.recognition.unknown', 'padatious:register_intent', 'padatious:register_entity', 'mycroft.skills.initialized' ] default_permissions = intent_messages + [ "speak", "mycroft.skill.handler.start", "mycroft.skill.handler.complete", "skill.converse.request", "skill.converse.response" ] def __init__(self, port=6989, *args, **kwargs): super().__init__(*args, **kwargs) self.bus_port = port intentbus = FakeBus() intentbus.on("message", self.handle_intent_service_message) self.intent_service = IntentService(intentbus) self.intent2skill = {} self.permission_overrides = {} self.ee = ExecutorEventEmitter() self.ee.on("localhive.skill", self.handle_skill_message) self.ee.on("localhive.utterance", self.intent_service.handle_utterance) # intent service is answering a skill / client def skill2peer(self, skill_id): for peer, client in self.clients.items(): if not client.get("instance"): continue if client["instance"].skill_id == skill_id: return peer return None def handle_intent_service_message(self, message): if isinstance(message, str): message = Message.deserialize(message) skill_id = message.context.get("skill_id") peers = message.context.get("destination") or [] # converse method handling if message.msg_type in ["skill.converse.request"]: skill_id = message.data.get("skill_id") message.context["skill_id"] = skill_id skill_peer = self.skill2peer(skill_id) LOG.info(f"Converse: {message.msg_type} " f"Skill: {skill_id} " f"Peer: {skill_peer}") message.context['source'] = "IntentService" message.context['destination'] = peers self.send2peer(message, skill_peer) elif message.msg_type in ["skill.converse.response"]: # just logging that it was received, converse method handled by # skill skill_id = message.data.get("skill_id") response = message.data.get("result") message.context["skill_id"] = skill_id skill_peer = self.skill2peer(skill_id) LOG.info(f"Converse Response: {response} " f"Skill: {skill_id} " f"Peer: {skill_peer}") message.context['source'] = skill_id message.context['destination'] = peers # intent found elif message.msg_type in self.intent2skill: skill_id = self.intent2skill[message.msg_type] skill_peer = self.skill2peer(skill_id) message.context["skill_id"] = skill_id LOG.info(f"Intent: {message.msg_type} " f"Skill: {skill_id} " f"Source: {peers} " f"Target: {skill_peer}") # trigger the skill message.context['source'] = "IntentService" LOG.debug(f"Triggering intent: {skill_peer}") self.send2peer(message, skill_peer) # skill registering intent elif message.msg_type in ["register_intent", "padatious:register_intent"]: LOG.info(f"Register Intent: {message.data['name']} " f"Skill: {message.context['skill_id']}") self.intent2skill[message.data["name"]] = skill_id def send2peer(self, message, peer): if peer in self.clients: LOG.debug(f"sending to: {peer}") client = self.clients[peer].get("instance") msg = HiveMessage(HiveMessageType.BUS, source_peer=self.peer, payload=message) self.interface.send(msg, client) # external skills / clients def handle_incoming_mycroft(self, message, client): """ external skill client sent a message message (Message): mycroft bus message object """ # message from a skill if message.context.get("skill_id"): self.ee.emit("localhive.skill", message) # message from a terminal if message.msg_type == "recognizer_loop:utterance": LOG.info(f"Utterance: {message.data['utterances']} " f"Peer: {client.peer}") message.context["source"] = client.peer self.ee.emit("localhive.utterance", message) def handle_skill_message(self, message): """ message sent by local/system skill""" if isinstance(message, str): message = Message.deserialize(message) skill_id = message.context.get("skill_id") intent_skill = self.intent2skill.get(message.msg_type) permitted = False # skill intents if intent_skill: permitted = True # skill_id permission override elif skill_id and skill_id in self.permission_overrides: if message.msg_type in self.permission_overrides[skill_id]: permitted = True # default permissions elif message.msg_type in self.default_permissions: permitted = True if permitted: peers = message.context.get('destination') or [] if isinstance(peers, str): peers = [peers] # check if it should be forwarded to some peer (skill/terminal) for peer in peers: if peer in self.clients: LOG.debug(f"destination: {message.context['destination']} " f"skill:{skill_id} " f"type:{message.msg_type}") self.send2peer(message, peer) # check if this message should be forwarded to intent service if message.msg_type in self.intent_messages or \ "IntentService" in peers: self.intent_service.bus.emit(message) else: self.handle_ignored_message(message) def handle_ignored_message(self, message): pass