Writing a Flask App the !Noob Way

This tutorial is not meant for beginners. It is assumed that the reader is familiar with the flask framework.

In this tutorial we will be writing a flask application using Flask Blueprints and Application Factory Pattern.

What are blueprints?

If you're working on a small project, keeping all of your code in one module isn't a bad idea. However, for large projects, it is usual practise to divide your project into numerous packages using Flask Blueprints.

A blueprint is a file that contains a single piece of functionality in your application. Consider a flask application built with blueprints as a collection of critical bits of functionality that work together to produce a complete web application.

It's a good idea to think about what blueprints you can break your application into before you start writing. I usually use three blueprints, api, user and admin, in my personal approach. The api blueprint is added to handle programmatic access to the web application resources. The user-related functionality is handled by the user blueprint, including registration, logout, login, password reset etc. The admin blueprint is in charge of the admin panel functionality and features.

|-app.py
|-.gitignore
|-README.md
|-requirements.txt
|-logs/
|-app_name/
    |-__init__.py
    |-.env
    |-models/
    |-utils/
    |-forms/
    |-config/
        |-config.py
        |-database.py
    |-static/
    |-templates/
        |-layout.html
        |-400.html
        |-403.html
        |-404.html
        |-405.html
        |-500.html
    |-routes/
        |-__init__.py
        |-api.py
        |-user.py
        |-admin.py
        |-templates
            |-user/
                |-register.html
            |-admin/
                |-panel.html

The following table gives an overview of what each file and folder does

File or Folder Description
app.py The file that contains the flask application instance for starting the application
requirements.py The file that contains all the dependencies of the application
logs/ The folder that contains application logs
app_name/.env "The file that contains environment variables like SECRET_KEY
app_name/routes The folder that contains the blueprints and templates related to them
app_name/__init__.py The file where we assemble the different components of the flask application
app_name/models/ The folder that contains the database models
app_name/utils/ The folder that contains the essential services like database access object (daos)
app_name/forms/ The folder that contains flask forms
app_name/config/config.py The file that contains configurations of the flask application
app_name/config/database.py The file that contains configurations of the database
app_name/static "The folder that contains all the css
app_name/templates The folder that contains the base template and error pages of the application

Because we've organised our application into blueprints, instead of sending requests to the flask application instance to be handled, the server now sends them to the appropriate blueprint. The blueprints must "register" with the flask application instance in order for the flask application instance to know about the project's blueprints and routes.

Configuration Management

There are several ways to configure a flask application, but in this tutorial we will be using an .env file and python objects.

In practise, you wouldn't want to hardcode the value of important parameters like the secret key, mail server username and password, and many more in the config.py file for security reasons.

The production-grade or the !noob way of setting important
parameters is to write them in an .env file. The python package python-dotenv is a handy little tool. After you've installed the package, you'll need to create a .env file in your project's root directory to define all of your environment variables. The load_dotenv() function in your config.py file is then used to load the environment configuration settings from the
.env file. You must remember to include the .env file to your .gitignore file if you use this technique.

Your .env file

CONFIG_ENV = Development
ENV = development
SECRET_KEY = 'DamnSIMpLeSecREtKEy'
DATABASE_URI = 
'mysql+pymysql://user:password@hostname:port/database_name'
MAIL_USERNAME = mail_user
MAIL_PASSWORD = mail_password
MAIL_DEFAULT_SENDER = mail_sender

Your config.py file

from os import path, environ 
from dotenv import load_dotenv

# Absolute path of app_name directory
BASE_DIR = path.abspath(path.dirname(path.dirname(__file__)))

# Loading configuration variable into the environment from .env file
load_dotenv(path.join(BASE_DIR, '.env'))

