Skip to content

Commit

Permalink
Merge 7b86b21 into f7a8c9a
Browse files Browse the repository at this point in the history
  • Loading branch information
GaretJax committed Aug 3, 2018
2 parents f7a8c9a + 7b86b21 commit 1480381
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 26 deletions.
8 changes: 4 additions & 4 deletions coolfig/__init__.py
Expand Up @@ -10,12 +10,12 @@ class DefaultSettings(schema.Settings):
settings = DefaultSettings(
providers.DictConfig(os.environ, prefix='MYAPP_'))
"""
from .schema import Value, Settings, computed_value, Dictionary
from .providers import EnvConfig, DictConfig
from .schema import Value, Settings, computed_value, Dictionary, Secret
from .providers import EnvConfig, DictConfig, SecretsConfig
from .django import load_django_settings


__version__ = '1.0.2'
__url__ = 'https://github.com/GaretJax/coolfig'
__all__ = ['Value', 'Dictionary', 'computed_value', 'Settings',
'EnvConfig', 'DictConfig', 'load_django_settings']
__all__ = ['Value', 'Dictionary', 'Secret', 'computed_value', 'Settings',
'EnvConfig', 'DictConfig', 'SecretsConfig', 'load_django_settings']
12 changes: 6 additions & 6 deletions coolfig/django.py
Expand Up @@ -6,16 +6,16 @@
import six

from . import types
from .schema import Settings, Value, DictValue, StaticValue
from .schema import Settings, Value, StaticValue, Secret, DictSecret


class BaseDjangoSettings(Settings):
SECRET_KEY = Value(str)
SECRET_KEY = Secret(str, fallback=True)
DEBUG = Value(types.boolean, default=False)
ALLOWED_HOSTS = Value(types.list(str), default=tuple())

DATABASES = DictValue(types.django_db_url, str.lower)
CACHES = DictValue(types.django_cache_url, str.lower)
DATABASES = DictSecret(types.django_db_url, str.lower, fallback=True)
CACHES = DictSecret(types.django_cache_url, str.lower, fallback=True)

# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
Expand Down Expand Up @@ -85,8 +85,8 @@ def make_django_settings(static_config, base=BaseDjangoSettings):


def load_django_settings(provider, static_config, base=BaseDjangoSettings,
apps=None, name=None):
apps=None, name=None, secrets_provider=None):
settings_class = make_django_settings(static_config, base)
settings = settings_class(provider)
settings = settings_class(provider, secrets_provider=secrets_provider)
settings.install(name)
settings.load_apps(apps)
26 changes: 26 additions & 0 deletions coolfig/providers.py
@@ -1,4 +1,5 @@
import os
import errno
from functools import partial


Expand Down Expand Up @@ -34,4 +35,29 @@ def iterprefixed(self, prefix):
yield (k[len(self._prefix):], self._conf_dict[k])


class EnvDirConfig(ConfigurationProvider):
def __init__(self, base_path, prefix=''):
self._base_path = base_path
self._prefix = prefix

def get(self, key):
path = os.path.join(self._base_path, key)
try:
with open(path) as fh:
return fh.read()
except IOError as e:
if e.errno == errno.EACCES: # Wrong permissions
raise
return NOT_PROVIDED # File does not exist

def iterprefixed(self, prefix):
prefix = self._prefix + prefix
if os.path.exists(self._base_path):
for k in os.listdir(self._base_path):
path = os.path.join(self._base_path, k)
if k.startswith(prefix) and os.path.isfile(path):
yield (k[len(self._prefix):], self.get(k))


EnvConfig = partial(DictConfig, os.environ)
SecretsConfig = partial(EnvDirConfig, '/run/secrets/')
59 changes: 48 additions & 11 deletions coolfig/schema.py
Expand Up @@ -10,7 +10,7 @@ class ImproperlyConfigured(Exception):


class ValueBase(object):
def __call__(self, settingsobj, config_provider, key):
def __call__(self, settingsobj, key):
raise NotImplementedError


Expand All @@ -20,9 +20,12 @@ def __init__(self, type, default=NOT_PROVIDED, key=None):
self.default = default
self.key = key

def __call__(self, settingsobj, config_provider, key):
def _get_provided_value(self, settingsobj, key):
return settingsobj.config_provider.get(key)

def __call__(self, settingsobj, key):
key = self.key if self.key else key
value = config_provider.get(key)
value = self._get_provided_value(settingsobj, key)
if value is NOT_PROVIDED and self.default is NOT_PROVIDED:
# Value is required but was not provided
raise ImproperlyConfigured('no value set for {}'.format(key))
Expand All @@ -37,13 +40,27 @@ def __call__(self, settingsobj, config_provider, key):
return self.type(value)


class Secret(Value):
def __init__(self, type, default=NOT_PROVIDED, key=None, fallback=False):
super(Secret, self).__init__(type, default, key)
self.fallback = fallback

def _get_provided_value(self, settingsobj, key):
value = NOT_PROVIDED
if settingsobj.secrets_provider:
value = settingsobj.secrets_provider.get(key)
if value is NOT_PROVIDED and self.fallback:
value = super(Secret, self)._get_provided_value(settingsobj, key)
return value


class ComputedValue(ValueBase):
def __init__(self, callable, *args, **kwargs):
self.callable = callable
self.args = args
self.kwargs = kwargs

def __call__(self, settingsobj, config_provider, key):
def __call__(self, settingsobj, key):
return self.callable(settingsobj, *self.args, **self.kwargs)


Expand All @@ -56,19 +73,37 @@ def __init__(self, type, keytype=str, *args, **kwargs):
super(DictValue, self).__init__(type, *args, **kwargs)
self.keytype = keytype

def __call__(self, settingsobj, config_provider, key):
def __call__(self, settingsobj, key):
key = (self.key if self.key else key) + '_'
return {self.keytype(k[len(key):]): self.type(v)
for k, v in config_provider.iterprefixed(key)}
for k, v in settingsobj.config_provider.iterprefixed(key)}


class DictSecret(DictValue):
def __init__(self, type, keytype=str, fallback=False, *args, **kwargs):
super(DictSecret, self).__init__(type, keytype, *args, **kwargs)
self.fallback = fallback

def __call__(self, settingsobj, key):
key = (self.key if self.key else key) + '_'
data = {}
for k, v in settingsobj.secrets_provider.iterprefixed(key):
data[self.keytype(k[len(key):])] = self.type(v)
if self.fallback and not data:
for k, v in settingsobj.config_provider.iterprefixed(key):
postfix = self.keytype((k[len(key):]))
if postfix not in data.keys():
data[postfix] = self.type(v)
return data


class Dictionary(ValueBase):
def __init__(self, spec):
self.spec = spec

def __call__(self, settingsobj, config_provider, key):
def __call__(self, settingsobj, key):
return {
key: value(settingsobj, config_provider, key)
key: value(settingsobj, key)
for key, value in iteritems(self.spec)
}

Expand All @@ -82,7 +117,7 @@ def __init__(self, cls, name, value):
def __get__(self, obj, objtype=None):
if obj is None:
return self
return self.value(obj, obj.config_provider, self.name)
return self.value(obj, self.name)

def __set__(self, obj, objtype=None):
raise AttributeError("can't set attribute")
Expand Down Expand Up @@ -112,7 +147,8 @@ def __init__(self, key):
def __call__(self, obj):
return getattr(obj, self.key)

ref = Reference

ref = Reference # NOQA


def bind_values(cls, clsdict):
Expand All @@ -137,8 +173,9 @@ def __iter__(self):


class SettingsBase(object):
def __init__(self, config_provider):
def __init__(self, config_provider, secrets_provider=None):
self.config_provider = config_provider
self.secrets_provider = secrets_provider

def __iter__(self):
return iter(self.__class__)
Expand Down
12 changes: 7 additions & 5 deletions coolfig/test_django.py
Expand Up @@ -18,7 +18,7 @@

from .django import BaseDjangoSettings, make_django_settings
from .django import load_django_settings
from .providers import DictConfig
from .providers import DictConfig, SecretsConfig
from .types import django_db_url
from . import Value, Settings

Expand Down Expand Up @@ -100,10 +100,12 @@ def test_iter():
'ROOT_URLCONF': 'test.urls',
})

s = settings_class(DictConfig({
'SECRET_KEY': 'test-secret-key'
}))
confdict = s.as_dict()
settings = settings_class(
DictConfig({'SECRET_KEY': 'test-secret-key'}),
secrets_provider=SecretsConfig(),
)
# Secrets Provider required since SECRET_KEY is a Secret
confdict = settings.as_dict()
assert confdict['INSTALLED_APPS'] == []
assert confdict['ROOT_URLCONF'] == 'test.urls'

Expand Down

0 comments on commit 1480381

Please sign in to comment.