Skip to content
This repository has been archived by the owner on Sep 8, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2576 from forslund/feature/voight-kampff-config
Browse files Browse the repository at this point in the history
Voight kampff config given
  • Loading branch information
forslund authored May 19, 2020
2 parents 8265258 + e08f630 commit 06e958c
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 50 deletions.
104 changes: 56 additions & 48 deletions mycroft/configuration/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@


def is_remote_list(values):
''' check if this list corresponds to a backend formatted collection of
dictionaries '''
"""Check if list corresponds to a backend formatted collection of dicts
"""
for v in values:
if not isinstance(v, dict):
return False
Expand All @@ -39,13 +39,11 @@ def is_remote_list(values):


def translate_remote(config, setting):
"""
Translate config names from server to equivalents usable
in mycroft-core.
"""Translate config names from server to equivalents for mycroft-core.
Args:
config: base config to populate
settings: remote settings to be translated
Arguments:
config: base config to populate
settings: remote settings to be translated
"""
IGNORED_SETTINGS = ["uuid", "@type", "active", "user", "device"]

Expand All @@ -69,12 +67,11 @@ def translate_remote(config, setting):


def translate_list(config, values):
"""
Translate list formated by mycroft server.
"""Translate list formated by mycroft server.
Args:
config (dict): target config
values (list): list from mycroft server config
Arguments:
config (dict): target config
values (list): list from mycroft server config
"""
for v in values:
module = v["@type"]
Expand All @@ -85,21 +82,18 @@ def translate_list(config, values):


class LocalConf(dict):
"""
Config dict from file.
"""
"""Config dictionary from file."""
def __init__(self, path):
super(LocalConf, self).__init__()
if path:
self.path = path
self.load_local(path)

def load_local(self, path):
"""
Load local json file into self.
"""Load local json file into self.
Args:
path (str): file to load
Arguments:
path (str): file to load
"""
if exists(path) and isfile(path):
try:
Expand All @@ -115,10 +109,10 @@ def load_local(self, path):
LOG.debug("Configuration '{}' not defined, skipping".format(path))

def store(self, path=None):
"""
Cache the received settings locally. The cache will be used if
the remote is unreachable to load settings that are as close
to the user's as possible
"""Cache the received settings locally.
The cache will be used if the remote is unreachable to load settings
that are as close to the user's as possible.
"""
path = path or self.path
with open(path, 'w') as f:
Expand All @@ -129,9 +123,7 @@ def merge(self, conf):


class RemoteConf(LocalConf):
"""
Config dict fetched from mycroft.ai
"""
"""Config dictionary fetched from mycroft.ai."""
def __init__(self, cache=None):
super(RemoteConf, self).__init__(None)

Expand Down Expand Up @@ -176,18 +168,23 @@ def __init__(self, cache=None):


class Configuration:
"""Namespace for operations on the configuration singleton."""
__config = {} # Cached config
__patch = {} # Patch config that skills can update to override config

@staticmethod
def get(configs=None, cache=True):
"""
Get configuration, returns cached instance if available otherwise
builds a new configuration dict.
"""Get configuration
Args:
configs (list): List of configuration dicts
cache (boolean): True if the result should be cached
Returns cached instance if available otherwise builds a new
configuration dict.
Arguments:
configs (list): List of configuration dicts
cache (boolean): True if the result should be cached
Returns:
(dict) configuration dictionary.
"""
if Configuration.__config:
return Configuration.__config
Expand All @@ -196,14 +193,14 @@ def get(configs=None, cache=True):

@staticmethod
def load_config_stack(configs=None, cache=False):
"""
load a stack of config dicts into a single dict
"""Load a stack of config dicts into a single dict
Args:
configs (list): list of dicts to load
cache (boolean): True if result should be cached
Arguments:
configs (list): list of dicts to load
cache (boolean): True if result should be cached
Returns: merged dict of all configuration files
Returns:
(dict) merged dict of all configuration files
"""
if not configs:
configs = [LocalConf(DEFAULT_CONFIG), RemoteConf(),
Expand Down Expand Up @@ -233,29 +230,40 @@ def load_config_stack(configs=None, cache=False):
def set_config_update_handlers(bus):
"""Setup websocket handlers to update config.
Args:
Arguments:
bus: Message bus client instance
"""
bus.on("configuration.updated", Configuration.updated)
bus.on("configuration.patch", Configuration.patch)
bus.on("configuration.patch.clear", Configuration.patch_clear)

@staticmethod
def updated(message):
"""
handler for configuration.updated, triggers an update
of cached config.
"""Handler for configuration.updated,
Triggers an update of cached config.
"""
Configuration.load_config_stack(cache=True)

@staticmethod
def patch(message):
"""
patch the volatile dict usable by skills
"""Patch the volatile dict usable by skills
Args:
message: Messagebus message should contain a config
in the data payload.
Arguments:
message: Messagebus message should contain a config
in the data payload.
"""
config = message.data.get("config", {})
merge_dict(Configuration.__patch, config)
Configuration.load_config_stack(cache=True)

@staticmethod
def patch_clear(message):
"""Clear the config patch space.
Arguments:
message: Messagebus message should contain a config
in the data payload.
"""
Configuration.__patch = {}
Configuration.load_config_stack(cache=True)
33 changes: 33 additions & 0 deletions mycroft/res/text/en-us/configurations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"unit system": {
"metric": {"system_unit": "metric"},
"imperial": {"system_unit": "imperial"}
},
"location": {
"stockholm": {
"location": {
"city": {
"name": "Stockholm",
"state": {
"code": "SE.18",
"country": {
"code": "SE",
"name": "Sweden"
},
"name": "Stockholm"
}
},
"coordinate": {
"latitude": 59.38306,
"longitude": 16.66667
},
"timezone": {
"code": "Europe/Stockholm",
"dst_offset": 7200000,
"name": "Europe/Stockholm",
"offset": 3600000
}
}
}
}
}
21 changes: 21 additions & 0 deletions test/integrationtests/voight_kampff/features/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from msm import MycroftSkillsManager
from mycroft.audio import wait_while_speaking
from mycroft.configuration import Configuration
from mycroft.messagebus.client import MessageBusClient
from mycroft.messagebus import Message
from mycroft.util import create_daemon
Expand Down Expand Up @@ -92,6 +93,9 @@ def before_all(context):
context.bus = bus
context.matched_message = None
context.log = log
context.original_config = {}
context.config = Configuration.get()
Configuration.set_config_update_handlers(bus)


