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 file specified by environment variable. This is
# useful for using different on development and production machines.
# Reference: http://flask.pocoo.org/docs/config/
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. Pro-tip: 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 THIS 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 setup.py from the starter files, which will look something like this example. Your versions might be different.

"""
Insta485 python package configuration.
"""

from setuptools import setup

setup(
    name='insta485',
    version='0.1.0',
    packages=['insta485'],
    include_package_data=True,
    install_requires=[
        'arrow==0.15.1',
        'bs4==0.0.1',
        'Flask==1.1.1',
        'html5validator==0.3.1',
        'pycodestyle==2.5.0',
        'pydocstyle==4.0.1',
        'pylint==2.3.1',
        'pytest==5.1.2',
        'requests==2.22.0',
        'sh==1.12.14',
    ],
)

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.

$ source env/bin/activate
$ which pip
/Users/awdeorio/src/eecs485/p2-insta485-serverside/env/bin/pip
$ 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.

$ export FLASK_DEBUG=True
$ export FLASK_APP=insta485
$ export INSTA485_SETTINGS=config.py
$ flask 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 setup.py 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 setup.py 
setup.py

Dev server run script

It’s a lot of work to set environment variables and run the flask utility with the correct options. Automate this with a script, bin/insta485run. It should:

  1. Call insta485db create if not database file exists
  2. Set FLASK_DEBUG, FLASK_APP and INSTA485_SETTINGS environment variables
  3. Run the development server on port 8000

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."""

    # Connect to database
    connection = insta485.model.get_db()

    # Query database
    cur = connection.execute(
        "SELECT username, fullname "
        "FROM users"
    )
    users = cur.fetchall()

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

Template

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

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

Run

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

$ ./bin/insta485run

Alternative: the hard way.

$ FLASK_DEBUG=True FLASK_APP=insta485 INSTA485_SETTINGS=config.py flask 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.

At this point, you should have a working Flask app from the previous section, or from making a copy of your insta485/ files from project 2. You may have different static files, templates, or views.

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

Create a Python module for the API.

$ mkdir insta485/api/

Add insta485/api/__init__.py:

"""Insta485 REST API."""

from insta485.api.likes import get_likes

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

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


@insta485.app.route('/api/v1/p/<int:postid_url_slug>/likes/', methods=["GET"])
def get_likes(postid_url_slug):
    """Return likes on postid.

    Example:
    {
      "logname_likes_this": 1,
      "likes_count": 3,
      "postid": 1,
      "url": "/api/v1/p/1/likes/"
    }
    """
    context = {
        "logname_likes_this": 1,
        "likes_count": 3,
        "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.

...
import insta485.api  # noqa: E402  pylint: disable=wrong-import-position
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 --matchdirs -I '__pycache__'
insta485
├── __init__.py
├── api
│   ├── __init__.py
│   └── index.py
├── config.py
├── model.py
├── static
│   ├── css
│   │   └── style.css
│   └── images
│       └── logo.png
├── templates
│   └── index.html
└── views
    ├── __init__.py
    └── index.py

Test your REST API.

$ ./bin/insta485run

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

{
  "logname_likes_this": 1,
  "likes_count": 3,
  "postid": 1,
  "url": "/api/v1/p/1/likes/"
}