p2-insta485-serverside

Flask Tutorial

This tutorial will help you set up a “hello world” Flask application in a Python virtual environment using a modular approach. The app will have a database, and shell scripts for maintenance.

Prerequisites

This tutorial assumes that you have already created a project folder (instructions). Your folder location might be different.

$ pwd
/Users/awdeorio/src/eecs485/p2-insta485-serverside

You have created a Python virtual environment and activated it (instructions). You paths may be different.

$ source env/bin/activate
$ echo $VIRTUAL_ENV
/Users/awdeorio/src/eecs485/p2-insta485-serverside/env
$ which python
/Users/awdeorio/src/eecs485/p2-insta485-serverside/env/bin/python
$ which pip
/Users/awdeorio/src/eecs485/p2-insta485-serverside/env/bin/pip

You have completed the SQLite Tutorial. Your version might be different. Your exact database output may be different.

$ sqlite3 --version
3.29.0 2019-07-10 17:32:03 fc82b73eaac8b36950e527f12c4b5dc1e147e6f4ad2217ae43ad82882a88bfa6
$ sqlite3 var/insta485.sqlite3 "SELECT * FROM users;"
username    fullname     
----------  -------------
awdeorio      Andrew DeOrio

Python package

Our Python app will live in a Python package, insta485.

$ pwd
/Users/awdeorio/src/eecs485/p2-insta485-serverside
$ mkdir insta485

A Python package is a directory containing an __init__.py file. Here’s insta485/__init__.py:

"""Insta485 package initializer."""
import flask

# app is a single object used by all the code modules in this package
app = flask.Flask(__name__)  # pylint: disable=invalid-name

# Read settings from config module (insta485/config.py)
app.config.from_object('insta485.config')

# Overlay settings read from a Python file whose path is set in the environment
# variable INSTA485_SETTINGS. Setting this environment variable is optional.
# Docs: http://flask.pocoo.org/docs/latest/config/
#
# EXAMPLE:
# $ export INSTA485_SETTINGS=secret_key_config.py
app.config.from_envvar('INSTA485_SETTINGS', silent=True)

# Tell our app about views and model.  This is dangerously close to a
# circular import, which is naughty, but Flask was designed that way.
# (Reference http://flask.pocoo.org/docs/patterns/packages/)  We're
# going to tell pylint and pycodestyle to ignore this coding style violation.
import insta485.views  # noqa: E402  pylint: disable=wrong-import-position
import insta485.model  # noqa: E402  pylint: disable=wrong-import-position

Config

Next, put app configuration variables in insta485/config.py. More about configuration in the flask config docs. Hint: use string literal concatenation to avoid lines that are too long.

"""Insta485 development configuration."""

import pathlib

# Root of this application, useful if it doesn't occupy an entire domain
APPLICATION_ROOT = '/'

# Secret key for encrypting cookies
SECRET_KEY = b'FIXME SET WITH: $ python3 -c "import os; print(os.urandom(24))" '
SESSION_COOKIE_NAME = 'login'

# File Upload to var/uploads/
INSTA485_ROOT = pathlib.Path(__file__).resolve().parent.parent
UPLOAD_FOLDER = INSTA485_ROOT/'var'/'uploads'
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])
MAX_CONTENT_LENGTH = 16 * 1024 * 1024

# Database file is var/insta485.sqlite3
DATABASE_FILENAME = INSTA485_ROOT/'var'/'insta485.sqlite3'

Change the SECRET_KEY in config.py that is used to encrypt session cookies. You can generate one like this:

$ python3 -c 'import os; print(os.urandom(24))'

Model

The database connection code will live in insta485/model.py. For now, we’ll just create an empty placeholder file. Later, in the Database connection section, we’ll add code.

$ pwd
/Users/awdeorio/src/eecs485/p2-insta485-serverside
$ touch insta485/model.py

Templates

Next, we’ll start working on the insta485/static/ directory. Add any CSS style sheets and optional images, like a logo.

$ tree insta485/static/
insta485/static/
├── css
│   └── style.css
└── images
    └── logo.png

Next we’ll add templates to, insta485/templates/. In EECS 485 project 2, you can reuse the templates from project 1 as a starting point. For now, we’re going to create a “hello world” template, insta485/templates/index.html:

<!DOCTYPE html>
<html lang="en">
Hello world!
</html>

Views

Next we’ll create a views module. This Python module contains functions that are executed when a user visits a URL. Most functions render a template and return the resulting HTML formatted text. Later, you’ll access the database, adding data to the context variable. Here’s a start to insta485/views/index.py

"""
Insta485 index (main) view.

URLs include:
/
"""
import flask
import insta485


@insta485.app.route('/')
def show_index():
    """Display / route."""
    context = {}
    return flask.render_template("index.html", **context)

To make views a proper Python package, it needs a insta485/views/__init__.py file.

