Skip to content

Commit

Permalink
Merge pull request #2256 from forslund/refactor-split-mycroftskill
Browse files Browse the repository at this point in the history
Refactor MycroftSkill
  • Loading branch information
forslund committed Aug 30, 2019
2 parents ac6f1f0 + 077df67 commit 75ad11b
Show file tree
Hide file tree
Showing 12 changed files with 1,072 additions and 543 deletions.
175 changes: 175 additions & 0 deletions mycroft/skills/event_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
"""
import json
import time
from datetime import datetime, timedelta
from threading import Thread, Lock
from os.path import isfile, join, expanduser

from mycroft.configuration import Configuration
from mycroft.messagebus.message import Message
from mycroft.util.log import LOG
from .mycroft_skill.event_container import EventContainer


def repeat_time(sched_time, repeat):
Expand Down Expand Up @@ -250,3 +252,176 @@ def shutdown(self):
self.clear_empty()
# Store all pending scheduled events
self.store()


class EventSchedulerInterface:
"""Interface for accessing the event scheduler over the message bus."""
def __init__(self, name, sched_id=None, bus=None):
self.name = name
self.sched_id = sched_id
self.bus = bus
self.events = EventContainer(bus)

self.scheduled_repeats = []

def set_bus(self, bus):
self.bus = bus
self.events.set_bus(bus)

def set_id(self, sched_id):
self.sched_id = sched_id

def _create_unique_name(self, name):
"""Return a name unique to this skill using the format
[skill_id]:[name].
Arguments:
name: Name to use internally
Returns:
str: name unique to this skill
"""
return str(self.sched_id) + ':' + (name or '')

def _schedule_event(self, handler, when, data, name, repeat_interval=None):
"""Underlying method for schedule_event and schedule_repeating_event.
Takes scheduling information and sends it off on the message bus.
Arguments:
handler: method to be called
when (datetime): time (in system timezone) for first
calling the handler, or None to
initially trigger <frequency> seconds
from now
data (dict, optional): data to send when the handler is called
name (str, optional): reference name, must be unique
repeat_interval (float/int): time in seconds between calls
"""
if isinstance(when, (int, float)) and when >= 0:
when = datetime.now() + timedelta(seconds=when)
if not name:
name = self.name + handler.__name__
unique_name = self._create_unique_name(name)
if repeat_interval:
self.scheduled_repeats.append(name) # store "friendly name"

data = data or {}
self.events.add(unique_name, handler, once=not repeat_interval)
event_data = {'time': time.mktime(when.timetuple()),
'event': unique_name,
'repeat': repeat_interval,
'data': data}
self.bus.emit(Message('mycroft.scheduler.schedule_event',
data=event_data))

def schedule_event(self, handler, when, data=None, name=None):
"""Schedule a single-shot event.
Arguments:
handler: method to be called
when (datetime/int/float): datetime (in system timezone) or
number of seconds in the future when the
handler should be called
data (dict, optional): data to send when the handler is called
name (str, optional): reference name
NOTE: This will not warn or replace a
previously scheduled event of the same
name.
"""
self._schedule_event(handler, when, data, name)

def schedule_repeating_event(self, handler, when, interval,
data=None, name=None):
"""Schedule a repeating event.
Arguments:
handler: method to be called
when (datetime): time (in system timezone) for first
calling the handler, or None to
initially trigger <frequency> seconds
from now
interval (float/int): time in seconds between calls
data (dict, optional): data to send when the handler is called
name (str, optional): reference name, must be unique
"""
# Do not schedule if this event is already scheduled by the skill
if name not in self.scheduled_repeats:
# If only interval is given set to trigger in [interval] seconds
# from now.
if not when:
when = datetime.now() + timedelta(seconds=interval)
self._schedule_event(handler, when, data, name, interval)
else:
LOG.debug('The event is already scheduled, cancel previous '
'event if this scheduling should replace the last.')

def update_scheduled_event(self, name, data=None):
"""Change data of event.
Arguments:
name (str): reference name of event (from original scheduling)
"""
data = data or {}
data = {
'event': self._create_unique_name(name),
'data': data
}
self.bus.emit(Message('mycroft.schedule.update_event',
data=data))

def cancel_scheduled_event(self, name):
"""Cancel a pending event. The event will no longer be scheduled
to be executed
Arguments:
name (str): reference name of event (from original scheduling)
"""
unique_name = self._create_unique_name(name)
data = {'event': unique_name}
if name in self.scheduled_repeats:
self.scheduled_repeats.remove(name)
if self.events.remove(unique_name):
self.bus.emit(Message('mycroft.scheduler.remove_event',
data=data))

def get_scheduled_event_status(self, name):
"""Get scheduled event data and return the amount of time left
Arguments:
name (str): reference name of event (from original scheduling)
Returns:
int: the time left in seconds
Raises:
Exception: Raised if event is not found
"""
event_name = self._create_unique_name(name)
data = {'name': event_name}

reply_name = 'mycroft.event_status.callback.{}'.format(event_name)
msg = Message('mycroft.scheduler.get_event', data=data)
status = self.bus.wait_for_response(msg, reply_type=reply_name)

if status:
event_time = int(status.data[0][0])
current_time = int(time.time())
time_left_in_seconds = event_time - current_time
LOG.info(time_left_in_seconds)
return time_left_in_seconds
else:
raise Exception("Event Status Messagebus Timeout")

def cancel_all_repeating_events(self):
"""Cancel any repeating events started by the skill."""
# NOTE: Gotta make a copy of the list due to the removes that happen
# in cancel_scheduled_event().
for e in list(self.scheduled_repeats):
self.cancel_scheduled_event(e)

def shutdown(self):
"""Shutdown the interface unregistering any event handlers."""
self.cancel_all_repeating_events()
self.events.clear()
2 changes: 1 addition & 1 deletion mycroft/skills/intent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@

from mycroft.configuration import Configuration
from mycroft.messagebus.message import Message
from mycroft.skills.core import open_intent_envelope
from mycroft.util.lang import set_active_lang
from mycroft.util.log import LOG
from mycroft.util.parse import normalize
from mycroft.metrics import report_timing, Stopwatch
from mycroft.skills.padatious_service import PadatiousService
from .intent_service_interface import open_intent_envelope


class AdaptIntent(IntentBuilder):
Expand Down
168 changes: 168 additions & 0 deletions mycroft/skills/intent_service_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Copyright 2018 Mycroft AI Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""The intent service interface offers a unified wrapper class for the
Intent Service. Including both adapt and padatious.
"""
from os.path import exists

