<commit_before># (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> and others
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import glob
import imp
import inspect
import os
import os.path
import sys
import warnings

from collections import defaultdict

from ansible import constants as C

try:
    from __main__ import display
except ImportError:
    from ansible.utils.display import Display
    display = Display()

# Global so that all instances of a PluginLoader will share the caches
MODULE_CACHE = {}
PATH_CACHE = {}
PLUGIN_PATH_CACHE = {}

def get_all_plugin_loaders():
    return [(name, obj) for (name, obj) in inspect.getmembers(sys.modules[__name__]) if isinstance(obj, PluginLoader)]

class PluginLoader:

    '''
    PluginLoader loads plugins from the configured plugin directories.

    It searches for plugins by iterating through the combined list of
    play basedirs, configured paths, and the python path.
    The first match is used.
    '''

    def __init__(self, class_name, package, config, subdir, aliases={}, required_base_class=None):

        self.class_name         = class_name
        self.base_class         = required_base_class
        self.package            = package
        self.config             = config
        self.subdir             = subdir
        self.aliases            = aliases

        if not class_name in MODULE_CACHE:
            MODULE_CACHE[class_name] = {}
        if not class_name in PATH_CACHE:
            PATH_CACHE[class_name] = None
        if not class_name in PLUGIN_PATH_CACHE:
            PLUGIN_PATH_CACHE[class_name] = defaultdict(dict)

        self._module_cache      = MODULE_CACHE[class_name]
        self._paths             = PATH_CACHE[class_name]
        self._plugin_path_cache = PLUGIN_PATH_CACHE[class_name]

        self._extra_dirs = []
        self._searched_paths = set()

    def __setstate__(self, data):
        '''
        Deserializer.
        '''

        class_name = data.get('class_name')
        package    = data.get('package')
        config     = data.get('config')
        subdir     = data.get('subdir')
        aliases    = data.get('aliases')
        base_class = data.get('base_class')

        PATH_CACHE[class_name] = data.get('PATH_CACHE')
        PLUGIN_PATH_CACHE[class_name] = data.get('PLUGIN_PATH_CACHE')

        self.__init__(class_name, package, config, subdir, aliases, base_class)
        self._extra_dirs = data.get('_extra_dirs', [])
        self._searched_paths = data.get('_searched_paths', set())

    def __getstate__(self):
        '''
        Serializer.
        '''

        return dict(
            class_name        = self.class_name,
            base_class        = self.base_class,
            package           = self.package,
            config            = self.config,
            subdir            = self.subdir,
            aliases           = self.aliases,
            _extra_dirs       = self._extra_dirs,
            _searched_paths   = self._searched_paths,
            PATH_CACHE        = PATH_CACHE[self.class_name],
            PLUGIN_PATH_CACHE = PLUGIN_PATH_CACHE[self.class_name],
        )

    def print_paths(self):
        ''' Returns a string suitable for printing of the search path '''

        # Uses a list to get the order right
        ret = []
        for i in self._get_paths():
            if i not in ret:
                ret.append(i)
        return os.pathsep.join(ret)

    def _all_directories(self, dir):
        results = []
        results.append(dir)
        for root, subdirs, files in os.walk(dir):
           if '__init__.py' in files:
               for x in subdirs:
                   results.append(os.path.join(root,x))
        return results

    def _get_package_paths(self):
        ''' Gets the path of a Python package '''

        paths = []
        if not self.package:
            return []
        if not hasattr(self, 'package_path'):
            m = __import__(self.package)
            parts = self.package.split('.')[1:]
            self.package_path = os.path.join(os.path.dirname(m.__file__), *parts)
        paths.extend(self._all_directories(self.package_path))
        return paths

    def _get_paths(self):
        ''' Return a list of paths to search for plugins in '''

        if self._paths is not None:
            return self._paths

        ret = self._extra_dirs[:]

        # look in any configured plugin paths, allow one level deep for subcategories
        if self.config is not None:
            configured_paths = self.config.split(os.pathsep)
            for path in configured_paths:
                path = os.path.realpath(os.path.expanduser(path))
                contents = glob.glob("%s/*" % path) + glob.glob("%s/*/*" % path)
                for c in contents:
                    if os.path.isdir(c) and c not in ret:
                        ret.append(c)
                if path not in ret:
                    ret.append(path)

        # look for any plugins installed in the package subtree
        ret.extend(self._get_package_paths())

        # HACK: because powershell modules are in the same directory
        # hierarchy as other modules we have to process them last.  This is
        # because powershell only works on windows but the other modules work
        # anywhere (possibly including windows if the correct language
        # interpreter is installed).  the non-powershell modules can have any
        # file extension and thus powershell modules are picked up in that.
        # The non-hack way to fix this is to have powershell modules be
        # a different PluginLoader/ModuleLoader.  But that requires changing
        # other things too (known thing to change would be PATHS_CACHE,
        # PLUGIN_PATHS_CACHE, and MODULE_CACHE.  Since those three dicts key
        # on the class_name and neither regular modules nor powershell modules
        # would have class_names, they would not work as written.
        reordered_paths = []
        win_dirs = []
        for path in ret:
            if path.endswith('windows'):
                win_dirs.append(path)
            else:
                reordered_paths.append(path)
        reordered_paths.extend(win_dirs)

        # cache and return the result
        self._paths = reordered_paths
        return reordered_paths

    def add_directory(self, directory, with_subdir=False):
        ''' Adds an additional directory to the search path '''

        directory = os.path.realpath(directory)

        if directory is not None:
            if with_subdir:
                directory = os.path.join(directory, self.subdir)
            if directory not in self._extra_dirs:
                # append the directory and invalidate the path cache
                self._extra_dirs.append(directory)
                self._paths = None

    def find_plugin(self, name, mod_type=''):
        ''' Find a plugin named name '''

        if mod_type:
            suffix = mod_type
        elif self.class_name:
            # Ansible plugins that run in the controller process (most plugins)
            suffix = '.py'
        else:
            # Only Ansible Modules.  Ansible modules can be any executable so
            # they can have any suffix
            suffix = ''

        # The particular cache to look for modules within.  This matches the
        # requested mod_type
        pull_cache = self._plugin_path_cache[suffix]
        try:
            return pull_cache[name]
        except KeyError:
            # Cache miss.  Now let's find the plugin
            pass

        # TODO: Instead of using the self._paths cache (PATH_CACHE) and
        #       self._searched_paths we could use an iterator.  Before enabling that
        #       we need to make sure we don't want to add additional directories
        #       (add_directory()) once we start using the iterator.  Currently, it
        #       looks like _get_paths() never forces a cache refresh so if we expect
        #       additional directories to be added later, it is buggy.
        for path in (p for p in self._get_paths() if p not in self._searched_paths and os.path.isdir(p)):
            try:
                full_paths = (os.path.join(path, f) for f in os.listdir(path))
            except OSError as e:
                display.warning("Error accessing plugin paths: %s" % str(e))

            for full_path in (f for f in full_paths if os.path.isfile(f) and not f.endswith('__init__.py')):
                full_name = os.path.basename(full_path)

                # HACK: We have no way of executing python byte
                # compiled files as ansible modules so specifically exclude them
                if full_path.endswith(('.pyc', '.pyo')):
                    continue

                splitname = os.path.splitext(full_name)
                base_name = splitname[0]
                try:
                    extension = splitname[1]
                except IndexError:
                    extension = ''

                # Module found, now enter it into the caches that match
                # this file
                if base_name not in self._plugin_path_cache['']:
                    self._plugin_path_cache[''][base_name] = full_path

                if full_name not in self._plugin_path_cache['']:
                    self._plugin_path_cache[''][full_name] = full_path

                if base_name not in self._plugin_path_cache[extension]:
                    self._plugin_path_cache[extension][base_name] = full_path

                if full_name not in self._plugin_path_cache[extension]:
                    self._plugin_path_cache[extension][full_name] = full_path

            self._searched_paths.add(path)
            try:
                return pull_cache[name]
            except KeyError:
                # Didn't find the plugin in this directory.  Load modules from
                # the next one
                pass

        # if nothing is found, try finding alias/deprecated
        if not name.startswith('_'):
            alias_name = '_' + name
            # We've already cached all the paths at this point
            if alias_name in pull_cache:
                if not os.path.islink(pull_cache[alias_name]):
                    display.deprecated('%s is kept for backwards compatibility '
                              'but usage is discouraged. The module '
                              'documentation details page may explain '
                              'more about this rationale.' %
                              name.lstrip('_'))
                return pull_cache[alias_name]

        return None

    def has_plugin(self, name):
        ''' Checks if a plugin named name exists '''

        return self.find_plugin(name) is not None

    __contains__ = has_plugin

    def _load_module_source(self, name, path):
        if name in sys.modules:
            # See https://github.com/ansible/ansible/issues/13110
            return sys.modules[name]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", RuntimeWarning)
            with open(path, 'r') as module_file:
                module = imp.load_source(name, path, module_file)
        return module

    def get(self, name, *args, **kwargs):
        ''' instantiates a plugin of the given name using arguments '''

        class_only = kwargs.pop('class_only', False)
        if name in self.aliases:
            name = self.aliases[name]
        path = self.find_plugin(name)
        if path is None:
            return None

        if path not in self._module_cache:
            self._module_cache[path] = self._load_module_source('.'.join([self.package, name]), path)

        obj = getattr(self._module_cache[path], self.class_name)
        if self.base_class:
            # The import path is hardcoded and should be the right place,
            # so we are not expecting an ImportError.
            module = __import__(self.package, fromlist=[self.base_class])
            # Check whether this obj has the required base class.
            try:
                plugin_class = getattr(module, self.base_class)
            except AttributeError:
                return None
            if not issubclass(obj, plugin_class):
                return None

        if not class_only:
            obj = obj(*args, **kwargs)

        return obj

    def all(self, *args, **kwargs):
        ''' instantiates all plugins with the same arguments '''

        class_only = kwargs.pop('class_only', False)
        for i in self._get_paths():
            matches = glob.glob(os.path.join(i, "*.py"))
            matches.sort()
            for path in matches:
                name, _ = os.path.splitext(path)
                if '__init__' in name:
                    continue

                if path not in self._module_cache:
                    self._module_cache[path] = self._load_module_source(name, path)

                obj = getattr(self._module_cache[path], self.class_name)
                if self.base_class:
                    # The import path is hardcoded and should be the right place,
                    # so we are not expecting an ImportError.
                    module = __import__(self.package, fromlist=[self.base_class])
                    # Check whether this obj has the required base class.
                    try:
                        plugin_class = getattr(module, self.base_class)
                    except AttributeError:
                        continue
                    if not issubclass(obj, plugin_class):
                        continue

                if not class_only:
                    obj = obj(*args, **kwargs)

                # set extra info on the module, in case we want it later
                setattr(obj, '_original_path', path)
                yield obj

