Banking Web App Stories — Part 4

Nat Retsel
4 min readMay 21, 2023

--

Continuing from part 3, in this part of the Banking Web App series, we will touch on the application factory and blueprint to deal with multiple app configurations. A reason we will need multiple app configurations is to separate testing from production. Some would add another configuration for development. We will add the different configurations in our config.py file:

import os

basedir = os.path.abspath(os.path.dirname(__file__))

class Config:


SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess sting' # used as an encrpyption or signing key. Flask uses this key in its mechanism for csrf protection


class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite://'

class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')

config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}

We create a Configbase class that contains settings integral to all configurations. The different subclasses contain the settings specific to development, testing and production configuration respectively and inherits the common settings from the base class.

Also note that the database URI is different for each configuration; we do not want to use production database to run testing or development on. We set the default database for testing to be in-memory as we do not need to store any data after testing.

Application factory

The current iteration of the structure creates the application in the global scope, preventing configuration changes after the application instance is created. To allow selection of configuration settings and the ability to create multiple application instances, we can move the creation of the application into a factory function, Create_app()shown in our package constructor app/__init__.py:

import os
from config import config
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, Blueprint
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_migrate import Migrate
from flask_login import LoginManager

basedir = os.path.abspath(os.path.dirname(__file__))


bootstrap = Bootstrap()
moment = Moment()
login = LoginManager()

db = SQLAlchemy()
migrate = Migrate()

def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])

bootstrap.init_app(app)
moment.init_app(app)
db.init_app(app)
login.init_app(app)
migrate.init_app(app, db)


return app

The extensions are first created uninitialized. They are fed the app and db instance inside create_app() once the app is configured. The factory function returns the created application instance, but is missing routes. When the application instance was in the global scope previously, routes can be easily defined with the @app.route() decorator. With the application created at runtime, the decorator exist only after create_app() is invoked, which is too late.

Blueprints

Luckily, we can use blueprints. It is similar to an application in that it can also define routes and error handlers, but they are dormant until the blueprint is registered with an application, becoming a part of it. By using a blueprint defined in the global scope, the routes and error handlers can be defined in similar fashion as we have done with the global application.

A subpackage inside the application package (app/main/__init__.py) will be created to host the first blueprint of the application:

from flask import Blueprint

main = Blueprint('main', __name__) # args: blueprint name, module / package of blueprint

from . import routes

Let’s register our blueprint inside the create_app() factory function:

import os
from config import config
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, Blueprint
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_migrate import Migrate
from flask_login import LoginManager

basedir = os.path.abspath(os.path.dirname(__file__))

bootstrap = Bootstrap()
moment = Moment()
login = LoginManager()

db = SQLAlchemy()
migrate = Migrate()

def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])

bootstrap.init_app(app)
moment.init_app(app)
db.init_app(app)
login.init_app(app)
migrate.init_app(app, db)

from .main import main as main_blueprint
app.register_blueprint(main_blueprint)

return app

As we have to define our routes with the blueprint and not the app, we use @main_route() instead of @app.route() and modifying the endpoint in our url_for with .<route>():

from flask import render_template, redirect, url_for, flash, session, request, Response
from flask_login import current_user, login_user, logout_user
from .forms import RegistrationForm, LoginForm
from app.models import User, Role
from .. import db
from . import main
from werkzeug.urls import url_parse


@main.route('/')
@main.route('/index')
def index() -> Response:
first_name = session.get('first_name')
return render_template('index.html', first_name=first_name)

@main.route('/register', methods=['GET', 'POST'])
def register() -> Response:
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
user_role = Role.query.filter_by(name='User').first()
user = User(first_name=form.first_name.data, last_name=form.last_name.data, email=form.email.data, role_id=user_role.id) # Defaults role to user role
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user! Please login')
return redirect(url_for('.login'))
return render_template('register.html', form=form)

Flask applies a namespace to all endpoints defined in a blueprint so that multiple blueprints can define view functions with the same endpoint name without collisions. We have also shifted routes.py and forms.py into app/main/.

Application script

We will know designate webapp.py as the high level application creation instance:

import os
from app import create_app, db
from app.models import User, Role
from flask_migrate import Migrate

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)

@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)

The app.shell_context_processor decorator registers the function as a shell context function. When the flask shell command runs, it will invoke this function and register the items returned by it in the shell session. The reason the function returns a dictionary and not a list is that for each item we have to also provide a name under which it will be referenced in the shell, which is given by the dictionary keys.

After adding the shell context processor function, we can work with database entities without having to import them.

At this stage, feel free to explore the different configurations. As you run, you may find new sqlite files appearing and the error pertaining to sqlite: table not found. Why is this so? Try to find the cause and fix it yourself.

Hint: Have we added the different roles? Why do need to do that? Do we have a query that in one of our routes that could break if there are no roles in the db?

In the next article, we look to write some unit testing before we move on to develop the transaction part of our application.

Part 5

--

--