The purpose of this project is to create an example application that is actually pretty useful.
While it is a toy application, lacking features that would be required to make it a product it includes all the components of a normal web application (Frontend presentation, Backend app logic, Database to store records).
This application is used in a separate blog post “Dev Setup Guide”
We will be using a basic folder structure from the Flask web framework [“Link”] (LINK).
Create a requirements.txt file at the project root (/dft) and enter the following:
flask
flask-sqlalchemy
sqlalchemy_utils
gunicorn
Next, use pip to install the required libraries
python3 -m pip install -r requirements.txt
Now we’re ready to start creating our app.py file which will receive requests from NGINX.
Create a file ‘app.py’ at the project root (/dft) and enter the following:
The first section of the file specifies all the python libraries to be imported.
import os
import datetime
from flask import Flask, request, render_template, redirect, flash, jsonify, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_utils import database_exists
from sqlalchemy import text
Next, we set up the Flask application configuration parameters. We define a secret key, and a SQL database.
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
# configure the database
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////app/database/app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# create the database
db = SQLAlchemy(app)
In this next section, we define some basic database tables.
Each table includes a name, and one or more columns. The columns must be defined as a specific data type.
form_entry_tags = db.Table(
"form_entry_tags",
db.Model.metadata,
db.Column("form_entries", db.Integer, db.ForeignKey("form_entry.id")),
db.Column("tags", db.Integer, db.ForeignKey("tag.id")),
)
class Tag(db.Model):
__name__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
class FormEntry(db.Model):
__name__ = "form_entries"
id = db.Column(db.Integer, primary_key=True)
uastring = db.Column(db.String(200))
message = db.Column(db.String(200))
timestamp = db.Column(db.DateTime)
tags = db.relationship("Tag", secondary=form_entry_tags)
image = db.Column(db.LargeBinary)
category_id = db.Column(db.Integer, db.ForeignKey("category.id"))
category = db.relationship("Category", backref="form_entries")
class Category(db.Model):
__name__ = "Category"
id = db.Column(db.Integer, primary_key=True)
label = db.Column(db.Integer, nullable=False)
description = db.Column(db.String(200))
Next, we define a function that initializes the database.
Taking care to not overwrite the database if one exists.
Once the database is created, seed values are entered for some tables.
At the end of this section we call the init_db function we defined above.
def init_db():
with app.app_context():
if not database_exists(db.engine.url):
db.create_all()
default_categories = [("Happy", "You are feeling happy")]
for label, description in default_categories:
category_obj = Category(label=label, description=description)
db.session.add(category_obj)
db.session.commit()
init_db()
In this code block we define our first Flask application route.
This route accepts both GET and POST HTTP requests.
If the user sends a GET request, a Jinja template ‘form.html’ is rendered.
When POSTing data, a new entry is added to the SQLite3 database, then the browser is re-directed to the entry form.
@app.route("/", methods=["GET", "POST"])
def form():
if request.method == "POST":
# Load categories table from the database
category = Category.query.filter_by(label=request.form["category"]).first()
# Create the form entry
entry = FormEntry(
category=category,
message=request.form["message"],
timestamp=datetime.datetime.now(),
uastring=request.headers.get("User-Agent"),
)
entry.image = request.files["image"].read()
# Create a new tag
tag_name = request.form.get("tags")
tag = Tag.query.filter_by(name=tag_name).first()
if not tag:
tag = Tag(name=tag_name)
db.session.add(tag)
# Add the tag to the FormEntry's tags list
entry.tags.append(tag)
# Save the form entry to the database
db.session.add(entry)
db.session.commit()
flash(f"{request.form['tags']}")
return redirect(url_for("form"))
else:
categories = Category.query.all()
return render_template("form.html", categories=categories)
Last of all, this code block tells flask to run the application, binding to all available IP addresses on the host, using port 5001
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001)
import os
import datetime
from flask import Flask, request, render_template, redirect, flash, jsonify, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_utils import database_exists
from sqlalchemy import text
#initialize an application object
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
# configure the database
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////app/database/app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# create the database
db = SQLAlchemy(app)
#define the database tables
form_entry_tags = db.Table(
"form_entry_tags",
db.Model.metadata,
db.Column("form_entries", db.Integer, db.ForeignKey("form_entry.id")),
db.Column("tags", db.Integer, db.ForeignKey("tag.id")),
)
class Tag(db.Model):
__name__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
class FormEntry(db.Model):
__name__ = "form_entries"
id = db.Column(db.Integer, primary_key=True)
uastring = db.Column(db.String(200))
message = db.Column(db.String(200))
timestamp = db.Column(db.DateTime)
tags = db.relationship("Tag", secondary=form_entry_tags)
image = db.Column(db.LargeBinary)
category_id = db.Column(db.Integer, db.ForeignKey("category.id"))
category = db.relationship("Category", backref="form_entries")
class Category(db.Model):
__name__ = "Category"
id = db.Column(db.Integer, primary_key=True)
label = db.Column(db.Integer, nullable=False)
description = db.Column(db.String(200))
# define the database initialization function
def init_db():
with app.app_context():
if not database_exists(db.engine.url):
db.create_all()
default_categories = [("Happy", "You are feeling happy")]
for label, description in default_categories:
category_obj = Category(label=label, description=description)
db.session.add(category_obj)
db.session.commit()
# initialize the database
init_db()
# define the root application route
@app.route("/", methods=["GET", "POST"])
def form():
if request.method == "POST":
# Load categories table from the database
category = Category.query.filter_by(label=request.form["category"]).first()
# Create the form entry
entry = FormEntry(
category=category,
message=request.form["message"],
timestamp=datetime.datetime.now(),
uastring=request.headers.get("User-Agent"),
)
entry.image = request.files["image"].read()
# Create a new tag
tag_name = request.form.get("tags")
tag = Tag.query.filter_by(name=tag_name).first()
if not tag:
tag = Tag(name=tag_name)
db.session.add(tag)
# Add the tag to the FormEntry's tags list
entry.tags.append(tag)
# Save the form entry to the database
db.session.add(entry)
db.session.commit()
flash(f"{request.form['tags']}")
return redirect(url_for("form"))
else:
categories = Category.query.all()
return render_template("form.html", categories=categories)
# run the application, bind to all addresses on port 5001
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001)
Flask supports a templating language called Jinja to render static content.
One feature that Flask and Jinja provide is extending templates. This allows us to define a page layout in a ‘base.html’ file, then import elsewhere.
Create a file ‘base.html’ in the templates folder folder example: /dft/templates/base.html
The contents of the base.html file should be as follows:
Create a file ‘form.html’ in the templates folder folder example: /dft/templates/form.html
The template file should contain the following code:
As you can see, Jinja allows passing and rendering variables from the python code in HTML which is then consumed by a web browser.
The full documentation on Jinja can be found here.
In a linux terminal, within the project directory /dft/, run the following command to start the flask application server:
python3 -m flask run
The flask application should start running.
But how does this work you might ask? The Flask application server looks for a file in the current working directory named app.py by default.
If you got this far, stop the flask server by pressing CTRL-C in the terminal and proceed to the last section.
Previously, we used the built-in web server functionality provided by Flask to run our app. During development it can be useful
to use the built-in wsgi/http server available in flask - but it has known limitations. In this section we cover how to deploy our app
directly to a linux host using NGINX and gunicorn.
In general, a request will be sent by a web browser, received by NGINX, forwarded to gunicorn as a WSGI request, then processed by our Flask application.
A response would then travel in reverse order through these services before finally arriving back at the web browser. Each ‘layer’ in the ‘stack’
On Debian based Linux distributions, Nginx can be installed using the following apt command:
sudo apt install nginx
Once installed, the nginx server must be configured to proxy requests from the web browser to our WSGI application server.
Locate, or create, the default nginx configuration file (/etc/nginx/conf/nginx.conf) and open it in a text editor.
Replace the contents with the following:
events {}
http {
upstream flask_app {
server app:5001;
}
server {
listen 5000;
server_name localhost;
client_max_body_size 10M;
location / {
proxy_pass http://flask_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
Keep in mind this nginx configuration assumes the nginx server will run on the same host as the Gunicorn WSGI application.
save the file and restart the Nginx server with the following command:
sudo systemctl restart nginx
Gunicorn is a production grade WSGI server that features caching, worker pooling, among other features.
For a full list of features offered by Gunicorn visit the official site here.
the gunicorn wsgi server was installed in a previous step, but we can confirm it is installed with the following command:
python3 -m pip install gunicorn
from the top level project directory (/dft/) run the following command to run the gunicorn WSGI server to host our app.py application.
gunicorn -w 4 -b 0.0.0.0:5001 app:app
Explanation of command options:
With this command running, our python application is being served on port 5001 and should be accessible using a web browser.
This blog will expand on the functionality of the Flask application in a separate post.
If you are interested to learn how to dockerize, deploy, and maintain this application on a production server, visit our docker blog post here.