action_loader = PluginLoader(
    'ActionModule',
    'ansible.plugins.action',
    C.DEFAULT_ACTION_PLUGIN_PATH,
    'action_plugins',
    required_base_class='ActionBase',
)

cache_loader = PluginLoader(
    'CacheModule',
    'ansible.plugins.cache',
    C.DEFAULT_CACHE_PLUGIN_PATH,
    'cache_plugins',
)

callback_loader = PluginLoader(
    'CallbackModule',
    'ansible.plugins.callback',
    C.DEFAULT_CALLBACK_PLUGIN_PATH,
    'callback_plugins',
)

connection_loader = PluginLoader(
    'Connection',
    'ansible.plugins.connection',
    C.DEFAULT_CONNECTION_PLUGIN_PATH,
    'connection_plugins',
    aliases={'paramiko': 'paramiko_ssh'},
    required_base_class='ConnectionBase',
)

shell_loader = PluginLoader(
    'ShellModule',
    'ansible.plugins.shell',
    'shell_plugins',
    'shell_plugins',
)

module_loader = PluginLoader(
    '',
    'ansible.modules',
    C.DEFAULT_MODULE_PATH,
    'library',
)

lookup_loader = PluginLoader(
    'LookupModule',
    'ansible.plugins.lookup',
    C.DEFAULT_LOOKUP_PLUGIN_PATH,
    'lookup_plugins',
    required_base_class='LookupBase',
)

vars_loader = PluginLoader(
    'VarsModule',
    'ansible.plugins.vars',
    C.DEFAULT_VARS_PLUGIN_PATH,
    'vars_plugins',
)

filter_loader = PluginLoader(
    'FilterModule',
    'ansible.plugins.filter',
    C.DEFAULT_FILTER_PLUGIN_PATH,
    'filter_plugins',
)

test_loader = PluginLoader(
    'TestModule',
    'ansible.plugins.test',
    C.DEFAULT_TEST_PLUGIN_PATH,
    'test_plugins'
)

fragment_loader = PluginLoader(
    'ModuleDocFragment',
    'ansible.utils.module_docs_fragments',
    os.path.join(os.path.dirname(__file__), 'module_docs_fragments'),
    '',
)

strategy_loader = PluginLoader(
    'StrategyModule',
    'ansible.plugins.strategy',
    C.DEFAULT_STRATEGY_PLUGIN_PATH,
    'strategy_plugins',
    required_base_class='StrategyBase',
)
<commit_msg>Sort plugins by basename to support ordering callbacks<commit_after># (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> and others
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import glob
import imp
import inspect
import os
import os.path
import sys
import warnings

from collections import defaultdict

from ansible import constants as C

try:
    from __main__ import display
except ImportError:
    from ansible.utils.display import Display
    display = Display()

# Global so that all instances of a PluginLoader will share the caches
MODULE_CACHE = {}
PATH_CACHE = {}
PLUGIN_PATH_CACHE = {}

def get_all_plugin_loaders():
    return [(name, obj) for (name, obj) in inspect.getmembers(sys.modules[__name__]) if isinstance(obj, PluginLoader)]

