diff options
-rw-r--r-- | changes/feature_2164_config-split | 1 | ||||
-rw-r--r-- | pkg/requirements.pip | 5 | ||||
-rw-r--r-- | src/leap/config/baseconfig.py | 186 | ||||
-rw-r--r-- | src/leap/config/leapsettings.py | 2 | ||||
-rw-r--r-- | src/leap/config/pluggableconfig.py | 475 | ||||
-rw-r--r-- | src/leap/config/prefixers.py | 133 | ||||
-rw-r--r-- | src/leap/config/providerconfig.py | 2 | ||||
-rw-r--r-- | src/leap/services/eip/eipconfig.py | 2 |
8 files changed, 6 insertions, 800 deletions
diff --git a/changes/feature_2164_config-split b/changes/feature_2164_config-split new file mode 100644 index 00000000..6fae00de --- /dev/null +++ b/changes/feature_2164_config-split @@ -0,0 +1 @@ + o Moves BaseConfig to leap.common.config. Closes:#2164 diff --git a/pkg/requirements.pip b/pkg/requirements.pip index 89917a53..0051380a 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -1,16 +1,15 @@ # in order of addition to the project. # do not change the ordering. # +argparse +# PySide # Use LEAP_VENV_SKIP_PYSIDE to avoid installing it! -jsonschema<=0.8 requests srp>=1.0.2 pyopenssl keyring -pyxdg -argparse python-dateutil psutil diff --git a/src/leap/config/baseconfig.py b/src/leap/config/baseconfig.py deleted file mode 100644 index f5c07184..00000000 --- a/src/leap/config/baseconfig.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- 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.config.pluggableconfig import PluggableConfig -from leap.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/config/leapsettings.py b/src/leap/config/leapsettings.py index 19ec4a9a..df9c9f11 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -24,7 +24,7 @@ import logging from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type -from leap.config.prefixers import get_platform_prefixer +from leap.common.config.prefixers import get_platform_prefixer logger = logging.getLogger(__name__) diff --git a/src/leap/config/pluggableconfig.py b/src/leap/config/pluggableconfig.py deleted file mode 100644 index 8535fa6b..00000000 --- a/src/leap/config/pluggableconfig.py +++ /dev/null @@ -1,475 +0,0 @@ -# -*- 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/config/prefixers.py b/src/leap/config/prefixers.py deleted file mode 100644 index 72211790..00000000 --- a/src/leap/config/prefixers.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- 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() diff --git a/src/leap/config/providerconfig.py b/src/leap/config/providerconfig.py index 7651863b..5aa0cc6e 100644 --- a/src/leap/config/providerconfig.py +++ b/src/leap/config/providerconfig.py @@ -22,7 +22,7 @@ import logging import os from leap.common.check import leap_assert -from leap.config.baseconfig import BaseConfig, LocalizedKey +from leap.common.config.baseconfig import BaseConfig, LocalizedKey from leap.config.provider_spec import leap_provider_spec logger = logging.getLogger(__name__) diff --git a/src/leap/services/eip/eipconfig.py b/src/leap/services/eip/eipconfig.py index 3f873878..4e74687a 100644 --- a/src/leap/services/eip/eipconfig.py +++ b/src/leap/services/eip/eipconfig.py @@ -22,7 +22,7 @@ import os import logging from leap.common.check import leap_assert, leap_assert_type -from leap.config.baseconfig import BaseConfig +from leap.common.config.baseconfig import BaseConfig from leap.config.providerconfig import ProviderConfig from leap.services.eip.eipspec import eipservice_config_spec |