Installing requirements

pull/2/head
sgoudham 4 years ago
parent 18b990c10d
commit 14ea256537

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2004-2020, Vinay Sajip
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,26 @@
Metadata-Version: 2.1
Name: config
Version: 0.5.0.post0
Summary: A hierarchical, easy-to-use, powerful configuration module for Python
Home-page: http://docs.red-dove.com/cfg/python.html
Author: Vinay Sajip
Author-email: vinay_sajip@red-dove.com
Maintainer: Vinay Sajip
Maintainer-email: vinay_sajip@red-dove.com
License: Copyright (C) 2004-2020 by Vinay Sajip. All Rights Reserved. See LICENSE for license.
Platform: any
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Software Development
This module allows a hierarchical configuration scheme with support for mappings and sequences, cross-references between one part of the configuration and another, the ability to flexibly access real Python objects without full-blown eval(), an include facility, simple expression evaluation and the ability to change, save, cascade and merge configurations. Interfaces easily with environment variables and command-line options.

@ -0,0 +1,12 @@
config-0.5.0.post0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
config-0.5.0.post0.dist-info/LICENSE,sha256=NpAuOJodhJmffkN3U8ThnyJHcn5SL4v4cnRc2Pg1r6w,1524
config-0.5.0.post0.dist-info/METADATA,sha256=ckMDJLQkRBs0upCgvHbR1uWMj9o_GEQ0qaexVnRy9WQ,1384
config-0.5.0.post0.dist-info/RECORD,,
config-0.5.0.post0.dist-info/WHEEL,sha256=h_aVn5OB2IERUjMbi2pucmR_zzWJtk303YXvhh60NJ8,110
config-0.5.0.post0.dist-info/top_level.txt,sha256=9hK4m828QBN59kTX5IVy40cPd9zUw5QWQF2AlSrXCJ4,7
config/__init__.py,sha256=IfOXd4eAO8BmzKm0_elrZYTFEVWpXEsQKSPrEGqtGtM,24881
config/__pycache__/__init__.cpython-36.pyc,,
config/__pycache__/parser.cpython-36.pyc,,
config/__pycache__/tokens.cpython-36.pyc,,
config/parser.py,sha256=i4Y0GOIerLbbnWUhpCC_cQS2Os232XWk7g91xdTJJa0,14313
config/tokens.py,sha256=O2OHc8KoWjSlhzoVKETX6HQyMjs5EQM0JnScNbAaL20,34534

@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.33.4)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