class PluginLoader:

    '''
    PluginLoader loads plugins from the configured plugin directories.

    It searches for plugins by iterating through the combined list of
    play basedirs, configured paths, and the python path.
    The first match is used.
    '''

    def __init__(self, class_name, package, config, subdir, aliases={}, required_base_class=None):

        self.class_name         = class_name
        self.base_class         = required_base_class
        self.package            = package
        self.config             = config
        self.subdir             = subdir
        self.aliases            = aliases

        if not class_name in MODULE_CACHE:
            MODULE_CACHE[class_name] = {}
        if not class_name in PATH_CACHE:
            PATH_CACHE[class_name] = None
        if not class_name in PLUGIN_PATH_CACHE:
            PLUGIN_PATH_CACHE[class_name] = defaultdict(dict)

        self._module_cache      = MODULE_CACHE[class_name]
        self._paths             = PATH_CACHE[class_name]
        self._plugin_path_cache = PLUGIN_PATH_CACHE[class_name]

        self._extra_dirs = []
        self._searched_paths = set()

    def __setstate__(self, data):
        '''
        Deserializer.
        '''

        class_name = data.get('class_name')
        package    = data.get('package')
        config     = data.get('config')
        subdir     = data.get('subdir')
        aliases    = data.get('aliases')
        base_class = data.get('base_class')

        PATH_CACHE[class_name] = data.get('PATH_CACHE')
        PLUGIN_PATH_CACHE[class_name] = data.get('PLUGIN_PATH_CACHE')

        self.__init__(class_name, package, config, subdir, aliases, base_class)
        self._extra_dirs = data.get('_extra_dirs', [])
        self._searched_paths = data.get('_searched_paths', set())

    def __getstate__(self):
        '''
        Serializer.
        '''

        return dict(
            class_name        = self.class_name,
            base_class        = self.base_class,
            package           = self.package,
            config            = self.config,
            subdir            = self.subdir,
            aliases           = self.aliases,
            _extra_dirs       = self._extra_dirs,
            _searched_paths   = self._searched_paths,
            PATH_CACHE        = PATH_CACHE[self.class_name],
            PLUGIN_PATH_CACHE = PLUGIN_PATH_CACHE[self.class_name],
        )

    def print_paths(self):
        ''' Returns a string suitable for printing of the search path '''

        # Uses a list to get the order right
        ret = []
        for i in self._get_paths():
            if i not in ret:
                ret.append(i)
        return os.pathsep.join(ret)

    def _all_directories(self, dir):
        results = []
        results.append(dir)
        for root, subdirs, files in os.walk(dir):
           if '__init__.py' in files:
               for x in subdirs:
                   results.append(os.path.join(root,x))
        return results

    def _get_package_paths(self):
        ''' Gets the path of a Python package '''

        paths = []
        if not self.package:
            return []
        if not hasattr(self, 'package_path'):
            m = __import__(self.package)
            parts = self.package.split('.')[1:]
            self.package_path = os.path.join(os.path.dirname(m.__file__), *parts)
        paths.extend(self._all_directories(self.package_path))
        return paths

    def _get_paths(self):
        ''' Return a list of paths to search for plugins in '''

        if self._paths is not None:
            return self._paths

        ret = self._extra_dirs[:]

        # look in any configured plugin paths, allow one level deep for subcategories
        if self.config is not None:
            configured_paths = self.config.split(os.pathsep)
            for path in configured_paths:
                path = os.path.realpath(os.path.expanduser(path))
                contents = glob.glob("%s/*" % path) + glob.glob("%s/*/*" % path)
                for c in contents:
                    if os.path.isdir(c) and c not in ret:
                        ret.append(c)
                if path not in ret:
                    ret.append(path)

        # look for any plugins installed in the package subtree
        ret.extend(self._get_package_paths())

        # HACK: because powershell modules are in the same directory
        # hierarchy as other modules we have to process them last.  This is
        # because powershell only works on windows but the other modules work
        # anywhere (possibly including windows if the correct language
        # interpreter is installed).  the non-powershell modules can have any
        # file extension and thus powershell modules are picked up in that.
        # The non-hack way to fix this is to have powershell modules be
        # a different PluginLoader/ModuleLoader.  But that requires changing
        # other things too (known thing to change would be PATHS_CACHE,
        # PLUGIN_PATHS_CACHE, and MODULE_CACHE.  Since those three dicts key
        # on the class_name and neither regular modules nor powershell modules
        # would have class_names, they would not work as written.
        reordered_paths = []
        win_dirs = []
        for path in ret:
            if path.endswith('windows'):
                win_dirs.append(path)
            else:
                reordered_paths.append(path)
        reordered_paths.extend(win_dirs)

        # cache and return the result
        self._paths = reordered_paths
        return reordered_paths

    def add_directory(self, directory, with_subdir=False):
        ''' Adds an additional directory to the search path '''

        directory = os.path.realpath(directory)

        if directory is not None:
            if with_subdir:
                directory = os.path.join(directory, self.subdir)
            if directory not in self._extra_dirs:
                # append the directory and invalidate the path cache
                self._extra_dirs.append(directory)
                self._paths = None

    def find_plugin(self, name, mod_type=''):
        ''' Find a plugin named name '''

        if mod_type:
            suffix = mod_type
        elif self.class_name:
            # Ansible plugins that run in the controller process (most plugins)
            suffix = '.py'
        else:
            # Only Ansible Modules.  Ansible modules can be any executable so
            # they can have any suffix
            suffix = ''

        # The particular cache to look for modules within.  This matches the
        # requested mod_type
        pull_cache = self._plugin_path_cache[suffix]
        try:
            return pull_cache[name]
        except KeyError:
            # Cache miss.  Now let's find the plugin
            pass

        # TODO: Instead of using the self._paths cache (PATH_CACHE) and
        #       self._searched_paths we could use an iterator.  Before enabling that
        #       we need to make sure we don't want to add additional directories
        #       (add_directory()) once we start using the iterator.  Currently, it
        #       looks like _get_paths() never forces a cache refresh so if we expect
        #       additional directories to be added later, it is buggy.
        for path in (p for p in self._get_paths() if p not in self._searched_paths and os.path.isdir(p)):
            try:
                full_paths = (os.path.join(path, f) for f in os.listdir(path))
            except OSError as e:
                display.warning("Error accessing plugin paths: %s" % str(e))

            for full_path in (f for f in full_paths if os.path.isfile(f) and not f.endswith('__init__.py')):
                full_name = os.path.basename(full_path)

                # HACK: We have no way of executing python byte
                # compiled files as ansible modules so specifically exclude them
                if full_path.endswith(('.pyc', '.pyo')):
                    continue

                splitname = os.path.splitext(full_name)
                base_name = splitname[0]
                try:
                    extension = splitname[1]
                except IndexError:
                    extension = ''

                # Module found, now enter it into the caches that match
                # this file
                if base_name not in self._plugin_path_cache['']:
                    self._plugin_path_cache[''][base_name] = full_path

                if full_name not in self._plugin_path_cache['']:
                    self._plugin_path_cache[''][full_name] = full_path

                if base_name not in self._plugin_path_cache[extension]:
                    self._plugin_path_cache[extension][base_name] = full_path

                if full_name not in self._plugin_path_cache[extension]:
                    self._plugin_path_cache[extension][full_name] = full_path

            self._searched_paths.add(path)
            try:
                return pull_cache[name]
            except KeyError:
                # Didn't find the plugin in this directory.  Load modules from
                # the next one
                pass

        # if nothing is found, try finding alias/deprecated
        if not name.startswith('_'):
            alias_name = '_' + name
            # We've already cached all the paths at this point
            if alias_name in pull_cache:
                if not os.path.islink(pull_cache[alias_name]):
                    display.deprecated('%s is kept for backwards compatibility '
                              'but usage is discouraged. The module '
                              'documentation details page may explain '
                              'more about this rationale.' %
                              name.lstrip('_'))
                return pull_cache[alias_name]

        return None

    def has_plugin(self, name):
        ''' Checks if a plugin named name exists '''

        return self.find_plugin(name) is not None

    __contains__ = has_plugin

    def _load_module_source(self, name, path):
        if name in sys.modules:
            # See https://github.com/ansible/ansible/issues/13110
            return sys.modules[name]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", RuntimeWarning)
            with open(path, 'r') as module_file:
                module = imp.load_source(name, path, module_file)
        return module

    def get(self, name, *args, **kwargs):
        ''' instantiates a plugin of the given name using arguments '''

        class_only = kwargs.pop('class_only', False)
        if name in self.aliases:
            name = self.aliases[name]
        path = self.find_plugin(name)
        if path is None:
            return None

        if path not in self._module_cache:
            self._module_cache[path] = self._load_module_source('.'.join([self.package, name]), path)

        obj = getattr(self._module_cache[path], self.class_name)
        if self.base_class:
            # The import path is hardcoded and should be the right place,
            # so we are not expecting an ImportError.
            module = __import__(self.package, fromlist=[self.base_class])
            # Check whether this obj has the required base class.
            try:
                plugin_class = getattr(module, self.base_class)
            except AttributeError:
                return None
            if not issubclass(obj, plugin_class):
                return None

        if not class_only:
            obj = obj(*args, **kwargs)

        return obj

    def all(self, *args, **kwargs):
        ''' instantiates all plugins with the same arguments '''

        class_only = kwargs.pop('class_only', False)
        all_matches = []

        for i in self._get_paths():
            all_matches.extend(glob.glob(os.path.join(i, "*.py")))

        for path in sorted(all_matches, key=lambda match: os.path.basename(match)):
            name, _ = os.path.splitext(path)
            if '__init__' in name:
                continue

            if path not in self._module_cache:
                self._module_cache[path] = self._load_module_source(name, path)

            obj = getattr(self._module_cache[path], self.class_name)
            if self.base_class:
                # The import path is hardcoded and should be the right place,
                # so we are not expecting an ImportError.
                module = __import__(self.package, fromlist=[self.base_class])
                # Check whether this obj has the required base class.
                try:
                   plugin_class = getattr(module, self.base_class)
                except AttributeError:
                   continue
                if not issubclass(obj, plugin_class):
                   continue

            if not class_only:
                obj = obj(*args, **kwargs)

            # set extra info on the module, in case we want it later
            setattr(obj, '_original_path', path)
            yield obj

action_loader = PluginLoader(
    'ActionModule',
    'ansible.plugins.action',
    C.DEFAULT_ACTION_PLUGIN_PATH,
    'action_plugins',
    required_base_class='ActionBase',
)

cache_loader = PluginLoader(
    'CacheModule',
    'ansible.plugins.cache',
    C.DEFAULT_CACHE_PLUGIN_PATH,
    'cache_plugins',
)

callback_loader = PluginLoader(
    'CallbackModule',
    'ansible.plugins.callback',
    C.DEFAULT_CALLBACK_PLUGIN_PATH,
    'callback_plugins',
)

connection_loader = PluginLoader(
    'Connection',
    'ansible.plugins.connection',
    C.DEFAULT_CONNECTION_PLUGIN_PATH,
    'connection_plugins',
    aliases={'paramiko': 'paramiko_ssh'},
    required_base_class='ConnectionBase',
)

shell_loader = PluginLoader(
    'ShellModule',
    'ansible.plugins.shell',
    'shell_plugins',
    'shell_plugins',
)

module_loader = PluginLoader(
    '',
    'ansible.modules',
    C.DEFAULT_MODULE_PATH,
    'library',
)

lookup_loader = PluginLoader(
    'LookupModule',
    'ansible.plugins.lookup',
    C.DEFAULT_LOOKUP_PLUGIN_PATH,
    'lookup_plugins',
    required_base_class='LookupBase',
)

vars_loader = PluginLoader(
    'VarsModule',
    'ansible.plugins.vars',
    C.DEFAULT_VARS_PLUGIN_PATH,
    'vars_plugins',
)

filter_loader = PluginLoader(
    'FilterModule',
    'ansible.plugins.filter',
    C.DEFAULT_FILTER_PLUGIN_PATH,
    'filter_plugins',
)

test_loader = PluginLoader(
    'TestModule',
    'ansible.plugins.test',
    C.DEFAULT_TEST_PLUGIN_PATH,
    'test_plugins'
)

fragment_loader = PluginLoader(
    'ModuleDocFragment',
    'ansible.utils.module_docs_fragments',
    os.path.join(os.path.dirname(__file__), 'module_docs_fragments'),
    '',
)

strategy_loader = PluginLoader(
    'StrategyModule',
    'ansible.plugins.strategy',
    C.DEFAULT_STRATEGY_PLUGIN_PATH,
    'strategy_plugins',
    required_base_class='StrategyBase',
)
<|endoftext|>


<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#     ||          ____  _ __
#  +------+      / __ )(_) /_______________ _____  ___
#  | 0xBC |     / __  / / __/ ___/ ___/ __ `/_  / / _ \
#  +------+    / /_/ / / /_/ /__/ /  / /_/ / / /_/  __/
#   ||  ||    /_____/_/\__/\___/_/   \__,_/ /___/\___/
#
#  Copyright (C) 2011-2013 Bitcraze AB
#
#  Crazyflie Nano Quadcopter Client
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.

#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

"""
This tab plots different logging data defined by configurations that has been
pre-configured.
"""

__author__ = 'Bitcraze AB'
__all__ = ['PlotTab']

import glob
import json
import logging
import os
import sys

logger = logging.getLogger(__name__)

from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import pyqtSlot, pyqtSignal, QThread, Qt
from PyQt4.QtGui import QMessageBox
from PyQt4.QtGui import QApplication, QStyledItemDelegate, QAbstractItemView
from PyQt4.QtCore import QAbstractItemModel, QModelIndex, QString, QVariant

from pprint import pprint
import datetime

from cfclient.ui.widgets.plotwidget import PlotWidget

from cflib.crazyflie.log import Log

from cfclient.ui.tab import Tab

