Use environment variables for configuration

As per https://12factor.net/

Use environs/python-dotenv for reading/parsing variables
master
Steven Loria 6 years ago
parent 72b224786f
commit 973bcd96cb
No known key found for this signature in database
GPG Key ID: 631262B829DDB506
  1. 1
      README.rst
  2. 10
      tasks.py
  3. 5
      {{cookiecutter.app_name}}/.env.example
  4. 6
      {{cookiecutter.app_name}}/.gitignore
  5. 3
      {{cookiecutter.app_name}}/Pipfile
  6. 18
      {{cookiecutter.app_name}}/README.rst
  7. 7
      {{cookiecutter.app_name}}/autoapp.py
  8. 2
      {{cookiecutter.app_name}}/package.json
  9. 3
      {{cookiecutter.app_name}}/requirements/prod.txt
  10. 3
      {{cookiecutter.app_name}}/tests/conftest.py
  11. 11
      {{cookiecutter.app_name}}/tests/settings.py
  12. 19
      {{cookiecutter.app_name}}/tests/test_config.py
  13. 3
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py
  14. 70
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py

@ -25,6 +25,7 @@ Features
- Bootstrap 3 and Font Awesome 4 with starter templates - Bootstrap 3 and Font Awesome 4 with starter templates
- Flask-SQLAlchemy with basic User model - Flask-SQLAlchemy with basic User model
- Easy database migrations with Flask-Migrate - Easy database migrations with Flask-Migrate
- Configuration in environment variables, as per `The Twelve-Factor App <https://12factor.net/config>`_
- Flask-WTForms with login and registration forms - Flask-WTForms with login and registration forms
- Flask-Login for authentication - Flask-Login for authentication
- Flask-Bcrypt for password hashing - Flask-Bcrypt for password hashing

@ -13,7 +13,6 @@ with open(os.path.join(HERE, 'cookiecutter.json'), 'r') as fp:
COOKIECUTTER_SETTINGS = json.load(fp) COOKIECUTTER_SETTINGS = json.load(fp)
# Match default value of app_name from cookiecutter.json # Match default value of app_name from cookiecutter.json
COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS['app_name']) COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS['app_name'])
AUTOAPP = os.path.join(COOKIE, 'autoapp.py')
REQUIREMENTS = os.path.join(COOKIE, 'requirements', 'dev.txt') REQUIREMENTS = os.path.join(COOKIE, 'requirements', 'dev.txt')
@ -42,7 +41,8 @@ def clean(ctx):
def _run_flask_command(ctx, command): def _run_flask_command(ctx, command):
ctx.run('FLASK_APP={0} flask {1}'.format(AUTOAPP, command), echo=True) os.chdir(COOKIE)
ctx.run('flask {0}'.format(command), echo=True)
@task(pre=[clean, build]) @task(pre=[clean, build])
@ -52,12 +52,14 @@ def test(ctx):
echo=True) echo=True)
_run_npm_command(ctx, 'run lint') _run_npm_command(ctx, 'run lint')
os.chdir(COOKIE) os.chdir(COOKIE)
shutil.copyfile(os.path.join(COOKIE, '.env.example'),
os.path.join(COOKIE, '.env'))
_run_flask_command(ctx, 'lint') _run_flask_command(ctx, 'lint')
_run_flask_command(ctx, 'test') _run_flask_command(ctx, 'test')
@task @task
def readme(ctx, browse=False): def readme(ctx, browse=False):
ctx.run("rst2html.py README.rst > README.html") ctx.run('rst2html.py README.rst > README.html')
if browse: if browse:
webbrowser.open_new_tab('README.html') webbrowser.open_new_tab('README.html')

@ -0,0 +1,5 @@
# Environment variable overrides for local development
FLASK_APP=autoapp.py
FLASK_ENV=development
DATABASE_URL="sqlite:////tmp/dev.db"
SECRET_KEY="not-so-secret"

@ -51,3 +51,9 @@ env/
# webpack-built files # webpack-built files
/{{cookiecutter.app_name}}/static/build/ /{{cookiecutter.app_name}}/static/build/
/{{cookiecutter.app_name}}/webpack/manifest.json /{{cookiecutter.app_name}}/webpack/manifest.json
# Configuration
.env
# Development database
*.db