class Config:
    """
    Base class configuration, common to all other config classes
    """

    SECRET_KEY = environ.get('SECRET_KEY', 'samplesecret_key')
    WTF_CSRF_ENABLED = True
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_USE_SSL = True
    MAIL_PORT = 465
    MAIL_USE_TLS = False
    MAIL_SUPPRESS_SEND = False
    MAIL_USERNAME = environ.get('MAIL_USERNAME', '')
    MAIL_PASSWORD = environ.get('MAIL_PASSWORD', '')
    MAIL_DEFAULT_SENDER = environ.get('MAIL_USERNAME', '')

class Development(Config):
   """Configuration settings for development environment"""
    DEBUG = True
    TESTING = False
    ENV = 'development'
    DATABASE_URI = environ.get("DATABASE_URI")

class Production(Config):
    """Configuration settings for production environment"""
    DEBUG = False
    TESTING = False
    ENV = 'production'
    DATABASE_URI = environ.get("PROD_DATABASE_URI")

class Testing(Config):
    """Configuration settings for testing environment"""
    TESTING = True
    WTF_CSRF_ENABLED = False
    MAIL_SUPPRESS_SEND = True
    DATABASE_URI = environ.get("TEST_DATABASE_URI")

Your app_name/__init__.py file

from flask import Flask
from os import environ
from .config.config import Development, Production

def create_app():

    app = Flask(__name__)

    if environ.get('CONFIG_ENV') == 'Development':
        app.config.from_object(Development())
    else:
        app.config.from_object(Production())

    @app.route("/",methods=['GET'])
    def index():
        return '<h2>App is Running</h2>'

    return app

Your app.py file

from os import environ
from app_name import create_app

app = create_app()

if __name__ == "__main__":
    app.run()

Creating Blueprints

Blueprints are defined by instantiating an instance of the Blueprint class. The arguments passed to the class constructor are the name of the blueprint and the name of the folder containing the templates belonging to the blueprint and a url prefix to differentiate between the similar routes of different blueprints. You then need to write the routes of that blueprint.

Your app_name/routes/api.py file

from flask import Blueprint

def create_blueprint():
    """Instantiating api blueprint and returning it"""

    api = Blueprint('api', __name__, 
    template_folder='templates', url_prefix='/api')

    @api.route('/info',methods=['GET'])
    def info():
        return '<h2>Sample Route</h2>'

    return api

Your app_name/routes/user.py file

from flask import Blueprint, render_template

def create_blueprint():
    """Instantiating user blueprint and returning it"""

    user = Blueprint('user', __name__, 
    template_folder='templates/user', url_prefix='/user')

    @user.route('/register',methods=['GET'])
    def register():
        return render_template('register.html')

    return user

Your app_name/routes/admin.py file

from flask import Blueprint, render_template

def create_blueprint():
    """Instantiating admin blueprint and returning it"""

    admin = Blueprint('admin', __name__, 
    template_folder='templates/admin', url_prefix='/admin')

    @admin.route('/panel',methods=['GET'])
    def panel():
        return render_template('panel.html')

    return admin

Important points to remember when working with blueprints

  • Any references to the app object must be replaced with references to the current_app object. This is because you no longer have direct access to the flask application instance when dealing with blueprints. It is only accessible through its proxy, current_app.

  • Make sure the url_for() method refers to the view function's blueprint. This is done to reflect the reality that certain blueprints have distinct view functionalities. url_for(user.register).

  • Make that the blueprint object is used by the decorator used to define any route.

Application Factory Pattern

We want to create flask applications with different configurations(Development, Production, Testing) without changing much in the actual code. Here, the function of factory method is to spin up different flask applications according to our need. The Application Factory Pattern is nothing but the well-known design pattern Factory Method Pattern.

Format to make an Application Factory Function

  • Instantiation of Flask Application
  • Loading Configurations of Flask Application
  • Registration of BLueprints
  • Registration of Error Handlers
  • Configuration of Logging Module

We covered the first two steps in the previous sections, so we will be continuing from the third step.

Registration of Blueprints

Blueprints are registered by passing the blueprint object to the register_blueprint() method the flask application instance exposes.

Your utils/register_blueprints.py file

from app_name.routes import user, admin, api