plot_tab_class = uic.loadUiType(sys.path[0] +
                                "/cfclient/ui/tabs/plotTab.ui")[0]

class LogConfigModel(QAbstractItemModel):
    """Model for log configurations in the ComboBox"""
    def __init__(self, parent=None):
        super(LogConfigModel, self).__init__(parent)
        self._nodes = []

    def add_block(self, block):
        self._nodes.append(block)
        self.layoutChanged.emit()

    def parent(self, index):
        """Re-implemented method to get the parent of the given index"""
        return QModelIndex()

    def remove_block(self, block):
        """Remove a block from the view"""
        raise NotImplementedError()

    def columnCount(self, parent):
        """Re-implemented method to get the number of columns"""
        return 1

    def rowCount(self, parent):
        """Re-implemented method to get the number of rows for a given index"""
        parent_item = parent.internalPointer()
        if parent.isValid():
            parent_item = parent.internalPointer()
            return 0
        else:
            return len(self._nodes)

    def index(self, row, column, parent):
        """Re-implemented method to get the index for a specified
        row/column/parent combination"""
        if not self._nodes:
            return QModelIndex()
        node = parent.internalPointer()
        if not node:
            index = self.createIndex(row, column, self._nodes[row])
            return index
        else:
            return self.createIndex(row, column, node.get_child(row))

    def data(self, index, role):
        """Re-implemented method to get the data for a given index and role"""
        node = index.internalPointer()
        if not index.isValid() or not 0 <= index.row() < len(self._nodes):
            return QVariant()
        if role == Qt.DisplayRole:
            return self._nodes[index.row()].name
        return QVariant()

    def reset(self):
        """Reset the model"""
        self._nodes = []
        self.layoutChanged.emit()

    def get_config(self, i):
        return self._nodes[i]

class PlotTab(Tab, plot_tab_class):
    """Tab for plotting logging data"""

    _log_data_signal = pyqtSignal(int, object, object)
    _log_error_signal = pyqtSignal(object, str)
    _disconnected_signal = pyqtSignal(str)

    colors = ['g', 'b', 'm', 'r', 'y', 'c']

    def __init__(self, tabWidget, helper, *args):
        super(PlotTab, self).__init__(*args)
        self.setupUi(self)

        self.tabName = "Plotter"
        self.menuName = "Plotter"

        self._log_error_signal.connect(self._logging_error)

        self._plot = PlotWidget(fps=30)
        # Check if we could find the PyQtImport. If not, then
        # set this tab as disabled
        self.enabled = self._plot.can_enable

        self._model = LogConfigModel()
        self.dataSelector.setModel(self._model)
        self._log_data_signal.connect(self._log_data_received)
        self.tabWidget = tabWidget
        self.helper = helper
        self.plotLayout.addWidget(self._plot)

        # Connect external signals if we can use the tab
        if self.enabled:
            self._disconnected_signal.connect(self._disconnected)
            self.helper.cf.disconnected.add_callback(
                self._disconnected_signal.emit)

            self.helper.cf.log.block_added_cb.add_callback(self._config_added)
            self.dataSelector.currentIndexChanged.connect(
                self._selection_changed)

        self._previous_config = None
        self._started_previous = False

    def _disconnected(self, link_uri):
        """Callback for when the Crazyflie has been disconnected"""
        self._model.reset()
        self._plot.removeAllDatasets()
        self._plot.set_title("")
        self.dataSelector.setCurrentIndex(-1)
        self._previous_config = None
        self._started_previous = False

    def _log_data_signal_wrapper(self, ts, data, logconf):
        """Wrapper for signal"""

        # For some reason the *.emit functions are not
        # the same over time (?!) so they cannot be registered and then
        # removed as callbacks.
        self._log_data_signal.emit(ts, data, logconf)

    def _log_error_signal_wrapper(self, config, msg):
        """Wrapper for signal"""

        # For some reason the *.emit functions are not
        # the same over time (?!) so they cannot be registered and then
        # removed as callbacks.
        self._log_error_signal.emit(config, msg)

    def _selection_changed(self, i):
        """Callback from ComboBox when a new item has been selected"""

        # Check if we have disconnected
        if i < 0:
            return
        # First check if we need to stop the old block
        if self._started_previous and self._previous_config:
            logger.debug("Should stop config [%s], stopping!",
                        self._previous_config.name)
            self._previous_config.delete()

        # Remove our callback for the previous config
        if self._previous_config:
            self._previous_config.data_received_cb.remove_callback(
                self._log_data_signal_wrapper)
            self._previous_config.error_cb.remove_callback(
                self._log_error_signal_wrapper)

        lg = self._model.get_config(i)
        if not lg.started:
            logger.debug("Config [%s] not started, starting!", lg.name)
            self._started_previous = True
            lg.start()
        else:
            self._started_previous = False
        self._plot.removeAllDatasets()
        color_selector = 0

        self._plot.set_title(lg.name)

        for d in lg.variables:
            self._plot.add_curve(d.name,
                                self.colors[color_selector % len(self.colors)])
            color_selector += 1
        lg.data_received_cb.add_callback(self._log_data_signal_wrapper)
        lg.error_cb.add_callback(self._log_error_signal_wrapper)

        self._previous_config = lg

    def _config_added(self, logconfig):
        """Callback from the log layer when a new config has been added"""
        logger.debug("Callback for new config [%s]", logconfig.name)
        self._model.add_block(logconfig)

    def _logging_error(self, log_conf, msg):
        """Callback from the log layer when an error occurs"""
        QMessageBox.about(self, "Plot error", "Error when starting log config"
                " [%s]: %s" % (log_conf.name, msg))

    def _log_data_received(self, timestamp, data, logconf):
        """Callback when the log layer receives new data"""

        # Check so that the incoming data belongs to what we are currently
        # logging
        if self._previous_config:
            if self._previous_config.name == logconf.name:
                self._plot.add_data(data, timestamp)
<commit_msg>Clear plot on connect, not on disconnect. Closes #113.<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#     ||          ____  _ __
#  +------+      / __ )(_) /_______________ _____  ___
#  | 0xBC |     / __  / / __/ ___/ ___/ __ `/_  / / _ \
#  +------+    / /_/ / / /_/ /__/ /  / /_/ / / /_/  __/
#   ||  ||    /_____/_/\__/\___/_/   \__,_/ /___/\___/
#
#  Copyright (C) 2011-2013 Bitcraze AB
#
#  Crazyflie Nano Quadcopter Client
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.

#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

"""
This tab plots different logging data defined by configurations that has been
pre-configured.
"""

__author__ = 'Bitcraze AB'
__all__ = ['PlotTab']

import glob
import json
import logging
import os
import sys

logger = logging.getLogger(__name__)

from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import pyqtSlot, pyqtSignal, QThread, Qt
from PyQt4.QtGui import QMessageBox
from PyQt4.QtGui import QApplication, QStyledItemDelegate, QAbstractItemView
from PyQt4.QtCore import QAbstractItemModel, QModelIndex, QString, QVariant

from pprint import pprint
import datetime

from cfclient.ui.widgets.plotwidget import PlotWidget

from cflib.crazyflie.log import Log

from cfclient.ui.tab import Tab

plot_tab_class = uic.loadUiType(sys.path[0] +
                                "/cfclient/ui/tabs/plotTab.ui")[0]

class LogConfigModel(QAbstractItemModel):
    """Model for log configurations in the ComboBox"""
    def __init__(self, parent=None):
        super(LogConfigModel, self).__init__(parent)
        self._nodes = []

    def add_block(self, block):
        self._nodes.append(block)
        self.layoutChanged.emit()

    def parent(self, index):
        """Re-implemented method to get the parent of the given index"""
        return QModelIndex()

    def remove_block(self, block):
        """Remove a block from the view"""
        raise NotImplementedError()

    def columnCount(self, parent):
        """Re-implemented method to get the number of columns"""
        return 1

    def rowCount(self, parent):
        """Re-implemented method to get the number of rows for a given index"""
        parent_item = parent.internalPointer()
        if parent.isValid():
            parent_item = parent.internalPointer()
            return 0
        else:
            return len(self._nodes)

    def index(self, row, column, parent):
        """Re-implemented method to get the index for a specified
        row/column/parent combination"""
        if not self._nodes:
            return QModelIndex()
        node = parent.internalPointer()
        if not node:
            index = self.createIndex(row, column, self._nodes[row])
            return index
        else:
            return self.createIndex(row, column, node.get_child(row))

    def data(self, index, role):
        """Re-implemented method to get the data for a given index and role"""
        node = index.internalPointer()
        if not index.isValid() or not 0 <= index.row() < len(self._nodes):
            return QVariant()
        if role == Qt.DisplayRole:
            return self._nodes[index.row()].name
        return QVariant()

    def reset(self):
        """Reset the model"""
        self._nodes = []
        self.layoutChanged.emit()

    def get_config(self, i):
        return self._nodes[i]

