21
loading...
This website collects cookies to deliver better user experience
Flask Blueprints
and Application Factory Pattern
.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
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 |
.env
file and python objects.secret key
, mail server username
and password
, and many more in the config.py
file for security reasons..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..env
fileCONFIG_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
config.py
filefrom 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")
app_name/__init__.py
filefrom 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
app.py
filefrom os import environ
from app_name import create_app
app = create_app()
if __name__ == "__main__":
app.run()
app_name/routes/api.py
filefrom 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
app_name/routes/user.py
filefrom 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
app_name/routes/admin.py
filefrom 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
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.
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
.register_blueprint()
method the flask application instance exposes.utils/register_blueprints.py
filefrom 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())
app_name/__init__.py
filefrom 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
loggers
, handlers
, filters
and formatters
.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.app_name/routes/admin.py
filefrom 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
from flask.logging import default_handler
app.logger.removeHandler(default_handler)
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)
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)
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.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.utils/log_config.py
fileimport 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)
200
(OK), for successful processing of the request302
(Found), for successfuly redirecting the client to a new URL400
(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!”.
500
(Internal Server Error): Usually occurs due to
programming errors or the server getting overloaded.403.html
, 500.html
, 404.html
and then render these error pages using error_handlers.utils/error_handlers.py
filedef 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
app_name/__init__.py
filefrom 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