"""Views, one for each Insta485 page."""
from insta485.views.index import show_index

Install

We’re getting close to being able to run our app. We need to install our app into our virtual environment so that flask can find the Python packages. Use the pyproject.toml from the starter files, which will look something like this example. Your versions might be different.

[build-system]
requires = ["setuptools>=64.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "insta485"
version = "0.1.0"
dependencies = [
    "arrow",
    "bs4",
    "Flask",
    "html5validator",
    "pycodestyle",
    "pydocstyle",
    "pylint",
    "pytest",
    "pytest-mock",
    "requests",
]
requires-python = ">=3.10"

[tool.setuptools]
packages = ["insta485"]

You should already have a virtual environment installed from the Python virtual environment section. Make sure it’s activated, then install the app into the virtual environment. You may have already installed the packages in requirements.txt.

$ source env/bin/activate
$ which pip
/Users/awdeorio/src/eecs485/p2-insta485-serverside/env/bin/pip
$ pip install -r requirements.txt
$ pip install -e .
...
Successfully installed ... insta485 ...

Run

We’re finally ready to run our app! We’ll use environment variables to put the development server in debug mode, specify the name of our app’s python module and locate an additional config file. These commands assume you’re using the bash shell.

$ flask --app insta485 --debug run --host 0.0.0.0 --port 8000
 * Serving Flask app "insta485"
 * Forcing debug mode on
 * Running on http://127.0.0.1:8000/ (Press CTRL+C to quit)

Browse to http://localhost:8000/ and you’ll see your “hello world” app.

Summary

At this point, we’ve create a Python module called insta485. The module is a directory containing Python source code. Here are the files you should have at this point:

$ tree insta485 -I '__pycache__'
insta485
├── __init__.py
├── config.py
├── model.py
├── static
│   ├── css
│   │   └── style.css
│   └── images
│       └── logo.png
├── templates
│   └── index.html
└── views
    ├── __init__.py
    └── index.py
$ ls pyproject.toml
pyproject.toml

Run Script

Write script called bin/insta485run that runs the development server.

If var/insta485.sqlite3 does not exist, print an error and exit non-zero.

$ ./bin/insta485run
Error: can't find database var/insta485.sqlite3
Try: ./bin/insta485db create

Run the development server on port 8000.

$ ./bin/insta485run
+ flask --app insta485 --debug run --host 0.0.0.0 --port 8000
...

Remember to check for shell script pitfalls.

Database connection

In this section, we will create a module that provides database connection helper functions. Later, we will use these functions from our views code.

First, make sure you’ve completed the SQLite Tutorial. You should be able to reset your database and query it.

$ ./bin/insta485db reset
$ sqlite3 var/insta485.sqlite3 "SELECT username, fullname FROM users;"
awdeorio|Andrew DeOrio
jflinn|Jason Flinn
michjc|Michael Cafarella
jag|H.V. Jagadish

Model

Add the following code to insta485/model.py. Notice that it reads the configuration parameter DATABASE_FILENAME from config.py. Also notice that it will automatically commit and close the connection when a request completes.

"""Insta485 model (database) API."""
import sqlite3
import flask
import insta485


def dict_factory(cursor, row):
    """Convert database row objects to a dictionary keyed on column name.

    This is useful for building dictionaries which are then used to render a
    template.  Note that this would be inefficient for large queries.
    """
    return {col[0]: row[idx] for idx, col in enumerate(cursor.description)}


def get_db():
    """Open a new database connection.

    Flask docs:
    https://flask.palletsprojects.com/en/1.0.x/appcontext/#storing-data
    """
    if 'sqlite_db' not in flask.g:
        db_filename = insta485.app.config['DATABASE_FILENAME']
        flask.g.sqlite_db = sqlite3.connect(str(db_filename))
        flask.g.sqlite_db.row_factory = dict_factory

        # Foreign keys have to be enabled per-connection.  This is an sqlite3
        # backwards compatibility thing.
        flask.g.sqlite_db.execute("PRAGMA foreign_keys = ON")

    return flask.g.sqlite_db


@insta485.app.teardown_appcontext
def close_db(error):
    """Close the database at the end of a request.

    Flask docs:
    https://flask.palletsprojects.com/en/1.0.x/appcontext/#storing-data
    """
    assert error or not error  # Needed to avoid superfluous style error
    sqlite_db = flask.g.pop('sqlite_db', None)
    if sqlite_db is not None:
        sqlite_db.commit()
        sqlite_db.close()

Using the model

In this section, we will modify a view and a template to include information from the database.

View

Edit a view to use the model. Modify insta485/views/index.py to look like this:

"""
Insta485 index (main) view.

URLs include:
/
"""
import flask
import insta485


@insta485.app.route('/')
def show_index():
    """Display / route."""

    # primer-spec-highlight-start
    # Connect to database
    connection = insta485.model.get_db()

    # Query database
    logname = "awdeorio"
    cur = connection.execute(
        "SELECT username, fullname "
        "FROM users "
        "WHERE username != ?",
        (logname, )
    )
    users = cur.fetchall()
    # primer-spec-highlight-end

    # Add database info to context
    context = {"users": users}
    return flask.render_template("index.html", **context)

This is as an example of a Python SQLite prepared statement

Template

Edit the template insta485/templates/index.html to print the users excluding awdeorio.

<!DOCTYPE html>
<html lang="en">
<!-- primer-spec-highlight-start -->
<body>
  <h1>Users</h1>
  {% for user in users %}
  <p>{{user.username}} {{user.fullname}}</p>
  {% endfor %}
</body>
<!-- primer-spec-highlight-end -->
</html>

Run

Run your dev server and browse to http://localhost:8000.

$ ./bin/insta485run

Alternative: the hard way.

$ flask --app insta485 --debug run --host 0.0.0.0 --port 8000

You should see the contents from the database displayed like this:

screenshot setup flask with database

REST API

This section is intended for EECS 485 project 3. Skip this section if you’re working on project 2.

Make sure you have an insta485 directory with an __init__.py and config.py. You can copy __init__.py from here, and config.py from here. You may have different static files, templates, or views. You don’t need any static files, templates, or views for this tutorial.

$ tree insta485 -I '__pycache__'
insta485
├── __init__.py
├── config.py
├── model.py
├── static
│   ├── css
│   │   └── style.css
│   └── images
│       └── logo.png
├── templates
│   └── index.html
└── views
    ├── __init__.py
    ...
    └── index.py

Use pip to install the the insta485/ package.

$ pip install -e .

Create a Python module for the API.

$ mkdir insta485/api/

Add insta485/api/__init__.py:

"""Insta485 REST API."""

from insta485.api.posts import get_post

Add insta485/api/posts.py. This sample is hard coded.

"""REST API for posts."""
import flask
import insta485


@insta485.app.route('/api/v1/posts/<int:postid_url_slug>/')
def get_post(postid_url_slug):
    """Return post on postid.

    Example:
    {
      "created": "2017-09-28 04:33:28",
      "imgUrl": "/uploads/122a7d27ca1d7420a1072f695d9290fad4501a41.jpg",
      "owner": "awdeorio",
      "ownerImgUrl": "/uploads/e1a7c5c32973862ee15173b0259e3efdb6a391af.jpg",
      "ownerShowUrl": "/users/awdeorio/",
      "postShowUrl": "/posts/1/",
      "postid": 1,
      "url": "/api/v1/posts/1/"
    }
    """
    context = {
        "created": "2017-09-28 04:33:28",
        "imgUrl": "/uploads/122a7d27ca1d7420a1072f695d9290fad4501a41.jpg",
        "owner": "awdeorio",
        "ownerImgUrl": "/uploads/e1a7c5c32973862ee15173b0259e3efdb6a391af.jpg",
        "ownerShowUrl": "/users/awdeorio/",
        "postShowUrl": f"/posts/{postid_url_slug}/",
        "postid": postid_url_slug,
        "url": flask.request.path,
    }
    return flask.jsonify(**context)

Tell the insta485 module that it now has one more sub-module. Add one line to insta485/__init__.py. It’s the line with import insta485.api.

...
# primer-spec-highlight-start
import insta485.api  # noqa: E402  pylint: disable=wrong-import-position
# primer-spec-highlight-end
import insta485.views  # noqa: E402  pylint: disable=wrong-import-position
import insta485.model  # noqa: E402  pylint: disable=wrong-import-position

Now, your files should look like this:

$ tree insta485 -I '__pycache__'
insta485
├── __init__.py
├── api
│   ├── __init__.py
│   └── posts.py
├── config.py

Test your REST API.

$ flask --app insta485 --debug run --host 0.0.0.0 --port 8000

You can also use ./bin/insta485run from project 2.

Navigate to http://localhost:8000/api/v1/posts/1/. You should see this JSON response:

{
  "created": "2017-09-28 04:33:28",
  "imgUrl": "/uploads/122a7d27ca1d7420a1072f695d9290fad4501a41.jpg",
  "owner": "awdeorio",
  "ownerImgUrl": "/uploads/e1a7c5c32973862ee15173b0259e3efdb6a391af.jpg",
  "ownerShowUrl": "/users/awdeorio/",
  "postShowUrl": "/posts/1/",
  "url": "/api/v1/posts/1/"
}

Acknowledgments

Original document written by Andrew DeOrio awdeorio@umich.edu.

This document is licensed under a Creative Commons Attribution-NonCommercial 4.0 License. You’re free to copy and share this document, but not to sell it. You may not share source code provided with this document.