class PlotTab(Tab, plot_tab_class):
    """Tab for plotting logging data"""

    _log_data_signal = pyqtSignal(int, object, object)
    _log_error_signal = pyqtSignal(object, str)
    _disconnected_signal = pyqtSignal(str)
    _connected_signal = pyqtSignal(str)

    colors = ['g', 'b', 'm', 'r', 'y', 'c']

    def __init__(self, tabWidget, helper, *args):
        super(PlotTab, self).__init__(*args)
        self.setupUi(self)

        self.tabName = "Plotter"
        self.menuName = "Plotter"

        self._log_error_signal.connect(self._logging_error)

        self._plot = PlotWidget(fps=30)
        # Check if we could find the PyQtImport. If not, then
        # set this tab as disabled
        self.enabled = self._plot.can_enable

        self._model = LogConfigModel()
        self.dataSelector.setModel(self._model)
        self._log_data_signal.connect(self._log_data_received)
        self.tabWidget = tabWidget
        self.helper = helper
        self.plotLayout.addWidget(self._plot)

        # Connect external signals if we can use the tab
        if self.enabled:
            self._disconnected_signal.connect(self._disconnected)
            self.helper.cf.disconnected.add_callback(
                self._disconnected_signal.emit)

            self._connected_signal.connect(self._connected)
            self.helper.cf.connected.add_callback(
                self._connected_signal.emit)

            self.helper.cf.log.block_added_cb.add_callback(self._config_added)
            self.dataSelector.currentIndexChanged.connect(
                self._selection_changed)

        self._previous_config = None
        self._started_previous = False

    def _connected(self, link_uri):
        """Callback when the Crazyflie has been disconnected"""
        self._plot.removeAllDatasets()
        self._plot.set_title("")

    def _disconnected(self, link_uri):
        """Callback for when the Crazyflie has been disconnected"""
        self._model.reset()
        self.dataSelector.setCurrentIndex(-1)
        self._previous_config = None
        self._started_previous = False

    def _log_data_signal_wrapper(self, ts, data, logconf):
        """Wrapper for signal"""

        # For some reason the *.emit functions are not
        # the same over time (?!) so they cannot be registered and then
        # removed as callbacks.
        self._log_data_signal.emit(ts, data, logconf)

    def _log_error_signal_wrapper(self, config, msg):
        """Wrapper for signal"""

        # For some reason the *.emit functions are not
        # the same over time (?!) so they cannot be registered and then
        # removed as callbacks.
        self._log_error_signal.emit(config, msg)

    def _selection_changed(self, i):
        """Callback from ComboBox when a new item has been selected"""

        # Check if we have disconnected
        if i < 0:
            return
        # First check if we need to stop the old block
        if self._started_previous and self._previous_config:
            logger.debug("Should stop config [%s], stopping!",
                        self._previous_config.name)
            self._previous_config.delete()

        # Remove our callback for the previous config
        if self._previous_config:
            self._previous_config.data_received_cb.remove_callback(
                self._log_data_signal_wrapper)
            self._previous_config.error_cb.remove_callback(
                self._log_error_signal_wrapper)

        lg = self._model.get_config(i)
        if not lg.started:
            logger.debug("Config [%s] not started, starting!", lg.name)
            self._started_previous = True
            lg.start()
        else:
            self._started_previous = False
        self._plot.removeAllDatasets()
        color_selector = 0

        self._plot.set_title(lg.name)

        for d in lg.variables:
            self._plot.add_curve(d.name,
                                self.colors[color_selector % len(self.colors)])
            color_selector += 1
        lg.data_received_cb.add_callback(self._log_data_signal_wrapper)
        lg.error_cb.add_callback(self._log_error_signal_wrapper)

        self._previous_config = lg

    def _config_added(self, logconfig):
        """Callback from the log layer when a new config has been added"""
        logger.debug("Callback for new config [%s]", logconfig.name)
        self._model.add_block(logconfig)

    def _logging_error(self, log_conf, msg):
        """Callback from the log layer when an error occurs"""
        QMessageBox.about(self, "Plot error", "Error when starting log config"
                " [%s]: %s" % (log_conf.name, msg))

    def _log_data_received(self, timestamp, data, logconf):
        """Callback when the log layer receives new data"""

        # Check so that the incoming data belongs to what we are currently
        # logging
        if self._previous_config:
            if self._previous_config.name == logconf.name:
                self._plot.add_data(data, timestamp)
<|endoftext|>


<commit_before>import sys
import os
sys.path.insert(0, os.path.abspath('.'))

from maddux.robots import simple_human_arm
from maddux.environment import Environment
from maddux.objects import Ball, Obstacle
import numpy as np

POS_CHANGE = 2.0
ACCURACY = 0.05

class Planning(object):

    def __init__(self, num_joints=4):
        self.actions = [-1, 1] * num_joints
        self.move_count = 0
        self.collected_rewards = []
        self.num_actions = len(self.actions)
        self.observation_size = len(self.actions)

        self.obstacles = [Obstacle([1, 2, 1], [2, 2.5, 1.5]),
                          Obstacle([3, 2, 1], [4, 2.5, 1.5])]
        self.ball = Ball([2.5, 2.5, 2.0], 0.25)

        q = np.array([0, 0, 0, np.pi / 2, 0, 0, 0])
        self.robot = simple_human_arm(1.0, 1.0, q, np.array([2.0, 2.0, 2.0]))

        room_dimensions = np.array([10.0, 10.0, 20.0])
        self.env = Environment(room_dimensions,
                               dynamic_objects=[self.ball],
                               static_objects=self.obstacles,
                               robot=self.robot)

    def observe(self):
        """Returns current observation"""
        distances = []
        for i, action in enumerate(self.actions):
            link = i / 2

            # Calculate old and new link positions
            q_old = self.robot.links[link].theta
            q_new = q_old + action * POS_CHANGE

            # Change to new angle to check the end effector position
            self.robot.update_link_angle(link, q_new)
            end_effector_pos = self.robot.end_effector_position()

            # Then reset back to old angle
            self.robot.update_link_angle(link, q_old)

            # Find the distance from our target (the ball)
            distance = np.linalg.norm(end_effector_pos -\
                                      self.ball.position)
            distances.append(distance)
        return np.array(distances)

    def perform_action(self, action):
        """Update internal state to reflect the fact that an
           action was taken
        :param action: Number of the action performed
        """
        # Update a specific links velocity
        link = action / 2
        q_old = self.robot.links[link].theta
        q_new = q_old + self.actions[action] * POS_CHANGE

        self.robot.update_link_angle(link, q_new)
        self.move_count += 1

    def is_over(self):
        """Check if simulation is over"""
        target = self.ball.position
        end_effector = self.robot.end_effector_position()
        return np.linalg.norm(target - end_effector) < ACCURACY

    def collect_reward(self, action):
        """Returns reward accumulated since last time this
        function was called.
        """
        # Calculate previous distance to target object (ball)
        old_dist = np.linalg.norm((self.robot.end_effector_position() -
                                   self.ball.position))
        # Then perform the action
        self.perform_action(action)

        for obstacle in self.obstacles:
            if self.robot.is_in_collision(obstacle):
                self.collected_rewards.append(-1)
                return -1

        # Find the distance from our target (the ball)
        new_dist = np.linalg.norm((self.robot.end_effector_position() -
                                   self.ball.position))
        # Reward it if it decreased the distance between end effector
        # and target (ball).
        reward = old_dist - new_dist
        self.collected_rewards.append(reward)
        return reward

    def display_actions(self):
        print "Moved {} times before throwing!".format(self.move_count)
        print "Last reward: {}".format(self.collected_rewards[-1])

<commit_msg>Changed position change per step to be reasonable<commit_after>import sys
import os
sys.path.insert(0, os.path.abspath('.'))

from maddux.robots import simple_human_arm
from maddux.environment import Environment
from maddux.objects import Ball, Obstacle
import numpy as np

POS_CHANGE = 0.1
ACCURACY = 0.05

class Planning(object):

    def __init__(self, num_joints=4):
        self.actions = [-1, 1] * num_joints
        self.move_count = 0
        self.collected_rewards = []
        self.num_actions = len(self.actions)
        self.observation_size = len(self.actions)

        self.obstacles = [Obstacle([1, 2, 1], [2, 2.5, 1.5]),
                          Obstacle([3, 2, 1], [4, 2.5, 1.5])]
        self.ball = Ball([2.5, 2.5, 2.0], 0.25)

        q = np.array([0, 0, 0, np.pi / 2, 0, 0, 0])
        self.robot = simple_human_arm(1.0, 1.0, q, np.array([2.0, 2.0, 2.0]))

        room_dimensions = np.array([10.0, 10.0, 20.0])
        self.env = Environment(room_dimensions,
                               dynamic_objects=[self.ball],
                               static_objects=self.obstacles,
                               robot=self.robot)

    def observe(self):
        """Returns current observation"""
        distances = []
        for i, action in enumerate(self.actions):
            link = i / 2

            # Calculate old and new link positions
            q_old = self.robot.links[link].theta
            q_new = q_old + action * POS_CHANGE

            # Change to new angle to check the end effector position
            self.robot.update_link_angle(link, q_new)
            end_effector_pos = self.robot.end_effector_position()

            # Then reset back to old angle
            self.robot.update_link_angle(link, q_old)

            # Find the distance from our target (the ball)
            distance = np.linalg.norm(end_effector_pos -\
                                      self.ball.position)
            distances.append(distance)
        return np.array(distances)

    def perform_action(self, action):
        """Update internal state to reflect the fact that an
           action was taken
        :param action: Number of the action performed
        """
        # Update a specific links velocity
        link = action / 2
        q_old = self.robot.links[link].theta
        q_new = q_old + self.actions[action] * POS_CHANGE

        self.robot.update_link_angle(link, q_new)
        self.move_count += 1

    def is_over(self):
        """Check if simulation is over"""
        target = self.ball.position
        end_effector = self.robot.end_effector_position()
        return np.linalg.norm(target - end_effector) < ACCURACY

    def collect_reward(self, action):
        """Returns reward accumulated since last time this
        function was called.
        """
        # Calculate previous distance to target object (ball)
        old_dist = np.linalg.norm((self.robot.end_effector_position() -
                                   self.ball.position))
        # Then perform the action
        self.perform_action(action)

        for obstacle in self.obstacles:
            if self.robot.is_in_collision(obstacle):
                self.collected_rewards.append(-1)
                return -1

        # Find the distance from our target (the ball)
        new_dist = np.linalg.norm((self.robot.end_effector_position() -
                                   self.ball.position))
        # Reward it if it decreased the distance between end effector
        # and target (ball).
        reward = old_dist - new_dist
        self.collected_rewards.append(reward)
        return reward

    def display_actions(self):
        print "Moved {} times before throwing!".format(self.move_count)
        print "Last reward: {}".format(self.collected_rewards[-1])

