├── .travis.yml ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── conf.py └── index.rst ├── flask_appconfig ├── __init__.py ├── cli.py ├── docker.py ├── env.py ├── heroku.py ├── middleware.py ├── server_backends.py ├── signals.py └── util.py ├── setup.py ├── tests ├── module.py ├── test_appenv.py ├── test_heroku.py └── test_simple.py └── tox.ini /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | env: 3 | - TOXENV=py27 4 | - TOXENV=py33 5 | install: pip install tox 6 | script: tox 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Marc Brinkmann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-AppConfig 2 | =============== 3 | 4 | Allows you to configure an application using pre-set methods. 5 | 6 | .. code-block:: python 7 | 8 | from flask_appconfig import AppConfig 9 | 10 | def create_app(configfile=None): 11 | app = Flask('myapp') 12 | AppConfig(app, configfile) 13 | return app 14 | 15 | The application returned by ``create_app`` will, in order: 16 | 17 | 1. Load default settings from a module called ``myapp.default_config``, if it 18 | exists. (method described in 19 | http://flask.pocoo.org/docs/config/#configuring-from-files ) 20 | 2. Load settings from a configuration file whose name is given in the 21 | environment variable ``MYAPP_CONFIG`` (see link from 1.). 22 | 3. Load json or string values directly from environment variables that start 23 | with a prefix of ``MYAPP_``, i.e. setting ``MYAPP_SQLALCHEMY_ECHO=true`` 24 | will cause the setting of ``SQLALCHEMY_ECHO`` to be ``True``. 25 | 26 | Any of these behaviors can be altered or disabled by passing the appropriate 27 | options to the constructor or ``init_app()``. 28 | 29 | 30 | Heroku support 31 | -------------- 32 | 33 | Flask-AppConfig supports configuring a number of services through 34 | ``HerokuConfig``: 35 | 36 | .. code-block:: python 37 | 38 | from flask_appconfig import HerokuConfig 39 | 40 | def create_app(configfile=None): 41 | app = Flask('myapp') 42 | HerokuConfig(app, configfile) 43 | return app 44 | 45 | Works like the example above, but environment variables set by various Heroku 46 | addons will be parsed as json and converted to configuration variables 47 | accordingly. Forexample, when enabling `Mailgun 48 | `_, the configuration of `Flask-Mail 49 | `_ will be automatically be set correctly. 50 | 51 | 52 | Using "ENV-only" 53 | ---------------- 54 | 55 | If you only want to use the environment-parsing functions of Flask-AppConfig, 56 | the appropriate functions are exposed: 57 | 58 | .. code-block:: python 59 | 60 | from flask_appconfig.heroku import from_heroku_envvars 61 | from flask_appconfig.env import from_envvars 62 | 63 | # from environment variables. note that you need to set the prefix, as 64 | # no auto-detection can be done without an app object 65 | from_envvars(app.config, prefix=app.name.upper() + '_') 66 | 67 | # also possible: parse heroku configuration values 68 | # any dict-like object will do as the first parameter 69 | from_heroku_envvars(app.config) 70 | 71 | 72 | Installation 73 | ------------ 74 | 75 | Via `PyPI `_:: 76 | 77 | $ pip install flask-appconfig 78 | 79 | Requires Python 2.7. 80 | 81 | 82 | flask utility 83 | ------------- 84 | 85 | If you want to get started quickly without thinking a lot about writing a run 86 | script, the ``flask`` utility supports the ``create_app``/factory pattern:: 87 | 88 | $ flask --app=myapp dev 89 | 90 | This will import a module ``myapp``, and call ``myapp.run(debug=True)``. 91 | 92 | Other options can come in handy as well:: 93 | 94 | $ flask --app=myapp dev -S -p 8000 95 | 96 | Runs the app on port 8080, with SSL enabled. You can also set the ``FLASK_APP`` 97 | environment variable or set ``FLASK_APP`` inside ``.env`` and omit the 98 | ``--app`` parameter. 99 | 100 | Note that the ``flask`` utility is subject to change, as it will conflict with 101 | the CLI functionality of Flask 1.0. The API is currently kept close, but it 102 | will see changes once Flask 1.0 is released. 103 | 104 | 105 | Flask-Debug and Flask-DebugToolbar support 106 | ****************************************** 107 | 108 | ``flask`` automatically activates Flask-Debug_ and Flask-DebugToolbar_ on 109 | your application; this allows to have it installed locally while not having to 110 | install any debug code in production. You can suppress this behavior with the 111 | ``-E``/``--no-flask-debug`` flag. 112 | 113 | Note that these features are only enabled if you install either of these 114 | extensions manually; they are not dependencies of Flask-Appconfig. 115 | 116 | .. _Flask-Debug: https://github.com/mbr/flask-debug 117 | .. _Flask-DebugToolbar: https://flask-debugtoolbar.readthedocs.org/ 118 | 119 | 120 | Thoughts on Configuration 121 | ------------------------- 122 | 123 | There is a lot of ways to configure a Flask application and often times, 124 | less-than-optimal ones are chosen in a hurry. 125 | 126 | This extension aims to do three things: 127 | 128 | 1. Set a "standard" of doing configuration that is flexible and in-line with 129 | the official docs and (what I consider) good practices. 130 | 2. Make it as convenient as possible to provide these configuration methods in 131 | an application. 132 | 3. Auto-configure on Heroku as much as possible without sacrificing 1. and 2. 133 | 134 | `12factor.net `_ seems to capture a good amount of good 135 | thoughts on the issue and Flask-Appconfig should aid you in writing an 136 | application that follows the principles laid out there. 137 | 138 | Providing defaults 139 | ****************** 140 | 141 | Defaults should be included and overridable, without altering the file 142 | containing the defaults. 143 | 144 | Separate code and configuration 145 | ******************************* 146 | 147 | It should be possible to install the app to a read-only (possibly system-wide) 148 | location, without having to store configuration files (or, even worse, 149 | configuration modules) inside its folders. 150 | 151 | Environment variables and instance folders make this possible. As an added 152 | benefit, configuration does not need to be stored alongside the code in version 153 | control. 154 | 155 | No code necessary for most deployments using the factory-method pattern 156 | *********************************************************************** 157 | 158 | When deploying with gunicorn, passing ``myapp:create_app()`` suffices to create 159 | an app instance, no boilerplate code to create the WSGI app should be necessary. 160 | 161 | Multiple instances 162 | ****************** 163 | 164 | Running multiple apps inside the same interpreter should also be possible. While 165 | this is slightly more complicated and may occasionally violate the "no-code" 166 | guideline above, it's still straightforward by using configuration file 167 | parameters. 168 | 169 | 170 | Development 171 | ----------- 172 | Flask-AppConfig is under "conceptional development". The API or semantics 173 | may change in the future. 174 | 175 | Send pull requests for more Heroku-apps to be supported. Send feedback via mail. 176 | 177 | Changelog 178 | --------- 179 | 180 | Backwards-incompatible changes, as they were introduced: 181 | 182 | 0.12 183 | **** 184 | * The ``configfile``-parameter has been deprecated. 185 | * Auto-discovery has been removed, pending decision on 186 | https://github.com/mitsuhiko/flask/pull/1536 187 | 188 | 0.11 189 | **** 190 | * The ``flaskdev`` tool has been replaced with ``flask``. 191 | * Using the new ``flask`` tool auto-reloading will also change by default. If a 192 | syntax error is introduced to the code, the app will try to restart after two 193 | seconds by default, instead of crashing. This can be suppressed with the 194 | '--extended-reload 0' flag. 195 | * If the app import fails, ``flask`` will add ``.`` to ``sys.path`` and try to 196 | to import once again. 197 | * Experimental commands ``serve`` and ``db`` have been added. 198 | 199 | 0.4 200 | *** 201 | * Environment variables are no longer prefixed with ``FLASK_`` by default, but 202 | rather use ``APPNAME_`` (with ``APPNAME`` being the applications name in 203 | uppercase). 204 | * ``MYAPP_SETTINGS`` became ``MYAPP_CONFIG``, ``default_settings`` became 205 | ``default_config``. 206 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -n 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Bootstrap.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Bootstrap.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Bootstrap" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Bootstrap" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | project = u'Flask-AppConfig' 4 | copyright = u'2015, Marc Brinkmann' 5 | version = '0.11.2' 6 | release = '0.11.2.dev1' 7 | 8 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'alabaster'] 9 | source_suffix = '.rst' 10 | master_doc = 'index' 11 | exclude_patterns = ['_build'] 12 | pygments_style = 'monokai' 13 | 14 | 15 | html_theme = 'alabaster' 16 | html_theme_options = { 17 | 'github_user': 'mbr', 18 | 'github_repo': 'flask-appconfig', 19 | 'github_banner': True, 20 | 'gratipay_user': 'mbr', 21 | 'github_button': False, 22 | 'show_powered_by': False, 23 | 24 | # required for monokai: 25 | 'pre_bg': '#292429', 26 | } 27 | html_sidebars = { 28 | '**': [ 29 | 'about.html', 30 | 'navigation.html', 31 | 'relations.html', 32 | 'searchbox.html', 33 | 'donate.html', 34 | ] 35 | } 36 | 37 | intersphinx_mapping = {'http://docs.python.org/': None, 38 | 'http://pythonhosted.org/Flask-SQLAlchemy/': None, 39 | 'http://flask.pocoo.org/docs/': None, 40 | } 41 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /flask_appconfig/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import warnings 5 | 6 | from . import env, heroku, docker 7 | from .util import try_import 8 | 9 | 10 | class AppConfig(object): 11 | def __init__(self, app=None, *args, **kwargs): 12 | if app: 13 | self.init_app(app, *args, **kwargs) 14 | 15 | def init_app(self, 16 | app, 17 | configfile=None, 18 | envvar=True, 19 | default_settings=True, 20 | from_envvars='json', 21 | from_envvars_prefix=None, 22 | enable_cli=True): 23 | 24 | if from_envvars_prefix is None: 25 | from_envvars_prefix = app.name.upper().replace('.', '_') + '_' 26 | 27 | if default_settings is True: 28 | defs = try_import(app.name + '.default_config') 29 | if defs: 30 | app.config.from_object(defs) 31 | elif default_settings: 32 | app.config.from_object(default_settings) 33 | 34 | # load supplied configuration file 35 | if configfile is not None: 36 | warnings.warn('The configfile-parameter is deprecated and will ' 37 | 'be removed (it is currently ignored). If you are ' 38 | 'trying to load a configuration file, use the ' 39 | '_CONFIG envvar instead. During tests, simply ' 40 | 'populate app.config before or after AppConfig.', 41 | DeprecationWarning) 42 | 43 | # load configuration file from environment 44 | if envvar is True: 45 | envvar = app.name.upper() + '_CONFIG' 46 | 47 | if envvar and envvar in os.environ: 48 | app.config.from_envvar(envvar) 49 | 50 | # load environment variables 51 | if from_envvars: 52 | env.from_envvars(app.config, 53 | from_envvars_prefix, 54 | as_json=('json' == from_envvars)) 55 | 56 | # register extension 57 | app.extensions = getattr(app, 'extensions', {}) 58 | app.extensions['appconfig'] = self 59 | 60 | # register command-line functions if available 61 | if enable_cli: 62 | cli_mod = try_import('flask_cli', 'flask.cli') 63 | 64 | if hasattr(cli_mod, 'FlaskCLI') and not hasattr(app, 'cli'): 65 | # auto-load flask-cli if installed 66 | cli_mod.FlaskCLI(app) 67 | 68 | if hasattr(app, 'cli'): 69 | from .cli import register_cli, register_db_cli 70 | register_cli(app.cli) 71 | 72 | # conditionally register db api 73 | if try_import('flask_sqlalchemy'): 74 | register_db_cli(app.cli, cli_mod) 75 | 76 | return app 77 | 78 | 79 | class HerokuConfig(AppConfig): 80 | def init_app(self, app, *args, **kwargs): 81 | super(HerokuConfig, self).init_app(app, *args, **kwargs) 82 | 83 | heroku.from_heroku_envvars(app.config) 84 | 85 | 86 | class DockerConfig(AppConfig): 87 | def init_app(self, app, *args, **kwargs): 88 | super(DockerConfig, self).init_app(app, *args, **kwargs) 89 | 90 | docker.from_docker_envvars(app.config) 91 | -------------------------------------------------------------------------------- /flask_appconfig/cli.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import os 3 | import socket 4 | import sys 5 | import time 6 | 7 | import click 8 | from flask import current_app 9 | 10 | from . import server_backends 11 | from .middleware import ReverseProxied 12 | from .signals import (db_before_reset, db_reset_dropped, db_reset_created, 13 | db_after_reset) 14 | from .util import try_import_obj 15 | 16 | ENV_DEFAULT = '.env' 17 | APP_ENVVAR = 'FLASK_APP' 18 | 19 | 20 | def register_cli(cli): 21 | @cli.command(help='Runs a development server with extras.') 22 | @click.option('--host', 23 | '-h', 24 | default='localhost', 25 | help='Hostname to bind to. Defaults to localhost') 26 | @click.option('--port', 27 | '-p', 28 | type=int, 29 | default=5000, 30 | help='Port to listen on. Defaults to 5000') 31 | @click.option('--ssl', 32 | '-S', 33 | flag_value='adhoc', 34 | default=None, 35 | help='Enable SSL with a self-signed cert') 36 | @click.option('--gen-secret-key/--no-gen-secret-key', 37 | default=True, 38 | help='Enable or disable automatic secret key generation') 39 | @click.option('--flask-debug/--no-flask-debug', 40 | '-e/-E', 41 | default=None, 42 | help='Enable/disable Flask-Debug or Flask-DebugToolbar ' 43 | 'extensions. By default, both are enabled if debug is ' 44 | 'enabled.') 45 | @click.option( 46 | '--extended-reload', 47 | '-R', 48 | default=2, 49 | type=float, 50 | help='Seconds before restarting the app if a non-recoverable ' 51 | 'exception occured (e.g. SyntaxError). Set this to 0 ' 52 | 'to disable (default: 2.0)') 53 | def dev(host, port, ssl, gen_secret_key, flask_debug, extended_reload): 54 | # FIXME: support all options of ``flask run`` 55 | app = current_app 56 | 57 | if not app.debug: 58 | click.echo(' * app.debug = True') 59 | app.debug = True # conveniently force debug mode 60 | extra_files = [] 61 | 62 | if gen_secret_key and app.config.get('SECRET_KEY', None) is None: 63 | app.config['SECRET_KEY'] = os.urandom(64) 64 | 65 | # add configuration file to extra_files if passed in 66 | config_env_name = app.name.upper() + '_CONFIG' 67 | if config_env_name in os.environ: 68 | extra_files.append(os.environ[config_env_name]) 69 | 70 | msgs = [] 71 | 72 | # try to load debug extensions 73 | if flask_debug is None: 74 | flask_debug = app.debug 75 | 76 | if flask_debug: 77 | Debug = try_import_obj('flask_debug', 'Debug') 78 | DebugToolbarExtension = try_import_obj('flask_debugtoolbar', 79 | 'DebugToolbarExtension') 80 | 81 | if Debug: 82 | Debug(app) 83 | app.config['SERVER_NAME'] = '{}:{}'.format(host, port) 84 | 85 | # taking off the safety wheels 86 | app.config['FLASK_DEBUG_DISABLE_STRICT'] = True 87 | 88 | if DebugToolbarExtension: 89 | # Flask-Debugtoolbar does not check for debugging settings at 90 | # runtime. this hack enabled debugging if desired before 91 | # initializing the extension 92 | if app.debug: 93 | 94 | # set the SECRET_KEY, but only if we're in debug-mode 95 | if not app.config.get('SECRET_KEY', None): 96 | msgs.append('SECRET_KEY not set, using insecure "devkey"') 97 | app.config['SECRET_KEY'] = 'devkey' 98 | 99 | DebugToolbarExtension(app) 100 | 101 | def on_off(ext): 102 | return 'on' if ext is not None else 'off' 103 | 104 | msgs.insert(0, 'Flask-Debug: {}'.format(on_off(Debug))) 105 | msgs.insert( 106 | 0, 'Flask-DebugToolbar: {}'.format(on_off(DebugToolbarExtension))) 107 | 108 | if msgs: 109 | click.echo(' * {}'.format(', '.join(msgs))) 110 | 111 | if extended_reload > 0: 112 | # we need to moneypatch the werkzeug reloader for this feature 113 | from werkzeug._reloader import ReloaderLoop 114 | orig_restart = ReloaderLoop.restart_with_reloader 115 | 116 | def _mp_restart(*args, **kwargs): 117 | while True: 118 | status = orig_restart(*args, **kwargs) 119 | 120 | if status == 0: 121 | break 122 | # an error occured, possibly a syntax or other 123 | click.secho( 124 | 'App exited with exit code {}. Will attempted restart ' 125 | ' in {} seconds.'.format(status, extended_reload), 126 | fg='red') 127 | time.sleep(extended_reload) 128 | 129 | return status 130 | 131 | ReloaderLoop.restart_with_reloader = _mp_restart 132 | 133 | app.run(host, port, ssl_context=ssl, extra_files=extra_files) 134 | 135 | @cli.command(help='Runs a production server.') 136 | @click.option('--host', 137 | '-H', 138 | default='0.0.0.0', 139 | help='Hostname to bind to. Defaults to 0.0.0.0') 140 | @click.option('--port', 141 | '-p', 142 | type=int, 143 | default=80, 144 | help='Port to listen on. Defaults to 80') 145 | @click.option('--processes', 146 | '-w', 147 | type=int, 148 | default=1, 149 | help='When possible, run this many instances in separate ' 150 | 'processes. 0 means determine automatically. Default: 1') 151 | @click.option('--backends', 152 | '-b', 153 | default=server_backends.DEFAULT, 154 | help='Comma-separated list of backends to try. Default: {}' 155 | .format(server_backends.DEFAULT)) 156 | @click.option( 157 | '--list', 158 | '-l', 159 | 'list_only', 160 | is_flag=True, 161 | help='Do not run server, but list available backends for app.') 162 | @click.option( 163 | '--reverse-proxied', 164 | is_flag=True, 165 | help='Enable HTTP-reverse proxy middleware. Do not activate ' 166 | 'this unless you need it, it becomes a security risks when used ' 167 | 'incorrectly.') 168 | def serve(host, port, processes, backends, list_only, reverse_proxied): 169 | if processes <= 0: 170 | processes = None 171 | 172 | click.secho('flask serve is currently experimental. Use it at your ' 173 | 'own risk', 174 | fg='yellow', 175 | err=True) 176 | app = current_app 177 | 178 | # we NEVER allow debug mode in production 179 | app.debug = False 180 | 181 | wsgi_app = app 182 | 183 | if reverse_proxied: 184 | wsgi_app = ReverseProxied(app) 185 | 186 | if list_only: 187 | found = False 188 | 189 | for backend in backends.split(','): 190 | try: 191 | bnd = server_backends.backends[backend] 192 | except KeyError: 193 | click.secho('{:20s} invalid'.format(backend), fg='red') 194 | continue 195 | 196 | info = bnd.get_info() 197 | 198 | if info is None: 199 | click.secho('{:20s} missing module'.format(backend), 200 | fg='red') 201 | continue 202 | 203 | fmt = {} 204 | if not found: 205 | fmt['fg'] = 'green' 206 | found = True 207 | 208 | click.secho( 209 | '{b.name:20s} {i.version:10s} {i.extra_info}'.format( 210 | b=bnd, i=info), 211 | **fmt) 212 | return 213 | 214 | # regular operation 215 | for backend in backends.split(','): 216 | bnd = server_backends.backends[backend] 217 | info = bnd.get_info() 218 | if not info: 219 | continue 220 | 221 | b = bnd(processes) 222 | 223 | rcfg = OrderedDict() 224 | rcfg['app'] = app.name 225 | rcfg['# processes'] = str(b.processes) 226 | rcfg['backend'] = str(b) 227 | rcfg['addr'] = '{}:{}'.format(host, port) 228 | 229 | for k, v in rcfg.items(): 230 | click.echo('{:15s}: {}'.format(k, v)) 231 | 232 | try: 233 | b.run_server(wsgi_app, host, port) 234 | sys.exit(0) # if the server exits normally, just quit 235 | except socket.error as e: 236 | if not port < 1024 or e.errno != 13: 237 | raise 238 | 239 | # helpful message when trying to run on port 80 without room 240 | # permissions 241 | click.echo('Could not open socket on {}:{}: {}. ' 242 | 'Do you have root permissions?' 243 | .format(host, port, e)) 244 | sys.exit(13) 245 | except RuntimeError as e: 246 | click.echo(str(e), err=True) 247 | sys.exit(1) 248 | else: 249 | click.echo('Exhausted list of possible backends', err=True) 250 | sys.exit(1) 251 | 252 | 253 | def register_db_cli(cli, cli_mod): 254 | # FIXME: currently disabled 255 | @cli.group(help='Flask-SQLAlchemy functions') 256 | @click.option('--echo/--no-echo', 257 | '-e/-E', 258 | default=None, 259 | help='Overrides SQLALCHEMY_ECHO') 260 | @cli_mod.with_appcontext 261 | def db(echo): 262 | # FIXME: currently broken. 263 | click.secho('flask db is currently experimental. Use it at your ' 264 | 'own risk', 265 | fg='yellow', 266 | err=True) 267 | 268 | # sanity check 269 | if 'sqlalchemy' not in current_app.extensions: 270 | click.secho('No SQLAlchemy extension loaded. Did you initialize ' 271 | 'your app?', 272 | fg='red', 273 | err=True) 274 | sys.exit(1) 275 | 276 | if echo is not None: 277 | current_app.config['SQLALCHEMY_ECHO'] = echo 278 | 279 | @db.command(help='Drop and recreated schema') 280 | def reset(): 281 | app = current_app 282 | db = current_app.extensions['sqlalchemy'].db 283 | 284 | # FIXME: this should be in a transaction, but flask-sqlalchemy 285 | # currently makes it hard to get it right. 286 | # 287 | # problems that occured: con is not the same as the connection used 288 | # by drop_all and create_all, causing deadlocks to occur 289 | db_before_reset.send(app, db=db, con=db.engine) 290 | 291 | db.drop_all() 292 | db_reset_dropped.send(app, db=db, con=db.engine) 293 | 294 | db.create_all() 295 | db_reset_created.send(app, db=db, con=db.engine) 296 | 297 | db_after_reset.send(app, db=db, con=db.engine) 298 | -------------------------------------------------------------------------------- /flask_appconfig/docker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from six.moves.urllib_parse import urlparse 5 | 6 | 7 | def from_docker_envvars(config): 8 | # linked postgres database (link name 'pg' or 'postgres') 9 | if 'PG_PORT' in os.environ: 10 | pg_url = urlparse(os.environ['PG_PORT']) 11 | 12 | if not pg_url.scheme == 'tcp': 13 | raise ValueError('Only tcp scheme supported for postgres') 14 | 15 | host, port = pg_url.netloc.split(':') 16 | 17 | uri = 'postgres://{user}:{password}@{host}:{port}/{database}'.format( 18 | user=os.environ.get('PG_ENV_POSTGRES_USER', 'postgres'), 19 | password=os.environ.get('PG_ENV_POSTGRES_PASSWORD', ''), 20 | host=host, 21 | port=port, 22 | database=os.environ.get('PG_ENV_POSTGRES_DB', 'postgres')) 23 | 24 | config['SQLALCHEMY_DATABASE_URI'] = uri 25 | 26 | if 'REDIS_PORT' in os.environ: 27 | redis_url = urlparse(os.environ['REDIS_PORT']) 28 | 29 | if not redis_url.scheme == 'tcp': 30 | raise ValueError('Only tcp scheme supported for redis') 31 | 32 | host, port = redis_url.netloc.split(':') 33 | 34 | uri = 'redis://{host}:{port}/0'.format(host=host, port=port, ) 35 | 36 | config['REDIS_URL'] = uri 37 | config['REDIS_HOST'] = host 38 | config['REDIS_PORT'] = int(port) 39 | -------------------------------------------------------------------------------- /flask_appconfig/env.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import os 5 | 6 | 7 | def from_envvars(conf, prefix=None, envvars=None, as_json=True): 8 | """Load environment variables as Flask configuration settings. 9 | 10 | Values are parsed as JSON. If parsing fails with a ValueError, 11 | values are instead used as verbatim strings. 12 | 13 | :param app: App, whose configuration should be loaded from ENVVARs. 14 | :param prefix: If ``None`` is passed as envvars, all variables from 15 | ``environ`` starting with this prefix are imported. The 16 | prefix is stripped upon import. 17 | :param envvars: A dictionary of mappings of environment-variable-names 18 | to Flask configuration names. If a list is passed 19 | instead, names are mapped 1:1. If ``None``, see prefix 20 | argument. 21 | :param as_json: If False, values will not be parsed as JSON first. 22 | """ 23 | if prefix is None and envvars is None: 24 | raise RuntimeError('Must either give prefix or envvars argument') 25 | 26 | # if it's a list, convert to dict 27 | if isinstance(envvars, list): 28 | envvars = {k: None for k in envvars} 29 | 30 | if not envvars: 31 | envvars = {k: k[len(prefix):] for k in os.environ.keys() 32 | if k.startswith(prefix)} 33 | 34 | for env_name, name in envvars.items(): 35 | if name is None: 36 | name = env_name 37 | 38 | if not env_name in os.environ: 39 | continue 40 | 41 | if as_json: 42 | try: 43 | conf[name] = json.loads(os.environ[env_name]) 44 | except ValueError: 45 | conf[name] = os.environ[env_name] 46 | else: 47 | conf[name] = os.environ[env_name] 48 | -------------------------------------------------------------------------------- /flask_appconfig/heroku.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | import warnings 6 | from six.moves.urllib_parse import urlparse 7 | 8 | from . import env 9 | 10 | HEROKU_POSTGRES_ENV_NAME_RE = re.compile('HEROKU_POSTGRESQL_[A-Z_]*URL') 11 | 12 | 13 | def from_heroku_envvars(config): 14 | var_map = { 15 | # SQL-Alchemy 16 | 'DATABASE_URL': 'SQLALCHEMY_DATABASE_URI', 17 | 18 | # Celery w/ RabbitMQ 19 | 'BROKER_URL': 'RABBITMQ_URL', 20 | 'REDISTOGO_URL': 'REDIS_URL', 21 | 'MONGOLAB_URI': 'MONGO_URI', 22 | 'MONGOHQ_URL': 'MONGO_URI', 23 | 'CLOUDANT_URL': 'COUCHDB_URL', 24 | 'MEMCACHIER_SERVERS': 'CACHE_MEMCACHED_SERVERS', 25 | 'MEMCACHIER_USERNAME': 'CACHE_MEMCACHED_USERNAME', 26 | 'MEMCACHIER_PASSWORD': 'CACHE_MEMCACHED_PASSWORD', 27 | } 28 | 29 | # search postgresql config using regex 30 | if 'DATABASE_URL' not in os.environ: 31 | for k in os.environ.keys(): 32 | if HEROKU_POSTGRES_ENV_NAME_RE.match(k): 33 | var_map[k] = 'SQLALCHEMY_DATABASE_URI' 34 | warnings.warn('Using {0} as the database URL. However, ' 35 | 'really should promote this or another URL ' 36 | 'to DATABASE_URL by running \'heroku pg:' 37 | 'promote {0}\''.format(k), RuntimeWarning) 38 | 39 | var_list = [ 40 | # Sentry 41 | 'SENTRY_DSN', 42 | 43 | # Exceptional 44 | 'EXCEPTIONAL_API_KEY', 45 | 46 | # Flask-GoogleFed 47 | 'GOOGLE_DOMAIN', 48 | 49 | # Mailgun 50 | 'MAILGUN_API_KEY', 51 | 'MAILGUN_SMTP_LOGIN', 52 | 'MAILGUN_SMTP_PASSWORD', 53 | 'MAILGUN_SMTP_PORT', 54 | 'MAILGUN_SMTP_SERVER', 55 | 56 | # SendGrid 57 | 'SENDGRID_USERNAME', 58 | 'SENDGRID_PASSWORD' 59 | ] 60 | 61 | # import the relevant envvars 62 | env.from_envvars(config, envvars=var_list, as_json=False) 63 | env.from_envvars(config, envvars=var_map, as_json=False) 64 | 65 | # fix up configuration 66 | if 'MAILGUN_SMTP_SERVER' in config: 67 | config['SMTP_SERVER'] = config['MAILGUN_SMTP_SERVER'] 68 | config['SMTP_PORT'] = config['MAILGUN_SMTP_PORT'] 69 | config['SMTP_LOGIN'] = config['MAILGUN_SMTP_LOGIN'] 70 | config['SMTP_PASSWORD'] = config['MAILGUN_SMTP_PASSWORD'] 71 | config['SMTP_USE_TLS'] = True 72 | elif 'SENDGRID_USERNAME' in config: 73 | config['SMTP_SERVER'] = 'smtp.sendgrid.net' 74 | config['SMTP_PORT'] = 25 75 | config['SMTP_LOGIN'] = config['SENDGRID_USERNAME'] 76 | config['SMTP_PASSWORD'] = config['SENDGRID_PASSWORD'] 77 | config['SMTP_USE_TLS'] = True 78 | 79 | # convert to Flask-Mail specific configuration 80 | if 'MAILGUN_SMTP_SERVER' in config or\ 81 | 'SENDGRID_PASSWORD' in config: 82 | 83 | config['MAIL_SERVER'] = config['SMTP_SERVER'] 84 | config['MAIL_PORT'] = config['SMTP_PORT'] 85 | config['MAIL_USE_TLS'] = config['SMTP_USE_TLS'] 86 | config['MAIL_USERNAME'] = config['SMTP_LOGIN'] 87 | config['MAIL_PASSWORD'] = config['SMTP_PASSWORD'] 88 | 89 | # for backwards compatiblity, redis: 90 | if 'REDIS_URL' in config: 91 | url = urlparse(config['REDIS_URL']) 92 | config['REDIS_HOST'] = url.hostname 93 | config['REDIS_PORT'] = url.port 94 | config['REDIS_PASSWORD'] = url.password 95 | # FIXME: missing db#? 96 | 97 | if 'MONGO_URI' in config: 98 | url = urlparse(config['MONGO_URI']) 99 | config['MONGODB_USER'] = url.username 100 | config['MONGODB_PASSWORD'] = url.password 101 | config['MONGODB_HOST'] = url.hostname 102 | config['MONGODB_PORT'] = url.port 103 | config['MONGODB_DB'] = url.path[1:] 104 | -------------------------------------------------------------------------------- /flask_appconfig/middleware.py: -------------------------------------------------------------------------------- 1 | # from: http://flask.pocoo.org/snippets/35/ 2 | # written by Peter Hansen 3 | 4 | 5 | class ReverseProxied(object): 6 | '''Wrap the application in this middleware and configure the 7 | front-end server to add these headers, to let you quietly bind 8 | this to a URL other than / and to an HTTP scheme that is 9 | different than what is used locally. 10 | 11 | In nginx: 12 | location /myprefix { 13 | proxy_pass http://192.168.0.1:5001; 14 | proxy_set_header Host $host; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Scheme $scheme; 17 | proxy_set_header X-Script-Name /myprefix; 18 | } 19 | 20 | :param app: the WSGI application 21 | ''' 22 | 23 | def __init__(self, app): 24 | self.app = app 25 | 26 | def __call__(self, environ, start_response): 27 | script_name = environ.get('HTTP_X_SCRIPT_NAME', '') 28 | if script_name: 29 | environ['SCRIPT_NAME'] = script_name 30 | path_info = environ['PATH_INFO'] 31 | if path_info.startswith(script_name): 32 | environ['PATH_INFO'] = path_info[len(script_name):] 33 | 34 | scheme = environ.get('HTTP_X_SCHEME', '') 35 | if scheme: 36 | environ['wsgi.url_scheme'] = scheme 37 | return self.app(environ, start_response) 38 | 39 | # pass through other attributes, like .run() when using werkzeug 40 | def __getattr__(self, key): 41 | return getattr(self.app, key) 42 | -------------------------------------------------------------------------------- /flask_appconfig/server_backends.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from multiprocessing import cpu_count 3 | 4 | from .util import try_import 5 | 6 | 7 | def _get_cpu_count(): 8 | try: 9 | return cpu_count() 10 | except NotImplementedError: 11 | raise RuntimeError('Could not determine CPU count and no ' 12 | '--instance-count supplied.') 13 | 14 | 15 | DEFAULT = 'tornado,meinheld,gunicorn,werkzeug-threaded,werkzeug' 16 | 17 | backends = {} 18 | 19 | 20 | def backend(name): 21 | def _(cls): 22 | backends[name] = cls 23 | cls.name = name 24 | return cls 25 | 26 | return _ 27 | 28 | 29 | BackendInfo = namedtuple('BackendInfo', 'version,extra_info') 30 | 31 | 32 | class ServerBackend(object): 33 | vulnerable = True 34 | 35 | def __init__(self, processes=None): 36 | if not hasattr(self, 'processes'): 37 | if processes is None: 38 | processes = _get_cpu_count() 39 | self.processes = processes 40 | 41 | @classmethod 42 | def get_info(cls): 43 | """Return information about backend and its availability. 44 | 45 | :return: A BackendInfo tuple if the import worked, none otherwise. 46 | """ 47 | mod = try_import(cls.mod_name) 48 | if not mod: 49 | return None 50 | version = getattr(mod, '__version__', None) or getattr(mod, 'version', 51 | None) 52 | return BackendInfo(version or 'deprecated', '') 53 | 54 | def run_server(self, app, host, port): 55 | raise NotImplementedError 56 | 57 | def __str__(self): 58 | return '{} {}'.format(self.name, self.get_info().version) 59 | 60 | 61 | @backend('werkzeug') 62 | class WerkzeugBackend(ServerBackend): 63 | threaded = False 64 | mod_name = 'werkzeug' 65 | 66 | def run_server(self, app, host, port): 67 | app.run(host, 68 | port, 69 | debug=False, 70 | use_evalex=False, 71 | threaded=self.threaded, 72 | processes=self.processes) 73 | 74 | 75 | @backend('werkzeug-threaded') 76 | class WerkzeugThreaded(WerkzeugBackend): 77 | threaded = True 78 | processes = 1 79 | 80 | 81 | @backend('tornado') 82 | class TornadoBackend(ServerBackend): 83 | mod_name = 'tornado' 84 | 85 | def run_server(self, app, host, port): 86 | from tornado.wsgi import WSGIContainer 87 | from tornado.httpserver import HTTPServer 88 | from tornado.ioloop import IOLoop 89 | 90 | http_server = HTTPServer(WSGIContainer(app)) 91 | http_server.listen(port, address=host) 92 | IOLoop.instance().start() 93 | 94 | 95 | @backend('gunicorn') 96 | class GUnicornBackend(ServerBackend): 97 | mod_name = 'gunicorn' 98 | 99 | def run_server(self, app, host, port): 100 | import gunicorn.app.base 101 | 102 | class FlaskGUnicornApp(gunicorn.app.base.BaseApplication): 103 | options = { 104 | 'bind': '{}:{}'.format(host, port), 105 | 'workers': self.processes 106 | } 107 | 108 | def load_config(self): 109 | for k, v in self.options.items(): 110 | self.cfg.set(k.lower(), v) 111 | 112 | def load(self): 113 | return app 114 | 115 | FlaskGUnicornApp().run() 116 | 117 | 118 | @backend('meinheld') 119 | class MeinHeldBackend(ServerBackend): 120 | mod_name = 'meinheld' 121 | 122 | def run_server(self, app, host, port): 123 | from meinheld import server 124 | 125 | server.listen((host, port)) 126 | server.run(app) 127 | -------------------------------------------------------------------------------- /flask_appconfig/signals.py: -------------------------------------------------------------------------------- 1 | from flask.signals import Namespace 2 | 3 | # signals 4 | signals = Namespace() 5 | db_before_reset = signals.signal('db-before-reset') 6 | db_reset_dropped = signals.signal('db-reset-dropped') 7 | db_reset_created = signals.signal('db-reset-created') 8 | db_after_reset = signals.signal('db-after-reset') 9 | -------------------------------------------------------------------------------- /flask_appconfig/util.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | 4 | def try_import(*module_names): 5 | for module_name in module_names: 6 | try: 7 | return import_module(module_name) 8 | except ImportError: 9 | continue 10 | 11 | 12 | def try_import_obj(module_name, name): 13 | mod = try_import(module_name) 14 | if mod: 15 | return getattr(mod, name, None) 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | from setuptools import setup, find_packages 7 | 8 | 9 | def read(fname): 10 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | 12 | 13 | setup( 14 | name='flask-appconfig', 15 | version='0.12.1.dev1', 16 | description=('Configures Flask applications in a canonical way. Also auto-' 17 | 'configures Heroku. Aims to standardize configuration.'), 18 | long_description=read('README.rst'), 19 | author='Marc Brinkmann', 20 | author_email='git@marcbrinkmann.de', 21 | url='http://github.com/mbr/flask-appconfig', 22 | license='MIT', 23 | packages=find_packages(exclude=['tests']), 24 | install_requires=['flask>=0.12', 'six', 'click'], 25 | classifiers=[ 26 | 'Programming Language :: Python :: 2', 27 | 'Programming Language :: Python :: 3', 28 | ]) 29 | -------------------------------------------------------------------------------- /tests/module.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbr/flask-appconfig/5264719ac9229339070b219a4358a3203ffd05b0/tests/module.py -------------------------------------------------------------------------------- /tests/test_appenv.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_appconfig import AppConfig 3 | 4 | 5 | def create_sample_app(): 6 | app = Flask('testapp') 7 | AppConfig(app) 8 | return app 9 | 10 | 11 | def test_envmocking(monkeypatch): 12 | monkeypatch.setenv('TESTAPP_CONFA', 'a') 13 | monkeypatch.setenv('TESTAPP_CONFB', 'b') 14 | 15 | app = create_sample_app() 16 | assert app.config['CONFA'] == 'a' 17 | assert app.config['CONFB'] == 'b' 18 | 19 | 20 | def create_submodule_app(): 21 | app = Flask('module.app') 22 | AppConfig(app) 23 | return app 24 | 25 | 26 | def test_envmocking_app_in_submodule(monkeypatch): 27 | monkeypatch.setenv('MODULE_APP_CONFA', 'a') 28 | monkeypatch.setenv('MODULE_APP_CONFB', 'b') 29 | 30 | app = create_submodule_app() 31 | assert app.config['CONFA'] == 'a' 32 | assert app.config['CONFB'] == 'b' 33 | -------------------------------------------------------------------------------- /tests/test_heroku.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_appconfig import HerokuConfig 3 | 4 | 5 | def create_sample_app(): 6 | app = Flask('testapp') 7 | HerokuConfig(app) 8 | return app 9 | 10 | 11 | def test_herokupostgres(monkeypatch): 12 | monkeypatch.setenv('HEROKU_POSTGRESQL_ORANGE_URL', 'heroku-db-uri') 13 | 14 | app = create_sample_app() 15 | assert app.config['SQLALCHEMY_DATABASE_URI'] == 'heroku-db-uri' 16 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | def test_package_importable(): 2 | import flask_appconfig 3 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27,py33,py34 3 | [testenv] 4 | deps=pytest 5 | commands=py.test 6 | --------------------------------------------------------------------------------