From 973bcd96cb2d7e62ee790ecd3d45d3e0421822de Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Mon, 6 Aug 2018 21:07:32 -0400 Subject: [PATCH] Use environment variables for configuration As per https://12factor.net/ Use environs/python-dotenv for reading/parsing variables --- README.rst | 1 + tasks.py | 10 +-- {{cookiecutter.app_name}}/.env.example | 5 ++ {{cookiecutter.app_name}}/.gitignore | 6 ++ {{cookiecutter.app_name}}/Pipfile | 3 + {{cookiecutter.app_name}}/README.rst | 18 ++--- {{cookiecutter.app_name}}/autoapp.py | 7 +- {{cookiecutter.app_name}}/package.json | 2 +- .../requirements/prod.txt | 3 + {{cookiecutter.app_name}}/tests/conftest.py | 3 +- {{cookiecutter.app_name}}/tests/settings.py | 11 +++ .../tests/test_config.py | 19 ----- .../{{cookiecutter.app_name}}/app.py | 3 +- .../{{cookiecutter.app_name}}/settings.py | 70 ++++++------------- 14 files changed, 65 insertions(+), 96 deletions(-) create mode 100644 {{cookiecutter.app_name}}/.env.example create mode 100644 {{cookiecutter.app_name}}/tests/settings.py delete mode 100644 {{cookiecutter.app_name}}/tests/test_config.py diff --git a/README.rst b/README.rst index 06a22b0..5998d13 100644 --- a/README.rst +++ b/README.rst @@ -25,6 +25,7 @@ Features - Bootstrap 3 and Font Awesome 4 with starter templates - Flask-SQLAlchemy with basic User model - Easy database migrations with Flask-Migrate +- Configuration in environment variables, as per `The Twelve-Factor App `_ - Flask-WTForms with login and registration forms - Flask-Login for authentication - Flask-Bcrypt for password hashing diff --git a/tasks.py b/tasks.py index 7e412ce..9d8d5df 100644 --- a/tasks.py +++ b/tasks.py @@ -13,7 +13,6 @@ with open(os.path.join(HERE, 'cookiecutter.json'), 'r') as fp: COOKIECUTTER_SETTINGS = json.load(fp) # Match default value of app_name from cookiecutter.json COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS['app_name']) -AUTOAPP = os.path.join(COOKIE, 'autoapp.py') REQUIREMENTS = os.path.join(COOKIE, 'requirements', 'dev.txt') @@ -42,7 +41,8 @@ def clean(ctx): 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]) @@ -52,12 +52,14 @@ def test(ctx): echo=True) _run_npm_command(ctx, 'run lint') 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, 'test') + @task def readme(ctx, browse=False): - ctx.run("rst2html.py README.rst > README.html") + ctx.run('rst2html.py README.rst > README.html') if browse: webbrowser.open_new_tab('README.html') - diff --git a/{{cookiecutter.app_name}}/.env.example b/{{cookiecutter.app_name}}/.env.example new file mode 100644 index 0000000..7236746 --- /dev/null +++ b/{{cookiecutter.app_name}}/.env.example @@ -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" diff --git a/{{cookiecutter.app_name}}/.gitignore b/{{cookiecutter.app_name}}/.gitignore index be87252..24d2ea2 100644 --- a/{{cookiecutter.app_name}}/.gitignore +++ b/{{cookiecutter.app_name}}/.gitignore @@ -51,3 +51,9 @@ env/ # webpack-built files /{{cookiecutter.app_name}}/static/build/ /{{cookiecutter.app_name}}/webpack/manifest.json + +# Configuration +.env + +# Development database +*.db diff --git a/{{cookiecutter.app_name}}/Pipfile b/{{cookiecutter.app_name}}/Pipfile index c195ac3..d9b88d9 100644 --- a/{{cookiecutter.app_name}}/Pipfile +++ b/{{cookiecutter.app_name}}/Pipfile @@ -40,6 +40,9 @@ Flask-Caching = ">=1.0.0" # Debug toolbar Flask-DebugToolbar = "==0.10.1" +# Environment variable parsing +environs = "==4.0.0" + [dev-packages] # Testing pytest = "==3.6.1" diff --git a/{{cookiecutter.app_name}}/README.rst b/{{cookiecutter.app_name}}/README.rst index c39e426..0a89530 100644 --- a/{{cookiecutter.app_name}}/README.rst +++ b/{{cookiecutter.app_name}}/README.rst @@ -8,13 +8,6 @@ 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 :: git clone https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}} @@ -24,17 +17,12 @@ Run the following commands to bootstrap your environment :: {%- else %} pip install -r requirements/dev.txt {%- endif %} + cp .env.example .env npm install npm start # run the webpack dev server and flask server using concurrently 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 database tables and perform the initial migration :: @@ -49,12 +37,14 @@ Deployment To deploy:: + export FLASK_ENV=production export FLASK_DEBUG=0 + export DATABASE_URL="" npm run build # build assets with webpack flask run # start the flask server 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 diff --git a/{{cookiecutter.app_name}}/autoapp.py b/{{cookiecutter.app_name}}/autoapp.py index f7ab4d6..8bcd8c8 100755 --- a/{{cookiecutter.app_name}}/autoapp.py +++ b/{{cookiecutter.app_name}}/autoapp.py @@ -1,10 +1,5 @@ # -*- coding: utf-8 -*- """Create an application instance.""" -from flask.helpers import get_debug_flag - 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() diff --git a/{{cookiecutter.app_name}}/package.json b/{{cookiecutter.app_name}}/package.json index 95a05b9..2e1b2e3 100644 --- a/{{cookiecutter.app_name}}/package.json +++ b/{{cookiecutter.app_name}}/package.json @@ -6,7 +6,7 @@ "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\"", "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\"" }, "repository": { diff --git a/{{cookiecutter.app_name}}/requirements/prod.txt b/{{cookiecutter.app_name}}/requirements/prod.txt index 41a6758..bf4b3d7 100644 --- a/{{cookiecutter.app_name}}/requirements/prod.txt +++ b/{{cookiecutter.app_name}}/requirements/prod.txt @@ -35,3 +35,6 @@ Flask-Caching>=1.0.0 # Debug toolbar Flask-DebugToolbar==0.10.1 + +# Environment variable parsing +environs==4.0.0 diff --git a/{{cookiecutter.app_name}}/tests/conftest.py b/{{cookiecutter.app_name}}/tests/conftest.py index ab709ff..5bb60c0 100644 --- a/{{cookiecutter.app_name}}/tests/conftest.py +++ b/{{cookiecutter.app_name}}/tests/conftest.py @@ -6,7 +6,6 @@ from webtest import TestApp from {{cookiecutter.app_name}}.app import create_app from {{cookiecutter.app_name}}.database import db as _db -from {{cookiecutter.app_name}}.settings import TestConfig from .factories import UserFactory @@ -14,7 +13,7 @@ from .factories import UserFactory @pytest.fixture def app(): """An application for the tests.""" - _app = create_app(TestConfig) + _app = create_app('tests.settings') ctx = _app.test_request_context() ctx.push() diff --git a/{{cookiecutter.app_name}}/tests/settings.py b/{{cookiecutter.app_name}}/tests/settings.py new file mode 100644 index 0000000..a13e159 --- /dev/null +++ b/{{cookiecutter.app_name}}/tests/settings.py @@ -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 diff --git a/{{cookiecutter.app_name}}/tests/test_config.py b/{{cookiecutter.app_name}}/tests/test_config.py deleted file mode 100644 index 755bed7..0000000 --- a/{{cookiecutter.app_name}}/tests/test_config.py +++ /dev/null @@ -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 diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py index 4740288..0879645 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py @@ -4,10 +4,9 @@ from flask import Flask, render_template 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}}.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/. :param config_object: The configuration object to use. diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py index 2bf866e..4c920b0 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py @@ -1,49 +1,23 @@ # -*- coding: utf-8 -*- -"""Application configuration.""" -import os - - -class Config(object): - """Base configuration.""" - - SECRET_KEY = os.environ.get('{{cookiecutter.app_name | upper}}_SECRET', 'secret-key') # TODO: Change me - APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory - PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) - BCRYPT_LOG_ROUNDS = 13 - DEBUG_TB_ENABLED = False # Disable Debug toolbar - DEBUG_TB_INTERCEPT_REDIRECTS = False - CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc. - SQLALCHEMY_TRACK_MODIFICATIONS = False - WEBPACK_MANIFEST_PATH = 'webpack/manifest.json' - - -class ProdConfig(Config): - """Production configuration.""" - - ENV = 'prod' - 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 +"""Application configuration. + +Most configuration is set via environment variables. + +For local development, use a .env file to set +environment variables. +""" +from environs import Env + +env = Env() +env.read_env() + +ENV = env.str('FLASK_ENV', default='production') +DEBUG = ENV == 'development' +SQLALCHEMY_DATABASE_URI = env.str('DATABASE_URL') +SECRET_KEY = env.str('SECRET_KEY') +BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13) +DEBUG_TB_ENABLED = DEBUG +DEBUG_TB_INTERCEPT_REDIRECTS = False +CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc. +SQLALCHEMY_TRACK_MODIFICATIONS = False +WEBPACK_MANIFEST_PATH = 'webpack/manifest.json'