<|endoftext|>


<commit_before>#    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.

"""Magnum Kubernetes RPC handler."""

from oslo_config import cfg

from magnum.common import clients
from magnum.common import exception
from magnum.conductor.handlers.common import kube_utils
from magnum import objects
from magnum.openstack.common._i18n import _
from magnum.openstack.common._i18n import _LW
from magnum.openstack.common import log as logging

LOG = logging.getLogger(__name__)

kubernetes_opts = [
    cfg.StrOpt('k8s_protocol',
               default='http',
               help=_('Default protocol of k8s master endpoint'
               ' (http or https).')),
    cfg.IntOpt('k8s_port',
               default=8080,
               help=_('Default port of the k8s master endpoint.')),
]

cfg.CONF.register_opts(kubernetes_opts, group='kubernetes')

def _retrieve_bay(context, obj):
    bay_uuid = obj.bay_uuid
    return objects.Bay.get_by_uuid(context, bay_uuid)

def _retrieve_baymodel(context, obj):
    return objects.BayModel.get_by_uuid(context, obj.baymodel_id)

def _retrieve_k8s_master_url(context, obj):
    apiserver_port = cfg.CONF.kubernetes.k8s_port
    if hasattr(obj, 'bay_uuid'):
        obj = _retrieve_bay(context, obj)

    baymodel = _retrieve_baymodel(context, obj)
    if baymodel.apiserver_port is not None:
        apiserver_port = baymodel.apiserver_port

    params = {
        'k8s_protocol': cfg.CONF.kubernetes.k8s_protocol,
        'k8s_port': apiserver_port,
        'api_address': obj.api_address
    }
    return "%(k8s_protocol)s://%(api_address)s:%(k8s_port)s" % params

def _object_has_stack(context, obj):
    osc = clients.OpenStackClients(context)
    if hasattr(obj, 'bay_uuid'):
        obj = _retrieve_bay(context, obj)

    stack = osc.heat().stacks.get(obj.stack_id)
    if (stack.stack_status == 'DELETE_COMPLETE' or
       stack.stack_status == 'DELETE_IN_PROGRESS'):
        return False
    else:
        return True

class Handler(object):
    """These are the backend operations.  They are executed by the backend
         service.  API calls via AMQP (within the ReST API) trigger the
         handlers to be called.

         This handler acts as an interface to executes kubectl command line
         services.
    """

    def __init__(self):
        super(Handler, self).__init__()
        self.kube_cli = kube_utils.KubeClient()

    def service_create(self, context, service):
        LOG.debug("service_create")
        k8s_master_url = _retrieve_k8s_master_url(context, service)
        # trigger a kubectl command
        status = self.kube_cli.service_create(k8s_master_url, service)
        if not status:
            return None
        # call the service object to persist in db
        service.create(context)
        return service

    def service_update(self, context, service):
        LOG.debug("service_update %s", service.uuid)
        # trigger a kubectl command
        status = self.kube_cli.service_update(service)
        if not status:
            return None
        # call the service object to persist in db
        service.refresh(context)
        return service

    def service_list(self, context):
        LOG.debug("service_list")
        return self.kube_cli.service_list()

    def service_delete(self, context, uuid):
        LOG.debug("service_delete %s", uuid)
        service = objects.Service.get_by_uuid(context, uuid)
        k8s_master_url = _retrieve_k8s_master_url(context, service)
        if _object_has_stack(context, service):
            # trigger a kubectl command
            status = self.kube_cli.service_delete(k8s_master_url, service.name)
            if not status:
                return None
        # call the service object to persist in db
        service.destroy(context)

    def service_get(self, context, uuid):
        LOG.debug("service_get %s", uuid)
        return self.kube_cli.service_get(uuid)

    def service_show(self, uuid):
        LOG.debug("service_show %s", uuid)
        return self.kube_cli.service_show(uuid)

    # Pod Operations
    def pod_create(self, context, pod):
        LOG.debug("pod_create")
        k8s_master_url = _retrieve_k8s_master_url(context, pod)
        # trigger a kubectl command
        status = self.kube_cli.pod_create(k8s_master_url, pod)
        # TODO(yuanying): Is this correct location of updating status?
        if not status:
            pod.status = 'failed'
        else:
            pod.status = 'pending'
        # call the pod object to persist in db
        # TODO(yuanying): parse pod file and,
        # - extract pod name and set it
        # - extract pod labels and set it
        # TODO(yuanying): Should kube_utils support definition_url?
        # When do we get pod labels and name?
        pod.create()
        return pod

    def pod_update(self, context, pod):
        LOG.debug("pod_update %s", pod.uuid)
        # trigger a kubectl command
        status = self.kube_cli.pod_update(pod)
        if not status:
            return None
        # call the pod object to persist in db
        pod.refresh(context)
        return pod

    def pod_list(self, context):
        LOG.debug("pod_list")
        return self.kube_cli.pod_list()

    def pod_delete(self, context, uuid):
        LOG.debug("pod_delete %s", uuid)
        # trigger a kubectl command
        pod = objects.Pod.get_by_uuid(context, uuid)
        k8s_master_url = _retrieve_k8s_master_url(context, pod)
        if _object_has_stack(context, pod):
            try:
                status = self.kube_cli.pod_delete(k8s_master_url, pod.name)

                if not status:
                    return None
            except exception.PodNotFound:
                msg = _LW("Pod '%s' not found on bay, "
                       "continuing to delete from database.")
                LOG.warn(msg, uuid)
        # call the pod object to persist in db
        pod.destroy(context)

    def pod_get(self, context, uuid):
        LOG.debug("pod_get %s", uuid)
        return self.kube_cli.pod_get(uuid)

    def pod_show(self, context, uuid):
        LOG.debug("pod_show %s", uuid)
        return self.kube_cli.pod_show(uuid)

    # Replication Controller Operations
    def rc_create(self, context, rc):
        LOG.debug("rc_create")
        k8s_master_url = _retrieve_k8s_master_url(context, rc)
        # trigger a kubectl command
        status = self.kube_cli.rc_create(k8s_master_url, rc)
        if not status:
            return None
        # call the rc object to persist in db
        rc.create(context)
        return rc

    def rc_update(self, context, rc):
        LOG.debug("rc_update %s", rc.uuid)
        # trigger a kubectl command
        status = self.kube_cli.rc_update(rc)
        if not status:
            return None
        # call the rc object to persist in db
        rc.refresh(context)
        return rc

    def rc_delete(self, context, uuid):
        LOG.debug("rc_delete %s", uuid)
        rc = objects.ReplicationController.get_by_uuid(context, uuid)
        k8s_master_url = _retrieve_k8s_master_url(context, rc)
        if _object_has_stack(context, rc):
            # trigger a kubectl command
            status = self.kube_cli.rc_delete(k8s_master_url, rc.name)
            if not status:
                return None
        # call the rc object to persist in db
        rc.destroy(context)
<commit_msg>Remove unused code in kube handler<commit_after>#    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.

"""Magnum Kubernetes RPC handler."""

from oslo_config import cfg

from magnum.common import clients
from magnum.common import exception
from magnum.conductor.handlers.common import kube_utils
from magnum import objects
from magnum.openstack.common._i18n import _
from magnum.openstack.common._i18n import _LW
from magnum.openstack.common import log as logging

LOG = logging.getLogger(__name__)

kubernetes_opts = [
    cfg.StrOpt('k8s_protocol',
               default='http',
               help=_('Default protocol of k8s master endpoint'
               ' (http or https).')),
    cfg.IntOpt('k8s_port',
               default=8080,
               help=_('Default port of the k8s master endpoint.')),
]

cfg.CONF.register_opts(kubernetes_opts, group='kubernetes')

def _retrieve_bay(context, obj):
    bay_uuid = obj.bay_uuid
    return objects.Bay.get_by_uuid(context, bay_uuid)

def _retrieve_baymodel(context, obj):
    return objects.BayModel.get_by_uuid(context, obj.baymodel_id)

