Source code for keg.config

import os.path as osp

import appdirs
from blazeutils.helpers import tolist
import flask
from pathlib import PurePath
from werkzeug.utils import (
    import_string,
    ImportStringError
)

from keg.utils import app_environ_get, pymodule_fpaths_to_objects


class ConfigurationError(Exception):
    pass


class SubstituteValue(object):
    def __init__(self, value):
        self.value = value


substitute = SubstituteValue


class Config(flask.Config):
    default_config_locations = [
        # Keg's defaults
        'keg.config.DefaultProfile',
        # Keg's defaults for the selected profile
        'keg.config.{profile}',
        # App defaults for all profiles
        '{app_import_name}.config.DefaultProfile',
        # apply the profile specific defaults that are in the app's config file
        '{app_import_name}.config.{profile}',
    ]

    def from_obj_if_exists(self, obj_location):
        try:
            self.from_object(obj_location)
            self.configs_found.append(obj_location)
        except ImportStringError as e:
            if obj_location not in str(e):
                raise

    def default_config_locations_parsed(self):
        retval = []
        for location in self.default_config_locations:
            # if no profile is given, the location want's one, that location isn't valid
            if '{profile}' in location and self.profile is None:
                continue

            retval.append(location.format(app_import_name=self.app_import_name,
                                          profile=self.profile))
        return retval

    def init_app(self, app_config_profile, app_import_name, app_root_path, use_test_profile,
                 config_file_objs=None):
        self.use_test_profile = use_test_profile
        self.profile = app_config_profile
        self.dirs = appdirs.AppDirs(app_import_name, appauthor=False, multipath=True)
        self.app_import_name = app_import_name
        self.app_root_path = app_root_path
        self.config_paths_unreadable = []

        if config_file_objs:
            self.config_file_objs = config_file_objs
        else:
            self.config_file_objs = []
            possible_config_fpaths = self.config_file_paths()
            fpaths_to_objects = pymodule_fpaths_to_objects(possible_config_fpaths)
            for fpath, objects, exc in fpaths_to_objects:
                if objects is None:
                    self.config_paths_unreadable.append((fpath, exc))
                else:
                    self.config_file_objs.append((fpath, objects))

        if self.profile is None:
            self.profile = self.determine_selected_profile()

        self.configs_found = []

        for dotted_location in self.default_config_locations_parsed():
            dotted_location = dotted_location.format(app_import_name=app_import_name,
                                                     profile=self.profile)
            self.from_obj_if_exists(dotted_location)

        # apply settings from any of this app's configuration files
        for fpath, objects in self.config_file_objs:
            if self.profile in objects:
                self.from_object(objects[self.profile])
                self.configs_found.append('{}:{}'.format(fpath, self.profile))

        sub_values = self.substitution_values()
        self.substitution_apply(sub_values)

    def config_file_paths(self):
        dirs = self.dirs

        config_fname = '{}-config.py'.format(self.app_import_name)

        dpaths = []
        if appdirs.system != 'win32':
            dpaths.extend(dirs.site_config_dir.split(':'))
            dpaths.append('/etc/{}'.format(self.app_import_name))
            dpaths.append('/etc')
        else:
            system_drive = PurePath(dirs.site_config_dir).drive
            system_etc_dir = PurePath(system_drive, '/', 'etc')
            dpaths.extend((
                dirs.site_config_dir,
                system_etc_dir.joinpath(self.app_import_name).__str__(),
                system_etc_dir.__str__()
            ))
        dpaths.append(dirs.user_config_dir)
        dpaths.append(osp.dirname(self.app_root_path))

        fpaths = [osp.join(dpath, config_fname) for dpath in dpaths]

        return fpaths

    def email_error_to(self):
        error_to = self.get('KEG_EMAIL_ERROR_TO')
        override_to = self.get('KEG_EMAIL_OVERRIDE_TO')
        if override_to:
            return tolist(override_to)
        return tolist(error_to)

    def determine_selected_profile(self):
        # if we find the value in the environment, use it
        profile = app_environ_get(self.app_import_name, 'CONFIG_PROFILE')
        if profile is not None:
            return profile

        use_test_profile = app_environ_get(self.app_import_name, 'USE_TEST_PROFILE', '')
        if use_test_profile.strip() or self.use_test_profile:
            return 'TestProfile'

        # look for it in the app's main config file (e.g. myapp.config)
        app_config = import_string('{}.config'.format(self.app_import_name), silent=True)
        if app_config and hasattr(app_config, 'DEFAULT_PROFILE'):
            profile = app_config.DEFAULT_PROFILE

        # Look for it in all the config files found.  This loops from lowest-priority config file
        # to highest priority, so the last file found with a value is kept.  Accordingly, any app
        # specific file has priority over the app's main config file, which could be set just above.
        for fpath, objects in self.config_file_objs:
            if 'DEFAULT_PROFILE' in objects:
                profile = objects['DEFAULT_PROFILE']

        return profile

    def substitution_values(self):
        return dict(
            user_log_dir=self.dirs.user_log_dir,
            app_import_name=self.app_import_name,
        )

    def substitution_apply(self, sub_values):
        for config_key, config_value in self.items():
            if not isinstance(config_value, SubstituteValue):
                continue
            new_value = config_value.value.format(**sub_values)
            self[config_key] = new_value


# The following three classes are default configuration profiles
class DefaultProfile(object):
    KEG_DIR_MODE = 0o777
    KEG_ENDPOINTS = dict(
        home='public.home',
        login='public.home',
        after_login='public.home',
        after_logout='public.home',
    )

    KEG_DB_DIALECT_OPTIONS = {}


class DevProfile(object):
    DEBUG = True


class TestProfile(object):
    DEBUG = True
    TESTING = True
    KEG_LOG_SYSLOG_ENABLED = False

    # By default, flask-webtest does not push an app context for each request. That
    # behavior does not match the flask stack entirely: flask pushes both an app
    # context and a request context on each request.
    # Having a fresh app context for each test request is important for libraries that
    # use flask.g to work properly (e.g. CSRF from flask-wtf, and user caching
    # in flask-login).
    FLASK_WEBTEST_PUSH_APP_CONTEXT = True

    # set this to allow generation of URLs without a request context
    SERVER_NAME = 'keg.example.com'

    # simple value for testing is fine
    SECRET_KEY = '12345'

    # Sane default values for testing to get rid of warnings.
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'