Flask Blueprints

In this post we will add configuration to the Agile Estimator application, allowing us to run it in development, test or production modes, and introduce Flask Blueprints.

In the last post we added a database to the app, but noted that it was difficult to configure the database for different environments. The way that estimator/__init__.py was written made app a global, thus it was created before we could change its configuration.

The solution is to use a create_app function that takes a configuration parameter. This poses some complications for other elements of the code, for example, the application routes which had access to the app object to create routes with the @app.route decorator, will no longer be able to refer to it at declaration stage.

We get around the problem by placing the routes in blueprints. When we create the app object, we will register the blueprint with it, rather than simply importing the route scripts.

Another hurdle is to set up the run and test configuration, as each will need to create the app instance rather than use an import statement.

The main drive of these changes is to avoid writing any code that requires configuration at import time. We should delay this so that it can be loaded and applied afterwards.

Configuration

Flask allows you to load configurations from either a Python script or from the environment variables. For example, the following snippet would let you override default options by modifying the environment variables (where the settings are contained in app/default_settings.py):

app = Flask(__name__)
app.config.from_object('app.default_settings')
app.config.from_envvar('YOURAPPLICATION_SETTINGS')

This approach is OK but may require a lot of repeated environment settings. A preferred approach would be to use some sort of inherited configuration such as that suggested by the Flask documentation. Let’s define a base Config object from which development, test, and production obects can extend, overriding or setting environment specific settings as required.

import os
base_dir = os.path.abspath(os.path.dirname(__file__))

class Config:
       SQLALCHEMY_TRACK_MODIFICATIONS = False

       @staticmethod
       def init_app(app):
               pass

class DevelopmentConfig(Config):
       DEBUG = True
       SQLALCHEMY_DATABASE_URI = 'sqlite:///'  os.path.join(base_dir, 'data.sqlite')

class TestConfig(Config):
       TESTING = True
       SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'

class ProductionConfig(Config):
       SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

config = {
    'development': DevelopmentConfig,
    'test': TestConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

The main difference between the configurations is the database, defined on the file system for development, as in-memory for test, and defined as an environment variable for production.

On line 22 we use a map to all access to each configuration based on the option given. By default, if no configuration is set when running the application, the development configuration will be chosen.

Define the Blueprints

The application has a controller that responds to browser requests and a controller that responds to API requests as REST operations. It seems appropriate to keep these separate, each defined in their own blueprint.

As mentioned, because we can no longer simply import the app object, we have to change the way each controller is defined. Previously we had something like this defined for the web controller (estimator/controller.py):

from estimator import app

@app.route('/')
def hello():
    return render_template('index.html')

The decorator @app.route is not available to us anymore. Let’s create a blueprint for this instead. The blueprint will be declared so that importing this file will give access to it.

from flask import Blueprint

web = Blueprint('web', __name__)
@web.route()'/')
def hello():
    return render_template('index.html')

The rest controller can be changed in a similar way except that this time the blueprint will be defined as api = Blueprint('api', __name__).

There is one further change in controllers to highlight at this point when using blueprints. When using url_for to build the full URL to include in the location header, we now need to refer to the method by prefixing with the blueprint name. So url_for('query_group', groupname=groupname) becomes url_for('api.query_group', groupname=groupname).

Create the Application

The app can no longer be simply delared so that it is created at startup. This leaves the current code in difficulty (estimator/__init__.py):

app = Flask(__name__)
bootstrap = Bootstrap(app) 
...
db = SQLAlchemy(app)

The constructors for the Bootstrap and SQLAlchemy objects, which we want to be available globally, take the app object as a constructor argument. This can be changed, however, so that the setting of the app can be done later. Their definition becomes:

bootstrap = Bootstrap()
db = SQLAlchemy()

The following is the create_app function that will now give access to the app object. Note that which configuration settings to load is passed as the parameter config_name.

def create_app(config_name):
	app = Flask(__name__)
	# load the appropriate config
	app.config.from_object(config[config_name])

	config[config_name].init_app(app)
	bootstrap.init_app(app)
	db.init_app(app)

	from .controllers import web as web_blueprint
	app.register_blueprint(web_blueprint)
	from .rest_controller import api as api_blueprint
	app.register_blueprint(api_blueprint)

	return app

Line 4 loads the configuration object we need with app.config.from_object and looking up the appropriate setting that we created earlier using the config_name as the key.

On lines 6-8 we can initialise both the bootstrap and db objects. Note also that the configuration can be initialised at this point. At the moment there is nothing in that function.

We import and register the blueprints on lines 10-13. This replaces the simple import of the route file from the previous version.

Runner

There is not much to change in run.py. Simply import the new create_app function and use it instead. Note that an environment variable can be used to change the configuration used.

+from estimator import create_app
+import os
+
+app = create_app(os.getenv('FLASK_CONFIG') or 'default')

Updating the Tests

We cannot simply import the app object into tests either. Better to move this into a Pytest fixture along with the database initialisation. This is what it looke like:

@pytest.fixture
def app():
	"Initialize the app in test mode, initialises the DB, and adds some test cases"
	app = create_app('test')
	with app.app_context():
		db.create_all()
		test_group = Group('TestGroup')
		db.session.add(test_group)
		existing_group = Group('GroupAlreadyExists')
		db.session.add(existing_group)
		db.session.commit()
	return app

Note that the application context needs to be explicitly specified on line 5. This is equivalent to calling app.app_context().push() and app.app_context().pop().

The definition of the the client fixture needs to change also. Fixtures can make use of previous fixtures, here we add the app fixture as a parameter to the client fixture:

@pytest.fixture
def client(app):
	"Create the test client as a fixture."
	client = app.test_client()
	client.testing = True
	return client

We require a another small change to the test_rest_controller.py, having already removed the database initialisation. Since the application context is not active during the verification, it is not possible to query the database without having an activated context.

First we add the app fixture to the method declaration (def test_create_group(client, app):) and this can activate a context to run the query.

	with app.app_context():
		g = Group.query.filter_by(name='NewGroup').first()
		assert g

Conclusion

The advice from reading multiple tutorials and blogs seems to be not to instantiate the app in code that can be simply imported to get a handle on it. The complications with configuration are the main reasons.

The fact that most of the tutorials start with the simpler configuration and migrate to using a create_app function is probably to make it easier to get excited by Flask. It’s easy to get started.

I am glad that I made this switch before going too much further into the development as there could have been more to change. As it was, this was a painless enough exercise.

The code for this project is stored on GitHub here: https://github.com/rkie/estimator

As always, send me any comments, questions, corrections. Even better, write a blog post and send me the link. My email: ronan@failedtofunction.com