mirror of
https://github.com/jazzband/django-configurations.git
synced 2026-03-16 22:20:27 +00:00
- this enables a value to load from environment if the env variable name is given at construction time - cache value of environment value now in property value - add __new__ to allow a construction of a given type from environment directly. In this case now Value instance is constructed but an instance of the desired type that is covered by the Value implementation. For Value it is str, for DictValue it is dict etc. - extend tests for this new behavior - extend test coverage for some other places
386 lines
12 KiB
Python
386 lines
12 KiB
Python
import ast
|
|
import copy
|
|
import decimal
|
|
import os
|
|
import sys
|
|
|
|
from django.core import validators
|
|
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
|
from django.utils import six
|
|
|
|
from .utils import import_by_path
|
|
|
|
|
|
def setup_value(target, name, value):
|
|
actual_value = value.setup(name)
|
|
# overwriting the original Value class with the result
|
|
setattr(target, name, value.value)
|
|
if value.multiple:
|
|
for multiple_name, multiple_value in actual_value.items():
|
|
setattr(target, multiple_name, multiple_value.value)
|
|
|
|
|
|
class Value(object):
|
|
"""
|
|
A single settings value that is able to interpret env variables
|
|
and implements a simple validation scheme.
|
|
"""
|
|
multiple = False
|
|
late_binding = False
|
|
|
|
@property
|
|
def value(self):
|
|
value = self.default
|
|
if not hasattr(self, '_value') and self.environ_name:
|
|
self.setup(self.environ_name)
|
|
if hasattr(self, '_value'):
|
|
value = self._value
|
|
return value
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
self._value = value
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
"""
|
|
checks if the creation can end up directly in the final value.
|
|
That is the case whenever environ = False or environ_name is given
|
|
"""
|
|
instance = object.__new__(cls)
|
|
instance.__init__(*args, **kwargs)
|
|
if not instance.late_binding:
|
|
if (instance.environ and instance.environ_name) \
|
|
or (not instance.environ and instance.default):
|
|
instance = instance.setup(instance.environ_name)
|
|
|
|
return instance
|
|
|
|
def __init__(self, default=None, environ=True, environ_name=None,
|
|
environ_prefix='DJANGO', *args, **kwargs):
|
|
if 'late_binding' in kwargs:
|
|
self.late_binding = kwargs.get('late_binding')
|
|
if isinstance(default, Value) and default.default:
|
|
self.default = copy.copy(default.default)
|
|
else:
|
|
self.default = default
|
|
self.environ = environ
|
|
if environ_prefix and environ_prefix.endswith('_'):
|
|
environ_prefix = environ_prefix[:-1]
|
|
self.environ_prefix = environ_prefix
|
|
self.environ_name = environ_name
|
|
|
|
def __str__(self):
|
|
return str(self.value)
|
|
|
|
def __repr__(self):
|
|
return repr(self.value)
|
|
|
|
def setup(self, name):
|
|
value = self.default
|
|
if self.environ:
|
|
if not self.environ_name:
|
|
environ_name = name.upper()
|
|
else:
|
|
environ_name = self.environ_name
|
|
if self.environ_prefix:
|
|
full_environ_name = '{0}_{1}'.format(self.environ_prefix,
|
|
environ_name)
|
|
else:
|
|
full_environ_name = environ_name
|
|
if full_environ_name in os.environ:
|
|
value = self.to_python(os.environ[full_environ_name])
|
|
self.value = value
|
|
return value
|
|
|
|
def to_python(self, value):
|
|
"""
|
|
Convert the given value of a environment variable into an
|
|
appropriate Python representation of the value.
|
|
This should be overriden when subclassing.
|
|
"""
|
|
return value
|
|
|
|
|
|
class MultipleMixin(object):
|
|
multiple = True
|
|
|
|
|
|
class BooleanValue(Value):
|
|
true_values = ('yes', 'y', 'true', '1')
|
|
false_values = ('no', 'n', 'false', '0', '')
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(BooleanValue, self).__init__(*args, **kwargs)
|
|
if self.default not in (True, False):
|
|
raise ValueError('Default value {0!r} is not a '
|
|
'boolean value'.format(self.default))
|
|
|
|
def to_python(self, value):
|
|
normalized_value = value.strip().lower()
|
|
if normalized_value in self.true_values:
|
|
return True
|
|
elif normalized_value in self.false_values:
|
|
return False
|
|
else:
|
|
raise ValueError('Cannot interpret '
|
|
'boolean value {0!r}'.format(value))
|
|
|
|
|
|
class CastingMixin(object):
|
|
exception = (TypeError, ValueError)
|
|
message = 'Cannot interpret value {0!r}'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(CastingMixin, self).__init__(*args, **kwargs)
|
|
if isinstance(self.caster, six.string_types):
|
|
self._caster = import_by_path(self.caster)
|
|
elif callable(self.caster):
|
|
self._caster = self.caster
|
|
else:
|
|
error = 'Cannot use caster of {0} ({1!r})'.format(self,
|
|
self.caster)
|
|
raise ValueError(error)
|
|
|
|
def to_python(self, value):
|
|
try:
|
|
return self._caster(value)
|
|
except self.exception:
|
|
raise ValueError(self.message.format(value))
|
|
|
|
|
|
class IntegerValue(CastingMixin, Value):
|
|
caster = int
|
|
|
|
|
|
class FloatValue(CastingMixin, Value):
|
|
caster = float
|
|
|
|
|
|
class DecimalValue(CastingMixin, Value):
|
|
caster = decimal.Decimal
|
|
exception = decimal.InvalidOperation
|
|
|
|
|
|
class ListValue(Value):
|
|
converter = None
|
|
message = 'Cannot interpret list item {0!r} in list {1!r}'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.separator = kwargs.pop('separator', ',')
|
|
converter = kwargs.pop('converter', None)
|
|
if converter is not None:
|
|
self.converter = converter
|
|
super(ListValue, self).__init__(*args, **kwargs)
|
|
# make sure the default is a list
|
|
if self.default is None:
|
|
self.default = []
|
|
# initial conversion
|
|
if self.converter is not None:
|
|
self.default = [self.converter(value) for value in self.default]
|
|
|
|
def to_python(self, value):
|
|
split_value = [v.strip() for v in value.strip().split(self.separator)]
|
|
# removing empty items
|
|
value_list = filter(None, split_value)
|
|
if self.converter is None:
|
|
return list(value_list)
|
|
|
|
converted_values = []
|
|
for list_value in value_list:
|
|
try:
|
|
converted_values.append(self.converter(list_value))
|
|
except (TypeError, ValueError):
|
|
raise ValueError(self.message.format(list_value, value))
|
|
return converted_values
|
|
|
|
|
|
class BackendsValue(ListValue):
|
|
|
|
def converter(self, value):
|
|
try:
|
|
import_by_path(value)
|
|
except ImproperlyConfigured as err:
|
|
six.reraise(ValueError, ValueError(err), sys.exc_info()[2])
|
|
return value
|
|
|
|
|
|
class TupleValue(ListValue):
|
|
message = 'Cannot interpret tuple item {0!r} in tuple {1!r}'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(TupleValue, self).__init__(*args, **kwargs)
|
|
if self.default is None:
|
|
self.default = ()
|
|
else:
|
|
self.default = tuple(self.default)
|
|
|
|
def to_python(self, value):
|
|
return tuple(super(TupleValue, self).to_python(value))
|
|
|
|
|
|
class SetValue(ListValue):
|
|
message = 'Cannot interpret set item {0!r} in set {1!r}'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(SetValue, self).__init__(*args, **kwargs)
|
|
if self.default is None:
|
|
self.default = set()
|
|
else:
|
|
self.default = set(self.default)
|
|
|
|
def to_python(self, value):
|
|
return set(super(SetValue, self).to_python(value))
|
|
|
|
|
|
class DictValue(Value):
|
|
message = 'Cannot interpret dict value {0!r}'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(DictValue, self).__init__(*args, **kwargs)
|
|
if self.default is None:
|
|
self.default = {}
|
|
else:
|
|
self.default = dict(self.default)
|
|
|
|
def to_python(self, value):
|
|
value = super(DictValue, self).to_python(value)
|
|
if not value:
|
|
return {}
|
|
try:
|
|
evaled_value = ast.literal_eval(value)
|
|
except ValueError:
|
|
raise ValueError(self.message.format(value))
|
|
if not isinstance(evaled_value, dict):
|
|
raise ValueError(self.message.format(value))
|
|
return evaled_value
|
|
|
|
|
|
class ValidationMixin(object):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ValidationMixin, self).__init__(*args, **kwargs)
|
|
if isinstance(self.validator, six.string_types):
|
|
self._validator = import_by_path(self.validator)
|
|
elif callable(self.validator):
|
|
self._validator = self.validator
|
|
else:
|
|
raise ValueError('Cannot use validator of '
|
|
'{0} ({1!r})'.format(self, self.validator))
|
|
self.to_python(self.default)
|
|
|
|
def to_python(self, value):
|
|
try:
|
|
self._validator(value)
|
|
except ValidationError:
|
|
raise ValueError(self.message.format(value))
|
|
else:
|
|
return value
|
|
|
|
|
|
class EmailValue(ValidationMixin, Value):
|
|
message = 'Cannot interpret email value {0!r}'
|
|
validator = 'django.core.validators.validate_email'
|
|
|
|
|
|
class URLValue(ValidationMixin, Value):
|
|
message = 'Cannot interpret URL value {0!r}'
|
|
validator = validators.URLValidator()
|
|
|
|
|
|
class IPValue(ValidationMixin, Value):
|
|
message = 'Cannot interpret IP value {0!r}'
|
|
validator = 'django.core.validators.validate_ipv46_address'
|
|
|
|
|
|
class RegexValue(ValidationMixin, Value):
|
|
message = "Regex doesn't match value {0!r}"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
regex = kwargs.pop('regex', None)
|
|
self.validator = validators.RegexValidator(regex=regex)
|
|
super(RegexValue, self).__init__(*args, **kwargs)
|
|
|
|
|
|
class PathValue(Value):
|
|
def __init__(self, *args, **kwargs):
|
|
self.check_exists = kwargs.pop('check_exists', True)
|
|
super(PathValue, self).__init__(*args, **kwargs)
|
|
|
|
def setup(self, name):
|
|
value = super(PathValue, self).setup(name)
|
|
value = os.path.expanduser(value)
|
|
if self.check_exists and not os.path.exists(value):
|
|
raise ValueError('Path {0!r} does not exist.'.format(value))
|
|
return os.path.abspath(value)
|
|
|
|
|
|
class SecretValue(Value):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs['environ'] = True
|
|
super(SecretValue, self).__init__(*args, **kwargs)
|
|
if self.default is not None:
|
|
raise ValueError('Secret values are only allowed to '
|
|
'be set as environment variables')
|
|
|
|
def setup(self, name):
|
|
value = super(SecretValue, self).setup(name)
|
|
if not value:
|
|
raise ValueError('Secret value {0!r} is not set'.format(name))
|
|
return value
|
|
|
|
|
|
class EmailURLValue(CastingMixin, MultipleMixin, Value):
|
|
caster = 'dj_email_url.parse'
|
|
message = 'Cannot interpret email URL value {0!r}'
|
|
late_binding = True
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault('environ', True)
|
|
kwargs.setdefault('environ_prefix', None)
|
|
kwargs.setdefault('environ_name', 'EMAIL_URL')
|
|
super(EmailURLValue, self).__init__(*args, **kwargs)
|
|
if self.default is None:
|
|
self.default = {}
|
|
else:
|
|
self.default = self.to_python(self.default)
|
|
|
|
|
|
class DictBackendMixin(Value):
|
|
default_alias = 'default'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.alias = kwargs.pop('alias', self.default_alias)
|
|
kwargs.setdefault('environ', True)
|
|
kwargs.setdefault('environ_prefix', None)
|
|
kwargs.setdefault('environ_name', self.environ_name)
|
|
super(DictBackendMixin, self).__init__(*args, **kwargs)
|
|
if self.default is None:
|
|
self.default = {}
|
|
else:
|
|
self.default = self.to_python(self.default)
|
|
|
|
def to_python(self, value):
|
|
value = super(DictBackendMixin, self).to_python(value)
|
|
return {self.alias: value}
|
|
|
|
|
|
class DatabaseURLValue(DictBackendMixin, CastingMixin, Value):
|
|
caster = 'dj_database_url.parse'
|
|
message = 'Cannot interpret database URL value {0!r}'
|
|
environ_name = 'DATABASE_URL'
|
|
late_binding = True
|
|
|
|
|
|
class CacheURLValue(DictBackendMixin, CastingMixin, Value):
|
|
caster = 'django_cache_url.parse'
|
|
message = 'Cannot interpret cache URL value {0!r}'
|
|
environ_name = 'CACHE_URL'
|
|
late_binding = True
|
|
|
|
|
|
class SearchURLValue(DictBackendMixin, CastingMixin, Value):
|
|
caster = 'dj_search_url.parse'
|
|
message = 'Cannot interpret Search URL value {0!r}'
|
|
environ_name = 'SEARCH_URL'
|
|
late_binding = True
|