def before_feature(context, feature):
Expand All @@ -110,9 +114,26 @@ def after_feature(context, feature):
sleep(1)


def reset_config(context):
"""Reset configuration with changes stored in original_config of context.
"""
context.log.info('Resetting patched configuration...')

context.bus.emit(Message('configuration.patch.clear'))
key = list(context.original_config)[0]
while context.config[key] != context.original_config[key]:
sleep(0.5)
context.original_config = {}


def after_scenario(context, scenario):
"""Wait for mycroft completion and reset any changed state."""
# TODO wait for skill handler complete
sleep(0.5)
wait_while_speaking()
context.bus.clear_messages()
context.matched_message = None

if context.original_config:
# something has changed, reset changes by done in the context
reset_config(context)
105 changes: 105 additions & 0 deletions test/integrationtests/voight_kampff/features/steps/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Copyright 2020 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.
#
import json
from os.path import join, exists
import time

from behave import given
from mycroft.messagebus import Message
from mycroft.util import resolve_resource_file


def patch_config(context, patch):
"""Apply patch to config and wait for it to take effect.
Arguments:
context: Behave context for test
patch: patch to apply
"""
# store originals in context
for key in patch:
# If this patch is redefining an already changed key don't update
if key not in context.original_config:
context.original_config[key] = context.config.get(key)

# Patch config
patch_config_msg = Message('configuration.patch', {'config': patch})
context.bus.emit(patch_config_msg)

# Wait until one of the keys has been updated
key = list(patch.keys())[0]
while context.config.get(key) != patch[key]:
time.sleep(0.5)


def get_config_file_definition(configs_path, config, value):
"""Read config definition file and return the matching patch dict.
Arguments:
configs_path: path to the configuration patch json file
config: config value to fetch from the file
value: predefined value to fetch
Returns:
Patch dictionary or None.
"""
with open(configs_path) as f:
configs = json.load(f)
return configs.get(config, {}).get(value)


def get_global_config_definition(context, config, value):
"""Get config definitions included with Mycroft.
Arguments:
context: behave test context
config: config value to fetch from the file
value: predefined value to fetch
Returns:
Patch dictionary or None.
"""
configs_path = resolve_resource_file(join('text', context.lang,
'configurations.json'))
return get_config_file_definition(configs_path, config, value)


def get_feature_config_definition(context, config, value):
"""Get config feature specific config defintion
Arguments:
context: behave test context
config: config value to fetch from the file
value: predefined value to fetch
Returns:
Patch dictionary or None.
"""
feature_config = context.feature.filename.replace('.feature',
'.config.json')
if exists(feature_config):
return get_config_file_definition(feature_config, config, value)
else:
return None


@given('the user\'s {config} is {value}')
def given_config(context, config, value):
"""Patch the configuration with a specific config."""
config = config.strip('"')
value = value.strip('"')
patch_dict = (get_feature_config_definition(context, config, value) or
get_global_config_definition(context, config, value))
patch_config(context, patch_dict)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2017 Mycroft AI Inc.
# Copyright 2020 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.
Expand Down
Loading

0 comments on commit 06e958c

Please sign in to comment.