def _retrieve_k8s_master_url(context, obj):
    apiserver_port = cfg.CONF.kubernetes.k8s_port
    if hasattr(obj, 'bay_uuid'):
        obj = _retrieve_bay(context, obj)

    baymodel = _retrieve_baymodel(context, obj)
    if baymodel.apiserver_port is not None:
        apiserver_port = baymodel.apiserver_port

    params = {
        'k8s_protocol': cfg.CONF.kubernetes.k8s_protocol,
        'k8s_port': apiserver_port,
        'api_address': obj.api_address
    }
    return "%(k8s_protocol)s://%(api_address)s:%(k8s_port)s" % params

def _object_has_stack(context, obj):
    osc = clients.OpenStackClients(context)
    if hasattr(obj, 'bay_uuid'):
        obj = _retrieve_bay(context, obj)

    stack = osc.heat().stacks.get(obj.stack_id)
    if (stack.stack_status == 'DELETE_COMPLETE' or
       stack.stack_status == 'DELETE_IN_PROGRESS'):
        return False
    else:
        return True

class Handler(object):
    """These are the backend operations.  They are executed by the backend
         service.  API calls via AMQP (within the ReST API) trigger the
         handlers to be called.

         This handler acts as an interface to executes kubectl command line
         services.
    """

    def __init__(self):
        super(Handler, self).__init__()
        self.kube_cli = kube_utils.KubeClient()

    def service_create(self, context, service):
        LOG.debug("service_create")
        k8s_master_url = _retrieve_k8s_master_url(context, service)
        # trigger a kubectl command
        status = self.kube_cli.service_create(k8s_master_url, service)
        if not status:
            return None
        # call the service object to persist in db
        service.create(context)
        return service

    def service_update(self, context, service):
        LOG.debug("service_update %s", service.uuid)
        # trigger a kubectl command
        status = self.kube_cli.service_update(service)
        if not status:
            return None
        # call the service object to persist in db
        service.refresh(context)
        return service

    def service_delete(self, context, uuid):
        LOG.debug("service_delete %s", uuid)
        service = objects.Service.get_by_uuid(context, uuid)
        k8s_master_url = _retrieve_k8s_master_url(context, service)
        if _object_has_stack(context, service):
            # trigger a kubectl command
            status = self.kube_cli.service_delete(k8s_master_url, service.name)
            if not status:
                return None
        # call the service object to persist in db
        service.destroy(context)

    # Pod Operations
    def pod_create(self, context, pod):
        LOG.debug("pod_create")
        k8s_master_url = _retrieve_k8s_master_url(context, pod)
        # trigger a kubectl command
        status = self.kube_cli.pod_create(k8s_master_url, pod)
        # TODO(yuanying): Is this correct location of updating status?
        if not status:
            pod.status = 'failed'
        else:
            pod.status = 'pending'
        # call the pod object to persist in db
        # TODO(yuanying): parse pod file and,
        # - extract pod name and set it
        # - extract pod labels and set it
        # TODO(yuanying): Should kube_utils support definition_url?
        # When do we get pod labels and name?
        pod.create()
        return pod

    def pod_update(self, context, pod):
        LOG.debug("pod_update %s", pod.uuid)
        # trigger a kubectl command
        status = self.kube_cli.pod_update(pod)
        if not status:
            return None
        # call the pod object to persist in db
        pod.refresh(context)
        return pod

    def pod_delete(self, context, uuid):
        LOG.debug("pod_delete %s", uuid)
        # trigger a kubectl command
        pod = objects.Pod.get_by_uuid(context, uuid)
        k8s_master_url = _retrieve_k8s_master_url(context, pod)
        if _object_has_stack(context, pod):
            try:
                status = self.kube_cli.pod_delete(k8s_master_url, pod.name)

                if not status:
                    return None
            except exception.PodNotFound:
                msg = _LW("Pod '%s' not found on bay, "
                       "continuing to delete from database.")
                LOG.warn(msg, uuid)
        # call the pod object to persist in db
        pod.destroy(context)

    # Replication Controller Operations
    def rc_create(self, context, rc):
        LOG.debug("rc_create")
        k8s_master_url = _retrieve_k8s_master_url(context, rc)
        # trigger a kubectl command
        status = self.kube_cli.rc_create(k8s_master_url, rc)
        if not status:
            return None
        # call the rc object to persist in db
        rc.create(context)
        return rc

    def rc_update(self, context, rc):
        LOG.debug("rc_update %s", rc.uuid)
        # trigger a kubectl command
        status = self.kube_cli.rc_update(rc)
        if not status:
            return None
        # call the rc object to persist in db
        rc.refresh(context)
        return rc

    def rc_delete(self, context, uuid):
        LOG.debug("rc_delete %s", uuid)
        rc = objects.ReplicationController.get_by_uuid(context, uuid)
        k8s_master_url = _retrieve_k8s_master_url(context, rc)
        if _object_has_stack(context, rc):
            # trigger a kubectl command
            status = self.kube_cli.rc_delete(k8s_master_url, rc.name)
            if not status:
                return None
        # call the rc object to persist in db
        rc.destroy(context)
<|endoftext|>


<commit_before>from ckan.common import request, _
from pylons import config
from ckan import model
from ckan.lib.mailer import mail_user
from ckan.lib.mailer import mail_recipient
from ckanext.reminder.model import Reminder, ReminderSubscriptionPackageAssociation
from ckan.logic import ValidationError

import ckan.logic as logic
import datetime

log = __import__('logging').getLogger(__name__)

def send_email_reminders(context, data_dict):
    if request.environ.get('paste.command_request'):
        send_reminders()

def get_datasets_with_reminders():
    now = datetime.datetime.now()
    search_dict = {
        'fq': 'reminder:' + now.strftime("%Y-%m-%d")
    }

    return logic.get_action('package_search')({}, search_dict)

def send_reminders():
    '''
    Sends reminder emails to site admin of datasets which have a reminder date set
    '''

    items = get_datasets_with_reminders()

    try:
        username = config.get('ckanext.reminder.recipient_username')
        recipient = model.User.get(username)

        if(items['results']):
            log.info('Number of datasets with reminders found: ' + len(items['results']))

        for item in items['results']:

            message_body = _('This is a reminder of a dataset expiration') + ': ' + config.get('ckanext.reminder.site_url') + '/dataset/' + item['name']
            mail_user(recipient, _('CKAN reminder'), message_body)

        if len(items['results']) == 0:
            log.debug("No pending reminders for current date")
        else:
            log.debug("Reminder emails sent")

    except Exception, ex:
        log.exception(ex)
        raise

def get_packages_for_user(context, data_dict):
    '''
    Returns a list of packages for the provided email address
    Returns an empty list if the unsubscribe does not match to the subscriber's token
    '''
    subscriber_email = data_dict.get('subscriber_email')
    unsubscribe_token = data_dict.get('unsubscribe_token')
    subscriber = Reminder.get_subscriber_dict(subscriber_email)

    packages = []

    # Make sure the given unsubscribe token matches the stored one
    # Return empty list if no match, for security
    if subscriber and subscriber.get('unsubscribe_token') == unsubscribe_token:
        package_ids = ReminderSubscriptionPackageAssociation.get_subscriber_package_ids(subscriber.get('id'))

        for package_id in package_ids:
            package = logic.get_action('package_show')({}, { 'name_or_id': package_id.package_id })
            if package:
                packages.append(package)
    return packages

def get_updated_packages_for_user(subscriber_id, previous_reminder_sent):
    subscriptions = ReminderSubscriptionPackageAssociation.get_subscriber_package_ids(subscriber_id)

    updated_packages = []
    for subscription in subscriptions:
        updated_package = logic.get_action('package_show')({}, { 'name_or_id': subscription.package_id })

        if updated_package:
            # Notify user of an updated package if not already notified
            if updated_package['metadata_modified'] > previous_reminder_sent:
                updated_packages.append(updated_package)

    return updated_packages

def send_notifications():
    '''
    Sends notification emails to site users of dataset changes
    The user needs to subscribe to the dataset to receive emails
    '''

    subscribed_users = Reminder.get_subscribed_users()

    for subscriber in subscribed_users:
        updated_packages = get_updated_packages_for_user(subscriber.id, subscriber.as_dict()['previous_reminder_sent'])
        stringified_updated_packages_list = ''
        for package in updated_packages:
            stringified_updated_packages_list += config.get('ckanext.reminder.site_url') + '/dataset/' + package.get('name') + '\n'

        if len(updated_packages) > 0:
            message_body = _('The following datasets have been updated') + ':\n' + stringified_updated_packages_list + \
                           '\nUnsubscribe from this newsletter: ' + config.get('ckanext.reminder.site_url') + '/reminder/' + \
                           subscriber.subscriber_email + '/unsubscribe/' + subscriber.unsubscribe_token

            mail_recipient(subscriber.subscriber_email, subscriber.subscriber_email, _('Dataset has been updated'), message_body)
            Reminder.update_previous_reminder_sent(subscriber.subscriber_email)

        log.info("Notification emails sent")

def subscribe_to_package(context, data_dict):
    '''Subscribe to a specific dataset (package).
    :param package: the name or id of the dataset to subscribe to
    :type package: string
    :param subscriber_email: the email with which to subscribe
    :type subscriber_email: string
    '''
    model = context.get('model')

    package_ref = data_dict.get('package')
    subscriber_email = data_dict.get('subscriber_email')
    error = None
    if not package_ref:
        error = _('You must supply a package id or name '
                  '(parameter "package").')
    elif not subscriber_email:
        error = _('You must supply a subscriber email (parameter "subscriber_email").')
    else:
        package = model.Package.get(package_ref)
        if not package:
            error = _('Not found') + ': %r' % package_ref
        else:
            subscriber = Reminder.get_or_add_subscriber(subscriber_email)
            if not ReminderSubscriptionPackageAssociation.exists(package_id=package.id,
                                                                 reminder_subscription_id=subscriber['id']):
                ReminderSubscriptionPackageAssociation.create(package_id=package.id,
                                                              reminder_subscription_id=subscriber['id'])
    if error:
        raise ValidationError(error)