def register_blueprints(app):
    """Registering all the blueprint objects"""

    app.register_blueprint(user.create_blueprint())
    app.register_blueprint(admin.create_blueprint())
    app.register_blueprint(api.create_blueprint())

Your app_name/__init__.py file

from flask import Flask
from os import environ
from .config.config import Development, Production
from .utils.register_blueprints import register_blueprints

def create_app():

    app = Flask(__name__)

    if environ.get('CONFIG_ENV') == 'Development':
        app.config.from_object(Development())
    else:
        app.config.from_object(Production())


    # Registering Blueprints
    register_blueprints(app)


    @app.route("/",methods=['GET'])
    def index():
        return '<h2>App is Running</h2>'

    return app

What is Logging?

It is the process of keeping official record of your application. It is used for recording events as they occur and is a great tool for debugging any issues and gaining information about your application's working.

You should log application specific errors as well as database specific errors. You may also opt to log important functionalities of your application. Any kind of sensitive information should not be logged as logs are stored in plaintext format.

Logging Module

Generally standard python logging module is used. The standard looging module has 4 submodules, which are loggers, handlers, filters and formatters.

Loggers are the objects that create log messages. When you produce a log message, you have to specify its criticality by using the function associated with the criticality level.The following are the criticalility levels (also known as logging levels), their numeric representations, and the functions that go with them:

  • Debug → 10 → debug()
  • Info → 20 → info()
  • Warning → 30 → warning()
  • Error → 40 → error()
  • Critical → 50 → critical()

A default logger object can be accessed and utilised without the requirement for any configuration. The app.logger object is exposed by every flask instance. If you're using blueprints, you'll need to use the current_app object, which is a proxy for the flask application instance.

Your app_name/routes/admin.py file

from flask import Blueprint, render_template, current_app

def create_blueprint():
    """Instantiating admin blueprint and returning it"""

    admin = Blueprint('admin', __name__, 
    template_folder='templates/admin', url_prefix='/admin')

    @admin.route('/panel',methods=['GET'])
    def panel():
        current_app.logger.info("Admin Panel Accessed")
        return render_template('panel.html')

    return admin

The default logger, unfortunately, simply prints to the console. As a result, if you want to log to a file, you must create a new logger instance. The default logger will continue to log, but you can turn it off if you want:

from flask.logging import default_handler
app.logger.removeHandler(default_handler)

You can set the minimum criticality level of the messages that should be logged when configuring a new logger instance. All log messages with a criticality of this value or greater will be logged, while those with a criticality of less than this value will be ignored. This is handy in instances where you don't want
to delete log calls from your source code but still want to limit the number of log messages. You can increase the minimum log level of messages that are written to the log, such as ERROR messages and higher.

Handlers are the objects that route log messages to the appropriate location. A stream handler is the default handler, and it transmits log messages to the terminal. To route log messages to different destinations, you can build different handler objects.

  • A FileHandler is used to log to a file. A SMTPHandler is used to deliver log messages as email.

  • The FileHandler function accepts the location and name of the log file you wish to write to and creates a FileHandler object that sends log messages to that file.

  • In most cases, the logs folder is used to store files that execute during runtime (logs and database files). This folder must be added to your .gitignore file in order for version control to ignore it.

file_handler = logging.FileHandler('logs/application.log')
app.logger.addHandler(file_handler)

The log messages are written to a single log file via the FileHandler object. As a result, the log file might quickly grow in size. Using the RotatingFileHandler object is a better option. It likewise saves log messages to a file, but if the existing log file surpasses a certain size, it produces a new one (maxBytes). Before overwriting the current files, it will generate a new file up to a given number of files (backupCount).

from logging.handlers import RotatingFileHandler

file_handler = RotatingFileHandler('logs/application.log', maxBytes=16384, backupCount=15)

Filters are used to add contextual information to log messages. When logging requests, for example, you can add a filter that includes the request's external IP address.

Formatters are used to specify the format of the log messages. A LogRecord object represents each log message. Log formatters are used to specify which LogRecord characteristics should be displayed and in what order they should be displayed.