from adapt.intent import Intent

from mycroft.messagebus.message import Message


class IntentServiceInterface:
"""Interface to communicate with the Mycroft intent service.
This class wraps the messagebus interface of the intent service allowing
for easier interaction with the service. It wraps both the Adapt and
Precise parts of the intent services.
"""
def __init__(self, bus=None):
self.bus = bus
self.registered_intents = []

def set_bus(self, bus):
self.bus = bus

def register_adapt_keyword(self, vocab_type, entity, aliases=None):
"""Send a message to the intent service to add an Adapt keyword.
vocab_type(str): Keyword reference
entity (str): Primary keyword
aliases (list): List of alternative kewords
"""
aliases = aliases or []
self.bus.emit(Message("register_vocab",
{'start': entity, 'end': vocab_type}))
for alias in aliases:
self.bus.emit(Message("register_vocab", {
'start': alias, 'end': vocab_type, 'alias_of': entity
}))

def register_adapt_regex(self, regex):
"""Register a regex with the intent service.
Arguments:
regex (str): Regex to be registered, (Adapt extracts keyword
reference from named match group.
"""
self.bus.emit(Message("register_vocab", {'regex': regex}))

def register_adapt_intent(self, name, intent_parser):
"""Register an Adapt intent parser object.
Serializes the intent_parser and sends it over the messagebus to
registered.
"""
self.bus.emit(Message("register_intent", intent_parser.__dict__))
self.registered_intents.append((name, intent_parser))

def detach_intent(self, intent_name):
"""Remove an intent from the intent service.
Arguments:
intent_name(str): Intent reference
"""
self.bus.emit(Message("detach_intent", {"intent_name": intent_name}))

def set_adapt_context(self, context, word, origin):
"""Set an Adapt context.
Arguments:
context (str): context keyword name
word (str): word to register
origin (str): original origin of the context (for cross context)
"""
self.bus.emit(Message('add_context',
{'context': context, 'word': word,
'origin': origin}))

def remove_adapt_context(self, context):
"""Remove an active Adapt context.
Arguments:
context(str): name of context to remove
"""
self.bus.emit(Message('remove_context', {'context': context}))

def register_padatious_intent(self, intent_name, filename):
"""Register a padatious intent file with Padatious.
Arguments:
intent_name(str): intent identifier
filename(str): complete file path for entity file
"""
if not isinstance(filename, str):
raise ValueError('Filename path must be a string')
if not exists(filename):
raise FileNotFoundError('Unable to find "{}"'.format(filename))

data = {"file_name": filename,
"name": intent_name}
self.bus.emit(Message("padatious:register_intent", data))
self.registered_intents.append((intent_name, data))

def register_padatious_entity(self, entity_name, filename):
"""Register a padatious entity file with Padatious.
Arguments:
entity_name(str): entity name
filename(str): complete file path for entity file
"""
if not isinstance(filename, str):
raise ValueError('Filename path must be a string')
if not exists(filename):
raise FileNotFoundError('Unable to find "{}"'.format(filename))

self.bus.emit(Message('padatious:register_entity', {
'file_name': filename,
'name': entity_name
}))

def __iter__(self):
"""Iterator over the registered intents.
Returns an iterator returning name-handler pairs of the registered
intent handlers.
"""
return iter(self.registered_intents)

def __contains__(self, val):
"""Checks if an intent name has been registered."""
return val in [i[0] for i in self.registered_intents]

def get_intent(self, intent_name):
"""Get intent from intent_name.
Arguments:
intent_name (str): name to find.
Returns:
Found intent or None if none were found.
"""
for name, intent in self:
if name == intent_name:
return intent
else:
return None


def open_intent_envelope(message):
"""Convert dictionary received over messagebus to Intent."""
intent_dict = message.data
return Intent(intent_dict.get('name'),
intent_dict.get('requires'),
intent_dict.get('at_least_one'),
intent_dict.get('optional'))
18 changes: 18 additions & 0 deletions mycroft/skills/mycroft_skill/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2019 Mycroft AI Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from .mycroft_skill import MycroftSkill
from .event_container import get_handler_name
from .decorators import (intent_handler, intent_file_handler,
resting_screen_handler)
Loading

0 comments on commit 75ad11b

Please sign in to comment.