- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Cloud
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.
Lab 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 ticketsWith 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 flaskFor 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__.pyscript 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 appNow we can test the bare-bones application. We're going to change the port to
3000because 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-965Our next step will be to install a library for interacting with PostgreSQL and configuring our database connection. For this, we'll use
psycopg2and theFlask-SQLAlchemyplugin. 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:02We'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.getso 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_sqlalchemypackage 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
Ticketclass that we can use to work with the information from our database and adbobject that we can initialize with our application configuration. That initialization needs to happen within ourcreate_appfunction 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 appNow 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
~/templatesand 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_appfunction in the__init__.pyfile:~/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 appFor 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
/ticketsURL. Our next step is to query our database for the tickets so that we can pass them to our templates. In theticketsfunction we'll fetch all of the items from the database to list out, and from thetickets_showfunction 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 appNotice that we had to add a little error handling to the
tickets_showview function because it's possible for someone to pass in an ID that doesn't exist. In this case, we're going to use theonefunction that will throw asqlalchemy.orm.exc.NoResultFounderror if there is not a matching row. We catch that error in ourexceptand 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
ticketsandticketvariables withintemplates/tickets_index.htmlandtemplates/tickets_show.htmlrespectively. 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_stringmethod using parenthesis, otherwise we'll render a message about there being a method bound to theticketvariable 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
jsonifyhelper 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 appThis looks great, but unfortunately, we receive an error when we try to access the
/api/ticketsURL 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_jsonmethod 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
About the author
Real skill practice before real-world application
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.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.