diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b51a723..fe5cf0c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -2,5 +2,5 @@ files = setup.py envcfg/__init__.py commit = True tag = False -current_version = 0.1.0 +current_version = 0.2.0 diff --git a/.travis.yml b/.travis.yml index 228281c..555dd30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" - "pypy" install: - "pip install ." diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..8b1bdd5 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,12 @@ +Changelog +========= + +0.2.0 +----- + +- Add "smart" format support. (#4) + +0.1.0 +----- + +The first release. diff --git a/README.rst b/README.rst index cf8a583..f4c5aac 100644 --- a/README.rst +++ b/README.rst @@ -25,8 +25,196 @@ Installation :: - $ pip install envcfg - $ pip freeze > requirements.txt # http://nvie.com/posts/pin-your-packages/ + $ pip install python-envcfg + + +Supported Formats +----------------- + +- ``import envcfg.raw.foo as config``: + Import each ``FOO_*`` environment variable as string. +- ``import envcfg.json.foo as config``: + Import each ``FOO_*`` environment variable as JSON body. +- ``import envcfg.smart.foo as config``: + Try to import each ``FOO_*`` environment variable as JSON body, if fail then import it as string. + +There is an example table: + ++----------------------+---------------------------+-----------------------+ +| Environment Variable | Python Import Statement | Python Variable Value | ++======================+===========================+=======================+ +| ``FOO_NAME=foo`` | ``envcfg.raw.foo.NAME`` | ``'foo'`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NAME="foo"`` | ``envcfg.raw.foo.NAME`` | ``'"foo"'`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NUM1=42`` | ``envcfg.raw.foo.NUM1`` | ``'42'`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NUM1="42"`` | ``envcfg.raw.foo.NUM1`` | ``'"42"'`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NAME=foo`` | ``envcfg.json.foo.NAME`` | *ImportError* | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NAME="foo"`` | ``envcfg.json.foo.NAME`` | ``'foo'`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NUM1=42`` | ``envcfg.json.foo.NUM1`` | ``42`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NUM1="42"`` | ``envcfg.json.foo.NUM1`` | ``'42'`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NAME=foo`` | ``envcfg.smart.foo.NAME`` | ``'foo'`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NAME="foo"`` | ``envcfg.smart.foo.NAME`` | ``'foo'`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NUM1=42`` | ``envcfg.smart.foo.NUM1`` | ``42`` | ++----------------------+---------------------------+-----------------------+ +| ``FOO_NUM1="42"`` | ``envcfg.smart.foo.NUM1`` | ``'42'`` | ++----------------------+---------------------------+-----------------------+ + +Examples +-------- + +Uses with Flask +~~~~~~~~~~~~~~~ + +1. Defines environment variables with a prefix:: + + $ cat .env # should not checked into VCS + # values are valid JSON expressions + MYAPP_DEBUG=true + MYAPP_SECRET_KEY='"7950ad141c7e4b3990631fcdf9a1d909"' + MYAPP_SQLALCHEMY_DATABASE_URI='"sqlite:///tmp/myapp.sqlite3"' + +2. Creates Flask app and loads config from python-envcfg:: + + $ cat myapp.py + ... + app = Flask(__name__) + app.config.from_object('envcfg.json.myapp') # MYAPP_ -> .myapp + ... + +3. Enters your app with those environment variables:: + + $ env $(cat .env | xargs) python myapp.py + + +Uses with Django +~~~~~~~~~~~~~~~~ + +1. Creates a django project and moves all sensitive config items into the + environment variables:: + + $ cat djapp/settings.py # codebase-scope config + ... + INSTALLED_APPS = ( + 'django.contrib.admin', + ) + ... + + $ cat .env # environment-scope config, should not checked into VCS + # values are valid JSON expressions + DJAPP_SECRET_KEY='"wo9g2o#jws=u"' + DJAPP_DEBUG=true + DJAPP_TEMPLATE_DEBUG=true + +2. Adds importing statements in the end of ``settings.py`` module:: + + $ tail -n 2 djapp/settings.py + # importing all config items stored in the environment variables + from envcfg.json.djapp import * # noqa + +3. Runs your Django app with environment variables:: + + $ env $(cat .env | xargs) python manage.py runserver + + +Uses with Tornado +~~~~~~~~~~~~~~~~~ + +1. Defines environment variables with a prefix:: + + $ cat .env + export TORAPP_PORT='8888' + export TORAPP_MYSQL_HOST='"127.0.0.1"' + export TORAPP_MYSQL_DATABASE='"database"' + + +2. Creates a Tornado project and loads config:: + + $ cat torapp/server.py + + from tornado.web import Application, RequestHandler + from tornado.ioloop import IOLoop + from tornado.options import define, options + from tordb import Connection + + + def options_from_object(*args, **kwargs): + module = __import__(*args, **kwargs) + for name, value in vars(module).items(): + name = name.lower() + if name in options._options: + options._options[name].set(value) + + + class IndexHandler(RequestHandler): + def initialize(self): + self.db = Connection(options.mysql_host, options.mysql_database) + + def get(self): + pass # some database operations with ``self.db`` + + + application = Application([ + (r'/', IndexHandler), + ]) + + define('port', type=int) + define('mysql_host', type=unicode) + define('mysql_database', type=unicode) + options_from_object('envcfg.json.torapp', fromlist=['torapp']) + + + if __name__ == '__main__': + application.listen(options.port) + IOLoop.instance().start() + + +3. Runs your Tornado app:: + + $ env $(cat .env | xargs) python server.py + + +Works on Projects +----------------- + +In development, we can work with per-project environments but no more typing +``source foo/bar``. + +I recommend to put your project-specified environment variables in +``{PROJECT_ROOT}/.env`` and mark the ``.env`` as ignored in your VCS. For +example, you can write ``/.env`` in ``.gitignore`` if you are using Git, and +put a ``.env.example`` as a copying template for new-cloned projects. + +And then, you can use some utility such as `honcho`_ or `autoenv`_ to apply +the ``.env`` automatically. + +For honcho:: + + $ echo 'MYPROJECT_DEBUG=true' >> .env + $ echo 'web: python manage.py runserver' >> Procfile + $ honcho run python manage.py check-debug + True + $ honcho start web + Starting development server at http://127.0.0.1:5000/ + ... + +For autoenv:: + + $ echo 'MYPROJECT_DEBUG=true' >> myproject/.env + $ cd myproject + $ python manage.py check-debug + True + $ python manage.py runserver + Starting development server at http://127.0.0.1:5000/ + ... Issues @@ -37,6 +225,8 @@ If you want to report bugs or request features, please create issues on .. _12-Factor: http://12factor.net +.. _honcho: https://github.com/nickstenning/honcho +.. _autoenv: https://github.com/kennethreitz/autoenv .. |Build Status| image:: https://travis-ci.org/tonyseek/python-envcfg.svg?branch=master,develop :target: https://travis-ci.org/tonyseek/python-envcfg @@ -44,7 +234,7 @@ If you want to report bugs or request features, please create issues on .. |Coverage Status| image:: https://img.shields.io/coveralls/tonyseek/python-envcfg/develop.svg :target: https://coveralls.io/r/tonyseek/python-envcfg :alt: Coverage Status -.. |Wheel Status| image:: https://pypip.in/wheel/python-envcfg/badge.svg +.. |Wheel Status| image:: https://img.shields.io/pypi/wheel/python-envcfg.svg :target: https://warehouse.python.org/project/python-envcfg :alt: Wheel Status .. |PyPI Version| image:: https://img.shields.io/pypi/v/python-envcfg.svg diff --git a/envcfg/__init__.py b/envcfg/__init__.py index b794fd4..7fd229a 100644 --- a/envcfg/__init__.py +++ b/envcfg/__init__.py @@ -1 +1 @@ -__version__ = '0.1.0' +__version__ = '0.2.0' diff --git a/envcfg/smart/__init__.py b/envcfg/smart/__init__.py new file mode 100644 index 0000000..5aae86f --- /dev/null +++ b/envcfg/smart/__init__.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import + +from .._hook import import_hook + + +@import_hook(__name__) +def value_processor(name, raw_name, raw_value): + import json + try: + return json.loads(raw_value) + except ValueError: + return raw_value + + +del import_hook +del value_processor diff --git a/setup.py b/setup.py index 143ca63..aced732 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='python-envcfg', - version='0.1.0', + version='0.2.0', author='Jiangge Zhang', author_email='tonyseek@gmail.com', description='Accessing environment variables with a magic module.', @@ -17,6 +17,7 @@ url='https://github.com/tonyseek/python-envcfg', license='MIT', packages=find_packages(), + keywords=['env', 'config', '12-factor'], classifiers=[ 'Development Status :: 3 - Alpha', 'License :: OSI Approved :: MIT License', diff --git a/tests/test_smart.py b/tests/test_smart.py new file mode 100644 index 0000000..b715a1d --- /dev/null +++ b/tests/test_smart.py @@ -0,0 +1,37 @@ +from pytest import raises + + +def test_success(environ): + environ['SUCHDOGE_BOOLEAN'] = 'true' + environ['SUCHDOGE_INTEGER'] = '42' + environ['SUCHDOGE_REAL'] = '42.42' + environ['SUCHDOGE_STRING'] = '"42"' + environ['SUCHDOGE_DICT'] = '{"value": 42}' + environ['SUCHDOGE_RAW_STR'] = 'foo' + + from envcfg.smart.suchdoge import ( + BOOLEAN, + INTEGER, + REAL, + STRING, + DICT, + RAW_STR, + ) + assert BOOLEAN is True + assert INTEGER == 42 + assert REAL == 42.42 + assert STRING == '42' + assert DICT == {'value': 42} + assert RAW_STR == 'foo' + + +def test_failed(environ): + with raises(ImportError) as einfo: + import envcfg.smart._private_module # noqa + assert einfo.value.args[0].startswith( + 'No module named envcfg.smart._private_module') + + with raises(ImportError) as einfo: + import envcfg.smart.INVALID_NAME # noqa + assert einfo.value.args[0].startswith( + 'No module named envcfg.smart.INVALID_NAME') diff --git a/tox.ini b/tox.ini index ce727ce..89f42a9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,pypy +envlist = py27,py33,py34,py35,pypy [testenv] deps = pytest