diff options
| author | Kali Kaneko <kali@leap.se> | 2013-04-09 23:52:21 +0900 | 
|---|---|---|
| committer | Kali Kaneko <kali@leap.se> | 2013-04-09 23:52:21 +0900 | 
| commit | 2611f4e5fd199d6c3b24d212c862ccf03fea93ff (patch) | |
| tree | e626bf6884b341af0db40a051a06534a2e7af7b6 | |
| parent | 666877be3fa00c69319db935549f3b8ea3c14f6e (diff) | |
add BaseConfig class and its dependencies
| -rw-r--r-- | README.rst | 7 | ||||
| -rw-r--r-- | pkg/requirements.pip | 4 | ||||
| -rw-r--r-- | setup.py | 8 | ||||
| -rw-r--r-- | src/leap/common/config/__init__.py | 0 | ||||
| -rw-r--r-- | src/leap/common/config/baseconfig.py | 186 | ||||
| -rw-r--r-- | src/leap/common/config/pluggableconfig.py | 475 | ||||
| -rw-r--r-- | src/leap/common/config/prefixers.py | 132 | 
7 files changed, 810 insertions, 2 deletions
| @@ -1,3 +1,10 @@  leap.common  ===========  A collection of shared utils used by the several python LEAP subprojects. + +* leap.common.cert +* leap.common.checks +* leap.common.config +* leap.common.events +* leap.common.files +* leap.common.testing diff --git a/pkg/requirements.pip b/pkg/requirements.pip new file mode 100644 index 0000000..ca21223 --- /dev/null +++ b/pkg/requirements.pip @@ -0,0 +1,4 @@ +jsonschema<=0.8 +pyxdg +protobuf +protobuf.socketrpc @@ -19,15 +19,19 @@ setup file for leap.common  """  from setuptools import setup, find_packages - +# XXX parse pkg/requirements.pip  requirements = [ +    "jsonschema", +    "pyxdg",      'protobuf',      'protobuf.socketrpc',  ]  dependency_links = [ -    'https://protobuf-socket-rpc.googlecode.com/files/protobuf.socketrpc-1.3.2-py2.6.egg#egg=protobuf.socketrpc', +    # XXX this link is only for py2.6??? +    # we need to get this packaged or included +    "https://protobuf-socket-rpc.googlecode.com/files/protobuf.socketrpc-1.3.2-py2.6.egg#egg=protobuf.socketrpc",  ] diff --git a/src/leap/common/config/__init__.py b/src/leap/common/config/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/common/config/__init__.py diff --git a/src/leap/common/config/baseconfig.py b/src/leap/common/config/baseconfig.py new file mode 100644 index 0000000..edb9b24 --- /dev/null +++ b/src/leap/common/config/baseconfig.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# baseconfig.py +# Copyright (C) 2013 LEAP +# +# 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 3 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, see <http://www.gnu.org/licenses/>. + +""" +Implements the abstract base class for configuration +""" + +import copy +import logging +import functools +import os + +from abc import ABCMeta, abstractmethod + +from leap.common.check import leap_assert +from leap.common.files import mkdir_p +from leap.common.config.pluggableconfig import PluggableConfig +from leap.common.config.prefixers import get_platform_prefixer + +logger = logging.getLogger(__name__) + + +class BaseConfig: +    """ +    Abstract base class for any JSON based configuration +    """ + +    __metaclass__ = ABCMeta + +    """ +    Standalone is a class wide parameter + +    @param standalone: if True it will return the prefix for a +    standalone application. Otherwise, it will return the system +    default for configuration storage. +    @type standalone: bool +    """ +    standalone = False + +    def __init__(self): +        self._data = {} +        self._config_checker = None + +    @abstractmethod +    def _get_spec(self): +        """ +        Returns the spec object for the specific configuration +        """ +        return None + +    def _safe_get_value(self, key): +        """ +        Tries to return a value only if the config has already been loaded + +        @rtype: depends on the config structure, dict, str, array, int +        @return: returns the value for the specified key in the config +        """ +        leap_assert(self._config_checker, "Load the config first") +        return self._config_checker.config[key] + +    def get_path_prefix(self): +        """ +        Returns the platform dependant path prefixer + +        """ +        return get_platform_prefixer().get_path_prefix( +            standalone=self.standalone) + +    def loaded(self): +        """ +        Returns True if the configuration has been already +        loaded. False otherwise +        """ +        return self._config_checker is not None + +    def save(self, path_list): +        """ +        Saves the current configuration to disk + +        @param path_list: list of components that form the relative +        path to configuration. The absolute path will be calculated +        depending on the platform. +        @type path_list: list + +        @return: True if saved to disk correctly, False otherwise +        """ +        config_path = os.path.join(self.get_path_prefix(), *(path_list[:-1])) +        mkdir_p(config_path) + +        try: +            self._config_checker.serialize(os.path.join(config_path, +                                                        path_list[-1])) +        except Exception as e: +            logger.warning("%s" % (e,)) +            raise +        return True + +    def load(self, path="", data=None, mtime=None): +        """ +        Loads the configuration from disk + +        @type path: str +        @param path: relative path to configuration. The absolute path +        will be calculated depending on the platform + +        @return: True if loaded from disk correctly, False otherwise +        """ + +        config_path = os.path.join(self.get_path_prefix(), +                                   path) + +        self._config_checker = PluggableConfig(format="json") +        self._config_checker.options = copy.deepcopy(self._get_spec()) + +        try: +            if data is None: +                self._config_checker.load(fromfile=config_path, mtime=mtime) +            else: +                self._config_checker.load(data, mtime=mtime) +        except Exception as e: +            logger.warning("Something went wrong while loading " + +                           "the config from %s\n%s" % (config_path, e)) +            self._config_checker = None +            return False +        return True + + +class LocalizedKey(object): +    """ +    Decorator used for keys that are localized in a configuration +    """ + +    def __init__(self, func, **kwargs): +        self._func = func + +    def __call__(self, instance, lang="en"): +        """ +        Tries to return the string for the specified language, otherwise +        informs the problem and returns an empty string + +        @param lang: language code +        @type lang: str + +        @return: localized value from the possible values returned by +        self._func +        """ +        descriptions = self._func(instance) +        description_lang = "" +        config_lang = "en" +        for key in descriptions.keys(): +            if lang.startswith(key): +                config_lang = key +                break + +        description_lang = descriptions[config_lang] +        return description_lang + +    def __get__(self, instance, instancetype): +        """ +        Implement the descriptor protocol to make decorating instance +        method possible. +        """ +        # Return a partial function with the first argument is the instance +        # of the class decorated. +        return functools.partial(self.__call__, instance) + +if __name__ == "__main__": +    try: +        config = BaseConfig()  # should throw TypeError for _get_spec +    except Exception as e: +        assert isinstance(e, TypeError), "Something went wrong" +        print "Abstract BaseConfig class is working as expected" diff --git a/src/leap/common/config/pluggableconfig.py b/src/leap/common/config/pluggableconfig.py new file mode 100644 index 0000000..8535fa6 --- /dev/null +++ b/src/leap/common/config/pluggableconfig.py @@ -0,0 +1,475 @@ +# -*- coding: utf-8 -*- +# pluggableconfig.py +# Copyright (C) 2013 LEAP +# +# 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 3 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, see <http://www.gnu.org/licenses/>. + +""" +generic configuration handlers +""" +import copy +import json +import logging +import os +import time +import urlparse + +import jsonschema + +#from leap.base.util.translations import LEAPTranslatable +from leap.common.check import leap_assert + + +logger = logging.getLogger(__name__) + + +__all__ = ['PluggableConfig', +           'adaptors', +           'types', +           'UnknownOptionException', +           'MissingValueException', +           'ConfigurationProviderException', +           'TypeCastException'] + +# exceptions + + +class UnknownOptionException(Exception): +    """exception raised when a non-configuration +    value is present in the configuration""" + + +class MissingValueException(Exception): +    """exception raised when a required value is missing""" + + +class ConfigurationProviderException(Exception): +    """exception raised when a configuration provider is missing, etc""" + + +class TypeCastException(Exception): +    """exception raised when a +    configuration item cannot be coerced to a type""" + + +class ConfigAdaptor(object): +    """ +    abstract base class for config adaotors for +    serialization/deserialization and custom validation +    and type casting. +    """ +    def read(self, filename): +        raise NotImplementedError("abstract base class") + +    def write(self, config, filename): +        with open(filename, 'w') as f: +            self._write(f, config) + +    def _write(self, fp, config): +        raise NotImplementedError("abstract base class") + +    def validate(self, config, schema): +        raise NotImplementedError("abstract base class") + + +adaptors = {} + + +class JSONSchemaEncoder(json.JSONEncoder): +    """ +    custom default encoder that +    casts python objects to json objects for +    the schema validation +    """ +    def default(self, obj): +        if obj is str: +            return 'string' +        if obj is unicode: +            return 'string' +        if obj is int: +            return 'integer' +        if obj is list: +            return 'array' +        if obj is dict: +            return 'object' +        if obj is bool: +            return 'boolean' + + +class JSONAdaptor(ConfigAdaptor): +    indent = 2 +    extensions = ['json'] + +    def read(self, _from): +        if isinstance(_from, file): +            _from_string = _from.read() +        if isinstance(_from, str): +            _from_string = _from +        return json.loads(_from_string) + +    def _write(self, fp, config): +        fp.write(json.dumps(config, +                 indent=self.indent, +                 sort_keys=True)) + +    def validate(self, config, schema_obj): +        schema_json = JSONSchemaEncoder().encode(schema_obj) +        schema = json.loads(schema_json) +        jsonschema.validate(config, schema) + + +adaptors['json'] = JSONAdaptor() + +# +# Adaptors +# +# Allow to apply a predefined set of types to the +# specs, so it checks the validity of formats and cast it +# to proper python types. + +# TODO: +# - HTTPS uri + + +class DateType(object): +    fmt = '%Y-%m-%d' + +    def to_python(self, data): +        return time.strptime(data, self.fmt) + +    def get_prep_value(self, data): +        return time.strftime(self.fmt, data) + + +class TranslatableType(object): +    """ +    a type that casts to LEAPTranslatable objects. +    Used for labels we get from providers and stuff. +    """ + +    def to_python(self, data): +        # TODO: add translatable +        return data  # LEAPTranslatable(data) + +    # needed? we already have an extended dict... +    #def get_prep_value(self, data): +        #return dict(data) + + +class URIType(object): + +    def to_python(self, data): +        parsed = urlparse.urlparse(data) +        if not parsed.scheme: +            raise TypeCastException("uri %s has no schema" % data) +        return parsed.geturl() + +    def get_prep_value(self, data): +        return data + + +class HTTPSURIType(object): + +    def to_python(self, data): +        parsed = urlparse.urlparse(data) +        if not parsed.scheme: +            raise TypeCastException("uri %s has no schema" % data) +        if parsed.scheme != "https": +            raise TypeCastException( +                "uri %s does not has " +                "https schema" % data) +        return parsed.geturl() + +    def get_prep_value(self, data): +        return data + + +types = { +    'date': DateType(), +    'uri': URIType(), +    'https-uri': HTTPSURIType(), +    'translatable': TranslatableType(), +} + + +class PluggableConfig(object): + +    options = {} + +    def __init__(self, +                 adaptors=adaptors, +                 types=types, +                 format=None): + +        self.config = {} +        self.adaptors = adaptors +        self.types = types +        self._format = format +        self.mtime = None +        self.dirty = False + +    @property +    def option_dict(self): +        if hasattr(self, 'options') and isinstance(self.options, dict): +            return self.options.get('properties', None) + +    def items(self): +        """ +        act like an iterator +        """ +        if isinstance(self.option_dict, dict): +            return self.option_dict.items() +        return self.options + +    def validate(self, config, format=None): +        """ +        validate config +        """ +        schema = self.options +        if format is None: +            format = self._format + +        if format: +            adaptor = self.get_adaptor(self._format) +            adaptor.validate(config, schema) +        else: +            # we really should make format mandatory... +            logger.error('no format passed to validate') + +        # first round of validation is ok. +        # now we proceed to cast types if any specified. +        self.to_python(config) + +    def to_python(self, config): +        """ +        cast types following first type and then format indications. +        """ +        unseen_options = [i for i in config if i not in self.option_dict] +        if unseen_options: +            raise UnknownOptionException( +                "Unknown options: %s" % ', '.join(unseen_options)) + +        for key, value in config.items(): +            _type = self.option_dict[key].get('type') +            if _type is None and 'default' in self.option_dict[key]: +                _type = type(self.option_dict[key]['default']) +            if _type is not None: +                tocast = True +                if not callable(_type) and isinstance(value, _type): +                    tocast = False +                if tocast: +                    try: +                        config[key] = _type(value) +                    except BaseException, e: +                        raise TypeCastException( +                            "Could not coerce %s, %s, " +                            "to type %s: %s" % (key, value, _type.__name__, e)) +            _format = self.option_dict[key].get('format', None) +            _ftype = self.types.get(_format, None) +            if _ftype: +                try: +                    config[key] = _ftype.to_python(value) +                except BaseException, e: +                    raise TypeCastException( +                        "Could not coerce %s, %s, " +                        "to format %s: %s" % (key, value, +                        _ftype.__class__.__name__, +                        e)) + +        return config + +    def prep_value(self, config): +        """ +        the inverse of to_python method, +        called just before serialization +        """ +        for key, value in config.items(): +            _format = self.option_dict[key].get('format', None) +            _ftype = self.types.get(_format, None) +            if _ftype and hasattr(_ftype, 'get_prep_value'): +                try: +                    config[key] = _ftype.get_prep_value(value) +                except BaseException, e: +                    raise TypeCastException( +                        "Could not serialize %s, %s, " +                        "by format %s: %s" % (key, value, +                        _ftype.__class__.__name__, +                        e)) +            else: +                config[key] = value +        return config + +    # methods for adding configuration + +    def get_default_values(self): +        """ +        return a config options from configuration defaults +        """ +        defaults = {} +        for key, value in self.items(): +            if 'default' in value: +                defaults[key] = value['default'] +        return copy.deepcopy(defaults) + +    def get_adaptor(self, format): +        """ +        get specified format adaptor or +        guess for a given filename +        """ +        adaptor = self.adaptors.get(format, None) +        if adaptor: +            return adaptor + +        # not registered in adaptors dict, let's try all +        for adaptor in self.adaptors.values(): +            if format in adaptor.extensions: +                return adaptor + +    def filename2format(self, filename): +        extension = os.path.splitext(filename)[-1] +        return extension.lstrip('.') or None + +    def serialize(self, filename, format=None, full=False): +        if not format: +            format = self._format +        if not format: +            format = self.filename2format(filename) +        if not format: +            raise Exception('Please specify a format') +            # TODO: more specific exception type + +        adaptor = self.get_adaptor(format) +        if not adaptor: +            raise Exception("Adaptor not found for format: %s" % format) + +        config = copy.deepcopy(self.config) +        serializable = self.prep_value(config) +        adaptor.write(serializable, filename) + +        if self.mtime: +            self.touch_mtime(filename) + +    def touch_mtime(self, filename): +        mtime = self.mtime +        os.utime(filename, (mtime, mtime)) + +    def deserialize(self, string=None, fromfile=None, format=None): +        """ +        load configuration from a file or string +        """ + +        def _try_deserialize(): +            if fromfile: +                with open(fromfile, 'r') as f: +                    content = adaptor.read(f) +            elif string: +                content = adaptor.read(string) +            return content + +        # XXX cleanup this! + +        if fromfile: +            leap_assert(os.path.exists(fromfile)) +            if not format: +                format = self.filename2format(fromfile) + +        if not format: +            format = self._format +        if format: +            adaptor = self.get_adaptor(format) +        else: +            adaptor = None + +        if adaptor: +            content = _try_deserialize() +            return content + +        # no adaptor, let's try rest of adaptors + +        adaptors = self.adaptors[:] + +        if format: +            adaptors.sort( +                key=lambda x: int( +                    format in x.extensions), +                reverse=True) + +        for adaptor in adaptors: +            content = _try_deserialize() +        return content + +    def set_dirty(self): +        self.dirty = True + +    def is_dirty(self): +        return self.dirty + +    def load(self, *args, **kwargs): +        """ +        load from string or file +        if no string of fromfile option is given, +        it will attempt to load from defaults +        defined in the schema. +        """ +        string = args[0] if args else None +        fromfile = kwargs.get("fromfile", None) +        mtime = kwargs.pop("mtime", None) +        self.mtime = mtime +        content = None + +        # start with defaults, so we can +        # have partial values applied. +        content = self.get_default_values() +        if string and isinstance(string, str): +            content = self.deserialize(string) + +        if not string and fromfile is not None: +            #import ipdb;ipdb.set_trace() +            content = self.deserialize(fromfile=fromfile) + +        if not content: +            logger.error('no content could be loaded') +            # XXX raise! +            return + +        # lazy evaluation until first level of nesting +        # to allow lambdas with context-dependant info +        # like os.path.expanduser +        for k, v in content.iteritems(): +            if callable(v): +                content[k] = v() + +        self.validate(content) +        self.config = content +        return True + + +def testmain():  # pragma: no cover + +    from tests import test_validation as t +    import pprint + +    config = PluggableConfig(_format="json") +    properties = copy.deepcopy(t.sample_spec) + +    config.options = properties +    config.load(fromfile='data.json') + +    print 'config' +    pprint.pprint(config.config) + +    config.serialize('/tmp/testserial.json') + +if __name__ == "__main__": +    testmain() diff --git a/src/leap/common/config/prefixers.py b/src/leap/common/config/prefixers.py new file mode 100644 index 0000000..27274bd --- /dev/null +++ b/src/leap/common/config/prefixers.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# prefixers.py +# Copyright (C) 2013 LEAP +# +# 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 3 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, see <http://www.gnu.org/licenses/>. + +""" +Platform dependant configuration path prefixers +""" +import os +import platform + +from abc import ABCMeta, abstractmethod +from xdg import BaseDirectory + +from leap.common.check import leap_assert + + +class Prefixer: +    """ +    Abstract prefixer class +    """ + +    __metaclass__ = ABCMeta + +    @abstractmethod +    def get_path_prefix(self, standalone=False): +        """ +        Returns the platform dependant path prefixer + +        @param standalone: if True it will return the prefix for a +        standalone application. Otherwise, it will return the system +        default for configuration storage. +        @type standalone: bool +        """ +        return "" + + +def get_platform_prefixer(): +    prefixer = globals()[platform.system() + "Prefixer"] +    leap_assert(prefixer, "Unimplemented platform prefixer: %s" % +                (platform.system(),)) +    return prefixer() + + +class LinuxPrefixer(Prefixer): +    """ +    Config prefixer for the Linux platform +    """ + +    def get_path_prefix(self, standalone=False): +        """ +        Returns the platform dependant path prefixer. +        This method expects an env variable named LEAP_CLIENT_PATH if +        standalone is used. + +        @param standalone: if True it will return the prefix for a +        standalone application. Otherwise, it will return the system +        default for configuration storage. +        @type standalone: bool +        """ +        config_dir = BaseDirectory.xdg_config_home +        if not standalone: +            return config_dir +        return os.path.join(os.getcwd(), "config") + + +class DarwinPrefixer(Prefixer): +    """ +    Config prefixer for the Darwin platform +    """ + +    def get_path_prefix(self, standalone=False): +        """ +        Returns the platform dependant path prefixer. +        This method expects an env variable named LEAP_CLIENT_PATH if +        standalone is used. + +        @param standalone: if True it will return the prefix for a +        standalone application. Otherwise, it will return the system +        default for configuration storage. +        @type standalone: bool +        """ +        config_dir = BaseDirectory.xdg_config_home +        if not standalone: +            return config_dir +        return os.getenv(os.getcwd(), "config") + + +class WindowsPrefixer(Prefixer): +    """ +    Config prefixer for the Windows platform +    """ + +    def get_path_prefix(self, standalone=False): +        """ +        Returns the platform dependant path prefixer. +        This method expects an env variable named LEAP_CLIENT_PATH if +        standalone is used. + +        @param standalone: if True it will return the prefix for a +        standalone application. Otherwise, it will return the system +        default for configuration storage. +        @type standalone: bool +        """ +        config_dir = BaseDirectory.xdg_config_home + +        if not standalone: +            return config_dir +        return os.path.join(os.getcwd(), "config") + +if __name__ == "__main__": +    try: +        abs_prefixer = Prefixer() +    except Exception as e: +        assert isinstance(e, TypeError), "Something went wrong" +        print "Abstract Prefixer class is working as expected" + +    linux_prefixer = LinuxPrefixer() +    print linux_prefixer.get_path_prefix(standalone=True) +    print linux_prefixer.get_path_prefix() | 