@ -40,6 +40,9 @@ Flask-Caching = ">=1.0.0"
# Debug toolbar # Debug toolbar
Flask-DebugToolbar = "==0.10.1" Flask-DebugToolbar = "==0.10.1"
# Environment variable parsing
environs = "==4.0.0"
[dev-packages] [dev-packages]
# Testing # Testing
pytest = "==3.6.1" pytest = "==3.6.1"

@ -8,13 +8,6 @@
Quickstart Quickstart
---------- ----------
First, set your app's secret key as an environment variable. For example,
add the following to ``.bashrc`` or ``.bash_profile``.
.. code-block:: bash
export {{cookiecutter.app_name | upper}}_SECRET='something-really-secret'
Run the following commands to bootstrap your environment :: Run the following commands to bootstrap your environment ::
git clone https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}} git clone https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}}
@ -24,17 +17,12 @@ Run the following commands to bootstrap your environment ::
{%- else %} {%- else %}
pip install -r requirements/dev.txt pip install -r requirements/dev.txt
{%- endif %} {%- endif %}
cp .env.example .env
npm install npm install
npm start # run the webpack dev server and flask server using concurrently npm start # run the webpack dev server and flask server using concurrently
You will see a pretty welcome screen. You will see a pretty welcome screen.
In general, before running shell commands, set the ``FLASK_APP`` and
``FLASK_DEBUG`` environment variables ::
export FLASK_APP=autoapp.py
export FLASK_DEBUG=1
Once you have installed your DBMS, run the following to create your app's Once you have installed your DBMS, run the following to create your app's
database tables and perform the initial migration :: database tables and perform the initial migration ::
@ -49,12 +37,14 @@ Deployment
To deploy:: To deploy::
export FLASK_ENV=production
export FLASK_DEBUG=0 export FLASK_DEBUG=0
export DATABASE_URL="<YOUR DATABASE URL>"
npm run build # build assets with webpack npm run build # build assets with webpack
flask run # start the flask server flask run # start the flask server
In your production environment, make sure the ``FLASK_DEBUG`` environment In your production environment, make sure the ``FLASK_DEBUG`` environment
variable is unset or is set to ``0``, so that ``ProdConfig`` is used. variable is unset or is set to ``0``.
Shell Shell