Some Attributes of LogRecords:

  • %(asctime)s - datetime when the LogRecord was created
  • %(filename)s - filename portion of pathname
  • %(funcName)s - name of function containing the logging call
  • %(levelname)s - logging level for the message
  • %(lineno)d - line number of source code where the logging call was issued (if available)
  • %(message)s - the logged message
  • %(module)s - module from which the logging call was issued

Configure Logging

You should configure logging before creating the flask application instance, otherwise it will use the default handler which writes log messages to the console. This is why the application factory function is used to configure logging.

Your utils/log_config.py file

import logging
from flask.logging import default_handler
from logging.handlers import RotatingFileHandler

def log_config(app):

    # Deactivate default flask logger 
    app.logger.removeHandler(default_handler)

    # File handler object
    file_handler = RotatingFileHandler('logs/application.log', maxBytes=16384, backupCount=15)

    # Set logging level of the file handler object so that it logs INFO and up
    file_handler.setLevel(logging.INFO)

    # file formatter object
    file_formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(filename)s: %(lineno)d]')

    # Apply file formatter object to the file handler object

    file_handler.setFormatter(file_formatter)

    # Add file handler object to the logger
    app.logger.addHandler(file_handler)

Customized Error Handling

An HTTP status code is included in the response message when a client sends a request to a web server. It's a three-digit number that represents the outcome of the request processing. Based on the initial digit, status codes are divided into five categories, each representing a different type of response:

  1. 1xx — Informational response
  2. 2xx — Successful responses
    • 200 (OK), for successful processing of the request
  3. 3xx — Redirects
    • 302 (Found), for successfuly redirecting the client to a new URL
  4. 4xx — Client errors
    • 400 (Bad Request): when the client makes a request that the server can’t understand or doesn’t allow.
    • 403 (Forbidden): when the client tries to access a restricted resource and doesn’t have authorization to do so.
    • 404 (Not Found): when a client requests a URL that the server does not recognise. The error message given should be something along the lines of “Sorry, what you are looking for just isn’t there!”.
  • 405 (Method Not Allowed): when a request method is not accepted by the view function that handles requests for a given route. The error message given should be along the lines of “Sorry, the method requested is not supported by this resource!”.
    1. 5xx — Server errors
  • 500 (Internal Server Error): Usually occurs due to programming errors or the server getting overloaded.

Create your custom error pages eg: 403.html, 500.html, 404.html and then render these error pages using error_handlers.

Your utils/error_handlers.py file

def error_handlers(app):

    # 403 - Forbidden
    @app.errorhandler(403)
    def forbidden(e):
        return render_template('403.html'), 403

    # 400 - Bad Request
    @app.errorhandler(400)
    def bad_request(e):
        return render_template('400.html'), 400

    # 404 - Page Not Found
    @app.errorhandler(404)
    def page_not_found(e):
        return render_template('404.html'), 404

    # 405 - Method Not Allowed
    @app.errorhandler(405)
    def method_not_allowed(e):
        return render_template('405.html'), 405

    # 500 - Internal Server Error
    @app.errorhandler(500)
    def server_error(e):
        return render_template('500.html'), 500

Final app_name/__init__.py file

from flask import Flask
from os import environ
from .config.config import Development, Production
from .utils.register_blueprints import register_blueprints
from .utils.error_handlers import error_handlers
from .utils.log_config import log_config

def create_app():

    app = Flask(__name__)

    if environ.get('CONFIG_ENV') == 'Development':
        app.config.from_object(Development())
    else:
        app.config.from_object(Production())


    # Registering Blueprints
    register_blueprints(app)

    # Configure Logging
    log_config(app)

    # Registering Error Handlers
    error_handlers(app)

    @app.route("/",methods=['GET'])
    def index():
        return '<h2>App is Running</h2>'

    return app

Conclusion

You may now brag to your buddies about not being a noob flask developer.

I hope you all enjoyed the article.

15