- Lab
- A Cloud Guru
Building a Web Application with Python and Flask
Python is a great language for web development and the community has built fantastic tools to make the process enjoyable. In this hands-on lab, we're going to build a web application using the Python web framework, Flask. Our application will present a JSON API and also render views with information coming from a PostgreSQL database. By the time we've finished, we'll have seen some of the power that Python provides when being used for web development. **NOTE: **To use **:set paste** in Vim, enter command mode by pressing Esc, then type **:set paste** and hit **Enter** to enable paste mode, which disables automatic formatting. After pasting your text, type** :set nopaste** to return to normal mode. Always check the top of the file as well to make sure the top pasted in ok.
Path Info
Table of Contents
-
Challenge
Create project and virtualenv.
To get started, we're going to create a directory to hold onto our project. We'll call this
tickets
:$ mkdir tickets $ cd tickets
With this set up, we're going to need to create our Virtualenv and install some of the dependencies that we know we need:
$ pipenv --python=$(which python3.7) install flask
For the rest of our work, we'll want to make sure that we're using our active Virtualenv. Let's activate it now:
$ pipenv shell (tickets) $
-
Challenge
Configure application and connect to the database.
We're ready to create the initial layout of the application and set up our database configuration. To begin, we'll create an
__init__.py
script to generate our application. We'll take an approach very similar to the official Flask tutorial, setting up an application factory, starting with a file named__init__.py
:~/tickets/init.py
import os from flask import Flask def create_app(test_config=None): app = Flask(__name__) app.config.from_mapping( SECRET_KEY=os.environ.get('SECRET_KEY', default='dev'), ) if test_config is None: app.config.from_pyfile('config.py', silent=True) else: app.config.from_mapping(test_config) return app
Now we can test the bare-bones application. We're going to change the port to
3000
because Linux Academy servers have that open by default:Note: You'll want to do this in a separate terminal instance so that we can keep it running. It will auto reload the code as we make changes.
(tickets) $ export FLASK_ENV=development (tickets) $ export FLASK_APP='.' (tickets) $ flask run --host=0.0.0.0 --port=3000 * Serving Flask app "." (lazy loading) * Environment: development * Debug mode: on * Running on http://0.0.0.0:3000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 112-739-965
Our next step will be to install a library for interacting with PostgreSQL and configuring our database connection. For this, we'll use
psycopg2
and theFlask-SQLAlchemy
plugin. Let's install these now:(tickets) $ pipenv install psycopg2 Flask-SQLAlchemy Installing psycopg2… Adding psycopg2 to Pipfile's [packages]… ✔ Installation Succeeded Installing Flask-SQLAlchemy… Adding Flask-SQLAlchemy to Pipfile's [packages]… ✔ Installation Succeeded Pipfile.lock (caf66b) out of date, updating to (662286)… Locking [dev-packages] dependencies… Locking [packages] dependencies… ✔ Success! Updated Pipfile.lock (caf66b)! Installing dependencies from Pipfile.lock (caf66b)… ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 9/9 — 00:00:02
We'll set up our database configuration with a new file called
config.py
:~/tickets/config.py
import os db_host = os.environ.get('DB_HOST', default='< DB_PRIVATE_IP >') db_name = os.environ.get('DB_NAME', default='dashboard') db_password = os.environ.get('DB_PASSWORD', default='secure_password') db_port = os.environ.get('DB_PORT', default='5432') db_user = os.environ.get('DB_USERNAME', default='dashboard') SQLALCHEMY_DATABASE_URI = f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
We're using environment variables and
os.environ.get
so that we can easily adjust these with environment variables. Next, let's create a file (models.py
) to contain our database logic and classes wrapping database tables. We'll need to pull in theflask_sqlalchemy
package to initialize a database object that won't actually connect to the application's database just yet:~/tickets/models.py
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class Ticket(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) status = db.Column(db.Integer, nullable=False) url = db.Column(db.String(100), nullable=True) statuses_dict = { 0: 'Reported', 1: 'In Progress', 2: 'In Review', 3: 'Resolved', } def status_string(self): return self.statuses_dict[self.status]
Now we have a
Ticket
class that we can use to work with the information from our database and adb
object that we can initialize with our application configuration. That initialization needs to happen within ourcreate_app
function in the__init__.py
:~/tickets/init.py
import os from flask import Flask def create_app(test_config=None): app = Flask(__name__) app.config.from_mapping( SECRET_KEY=(os.getenv('SECRET_KEY') or 'dev'), ) if test_config is None: # Load configuration from config.py app.config.from_pyfile('config.py', silent=True) else: app.config.from_mapping(test_config) from .models import db db.init_app(app) return app
Now we have a working application configuration that can communicate with our database.
-
Challenge
Render `/tickets` and `/tickets/:id` views.
For the HTML based views we're going to use some HTML templates that a co-worker created for us that have some comments on where to put the dynamic information. These files can be found within
~/templates
and we also need to move over the styles from~/static
. Let's copy those directories into our application now:(tickets) $ mv ~/templates . (tickets) $ mv ~/static .
Before worrying about the templates themselves, let's create the request handler functions. These handlers will be defined as functions within our
create_app
function in the__init__.py
file:~/tickets/init.py
import os from flask import Flask, redirect, render_template, url_for def create_app(test_config=None): app = Flask(__name__) app.config.from_mapping( SECRET_KEY=(os.getenv('SECRET_KEY') or 'dev'), ) if test_config is None: # Load configuration from config.py app.config.from_pyfile('config.py', silent=True) else: app.config.from_mapping(test_config) from .models import db db.init_app(app) @app.route('/') def index(): return redirect(url_for('tickets')) @app.route('/tickets') def tickets(): return render_template('tickets_index.html') @app.route('/tickets/<int:ticket_id>') def tickets_show(ticket_id): return render_template('tickets_show.html') return app
For now, we just want to make sure that our templates are being rendered out properly. If we reload the browser we should see them and if we visit the root URL we will be redirected to the
/tickets
URL. Our next step is to query our database for the tickets so that we can pass them to our templates. In thetickets
function we'll fetch all of the items from the database to list out, and from thetickets_show
function we'll fetch just one based on the ID that is passed into the URL. Let's add this logic now:~/tickets/init.py
import os from flask import Flask, abort, redirect, render_template, url_for def create_app(test_config=None): app = Flask(__name__) app.config.from_mapping( SECRET_KEY=(os.getenv('SECRET_KEY') or 'dev'), ) if test_config is None: # Load configuration from config.py app.config.from_pyfile('config.py', silent=True) else: app.config.from_mapping(test_config) from .models import db, Ticket db.init_app(app) from sqlalchemy.orm import exc @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @app.route('/') def index(): return redirect(url_for('tickets')) @app.route('/tickets') def tickets(): tickets = Ticket.query.all() return render_template('tickets_index.html', tickets=tickets) @app.route('/tickets/<int:ticket_id>') def tickets_show(ticket_id): try: ticket = Ticket.query.filter_by(id=ticket_id).one() return render_template('tickets_show.html', ticket=ticket) except exc.NoResultFound: abort(404) return app
Notice that we had to add a little error handling to the
tickets_show
view function because it's possible for someone to pass in an ID that doesn't exist. In this case, we're going to use theone
function that will throw asqlalchemy.orm.exc.NoResultFound
error if there is not a matching row. We catch that error in ourexcept
and then Flask will automatically run the function that we've decorated with@app.errorhandler(404)
.The last thing that we need to do is utilize the
tickets
andticket
variables withintemplates/tickets_index.html
andtemplates/tickets_show.html
respectively. Here are the lines that we adjusted in each:~/tickets/templates/tickets_index.html
<!-- Contents above this comment were omitted --> <!-- EXAMPLE ROW, substitute the real information from the tickets in the database --> {% for ticket in tickets %} <tr> <th>{{ticket.id}}</th> <td>{{ticket.name}}</td> <td>{{ticket.status_string()}}</td> <td> <a href="{{ticket.url}}">{{ticket.url}}</a> </td> <td> <a href="{{url_for('tickets_show', ticket_id=ticket.id)}}">Details</a> </td> </tr> {% endfor %} <!-- Contens below this comment were omitted -->
~/tickets/templates/tickets_show.html
{% extends "layout.html" %} {% block title %}Ticket - {{ticket.name}}{% endblock %} {% block body %} <div class="content"> <p><strong>Name:</strong> {{ticket.name}}</p> <p><strong>Status:</strong> {{ticket.status_string()}}</p> <p><strong>URL:</strong> <a href="{{ticket.url}}" target="_blank">{{ticket.url}}</a></p> </div> {% endblock %}
Note: It's important that we call the
status_string
method using parenthesis, otherwise we'll render a message about there being a method bound to theticket
variable on the page. -
Challenge
Add `/api/tickets` and `/api/tickets/:id` JSON API endpoints.
We had to do quite a bit to render our HTML views, but that lays the groundwork for us to pretty easily create our API view functions. Let's utilize the
jsonify
helper that Flask provides to create our final two view functions with__init__.py
.~/tickets/init.py
import os from flask import Flask, abort, redirect, render_template, url_for, jsonify def create_app(test_config=None): app = Flask(__name__) app.config.from_mapping( SECRET_KEY=(os.getenv('SECRET_KEY') or 'dev'), ) if test_config is None: # Load configuration from config.py app.config.from_pyfile('config.py', silent=True) else: app.config.from_mapping(test_config) from .models import db, Ticket db.init_app(app) from sqlalchemy.orm import exc @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @app.route('/') def index(): return redirect(url_for('tickets')) @app.route('/tickets') def tickets(): tickets = Ticket.query.all() return render_template('tickets_index.html', tickets=tickets) @app.route('/tickets/<int:ticket_id>') def tickets_show(ticket_id): try: ticket = Ticket.query.filter_by(id=ticket_id).one() return render_template('tickets_show.html', ticket=ticket) except exc.NoResultFound: abort(404) @app.route('/api/tickets') def api_tickets(): tickets = Ticket.query.all() return jsonify(tickets) @app.route('/api/tickets/<int:ticket_id>') def api_tickets_show(ticket_id): try: ticket = Ticket.query.filter_by(id=ticket_id).one() return jsonify(ticket) except exc.NoResultFound: return jsonify({'error': 'Ticket not found'}), 404 return app
This looks great, but unfortunately, we receive an error when we try to access the
/api/tickets
URL in the browser becauseObject of type Ticket is not JSON serializable
. This is something that we'll need to fix within our model by adding ato_json
method that we can call before we pass our object tojsonify
:~/tickets/models.py
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class Ticket(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) status = db.Column(db.Integer, nullable=False) url = db.Column(db.String(100), nullable=True) statuses_dict = { 0: 'Reported', 1: 'In Progress', 2: 'In Review', 3: 'Resolved', } def status_string(self): return self.statuses_dict[self.status] def to_json(self): """ Return the JSON serializable format """ return { 'id': self.id, 'name': self.name, 'status': self.status_string(), 'url': self.url }
Now we can utilize this function in our view functions:
~/tickets/init.py
# Extra code omitted @app.route('/api/tickets') def api_tickets(): tickets = Ticket.query.all() return jsonify([ticket.to_json() for ticket in tickets]) @app.route('/api/tickets/<int:ticket_id>') def api_tickets_show(ticket_id): try: ticket = Ticket.query.filter_by(id=ticket_id).one() return jsonify(ticket.to_json()) except exc.NoResultFound: return jsonify({'error': 'Ticket not found'}), 404 # Extra code omitted
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.