@ -0,0 +1,785 @@
#
# Copyright 2019-2020 by Vinay Sajip. All Rights Reserved.
#
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
import datetime
import importlib
import io
import logging
import os
import re
import sys
from .tokens import Token, SCALAR_TOKENS, WORD, BACKTICK, DOLLAR
from .parser import (
Parser,
MappingBody,
ListBody,
ASTNode,
ODict,
open_file,
RecognizerError,
ParserError,
)
__all__ = ['Config', 'ConfigFormatError', 'ConfigError']
logger = logging.getLogger(__name__)
if sys.version_info[0] < 3:
class timezone(datetime.tzinfo):
def __init__(self, td):
self.td = td
def utcoffset(self, dt):
return self.td
def dst(self, dt): # pragma: no cover
return datetime.timedelta(0)
else:
from datetime import timezone
basestring = str
__version__ = '0.5.0.post0'
class ConfigFormatError(ParserError):
pass
class ConfigError(ValueError):
pass
# This is a marker used in get(key, defaultValue) to catch rather than KeyError
class KeyNotFoundError(ConfigError):
pass
def _parse_path(path):
p = Parser(io.StringIO(path))
try:
p.advance()
if p.token.kind != WORD:
raise ConfigError('Invalid path: %s' % path)
result = p.primary()
if not p.at_end:
raise ConfigError('Invalid path: %s' % path)
except RecognizerError as e:
raise ConfigError('Invalid path: %s: %s' % (path, e))
return result
def _path_iterator(path):
def visit(node):
if isinstance(node, Token):
yield node
else:
op = node['op']
if 'operand' in node:
for val in visit(node['operand']):
yield val
else:
for val in visit(node['lhs']):
yield val
if op == '.':
yield op, node['rhs'].value
else:
assert op in ('[', ':')
yield op, node['rhs']
for v in visit(path):
yield v
def _to_source(node):
if isinstance(node, Token):
return str(node.value)
pi = _path_iterator(node)
parts = [next(pi).value]
for op, operand in pi:
if op == '.':
parts.append('.')
parts.append(operand)
elif op == ':':
parts.append('[')
start, stop, step = operand
if start is not None:
parts.append(_to_source(start))
parts.append(':')
if stop is not None:
parts.append(_to_source(stop))
if step is not None:
parts.append(':')
parts.append(_to_source(step))
parts.append(']')
elif op == '[':
parts.append('[')
parts.append(_to_source(operand))
parts.append(']')
else: # pragma: no cover
raise ConfigError('Unable to navigate: %s' % node)
return ''.join(parts)
def _unwrap(o):
if isinstance(o, DictWrapper):
result = o.as_dict()
elif isinstance(o, ListWrapper):
result = o.as_list()
else:
result = o
return result
# noinspection PyUnboundLocalVariable
_SYSTEM_TYPES = (basestring, bool, int, float, datetime.datetime, datetime.date)
# noinspection PyTypeChecker
_SCALAR_TYPES = _SYSTEM_TYPES + (Token,)
def _merge_dicts(target, source):
for k, v in source.items():
if k in target and isinstance(target[k], dict) and isinstance(v, Mapping):
_merge_dicts(target[k], v)
else:
target[k] = source[k]
# use negative lookahead to disallow anything starting with a digit.
_IDENTIFIER_PATTERN = re.compile(r'^(?!\d)(\w+)$', re.U)
def is_identifier(s):
return bool(_IDENTIFIER_PATTERN.match(s))
class DictWrapper(object):
def __init__(self, config, data):
self.config = config
self._data = data
def get(self, key, default=None):
try:
return self[key]
except KeyNotFoundError:
return default
def __getitem__(self, key):
if not isinstance(key, basestring):
raise ConfigError('string required, but found %r' % key)
data = self._data
config = self.config
if key in data or is_identifier(key):
try:
result = config._evaluated(data[key])
except KeyError:
raise KeyNotFoundError('not found in configuration: %s' % key)
else:
path = _parse_path(key)
result = config._get_from_path(path)
# data[key] = result
return result
def __len__(self): # pragma: no cover
return len(self._data)
def __contains__(self, key):
return key in self._data
def __or__(self, other):
assert isinstance(other, type(self))
data = self.as_dict()
_merge_dicts(data, other.as_dict())
return type(self)(self.config, data)
__add__ = __or__
def __sub__(self, other):
assert isinstance(other, type(self))
data = dict(self._data)
for k in other._data:
data.pop(k, None)
return type(self)(self.config, data)
def as_dict(self):
result = {}
for k, v in self._data.items():
v = self[k]
if isinstance(v, (DictWrapper, Config)):
v = v.as_dict()
elif isinstance(v, ListWrapper):
v = v.as_list()
result[k] = v
return result
def __repr__(self):
s = str(', '.join(self._data.keys()))
if len(s) > 60:
s = s[:57] + '...'
return '%s(%s)' % (self.__class__.__name__, s)
class ListWrapper(object):
def __init__(self, config, data):
self.config = config
self._data = data
def __len__(self):
return len(self._data)
def __getitem__(self, index):
assert isinstance(index, int) # slices handled in Evaluator
result = self._data[index]
evaluated = self.config._evaluated(result)
if evaluated is not result:
self._data[index] = result = evaluated
return result
def __add__(self, other):
assert isinstance(other, type(self))
return type(self)(self.config, self.as_list() + other.as_list())
def as_list(self):
result = []
for item in self:
if isinstance(item, (DictWrapper, Config)):
item = item.as_dict()
elif isinstance(item, ListWrapper):
item = item.as_list()
result.append(item)
return result
def __repr__(self):
s = str(self.as_list())
if len(s) > 60:
s = s[:57] + '...'
return '%s(%s)' % (self.__class__.__name__, s)
_ISO_DATETIME_PATTERN = re.compile(
r'\d{4}-\d{2}-\d{2}(([ T])'
r'((?P<time>\d{2}:\d{2}:\d{2})'
r'(?P<ms>\.\d{1,6})?'
r'((?P<sign>[+-])(?P<oh>\d{2}):'
r'(?P<om>\d{2})(:(?P<os>\d{2})'
r'(?P<oms>\.\d{1,6})?)?)?))?$'
)
_DOTTED_WORDS = r'[A-Za-z_]\w*(\.[A-Za-z_]\w*)*'
_COLON_OBJECT_PATTERN = re.compile('%s:(%s)?$' % (_DOTTED_WORDS, _DOTTED_WORDS))
_DOTTED_OBJECT_PATTERN = re.compile('%s$' % _DOTTED_WORDS)
_ENV_VALUE_PATTERN = re.compile(r'\$(\w+)(\|(.*))?$')
class Evaluator(object):
"""
This class is used to evaluate AST nodes. A separate class for this (rather
than putting the functionality in Config) because an evaluation context is
sometimes required. For example, resolving references needs to keep track
of references already seen in an evaluation, to catch circular references.
That needs to be done per-evaluation.
"""
op_map = {
'@': 'eval_at',
'$': 'eval_reference',
':': 'eval_slice',
'+': 'eval_add',
'-': 'eval_subtract',
'*': 'eval_multiply',
'**': 'eval_power',
'/': 'eval_divide',
'//': 'eval_integer_divide',
'%': 'eval_modulo',
'<<': 'eval_left_shift',
'>>': 'eval_right_shift',
'|': 'eval_bitor',
'&': 'eval_bitand',
'^': 'eval_bitxor',
'or': 'eval_logor',
'and': 'eval_logand',
}
def __init__(self, config):
self.config = config
self.refs_seen = {}
def evaluate(self, node):
result = node
if isinstance(node, Token):
if node.kind in SCALAR_TOKENS:
result = node.value
elif node.kind == WORD:
try:
result = self.config.context[node.value]
except KeyError:
msg = 'Unknown variable \'%s\' at %s' % (node.value, node.start)
raise ConfigError(msg)
elif node.kind == BACKTICK:
result = self.config.convert_string(node.value)
else: # pragma: no cover
raise NotImplementedError('Unable to evaluate %s' % node)
elif isinstance(node, ASTNode):
op = node['op']
meth = self.op_map[op]
result = getattr(self, meth)(node)
if isinstance(result, (MappingBody, ListBody)):
result = self.config._wrap(result)
return result
def eval_at(self, node):
operand = node['operand']
config = self.config
fn = config._evaluated(operand)
if not isinstance(fn, basestring):
raise ConfigError('@ operand must be a string, but is %s' % fn)
found = False
if os.path.isabs(fn):
if os.path.isfile(fn):
p = fn
found = True
else:
p = os.path.join(config.rootdir, fn)
if os.path.isfile(p):
found = True
else:
for ip in config.include_path:
p = os.path.join(ip, fn)
if os.path.isfile(p):
found = True
break
if not found:
raise ConfigError('Unable to locate %s' % fn)
with open_file(p) as f:
p = Parser(f)
result = p.container()
if isinstance(result, MappingBody):
cfg = Config(
None, context=config.context, cache=self.config._cache is not None
)
cfg._parent = config
cfg._data = cfg._wrap_mapping(result)
result = cfg
return result
def _get_from_path(self, path):
def is_ref(n):
return isinstance(n, ASTNode) and n['op'] == DOLLAR
pi = _path_iterator(path)
first = next(pi)
config = self.config
node = config._data[first.value]
result = self.evaluate(node)
# We start the evaluation with the current instance, but a path may
# cross sub-configuration boundaries, and references must always be
# evaluated in the context of the immediately enclosing configuration,
# not the top-level configuration (references are relative to the
# root of the enclosing configuration - otherwise configurations would
# not be standalone. So whenever we cross a sub-configuration boundary,
# the current_evaluator has to be pegged to that sub-configuration.
current_evaluator = self
for op, operand in pi:
need_string = False
if not isinstance(result, (ListWrapper, DictWrapper, Config)):
container = result
else:
container = result._data
if isinstance(result, Config):
current_evaluator = result._evaluator
need_string = not isinstance(result, ListWrapper)
sliced = False
if isinstance(operand, tuple):
operand = slice(*[current_evaluator.evaluate(item) for item in operand])
sliced = True
if not isinstance(result, ListWrapper):
raise ConfigError('slices can only operate on lists')
elif op != '.':
operand = current_evaluator.evaluate(operand)
if need_string:
if not isinstance(operand, basestring):
raise ConfigError('string required, but found %r' % operand)
elif not isinstance(operand, (int, slice)):
raise ConfigError(
'integer or slice required, but found \'%s\'' % operand
)
try:
v = container[operand] # always use indexing, never attr
except IndexError:
raise ConfigError('index out of range: %s' % operand)
except KeyError:
raise KeyNotFoundError('not found in configuration: %s' % operand)
if is_ref(v):
vid = id(v)
if vid in current_evaluator.refs_seen:
parts = []
for v in current_evaluator.refs_seen.values():
parts.append(
'%s %s' % (_to_source(v), v['operand']['lhs'].start)
)
s = ', '.join(sorted(parts))
raise ConfigError('Circular reference: %s' % s)
current_evaluator.refs_seen[vid] = v
if sliced:
v = ListBody(v) # ListBody gets wrapped, but not list
# v = config._wrap(v)
evaluated = current_evaluator.evaluate(v)
if evaluated is not v:
container[operand] = evaluated
result = evaluated
return result
def eval_reference(self, node):
result = self._get_from_path(node['operand'])
return result
def eval_add(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
result = lhs + rhs
if isinstance(result, list):
result = ListWrapper(lhs.config, result)
return result
except TypeError:
raise ConfigError('Unable to add %s to %s' % (rhs, lhs))
def eval_subtract(self, node):
if 'operand' in node:
# unary
operand = self.evaluate(node['operand'])
try:
return -operand
except TypeError:
raise ConfigError('Unable to negate %s' % operand)
else:
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs - rhs
except TypeError:
raise ConfigError('Unable to subtract %s from %s' % (rhs, lhs))
def eval_multiply(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs * rhs
except TypeError:
raise ConfigError('Unable to multiply %s by %s' % (lhs, rhs))
def eval_divide(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs / rhs
except TypeError:
raise ConfigError('Unable to divide %s by %s' % (lhs, rhs))
def eval_integer_divide(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs // rhs
except TypeError:
raise ConfigError('Unable to integer-divide %s by %s' % (lhs, rhs))
def eval_modulo(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs % rhs
except TypeError:
raise ConfigError('Unable to compute %s modulo %s' % (lhs, rhs))
def eval_power(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs ** rhs
except TypeError:
raise ConfigError('Unable to divide %s by %s' % (lhs, rhs))
def eval_left_shift(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs << rhs
except TypeError:
raise ConfigError('Unable to left-shift %s by %s' % (lhs, rhs))
def eval_right_shift(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs >> rhs
except TypeError:
raise ConfigError('Unable to right-shift %s by %s' % (lhs, rhs))
def eval_bitor(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs | rhs
except TypeError:
raise ConfigError('Unable to bitwise-or %s and %s' % (lhs, rhs))
def eval_bitand(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs & rhs
except TypeError:
raise ConfigError('Unable to bitwise-and %s and %s' % (lhs, rhs))
def eval_bitxor(self, node):
lhs = self.evaluate(node['lhs'])
rhs = self.evaluate(node['rhs'])
try:
return lhs ^ rhs
except TypeError:
raise ConfigError('Unable to bitwise-xor %s and %s' % (lhs, rhs))
def eval_logor(self, node):
lhs = self.evaluate(node['lhs'])
return lhs or self.evaluate(node['rhs'])
def eval_logand(self, node):
lhs = self.evaluate(node['lhs'])
return lhs and self.evaluate(node['rhs'])
def _walk_dotted_path(result, dotted_path):
if isinstance(dotted_path, basestring):
parts = dotted_path.split('.')
else:
parts = dotted_path
for p in parts:
result = getattr(result, p)
return result
def _resolve_dotted_object(s):
parts = s.split('.')
modname = parts.pop(0)
# first part must be a module/package.
result = importlib.import_module(modname)
while parts:
p = parts[0]
s = '%s.%s' % (modname, p)
try:
result = importlib.import_module(s)
parts.pop(0)
modname = s
except ImportError:
result = _walk_dotted_path(result, parts)
break
return result
def _resolve_colon_object(s):
module_name, dotted_path = s.split(':')
if module_name in sys.modules:
mod = sys.modules[module_name]
else:
mod = importlib.import_module(module_name)
if not dotted_path:
result = mod
else:
result = _walk_dotted_path(mod, dotted_path)
return result
def _default_convert_string(s):
result = s
m = _ISO_DATETIME_PATTERN.match(s)
if m:
gd = m.groupdict()
if not gd['time']:
result = datetime.datetime.strptime(m.string, '%Y-%m-%d').date()
else:
s = '%s %s' % (m.string[:10], gd['time'])
result = datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
if gd['ms']:
ms = int(float(gd['ms']) * 1e6)
result = result.replace(microsecond=ms)
if gd['oh']:
oh = int(gd['oh'], 10)
om = int(gd['om'], 10)
osec = oms = 0
if sys.version_info[:2] >= (3, 7):
if gd['os']:
osec = int(gd['os'], 10)
if gd['oms']:
oms = int(float(gd['oms']) * 1e6)
td = datetime.timedelta(
hours=oh, minutes=om, seconds=osec, microseconds=oms
)
if gd['sign'] == '-':
td = -td
tzinfo = timezone(td)
result = result.replace(tzinfo=tzinfo)
else:
m = _ENV_VALUE_PATTERN.match(s)
if m:
key, _, default = m.groups()
result = os.environ.get(key, default)
else:
m = _COLON_OBJECT_PATTERN.match(s)
try:
if m:
result = _resolve_colon_object(s)
else:
m = _DOTTED_OBJECT_PATTERN.match(s)
if m:
result = _resolve_dotted_object(s)
except ImportError:
pass
return result
class Config(object):
def __init__(self, stream_or_path, **kwargs):
self.context = kwargs.get('context', {})
self.include_path = kwargs.get('include_path', [])
self.no_duplicates = kwargs.get('no_duplicates', True)
self.strict_conversions = kwargs.get('strict_conversions', True)
cache = kwargs.get('cache', False)
self.path = kwargs.get('path')
self.rootdir = kwargs.get('rootdir')
self._can_close = False
self._parent = self._data = self._stream = None
self._cache = {} if cache else None
self._string_converter = _default_convert_string
self._evaluator = Evaluator(self)
if stream_or_path:
if isinstance(stream_or_path, basestring):
self.load_file(stream_or_path, kwargs.get('encoding'))
else:
self.load(stream_or_path)
def _wrap_mapping(self, items):
data = ODict()
seen = {}
result = DictWrapper(self, data)
for k, v in items:
key = k.value
if self.no_duplicates:
if key in seen:
msg = 'Duplicate key %s seen at %s ' '(previously at %s)' % (
key,
k.start,
seen[key],
)
raise ConfigError(msg)
seen[key] = k.start
data[key] = v
return result
def _get_from_path(self, path):
# convenience method
evaluator = self._evaluator
evaluator.refs_seen.clear()
return evaluator._get_from_path(path)
def convert_string(self, s):
result = self._string_converter(s)
if result is s and self.strict_conversions:
raise ConfigError('Unable to convert string \'%s\'' % s)
return result
def _wrap(self, o):
if isinstance(o, MappingBody):
result = self._wrap_mapping(o)
elif isinstance(o, ListBody):
result = ListWrapper(self, o)
else:
result = o
return result
def _evaluated(self, v):
return self._evaluator.evaluate(v)
def _get(self, key, default=None):
cached = self._cache is not None
if cached and key in self._cache:
result = self._cache[key]
else:
result = self._data.get(key, default)
if cached:
self._cache[key] = result
return result
def get(self, key, default=None):
return _unwrap(self._get(key, default))
def __getitem__(self, key):
cached = self._cache is not None
if cached and key in self._cache:
result = self._cache[key]
else:
result = self._data[key]
if cached:
self._cache[key] = result
return _unwrap(result)
def __contains__(self, key):
return key in self._data
def as_dict(self):
return self._data.as_dict()
# for compatibility with old code base (and its tests)
def __len__(self):
result = 0
if self._data is not None:
result = len(self._data)
return result
def load(self, stream):
self._stream = stream
path = self.path
if path is None:
path = getattr(stream, 'name', None)
if path is None:
rootdir = os.getcwd()
else:
rootdir = os.path.dirname(os.path.abspath(path))
self.rootdir = rootdir
self.path = path
try:
p = Parser(stream)
items = p.container()
except ParserError as pe:
cfe = ConfigFormatError(*pe.args)
cfe.location = pe.location
raise cfe
if not isinstance(items, MappingBody):
raise ConfigError('Root configuration must be a mapping')
self._data = self._wrap_mapping(items)
if self._cache is not None:
self._cache.clear()
def load_file(self, path, encoding=None):
with io.open(path, encoding=encoding or 'utf-8') as f:
self.load(f)
def close(self):
if self._can_close and self._stream:
self._stream.close()
def __getattr__(self, key):
import warnings
msg = 'Attribute access is deprecated; use indexed access instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
return self._data[key]

@ -0,0 +1,511 @@
# -*- coding: utf-8 -*-
#
# Copyright 2018-2020 by Vinay Sajip. All Rights Reserved.
#
from collections import OrderedDict
import functools
import io
import logging
import sys
from .tokens import *
logger = logging.getLogger(__name__)
open_file = functools.partial(io.open, encoding='utf-8')
class ParserError(RecognizerError):
pass
class ODict(OrderedDict):
"""
This class preserves insertion order for display purposes but is
otherwise just a dict.
"""
def __repr__(self): # pragma: no cover
result = []
for k, v in self.items():
result.append('%r: %r' % (k, v))
return '{%s}' % ', '.join(result)
class ASTNode(ODict):
start = end = None
class MappingBody(list):
start = end = None
class ListBody(list):
start = end = None
def set_positions(node, start, end):
node.start = start
node.end = end
def make_unary_expr(op, operand):
"""
This function makes an AST node for a unary expression
"""
result = ASTNode()
# The str() calls are to avoid repr ugliness in 2.x
result[str('op')] = str(op)
result[str('operand')] = operand
return result
def make_binary_expr(op, lhs, rhs):
"""
This function makes an AST node for a binary expression
"""
result = ASTNode()
# The str() calls are to avoid repr ugliness in 2.x
result[str('op')] = str(op)
result[str('lhs')] = lhs
result[str('rhs')] = rhs
return result
def invalid_index(n, pos):
raise ParserError(
'Invalid index at %s: expected 1 expression, ' 'found %d' % (pos, n)
)
TOKEN_REPRS = {
WORD: 'identifier',
NEWLINE: 'newline',
INTEGER: 'integer',
FLOAT: 'floating-point value',
COMPLEX: 'complex value',
STRING: 'string',
BACKTICK: 'backtick-string',
EOF: 'EOF',
}
def token_repr(kind):
if kind in TOKEN_REPRS:
return TOKEN_REPRS[kind]
if sys.version_info[0] == 2:
return "'%s'" % kind
return '%r' % kind
VALUE_STARTERS = {WORD, INTEGER, FLOAT, COMPLEX, STRING, BACKTICK, TRUE, FALSE, NONE}
class Parser(object):
def __init__(self, stream=None):
self.stream = stream
if stream:
self.tokenizer = Tokenizer(stream)
else:
self.tokenizer = None
self.token = None
def _make_stream(self, text):
if not isinstance(text, text_type):
text = text.decode('utf-8')
self.stream = io.StringIO(text)
self.tokenizer = Tokenizer(self.stream)
self.token = None
@property
def at_end(self):
return self.token.kind == EOF
@property
def remaining(self): # for debugging
return self.tokenizer.remaining
def advance(self):
self.token = self.tokenizer.get_token()
return self.token.kind
def expect(self, tt):
if self.token.kind != tt:
pe = ParserError(
'Expected %s, got %s at %s' % (token_repr(tt), token_repr(self.token.kind), self.token.start)
)
pe.location = self.token.start
raise pe
result = self.token
self.advance()
return result
def consume_newlines(self):
tt = self.token.kind
while tt == NEWLINE:
tt = self.advance()
return tt
def object_key(self):
if self.token.kind == STRING:
result = self.strings()
else:
result = self.token
self.advance()
return result
def mapping_body(self):
result = MappingBody()
start = self.token.start
tt = self.consume_newlines()
if tt in (RCURLY, EOF):
end = self.token.end
set_positions(result, start, end)
return result
if tt not in (WORD, STRING):
pe = ParserError(
'Unexpected %s at %s' % (token_repr(self.token.kind), self.token.start)
)
pe.location = self.token.start
raise pe
while tt in (WORD, STRING):
key = self.object_key()
if self.token.kind not in (COLON, ASSIGN):
pe = ParserError(
'Unexpected %s at %s'
% (token_repr(self.token.kind), self.token.start)
)
pe.location = self.token.start
raise pe
self.advance()
self.consume_newlines()
value = self.expr()
result.append((key, value))
tt = self.token.kind
if tt in (NEWLINE, COMMA):
self.advance()
tt = self.consume_newlines()
elif tt not in (RCURLY, EOF):
pe = ParserError(
'Unexpected %s at %s'
% (token_repr(self.token.kind), self.token.start)
)
pe.location = self.token.start
raise pe
return result
def strings(self):
result = self.token
start = result.start
s = result.value
if self.advance() == STRING:
all_text = []
while True:
all_text.append(s)
s = self.token.value
end = self.token.end
if self.advance() != STRING:
break
all_text.append(s)
s = ''.join(all_text)
result = Token(STRING, s, s)
result.start = start
result.end = end
return result
def list_body(self):
result = ListBody()
start = self.token.start
end = self.token.end
tt = self.consume_newlines()
while tt in (
LCURLY,
LBRACK,
LPAREN,
AT,
DOLLAR,
BACKTICK,
PLUS,
MINUS,
BITNOT,
INTEGER,
FLOAT,
COMPLEX,
TRUE,
FALSE,
NONE,
NOT,
STRING,
WORD,
):
value = self.expr()
end = self.token.end
result.append(value)
tt = self.token.kind
if tt not in (NEWLINE, COMMA):
break
self.advance()
tt = self.consume_newlines()
set_positions(result, start, end)
return result
def list(self):
self.expect(LBRACK)
result = self.list_body()
self.expect(RBRACK)
return result
def value(self):
tt = self.token.kind
if tt not in VALUE_STARTERS:
pe = ParserError(
'Unexpected %s when looking for value: %s'
% (token_repr(tt), self.token.start)
)
pe.location = self.token.start
raise pe
if tt == STRING:
token = self.strings()
else:
token = self.token
self.advance()
return token
def mapping(self):
start = self.expect(LCURLY).start
result = self.mapping_body()
end = self.expect(RCURLY).end
set_positions(result, start, end)
return result
def container(self):
self.advance()
k = self.consume_newlines()
if k == LCURLY:
result = self.mapping()
elif k == LBRACK:
result = self.list()
elif k in (WORD, STRING, EOF):
result = self.mapping_body()
else:
pe = ParserError(
'Unexpected %s at %s' % (token_repr(self.token.kind), self.token.start)
)
pe.location = self.token.start
raise pe
self.consume_newlines()
return result
def atom(self):
tt = self.token.kind
if tt == LCURLY:
result = self.mapping()
elif tt == LBRACK:
result = self.list()
elif tt in VALUE_STARTERS:
result = self.value()
elif tt == DOLLAR:
self.advance()
self.expect(LCURLY)
result = self.primary()
self.expect(RCURLY)
result = make_unary_expr(tt, result)
elif tt == LPAREN:
self.advance()
result = self.expr()
self.expect(RPAREN)
else:
# import pdb; pdb.set_trace()
pe = ParserError(
'Unexpected %s at %s' % (token_repr(self.token.kind), self.token.start)
)
pe.location = self.token.start
raise pe
return result
# noinspection PyUnboundLocalVariable
def trailer(self):
tt = self.token.kind
if tt != LBRACK:
self.expect(DOT)
result = self.expect(WORD)
else:
def get_slice_element():
lb = self.list_body()
n = len(lb)
if n != 1:
invalid_index(n, lb.start)
return lb[0]
tt = self.advance()
is_slice = False
if tt == COLON:
# it's a slice like [:xyz:abc]
start = None
is_slice = True
else:
elem = get_slice_element()
tt = self.token.kind
if tt != COLON:
result = elem
else:
start = elem
is_slice = True
if not is_slice:
tt = LBRACK
else:
step = stop = None
# at this point start is either None (if foo[:xyz]) or a
# value representing the start. We are pointing at the COLON
# after the start value
tt = self.advance()
if tt == COLON: # no stop, but there might be a step
tt = self.advance()
if tt != RBRACK:
step = get_slice_element()
elif tt != RBRACK:
stop = get_slice_element()
tt = self.token.kind
if tt == COLON:
tt = self.advance()
if tt != RBRACK:
step = get_slice_element()
result = (start, stop, step)
tt = COLON
self.expect(RBRACK)
return tt, result
def primary(self):
result = self.atom()
while self.token.kind in (LBRACK, DOT):
op, rhs = self.trailer()
result = make_binary_expr(op, result, rhs)
return result
def power(self):
result = self.primary()
if self.token.kind == POWER:
self.advance()
rhs = self.u_expr()
result = make_binary_expr(POWER, result, rhs)
return result
def u_expr(self):
tt = self.token.kind
if tt not in (PLUS, MINUS, BITNOT, AT):
result = self.power()
else:
self.advance()
result = make_unary_expr(tt, self.u_expr())
return result
def mul_expr(self):
result = self.u_expr()
while self.token.kind in (STAR, SLASH, SLASHSLASH, MODULO):
op = self.token.kind
self.advance()
rhs = self.u_expr()
result = make_binary_expr(op, result, rhs)
return result
def add_expr(self):
result = self.mul_expr()
while self.token.kind in (PLUS, MINUS):
op = self.token.kind
self.advance()
rhs = self.mul_expr()
result = make_binary_expr(op, result, rhs)
return result
def shift_expr(self):
result = self.add_expr()
while self.token.kind in (LSHIFT, RSHIFT):
op = self.token.kind
self.advance()
rhs = self.add_expr()
result = make_binary_expr(op, result, rhs)
return result
def bitand_expr(self):
result = self.shift_expr()
while self.token.kind == BITAND:
self.advance()
rhs = self.shift_expr()
result = make_binary_expr(BITAND, result, rhs)
return result
def bitxor_expr(self):
result = self.bitand_expr()
while self.token.kind == BITXOR:
self.advance()
rhs = self.bitand_expr()
result = make_binary_expr(BITXOR, result, rhs)
return result
def bitor_expr(self):
result = self.bitxor_expr()
while self.token.kind == BITOR:
self.advance()
rhs = self.bitxor_expr()
result = make_binary_expr(BITOR, result, rhs)
return result
def comp_op(self):
result = self.token.kind
tt = self.advance()
advance = False
if result == IS and tt == NOT:
result = 'is not'
advance = True
elif result == NOT and tt == IN:
result = 'not in'
advance = True
if advance:
self.advance()
return result
def comparison(self):
result = self.bitor_expr()
while self.token.kind in (LE, LT, GE, GT, EQ, NEQ, ALT_NEQ, IS, IN, NOT):
op = self.comp_op()
rhs = self.bitor_expr()
result = make_binary_expr(op, result, rhs)
return result
def not_expr(self):
if self.token.kind != NOT:
result = self.comparison()
else:
self.advance()
result = make_unary_expr(NOT, self.not_expr())
return result
def and_expr(self):
result = self.not_expr()
while self.token.kind == AND:
self.advance()
rhs = self.not_expr()
result = make_binary_expr(AND, result, rhs)
return result
def or_expr(self):
result = self.and_expr()
while self.token.kind == OR:
self.advance()
rhs = self.and_expr()
result = make_binary_expr(OR, result, rhs)
return result
expr = or_expr
def parse(self, text, rule='mapping_body'):
self._make_stream(text)
self.advance()
method = getattr(self, rule, None)
if not method:
raise ValueError('no such rule: %s' % rule)
return method()

File diff suppressed because it is too large Load Diff

@ -1,418 +0,0 @@
Metadata-Version: 1.1
Name: python-decouple
Version: 3.3
Summary: Strict separation of settings from code.
Home-page: http://github.com/henriquebastos/python-decouple/
Author: Henrique Bastos
Author-email: henrique@bastos.net
License: MIT
Description: Python Decouple: Strict separation of settings from code
========================================================
*Decouple* helps you to organize your settings so that you can
change parameters without having to redeploy your app.
It also makes it easy for you to:
#. store parameters in *ini* or *.env* files;
#. define comprehensive default values;
#. properly convert values to the correct data type;
#. have **only one** configuration module to rule all your instances.
It was originally designed for Django, but became an independent generic tool
for separating settings from code.
.. image:: https://img.shields.io/travis/henriquebastos/python-decouple.svg
:target: https://travis-ci.org/henriquebastos/python-decouple
:alt: Build Status
.. image:: https://landscape.io/github/henriquebastos/python-decouple/master/landscape.png
:target: https://landscape.io/github/henriquebastos/python-decouple/master
:alt: Code Health
.. image:: https://img.shields.io/pypi/v/python-decouple.svg
:target: https://pypi.python.org/pypi/python-decouple/
:alt: Latest PyPI version
.. contents:: Summary
Why?
====
Web framework's settings stores many different kinds of parameters:
* Locale and i18n;
* Middlewares and Installed Apps;
* Resource handles to the database, Memcached, and other backing services;
* Credentials to external services such as Amazon S3 or Twitter;
* Per-deploy values such as the canonical hostname for the instance.
The first 2 are *project settings* the last 3 are *instance settings*.
You should be able to change *instance settings* without redeploying your app.
Why not just use environment variables?
---------------------------------------
*Envvars* works, but since ``os.environ`` only returns strings, it's tricky.
Let's say you have an *envvar* ``DEBUG=False``. If you run:
.. code-block:: python
if os.environ['DEBUG']:
print True
else:
print False
It will print **True**, because ``os.environ['DEBUG']`` returns the **string** ``"False"``.
Since it's a non-empty string, it will be evaluated as True.
*Decouple* provides a solution that doesn't look like a workaround: ``config('DEBUG', cast=bool)``.
Usage
=====
Install:
.. code-block:: console
pip install python-decouple
Then use it on your ``settings.py``.
#. Import the ``config`` object:
.. code-block:: python
from decouple import config
#. Retrieve the configuration parameters:
.. code-block:: python
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
Encodings
---------
Decouple's default encoding is `UTF-8`.
But you can specify your preferred encoding.
Since `config` is lazy and only opens the configuration file when it's first needed, you have the chance to change
it's encoding right after import.
.. code-block:: python
from decouple import config
config.encoding = 'cp1251'
SECRET_KEY = config('SECRET_KEY')
If you wish to fallback to your system's default encoding do:
.. code-block:: python
import locale
from decouple import config
config.encoding = locale.getpreferredencoding(False)
SECRET_KEY = config('SECRET_KEY')
Where the settings data are stored?
-----------------------------------
*Decouple* supports both *.ini* and *.env* files.
Ini file
~~~~~~~~
Simply create a ``settings.ini`` next to your configuration module in the form:
.. code-block:: ini
[settings]
DEBUG=True
TEMPLATE_DEBUG=%(DEBUG)s
SECRET_KEY=ARANDOMSECRETKEY
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
PERCENTILE=90%%
#COMMENTED=42
*Note*: Since ``ConfigParser`` supports *string interpolation*, to represent the character ``%`` you need to escape it as ``%%``.
Env file
~~~~~~~~
Simply create a ``.env`` text file on your repository's root directory in the form:
.. code-block:: console
DEBUG=True
TEMPLATE_DEBUG=True
SECRET_KEY=ARANDOMSECRETKEY
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
PERCENTILE=90%
#COMMENTED=42
Example: How do I use it with Django?
-------------------------------------
Given that I have a ``.env`` file at my repository root directory, here is a snippet of my ``settings.py``.
I also recommend using `pathlib <https://docs.python.org/3/library/pathlib.html>`_
and `dj-database-url <https://pypi.python.org/pypi/dj-database-url/>`_.
.. code-block:: python
# coding: utf-8
from decouple import config
from unipath import Path
from dj_database_url import parse as db_url
BASE_DIR = Path(__file__).parent
DEBUG = config('DEBUG', default=False, cast=bool)
TEMPLATE_DEBUG = DEBUG
DATABASES = {
'default': config(
'DATABASE_URL',
default='sqlite:///' + BASE_DIR.child('db.sqlite3'),
cast=db_url
)
}
TIME_ZONE = 'America/Sao_Paulo'
USE_L10N = True
USE_TZ = True
SECRET_KEY = config('SECRET_KEY')
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
# ...
Attention with *undefined* parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On the above example, all configuration parameters except ``SECRET_KEY = config('SECRET_KEY')``
have a default value to fallback if it does not exist on the ``.env`` file.
If ``SECRET_KEY`` is not present in the ``.env``, *decouple* will raise an ``UndefinedValueError``.
This *fail fast* policy helps you avoid chasing misbehaviors when you eventually forget a parameter.
Overriding config files with environment variables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes you may want to change a parameter value without having to edit the ``.ini`` or ``.env`` files.
Since version 3.0, *decouple* respects the *unix way*.
Therefore environment variables have precedence over config files.
To override a config parameter you can simply do:
.. code-block:: console
DEBUG=True python manage.py
How it works?
=============
*Decouple* always searches for *Options* in this order:
#. Environment variables;
#. Repository: ini or .env file;
#. default argument passed to config.
There are 4 classes doing the magic:
- ``Config``
Coordinates all the configuration retrieval.
- ``RepositoryIni``
Can read values from ``os.environ`` and ini files, in that order.
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
- ``RepositoryEnv``
Can read values from ``os.environ`` and ``.env`` files.
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
- ``AutoConfig``
This is a *lazy* ``Config`` factory that detects which configuration repository you're using.
It recursively searches up your configuration module path looking for a
``settings.ini`` or a ``.env`` file.
Optionally, it accepts ``search_path`` argument to explicitly define
where the search starts.
The **config** object is an instance of ``AutoConfig`` that instantiates a ``Config`` with the proper ``Repository``
on the first time it is used.
Understanding the CAST argument
-------------------------------
By default, all values returned by ``decouple`` are ``strings``, after all they are
read from ``text files`` or the ``envvars``.
However, your Python code may expect some other value type, for example:
* Django's ``DEBUG`` expects a boolean ``True`` or ``False``.
* Django's ``EMAIL_PORT`` expects an ``integer``.
* Django's ``ALLOWED_HOSTS`` expects a ``list`` of hostnames.
* Django's ``SECURE_PROXY_SSL_HEADER`` expects a ``tuple`` with two elements, the name of the header to look for and the required value.
To meet this need, the ``config`` function accepts a ``cast`` argument which
receives any *callable*, that will be used to *transform* the string value
into something else.
Let's see some examples for the above mentioned cases:
.. code-block:: python
>>> os.environ['DEBUG'] = 'False'
>>> config('DEBUG', cast=bool)
False
>>> os.environ['EMAIL_PORT'] = '42'
>>> config('EMAIL_PORT', cast=int)
42
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
>>> config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
['.localhost', '.herokuapp.com']
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
('HTTP_X_FORWARDED_PROTO', 'https')
As you can see, ``cast`` is very flexible. But the last example got a bit complex.
Built in Csv Helper
~~~~~~~~~~~~~~~~~~~
To address the complexity of the last example, *Decouple* comes with an extensible *Csv helper*.
Let's improve the last example:
.. code-block:: python
>>> from decouple import Csv
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
>>> config('ALLOWED_HOSTS', cast=Csv())
['.localhost', '.herokuapp.com']
You can also have a `default` value that must be a string to be processed by `Csv`.
.. code-block:: python
>>> from decouple import Csv
>>> config('ALLOWED_HOSTS', default='127.0.0.1', cast=Csv())
['127.0.0.1']
You can also parametrize the *Csv Helper* to return other types of data.
.. code-block:: python
>>> os.environ['LIST_OF_INTEGERS'] = '1,2,3,4,5'
>>> config('LIST_OF_INTEGERS', cast=Csv(int))
[1, 2, 3, 4, 5]
>>> os.environ['COMPLEX_STRING'] = '%virtual_env%\t *important stuff*\t trailing spaces '
>>> csv = Csv(cast=lambda s: s.upper(), delimiter='\t', strip=' %*')
>>> csv(os.environ['COMPLEX_STRING'])
['VIRTUAL_ENV', 'IMPORTANT STUFF', 'TRAILING SPACES']
By default *Csv* returns a ``list``, but you can get a ``tuple`` or whatever you want using the ``post_process`` argument:
.. code-block:: python
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
('HTTP_X_FORWARDED_PROTO', 'https')
Contribute
==========
Your contribution is welcome.
Setup your development environment:
.. code-block:: console
git clone git@github.com:henriquebastos/python-decouple.git
cd python-decouple
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
tox
*Decouple* supports both Python 2.7 and 3.6. Make sure you have both installed.
I use `pyenv <https://github.com/pyenv/pyenv#simple-python-version-management-pyenv>`_ to
manage multiple Python versions and I described my workspace setup on this article:
`The definitive guide to setup my Python workspace
<https://medium.com/@henriquebastos/the-definitive-guide-to-setup-my-python-workspace-628d68552e14>`_
You can submit pull requests and issues for discussion. However I only
consider merging tested code.
License
=======
The MIT License (MIT)
Copyright (c) 2017 Henrique Bastos <henrique at bastos dot net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Platform: any
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries

@ -1,11 +0,0 @@
LICENSE
MANIFEST.in
README.rst
decouple.py
setup.cfg
setup.py
python_decouple.egg-info/PKG-INFO
python_decouple.egg-info/SOURCES.txt
python_decouple.egg-info/dependency_links.txt
python_decouple.egg-info/not-zip-safe
python_decouple.egg-info/top_level.txt

@ -1,7 +0,0 @@
..\__pycache__\decouple.cpython-36.pyc
..\decouple.py
PKG-INFO
SOURCES.txt
dependency_links.txt
not-zip-safe
top_level.txt

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2013 Henrique Bastos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,420 @@
Metadata-Version: 2.1
Name: python-decouple
Version: 3.3
Summary: Strict separation of settings from code.
Home-page: http://github.com/henriquebastos/python-decouple/
Author: Henrique Bastos
Author-email: henrique@bastos.net
License: MIT
Platform: any
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Python Decouple: Strict separation of settings from code
========================================================
*Decouple* helps you to organize your settings so that you can
change parameters without having to redeploy your app.
It also makes it easy for you to:
#. store parameters in *ini* or *.env* files;
#. define comprehensive default values;
#. properly convert values to the correct data type;
#. have **only one** configuration module to rule all your instances.
It was originally designed for Django, but became an independent generic tool
for separating settings from code.
.. image:: https://img.shields.io/travis/henriquebastos/python-decouple.svg
:target: https://travis-ci.org/henriquebastos/python-decouple
:alt: Build Status
.. image:: https://landscape.io/github/henriquebastos/python-decouple/master/landscape.png
:target: https://landscape.io/github/henriquebastos/python-decouple/master
:alt: Code Health
.. image:: https://img.shields.io/pypi/v/python-decouple.svg
:target: https://pypi.python.org/pypi/python-decouple/
:alt: Latest PyPI version
.. contents:: Summary
Why?
====
Web framework's settings stores many different kinds of parameters:
* Locale and i18n;
* Middlewares and Installed Apps;
* Resource handles to the database, Memcached, and other backing services;
* Credentials to external services such as Amazon S3 or Twitter;
* Per-deploy values such as the canonical hostname for the instance.
The first 2 are *project settings* the last 3 are *instance settings*.
You should be able to change *instance settings* without redeploying your app.
Why not just use environment variables?
---------------------------------------
*Envvars* works, but since ``os.environ`` only returns strings, it's tricky.
Let's say you have an *envvar* ``DEBUG=False``. If you run:
.. code-block:: python
if os.environ['DEBUG']:
print True
else:
print False
It will print **True**, because ``os.environ['DEBUG']`` returns the **string** ``"False"``.
Since it's a non-empty string, it will be evaluated as True.
*Decouple* provides a solution that doesn't look like a workaround: ``config('DEBUG', cast=bool)``.
Usage
=====
Install:
.. code-block:: console
pip install python-decouple
Then use it on your ``settings.py``.
#. Import the ``config`` object:
.. code-block:: python
from decouple import config
#. Retrieve the configuration parameters:
.. code-block:: python
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
Encodings
---------
Decouple's default encoding is `UTF-8`.
But you can specify your preferred encoding.
Since `config` is lazy and only opens the configuration file when it's first needed, you have the chance to change
it's encoding right after import.
.. code-block:: python
from decouple import config
config.encoding = 'cp1251'
SECRET_KEY = config('SECRET_KEY')
If you wish to fallback to your system's default encoding do:
.. code-block:: python
import locale
from decouple import config
config.encoding = locale.getpreferredencoding(False)
SECRET_KEY = config('SECRET_KEY')
Where the settings data are stored?
-----------------------------------
*Decouple* supports both *.ini* and *.env* files.
Ini file
~~~~~~~~
Simply create a ``settings.ini`` next to your configuration module in the form:
.. code-block:: ini
[settings]
DEBUG=True
TEMPLATE_DEBUG=%(DEBUG)s
SECRET_KEY=ARANDOMSECRETKEY
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
PERCENTILE=90%%
#COMMENTED=42
*Note*: Since ``ConfigParser`` supports *string interpolation*, to represent the character ``%`` you need to escape it as ``%%``.
Env file
~~~~~~~~
Simply create a ``.env`` text file on your repository's root directory in the form:
.. code-block:: console
DEBUG=True
TEMPLATE_DEBUG=True
SECRET_KEY=ARANDOMSECRETKEY
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
PERCENTILE=90%
#COMMENTED=42
Example: How do I use it with Django?
-------------------------------------
Given that I have a ``.env`` file at my repository root directory, here is a snippet of my ``settings.py``.
I also recommend using `pathlib <https://docs.python.org/3/library/pathlib.html>`_
and `dj-database-url <https://pypi.python.org/pypi/dj-database-url/>`_.
.. code-block:: python
# coding: utf-8
from decouple import config
from unipath import Path
from dj_database_url import parse as db_url
BASE_DIR = Path(__file__).parent
DEBUG = config('DEBUG', default=False, cast=bool)
TEMPLATE_DEBUG = DEBUG
DATABASES = {
'default': config(
'DATABASE_URL',
default='sqlite:///' + BASE_DIR.child('db.sqlite3'),
cast=db_url
)
}
TIME_ZONE = 'America/Sao_Paulo'
USE_L10N = True
USE_TZ = True
SECRET_KEY = config('SECRET_KEY')
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
# ...
Attention with *undefined* parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On the above example, all configuration parameters except ``SECRET_KEY = config('SECRET_KEY')``
have a default value to fallback if it does not exist on the ``.env`` file.
If ``SECRET_KEY`` is not present in the ``.env``, *decouple* will raise an ``UndefinedValueError``.
This *fail fast* policy helps you avoid chasing misbehaviors when you eventually forget a parameter.
Overriding config files with environment variables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes you may want to change a parameter value without having to edit the ``.ini`` or ``.env`` files.
Since version 3.0, *decouple* respects the *unix way*.
Therefore environment variables have precedence over config files.
To override a config parameter you can simply do:
.. code-block:: console
DEBUG=True python manage.py
How it works?
=============
*Decouple* always searches for *Options* in this order:
#. Environment variables;
#. Repository: ini or .env file;
#. default argument passed to config.
There are 4 classes doing the magic:
- ``Config``
Coordinates all the configuration retrieval.
- ``RepositoryIni``
Can read values from ``os.environ`` and ini files, in that order.
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
- ``RepositoryEnv``
Can read values from ``os.environ`` and ``.env`` files.
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
- ``AutoConfig``
This is a *lazy* ``Config`` factory that detects which configuration repository you're using.
It recursively searches up your configuration module path looking for a
``settings.ini`` or a ``.env`` file.
Optionally, it accepts ``search_path`` argument to explicitly define
where the search starts.
The **config** object is an instance of ``AutoConfig`` that instantiates a ``Config`` with the proper ``Repository``
on the first time it is used.
Understanding the CAST argument
-------------------------------
By default, all values returned by ``decouple`` are ``strings``, after all they are
read from ``text files`` or the ``envvars``.
However, your Python code may expect some other value type, for example:
* Django's ``DEBUG`` expects a boolean ``True`` or ``False``.
* Django's ``EMAIL_PORT`` expects an ``integer``.
* Django's ``ALLOWED_HOSTS`` expects a ``list`` of hostnames.
* Django's ``SECURE_PROXY_SSL_HEADER`` expects a ``tuple`` with two elements, the name of the header to look for and the required value.
To meet this need, the ``config`` function accepts a ``cast`` argument which
receives any *callable*, that will be used to *transform* the string value
into something else.
Let's see some examples for the above mentioned cases:
.. code-block:: python
>>> os.environ['DEBUG'] = 'False'
>>> config('DEBUG', cast=bool)
False
>>> os.environ['EMAIL_PORT'] = '42'
>>> config('EMAIL_PORT', cast=int)
42
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
>>> config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
['.localhost', '.herokuapp.com']
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
('HTTP_X_FORWARDED_PROTO', 'https')
As you can see, ``cast`` is very flexible. But the last example got a bit complex.
Built in Csv Helper
~~~~~~~~~~~~~~~~~~~
To address the complexity of the last example, *Decouple* comes with an extensible *Csv helper*.
Let's improve the last example:
.. code-block:: python
>>> from decouple import Csv
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
>>> config('ALLOWED_HOSTS', cast=Csv())
['.localhost', '.herokuapp.com']
You can also have a `default` value that must be a string to be processed by `Csv`.
.. code-block:: python
>>> from decouple import Csv
>>> config('ALLOWED_HOSTS', default='127.0.0.1', cast=Csv())
['127.0.0.1']
You can also parametrize the *Csv Helper* to return other types of data.
.. code-block:: python
>>> os.environ['LIST_OF_INTEGERS'] = '1,2,3,4,5'
>>> config('LIST_OF_INTEGERS', cast=Csv(int))
[1, 2, 3, 4, 5]
>>> os.environ['COMPLEX_STRING'] = '%virtual_env%\t *important stuff*\t trailing spaces '
>>> csv = Csv(cast=lambda s: s.upper(), delimiter='\t', strip=' %*')
>>> csv(os.environ['COMPLEX_STRING'])
['VIRTUAL_ENV', 'IMPORTANT STUFF', 'TRAILING SPACES']
By default *Csv* returns a ``list``, but you can get a ``tuple`` or whatever you want using the ``post_process`` argument:
.. code-block:: python
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
('HTTP_X_FORWARDED_PROTO', 'https')
Contribute
==========
Your contribution is welcome.
Setup your development environment:
.. code-block:: console
git clone git@github.com:henriquebastos/python-decouple.git
cd python-decouple
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
tox
*Decouple* supports both Python 2.7 and 3.6. Make sure you have both installed.
I use `pyenv <https://github.com/pyenv/pyenv#simple-python-version-management-pyenv>`_ to
manage multiple Python versions and I described my workspace setup on this article:
`The definitive guide to setup my Python workspace
<https://medium.com/@henriquebastos/the-definitive-guide-to-setup-my-python-workspace-628d68552e14>`_
You can submit pull requests and issues for discussion. However I only
consider merging tested code.
License
=======
The MIT License (MIT)
Copyright (c) 2017 Henrique Bastos <henrique at bastos dot net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,8 @@
__pycache__/decouple.cpython-36.pyc,,
decouple.py,sha256=wwJojLoN8vLIeHVQJWhRT3vz3Bm4jUeNc-vIFqoYeM8,6548
python_decouple-3.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
python_decouple-3.3.dist-info/LICENSE,sha256=TW48LvIp7DwaSG9SyddiNenrfJTbjscai_8K9g5PCqQ,1076
python_decouple-3.3.dist-info/METADATA,sha256=XATX3mnYM7Gjv69Y-2501UskD1sH46rFppirjLd0JHs,13053
python_decouple-3.3.dist-info/RECORD,,
python_decouple-3.3.dist-info/WHEEL,sha256=YUYzQ6UQdoqxXjimOitTqynltBCkwY6qlTfTh2IzqQU,97
python_decouple-3.3.dist-info/top_level.txt,sha256=4Z_ufgotqDCUtAtgtampXl_P-jKS9H5ifDfU0J3hD5U,9

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.34.2)
Root-Is-Purelib: true
Tag: py3-none-any
Loading…
Cancel
Save