├── .bumpversion.cfg ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── README.rst ├── envcfg ├── __init__.py ├── _hook.py ├── json │ └── __init__.py ├── raw │ └── __init__.py └── smart │ └── __init__.py ├── setup.cfg ├── setup.py ├── tests ├── conftest.py ├── test_json.py ├── test_raw.py └── test_smart.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | files = setup.py envcfg/__init__.py 3 | commit = True 4 | tag = False 5 | current_version = 0.2.0 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | .cache 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Editor 34 | *.sw[po] 35 | 36 | # Sphinx 37 | docs/_build 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "pypy" 8 | install: 9 | - "pip install ." 10 | - "pip install pytest>=2.4.2 -U" 11 | - "pip install pytest-cov pytest-pep8 coveralls" 12 | - "touch tests/__init__.py" 13 | script: "py.test --cov envcfg --pep8 tests" 14 | after_success: 15 | coveralls 16 | branches: 17 | only: 18 | - master 19 | - develop 20 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.2.0 5 | ----- 6 | 7 | - Add "smart" format support. (#4) 8 | 9 | 0.1.0 10 | ----- 11 | 12 | The first release. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Jiangge Zhang 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Build Status| |Coverage Status| |PyPI Version| |PyPI Downloads| |Wheel Status| 2 | 3 | python-envcfg 4 | ============= 5 | 6 | Accessing environment variables with a magic module. 7 | 8 | :: 9 | 10 | >>> import os 11 | >>> from envcfg.raw.python import CONFIGURE_OPTS 12 | >>> 13 | >>> CONFIGURE_OPTS 14 | '--enable-shared --enable-universalsdk=/ --with-universal-archs=intel' 15 | >>> CONFIGURE_OPTS == os.environ['PYTHON_CONFIGURE_OPTS'] 16 | True 17 | 18 | It works with many frameworks such as Django and Flask. Then you can store your 19 | config in the environment variables instead of framework-specific config files. 20 | It is recommended by 12-Factor_. 21 | 22 | 23 | Installation 24 | ------------ 25 | 26 | :: 27 | 28 | $ pip install python-envcfg 29 | 30 | 31 | Supported Formats 32 | ----------------- 33 | 34 | - ``import envcfg.raw.foo as config``: 35 | Import each ``FOO_*`` environment variable as string. 36 | - ``import envcfg.json.foo as config``: 37 | Import each ``FOO_*`` environment variable as JSON body. 38 | - ``import envcfg.smart.foo as config``: 39 | Try to import each ``FOO_*`` environment variable as JSON body, if fail then import it as string. 40 | 41 | There is an example: 42 | 43 | +-------------------+------------------------+-------------------------+--------------------------+ 44 | | | ``envcfg.raw.foo.VAL`` | ``envcfg.json.foo.VAL`` | ``envcfg.smart.foo.VAL`` | 45 | +===================+========================+=========================+==========================+ 46 | | ``FOO_VAL=foo`` | ``'foo'`` | ``ImportError`` | ``'foo'`` | 47 | +-------------------+------------------------+-------------------------+--------------------------+ 48 | | ``FOO_VAL="foo"`` | ``'"foo"'`` | ``'foo'`` | ``'foo'`` | 49 | +-------------------+------------------------+-------------------------+--------------------------+ 50 | | ``FOO_VAL=42`` | ``'42'`` | ``42`` | ``42`` | 51 | +-------------------+------------------------+-------------------------+--------------------------+ 52 | | ``FOO_VAL="42"`` | ``'"42"'`` | ``'42'`` | ``'42'`` | 53 | +-------------------+------------------------+-------------------------+--------------------------+ 54 | 55 | Framework Integration 56 | --------------------- 57 | 58 | Flask 59 | ~~~~~ 60 | 61 | 1. Defines environment variables with a prefix:: 62 | 63 | $ cat .env # should not checked into VCS 64 | # values are valid JSON expressions 65 | MYAPP_DEBUG=true 66 | MYAPP_SECRET_KEY='"7950ad141c7e4b3990631fcdf9a1d909"' 67 | MYAPP_SQLALCHEMY_DATABASE_URI='"sqlite:///tmp/myapp.sqlite3"' 68 | 69 | 2. Creates Flask app and loads config from python-envcfg:: 70 | 71 | $ cat myapp.py 72 | ... 73 | app = Flask(__name__) 74 | app.config.from_object('envcfg.json.myapp') # MYAPP_ -> .myapp 75 | ... 76 | 77 | 3. Enters your app with those environment variables:: 78 | 79 | $ env $(cat .env | xargs) python myapp.py 80 | 81 | 82 | Django 83 | ~~~~~~ 84 | 85 | 1. Creates a django project and moves all sensitive config items into the 86 | environment variables:: 87 | 88 | $ cat djapp/settings.py # codebase-scope config 89 | ... 90 | INSTALLED_APPS = ( 91 | 'django.contrib.admin', 92 | ) 93 | ... 94 | 95 | $ cat .env # environment-scope config, should not checked into VCS 96 | # values are valid JSON expressions 97 | DJAPP_SECRET_KEY='"wo9g2o#jws=u"' 98 | DJAPP_DEBUG=true 99 | DJAPP_TEMPLATE_DEBUG=true 100 | 101 | 2. Adds importing statements in the end of ``settings.py`` module:: 102 | 103 | $ tail -n 2 djapp/settings.py 104 | # importing all config items stored in the environment variables 105 | from envcfg.json.djapp import * # noqa 106 | 107 | 3. Runs your Django app with environment variables:: 108 | 109 | $ env $(cat .env | xargs) python manage.py runserver 110 | 111 | 112 | Tornado 113 | ~~~~~~~ 114 | 115 | 1. Defines environment variables with a prefix:: 116 | 117 | $ cat .env 118 | export TORAPP_PORT='8888' 119 | export TORAPP_MYSQL_HOST='"127.0.0.1"' 120 | export TORAPP_MYSQL_DATABASE='"database"' 121 | 122 | 123 | 2. Creates a Tornado project and loads config:: 124 | 125 | $ cat torapp/server.py 126 | 127 | from tornado.web import Application, RequestHandler 128 | from tornado.ioloop import IOLoop 129 | from tornado.options import define, options 130 | from tordb import Connection 131 | 132 | 133 | def options_from_object(*args, **kwargs): 134 | module = __import__(*args, **kwargs) 135 | for name, value in vars(module).items(): 136 | name = name.lower() 137 | if name in options._options: 138 | options._options[name].set(value) 139 | 140 | 141 | class IndexHandler(RequestHandler): 142 | def initialize(self): 143 | self.db = Connection(options.mysql_host, options.mysql_database) 144 | 145 | def get(self): 146 | pass # some database operations with ``self.db`` 147 | 148 | 149 | application = Application([ 150 | (r'/', IndexHandler), 151 | ]) 152 | 153 | define('port', type=int) 154 | define('mysql_host', type=unicode) 155 | define('mysql_database', type=unicode) 156 | options_from_object('envcfg.json.torapp', fromlist=['torapp']) 157 | 158 | 159 | if __name__ == '__main__': 160 | application.listen(options.port) 161 | IOLoop.instance().start() 162 | 163 | 164 | 3. Runs your Tornado app:: 165 | 166 | $ env $(cat .env | xargs) python server.py 167 | 168 | 169 | Works on Projects 170 | ----------------- 171 | 172 | In development, we can work with per-project environments but no more typing 173 | ``source foo/bar``. 174 | 175 | I recommend to put your project-specified environment variables in 176 | ``{PROJECT_ROOT}/.env`` and mark the ``.env`` as ignored in your VCS. For 177 | example, you can write ``/.env`` in ``.gitignore`` if you are using Git, and 178 | put a ``.env.example`` as a copying template for new-cloned projects. 179 | 180 | And then, you can use some utility such as `honcho`_ or `autoenv`_ to apply 181 | the ``.env`` automatically. 182 | 183 | For honcho:: 184 | 185 | $ echo 'MYPROJECT_DEBUG=true' >> .env 186 | $ echo 'web: python manage.py runserver' >> Procfile 187 | $ honcho run python manage.py check-debug 188 | True 189 | $ honcho start web 190 | Starting development server at http://127.0.0.1:5000/ 191 | ... 192 | 193 | For autoenv:: 194 | 195 | $ echo 'MYPROJECT_DEBUG=true' >> myproject/.env 196 | $ cd myproject 197 | $ python manage.py check-debug 198 | True 199 | $ python manage.py runserver 200 | Starting development server at http://127.0.0.1:5000/ 201 | ... 202 | 203 | 204 | Issues 205 | ------ 206 | 207 | If you want to report bugs or request features, please create issues on 208 | `GitHub Issues `_. 209 | 210 | Alternatives 211 | ------------ 212 | 213 | - https://github.com/henriquebastos/python-decouple 214 | - https://github.com/pallets/click 215 | 216 | 217 | .. _12-Factor: http://12factor.net 218 | .. _honcho: https://github.com/nickstenning/honcho 219 | .. _autoenv: https://github.com/kennethreitz/autoenv 220 | 221 | .. |Build Status| image:: https://travis-ci.org/tonyseek/python-envcfg.svg?branch=master,develop 222 | :target: https://travis-ci.org/tonyseek/python-envcfg 223 | :alt: Build Status 224 | .. |Coverage Status| image:: https://img.shields.io/coveralls/tonyseek/python-envcfg/develop.svg 225 | :target: https://coveralls.io/r/tonyseek/python-envcfg 226 | :alt: Coverage Status 227 | .. |Wheel Status| image:: https://img.shields.io/pypi/wheel/python-envcfg.svg 228 | :target: https://warehouse.python.org/project/python-envcfg 229 | :alt: Wheel Status 230 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/python-envcfg.svg 231 | :target: https://pypi.python.org/pypi/python-envcfg 232 | :alt: PyPI Version 233 | .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/python-envcfg.svg 234 | :target: https://pypi.python.org/pypi/python-envcfg 235 | :alt: Downloads 236 | -------------------------------------------------------------------------------- /envcfg/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.0' 2 | -------------------------------------------------------------------------------- /envcfg/_hook.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import types 5 | 6 | 7 | class ImportHook(object): 8 | 9 | re_module_name = re.compile(r'[a-z][a-z0-9_]*') 10 | 11 | def __init__(self, wrapper_module, value_processor): 12 | self.wrapper_module = wrapper_module 13 | self.wrapper_prefix = wrapper_module + '.' 14 | self.value_processor = value_processor 15 | 16 | def __eq__(self, other): 17 | return self.__class__.__module__ == other.__class__.__module__ and \ 18 | self.__class__.__name__ == other.__class__.__name__ and \ 19 | self.wrapper_module == other.wrapper_module 20 | 21 | def __ne__(self, other): 22 | return not self.__eq__(other) 23 | 24 | def install(self): 25 | sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] 26 | 27 | def find_module(self, fullname, path=None): 28 | if fullname.startswith(self.wrapper_prefix): 29 | return self 30 | 31 | def load_module(self, fullname): 32 | if fullname in sys.modules: 33 | return sys.modules[fullname] 34 | 35 | prefix_name = fullname[len(self.wrapper_prefix):] 36 | if not self.re_module_name.match(prefix_name): 37 | error_msg = ('No module named {0}\n\nThe name of envvar module ' 38 | 'should matched {1.pattern}') 39 | raise ImportError(error_msg.format(fullname, self.re_module_name)) 40 | 41 | module = types.ModuleType(fullname) 42 | for name, value in self.load_environ(prefix_name): 43 | setattr(module, name, value) 44 | sys.modules[fullname] = module 45 | 46 | return module 47 | 48 | def load_environ(self, prefix_name): 49 | prefix = prefix_name.upper() + '_' 50 | for raw_name, raw_value in os.environ.items(): 51 | if not raw_name.startswith(prefix): 52 | continue 53 | if raw_name == prefix: 54 | continue 55 | name = raw_name[len(prefix):] 56 | value = self.value_processor(name, raw_name, raw_value) 57 | yield name, value 58 | 59 | 60 | def import_hook(wrapper_module): 61 | def wrapper(fn): 62 | hook = ImportHook(wrapper_module, value_processor=fn) 63 | hook.install() 64 | return wrapper 65 | -------------------------------------------------------------------------------- /envcfg/json/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .._hook import import_hook 4 | 5 | 6 | @import_hook(__name__) 7 | def value_processor(name, raw_name, raw_value): 8 | import json 9 | try: 10 | value = json.loads(raw_value) 11 | except ValueError: 12 | error_msg = ( 13 | '{0}={1!r} found but {1!r} is not a valid json value.\n\n' 14 | 'You may want {0}=\'"{1}"\' if the value should be a string.') 15 | raise ImportError(error_msg.format(raw_name, raw_value)) 16 | return value 17 | 18 | 19 | del import_hook 20 | del value_processor 21 | -------------------------------------------------------------------------------- /envcfg/raw/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .._hook import import_hook 4 | 5 | 6 | @import_hook(__name__) 7 | def value_processor(name, raw_name, raw_value): 8 | return raw_value 9 | 10 | 11 | del import_hook 12 | del value_processor 13 | -------------------------------------------------------------------------------- /envcfg/smart/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .._hook import import_hook 4 | 5 | 6 | @import_hook(__name__) 7 | def value_processor(name, raw_name, raw_value): 8 | import json 9 | try: 10 | return json.loads(raw_value) 11 | except ValueError: 12 | return raw_value 13 | 14 | 15 | del import_hook 16 | del value_processor 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pep8ignore = 3 | docs/conf.py ALL 4 | docs/_themes/* ALL 5 | [bdist_wheel] 6 | universal = 1 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | with open('README.rst') as readme: 5 | next(readme) # skip the first line 6 | long_description = ''.join(readme).strip() 7 | 8 | 9 | setup( 10 | name='python-envcfg', 11 | version='0.2.0', 12 | author='Jiangge Zhang', 13 | author_email='tonyseek@gmail.com', 14 | description='Accessing environment variables with a magic module.', 15 | long_description=long_description, 16 | platforms=['Any'], 17 | url='https://github.com/tonyseek/python-envcfg', 18 | license='MIT', 19 | packages=find_packages(), 20 | keywords=['env', 'config', '12-factor'], 21 | classifiers=[ 22 | 'Development Status :: 3 - Alpha', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Operating System :: OS Independent', 25 | 'Programming Language :: Python', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3.3', 28 | 'Programming Language :: Python :: 3.4', 29 | 'Topic :: Software Development :: Libraries', 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pytest import fixture 4 | 5 | 6 | @fixture(scope='function') 7 | def environ(request): 8 | origin = dict(os.environ) 9 | 10 | @request.addfinalizer 11 | def restore_environ(): 12 | os.environ.clear() 13 | os.environ.update(origin) 14 | 15 | return os.environ 16 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | 4 | def test_success(environ): 5 | environ['ENVCFG_JSON_1_BOOLEAN'] = 'true' 6 | environ['ENVCFG_JSON_1_INTEGER'] = '42' 7 | environ['ENVCFG_JSON_1_REAL'] = '42.42' 8 | environ['ENVCFG_JSON_1_STRING'] = '"42"' 9 | environ['ENVCFG_JSON_1_DICT'] = '{"value": 42}' 10 | 11 | from envcfg.json.envcfg_json_1 import ( 12 | BOOLEAN, 13 | INTEGER, 14 | REAL, 15 | STRING, 16 | DICT, 17 | ) 18 | assert BOOLEAN is True 19 | assert INTEGER == 42 20 | assert REAL == 42.42 21 | assert STRING == '42' 22 | assert DICT == {'value': 42} 23 | 24 | 25 | def test_failed(environ): 26 | environ['ENVCFG_JSON_2_INVALID'] = 'foo' 27 | 28 | with raises(ImportError) as einfo: 29 | import envcfg.json._private_module # noqa 30 | assert einfo.value.args[0].startswith( 31 | 'No module named envcfg.json._private_module') 32 | 33 | with raises(ImportError) as einfo: 34 | import envcfg.json.INVALID_NAME # noqa 35 | assert einfo.value.args[0].startswith( 36 | 'No module named envcfg.json.INVALID_NAME') 37 | 38 | with raises(ImportError) as einfo: 39 | import envcfg.json.envcfg_json_2 # noqa 40 | assert 'is not a valid json value' in einfo.value.args[0] 41 | -------------------------------------------------------------------------------- /tests/test_raw.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | 4 | def test_success(environ): 5 | environ['ENVCFG_RAW_1_BOOLEAN'] = 'true' 6 | environ['ENVCFG_RAW_1_INTEGER'] = '42' 7 | environ['ENVCFG_RAW_1_REAL'] = '42.42' 8 | environ['ENVCFG_RAW_1_STRING'] = '"42"' 9 | environ['ENVCFG_RAW_1_DICT'] = '{"value": 42}' 10 | 11 | from envcfg.raw.envcfg_raw_1 import ( 12 | BOOLEAN, 13 | INTEGER, 14 | REAL, 15 | STRING, 16 | DICT, 17 | ) 18 | assert BOOLEAN == 'true' 19 | assert INTEGER == '42' 20 | assert REAL == '42.42' 21 | assert STRING == '"42"' 22 | assert DICT == '{"value": 42}' 23 | 24 | 25 | def test_failed(): 26 | with raises(ImportError) as einfo: 27 | import envcfg.raw._private_module # noqa 28 | assert einfo.value.args[0].startswith( 29 | 'No module named envcfg.raw._private_module') 30 | 31 | with raises(ImportError) as einfo: 32 | import envcfg.raw.INVALID_NAME # noqa 33 | assert einfo.value.args[0].startswith( 34 | 'No module named envcfg.raw.INVALID_NAME') 35 | -------------------------------------------------------------------------------- /tests/test_smart.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | 4 | def test_success(environ): 5 | environ['SUCHDOGE_BOOLEAN'] = 'true' 6 | environ['SUCHDOGE_INTEGER'] = '42' 7 | environ['SUCHDOGE_REAL'] = '42.42' 8 | environ['SUCHDOGE_STRING'] = '"42"' 9 | environ['SUCHDOGE_DICT'] = '{"value": 42}' 10 | environ['SUCHDOGE_RAW_STR'] = 'foo' 11 | 12 | from envcfg.smart.suchdoge import ( 13 | BOOLEAN, 14 | INTEGER, 15 | REAL, 16 | STRING, 17 | DICT, 18 | RAW_STR, 19 | ) 20 | assert BOOLEAN is True 21 | assert INTEGER == 42 22 | assert REAL == 42.42 23 | assert STRING == '42' 24 | assert DICT == {'value': 42} 25 | assert RAW_STR == 'foo' 26 | 27 | 28 | def test_failed(environ): 29 | with raises(ImportError) as einfo: 30 | import envcfg.smart._private_module # noqa 31 | assert einfo.value.args[0].startswith( 32 | 'No module named envcfg.smart._private_module') 33 | 34 | with raises(ImportError) as einfo: 35 | import envcfg.smart.INVALID_NAME # noqa 36 | assert einfo.value.args[0].startswith( 37 | 'No module named envcfg.smart.INVALID_NAME') 38 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,py35,pypy 3 | [testenv] 4 | deps = 5 | pytest 6 | pytest-cov 7 | pytest-pep8 8 | commands = 9 | py.test \ 10 | --cov {envsitepackagesdir}/envcfg \ 11 | --pep8 \ 12 | tests 13 | --------------------------------------------------------------------------------