def unsubscribe(context, data_dict):
    'Unsubscribe a specific package'
    package_id = data_dict.get('package_id')
    subscriber_email = data_dict.get('subscriber_email')
    unsubscribe_token = data_dict.get('unsubscribe_token')
    subscriber = Reminder.get_subscriber_dict(subscriber_email)

    error = None
    if not package_id:
        error = _('You must supply a package id '
                  '(parameter "package_id").')
    elif not subscriber_email:
        error = _('You must supply a subscriber email (parameter "subscriber_email").')
    elif not unsubscribe_token:
        error = _('You must supply an unsubscribe token (parameter "unsubscribe_token").')

    if not error and unsubscribe_token == subscriber.get('unsubscribe_token'):
        ReminderSubscriptionPackageAssociation.remove(package_id=package_id,
                                                      reminder_subscription_id=subscriber.get('id'))
    if error:
        raise ValidationError(error)

def unsubscribe_all(context, data_dict):
    'Unsubscribe all packages'
    subscriber_email = data_dict.get('subscriber_email')
    unsubscribe_token = data_dict.get('unsubscribe_token')
    subscriber = Reminder.get_subscriber_dict(subscriber_email)

    error = None
    if not subscriber_email:
        error = _('You must supply a subscriber email (parameter "subscriber_email").')
    elif not unsubscribe_token:
        error = _('You must supply an unsubscribe token (parameter "unsubscribe_token").')

    if not error and unsubscribe_token == subscriber.get('unsubscribe_token'):

        package_ids = ReminderSubscriptionPackageAssociation.get_subscriber_package_ids(subscriber.get('id'))

        for package_id in package_ids:
            ReminderSubscriptionPackageAssociation.remove(package_id=package_id.package_id,
                                                          reminder_subscription_id=subscriber.get('id'))
    if error:
        raise ValidationError(error)<commit_msg>Add better logging, fix string concatenation error when logging amount of found datasets<commit_after>from ckan.common import request, _
from pylons import config
from ckan import model
from ckan.lib.mailer import mail_user
from ckan.lib.mailer import mail_recipient
from ckanext.reminder.model import Reminder, ReminderSubscriptionPackageAssociation
from ckan.logic import ValidationError

import ckan.logic as logic
import datetime

log = __import__('logging').getLogger(__name__)

def send_email_reminders(context, data_dict):
    if request.environ.get('paste.command_request'):
        send_reminders()

def get_datasets_with_reminders():
    now = datetime.datetime.now()
    search_dict = {
        'fq': 'reminder:' + now.strftime("%Y-%m-%d")
    }

    return logic.get_action('package_search')({}, search_dict)

def send_reminders():
    '''
    Sends reminder emails to site admin of datasets which have a reminder date set
    '''

    items = get_datasets_with_reminders()

    try:
        username = config.get('ckanext.reminder.recipient_username')
        recipient = model.User.get(username)

        if(items['results']):
            log.info('Number of datasets with reminders found: ' + str( len(items['results']) ))
        else:
            log.info('No datasets found with reminder set to current date')

        for item in items['results']:

            message_body = _('This is a reminder of a dataset expiration') + ': ' + config.get('ckanext.reminder.site_url') + '/dataset/' + item['name']
            mail_user(recipient, _('CKAN reminder'), message_body)

        if len(items['results']) == 0:
            log.debug("No pending reminders for current date")
        else:
            log.debug("Reminder emails sent")

    except Exception, ex:
        log.exception(ex)
        raise

def get_packages_for_user(context, data_dict):
    '''
    Returns a list of packages for the provided email address
    Returns an empty list if the unsubscribe does not match to the subscriber's token
    '''
    subscriber_email = data_dict.get('subscriber_email')
    unsubscribe_token = data_dict.get('unsubscribe_token')
    subscriber = Reminder.get_subscriber_dict(subscriber_email)

    packages = []

    # Make sure the given unsubscribe token matches the stored one
    # Return empty list if no match, for security
    if subscriber and subscriber.get('unsubscribe_token') == unsubscribe_token:
        package_ids = ReminderSubscriptionPackageAssociation.get_subscriber_package_ids(subscriber.get('id'))

        for package_id in package_ids:
            package = logic.get_action('package_show')({}, { 'name_or_id': package_id.package_id })
            if package:
                packages.append(package)
    return packages

def get_updated_packages_for_user(subscriber_id, previous_reminder_sent):
    subscriptions = ReminderSubscriptionPackageAssociation.get_subscriber_package_ids(subscriber_id)

    updated_packages = []
    for subscription in subscriptions:
        updated_package = logic.get_action('package_show')({}, { 'name_or_id': subscription.package_id })

        if updated_package:
            # Notify user of an updated package if not already notified
            if updated_package['metadata_modified'] > previous_reminder_sent:
                updated_packages.append(updated_package)

    return updated_packages

def send_notifications():
    '''
    Sends notification emails to site users of dataset changes
    The user needs to subscribe to the dataset to receive emails
    '''

    subscribed_users = Reminder.get_subscribed_users()

    for subscriber in subscribed_users:
        updated_packages = get_updated_packages_for_user(subscriber.id, subscriber.as_dict()['previous_reminder_sent'])
        stringified_updated_packages_list = ''
        for package in updated_packages:
            stringified_updated_packages_list += config.get('ckanext.reminder.site_url') + '/dataset/' + package.get('name') + '\n'

        if len(updated_packages) > 0:
            message_body = _('The following datasets have been updated') + ':\n' + stringified_updated_packages_list + \
                           '\nUnsubscribe from this newsletter: ' + config.get('ckanext.reminder.site_url') + '/reminder/' + \
                           subscriber.subscriber_email + '/unsubscribe/' + subscriber.unsubscribe_token

            mail_recipient(subscriber.subscriber_email, subscriber.subscriber_email, _('Dataset has been updated'), message_body)
            Reminder.update_previous_reminder_sent(subscriber.subscriber_email)

        log.info("Notification emails sent")

def subscribe_to_package(context, data_dict):
    '''Subscribe to a specific dataset (package).
    :param package: the name or id of the dataset to subscribe to
    :type package: string
    :param subscriber_email: the email with which to subscribe
    :type subscriber_email: string
    '''
    model = context.get('model')

    package_ref = data_dict.get('package')
    subscriber_email = data_dict.get('subscriber_email')
    error = None
    if not package_ref:
        error = _('You must supply a package id or name '
                  '(parameter "package").')
    elif not subscriber_email:
        error = _('You must supply a subscriber email (parameter "subscriber_email").')
    else:
        package = model.Package.get(package_ref)
        if not package:
            error = _('Not found') + ': %r' % package_ref
        else:
            subscriber = Reminder.get_or_add_subscriber(subscriber_email)
            if not ReminderSubscriptionPackageAssociation.exists(package_id=package.id,
                                                                 reminder_subscription_id=subscriber['id']):
                ReminderSubscriptionPackageAssociation.create(package_id=package.id,
                                                              reminder_subscription_id=subscriber['id'])
    if error:
        raise ValidationError(error)

def unsubscribe(context, data_dict):
    'Unsubscribe a specific package'
    package_id = data_dict.get('package_id')
    subscriber_email = data_dict.get('subscriber_email')
    unsubscribe_token = data_dict.get('unsubscribe_token')
    subscriber = Reminder.get_subscriber_dict(subscriber_email)

    error = None
    if not package_id:
        error = _('You must supply a package id '
                  '(parameter "package_id").')
    elif not subscriber_email:
        error = _('You must supply a subscriber email (parameter "subscriber_email").')
    elif not unsubscribe_token:
        error = _('You must supply an unsubscribe token (parameter "unsubscribe_token").')

    if not error and unsubscribe_token == subscriber.get('unsubscribe_token'):
        ReminderSubscriptionPackageAssociation.remove(package_id=package_id,
                                                      reminder_subscription_id=subscriber.get('id'))
    if error:
        raise ValidationError(error)

def unsubscribe_all(context, data_dict):
    'Unsubscribe all packages'
    subscriber_email = data_dict.get('subscriber_email')
    unsubscribe_token = data_dict.get('unsubscribe_token')
    subscriber = Reminder.get_subscriber_dict(subscriber_email)

    error = None
    if not subscriber_email:
        error = _('You must supply a subscriber email (parameter "subscriber_email").')
    elif not unsubscribe_token:
        error = _('You must supply an unsubscribe token (parameter "unsubscribe_token").')

    if not error and unsubscribe_token == subscriber.get('unsubscribe_token'):

        package_ids = ReminderSubscriptionPackageAssociation.get_subscriber_package_ids(subscriber.get('id'))

        for package_id in package_ids:
            ReminderSubscriptionPackageAssociation.remove(package_id=package_id.package_id,
                                                          reminder_subscription_id=subscriber.get('id'))
    if error:
        raise ValidationError(error)<|endoftext|>