@ -1,10 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Create an application instance.""" """Create an application instance."""
from flask.helpers import get_debug_flag
from {{cookiecutter.app_name}}.app import create_app from {{cookiecutter.app_name}}.app import create_app
from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig
CONFIG = DevConfig if get_debug_flag() else ProdConfig
app = create_app(CONFIG) app = create_app()

@ -6,7 +6,7 @@
"build": "NODE_ENV=production webpack --progress --colors -p", "build": "NODE_ENV=production webpack --progress --colors -p",
"start": "concurrently -n \"WEBPACK,FLASK\" -c \"bgBlue.bold,bgMagenta.bold\" \"npm run webpack-dev-server\" \"npm run flask-server\"", "start": "concurrently -n \"WEBPACK,FLASK\" -c \"bgBlue.bold,bgMagenta.bold\" \"npm run webpack-dev-server\" \"npm run flask-server\"",
"webpack-dev-server": "NODE_ENV=debug webpack-dev-server --port 2992 --hot --inline", "webpack-dev-server": "NODE_ENV=debug webpack-dev-server --port 2992 --hot --inline",
"flask-server": "FLASK_APP=$PWD/autoapp.py FLASK_DEBUG=1 flask run", "flask-server": "flask run",
"lint": "eslint \"assets/js/*.js\"" "lint": "eslint \"assets/js/*.js\""
}, },
"repository": { "repository": {

@ -35,3 +35,6 @@ Flask-Caching>=1.0.0
# Debug toolbar # Debug toolbar
Flask-DebugToolbar==0.10.1 Flask-DebugToolbar==0.10.1
# Environment variable parsing
environs==4.0.0

@ -6,7 +6,6 @@ from webtest import TestApp
from {{cookiecutter.app_name}}.app import create_app from {{cookiecutter.app_name}}.app import create_app
from {{cookiecutter.app_name}}.database import db as _db from {{cookiecutter.app_name}}.database import db as _db
from {{cookiecutter.app_name}}.settings import TestConfig
from .factories import UserFactory from .factories import UserFactory
@ -14,7 +13,7 @@ from .factories import UserFactory
@pytest.fixture @pytest.fixture
def app(): def app():
"""An application for the tests.""" """An application for the tests."""
_app = create_app(TestConfig) _app = create_app('tests.settings')
ctx = _app.test_request_context() ctx = _app.test_request_context()
ctx.push() ctx.push()

@ -0,0 +1,11 @@
"""Settings module for test app."""
ENV = 'development'
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite://'
SECRET_KEY = 'not-so-secret-in-tests'
BCRYPT_LOG_ROUNDS = 4 # For faster tests; needs at least 4 to avoid "ValueError: Invalid rounds"
DEBUG_TB_ENABLED = False
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
SQLALCHEMY_TRACK_MODIFICATIONS = False
WEBPACK_MANIFEST_PATH = 'webpack/manifest.json'
WTF_CSRF_ENABLED = False # Allows form testing

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
"""Test configs."""
from {{cookiecutter.app_name}}.app import create_app
from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig
def test_production_config():
"""Production config."""
app = create_app(ProdConfig)
assert app.config['ENV'] == 'prod'
assert app.config['DEBUG'] is False
assert app.config['DEBUG_TB_ENABLED'] is False
def test_dev_config():
"""Development config."""
app = create_app(DevConfig)
assert app.config['ENV'] == 'dev'
assert app.config['DEBUG'] is True

@ -4,10 +4,9 @@ from flask import Flask, render_template
from {{cookiecutter.app_name}} import commands, public, user from {{cookiecutter.app_name}} import commands, public, user
from {{cookiecutter.app_name}}.extensions import bcrypt, cache, csrf_protect, db, debug_toolbar, login_manager, migrate, webpack from {{cookiecutter.app_name}}.extensions import bcrypt, cache, csrf_protect, db, debug_toolbar, login_manager, migrate, webpack
from {{cookiecutter.app_name}}.settings import ProdConfig
def create_app(config_object=ProdConfig): def create_app(config_object='{{cookiecutter.app_name}}.settings'):
"""An application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. """An application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
:param config_object: The configuration object to use. :param config_object: The configuration object to use.

@ -1,49 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Application configuration.""" """Application configuration.
import os
Most configuration is set via environment variables.
class Config(object): For local development, use a .env file to set
"""Base configuration.""" environment variables.
"""
SECRET_KEY = os.environ.get('{{cookiecutter.app_name | upper}}_SECRET', 'secret-key') # TODO: Change me from environs import Env
APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory
PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) env = Env()
BCRYPT_LOG_ROUNDS = 13 env.read_env()
DEBUG_TB_ENABLED = False # Disable Debug toolbar
DEBUG_TB_INTERCEPT_REDIRECTS = False ENV = env.str('FLASK_ENV', default='production')
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc. DEBUG = ENV == 'development'
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_DATABASE_URI = env.str('DATABASE_URL')
WEBPACK_MANIFEST_PATH = 'webpack/manifest.json' SECRET_KEY = env.str('SECRET_KEY')
BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13)
DEBUG_TB_ENABLED = DEBUG
class ProdConfig(Config): DEBUG_TB_INTERCEPT_REDIRECTS = False
"""Production configuration.""" CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
SQLALCHEMY_TRACK_MODIFICATIONS = False
ENV = 'prod' WEBPACK_MANIFEST_PATH = 'webpack/manifest.json'
DEBUG = False
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example' # TODO: Change me
DEBUG_TB_ENABLED = False # Disable Debug toolbar
class DevConfig(Config):
"""Development configuration."""
ENV = 'dev'
DEBUG = True
DB_NAME = 'dev.db'
# Put the db file in project root
DB_PATH = os.path.join(Config.PROJECT_ROOT, DB_NAME)
SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(DB_PATH)
DEBUG_TB_ENABLED = True
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
class TestConfig(Config):
"""Test configuration."""
TESTING = True
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite://'
BCRYPT_LOG_ROUNDS = 4 # For faster tests; needs at least 4 to avoid "ValueError: Invalid rounds"
WTF_CSRF_ENABLED = False # Allows form testing

Loading…
Cancel
Save