chat485
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, a web page (server-rendered HTML or a static shell that hosts a React frontend), and a REST API.
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-chat485-serverside
You have created a Python virtual environment and activated it (instructions). Your paths may be different.
$ source env/bin/activate
$ echo $VIRTUAL_ENV
/Users/awdeorio/src/eecs485/p2-chat485-serverside/env
$ which python
/Users/awdeorio/src/eecs485/p2-chat485-serverside/env/bin/python
$ which pip
/Users/awdeorio/src/eecs485/p2-chat485-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/chat485.sqlite3 "SELECT email, fullname FROM users;"
email fullname
------------------ -------------
awdeorio@umich.edu Andrew DeOrio
Python package
Our Python app will live in a Python package, chat485.
$ mkdir chat485
A Python package is a directory containing an __init__.py file. Here’s chat485/__init__.py:
"""Chat485 package initializer."""
import flask
# app is a single object used by all the code modules in this package
app = flask.Flask(__name__)
# Read settings from config module (chat485/config.py)
app.config.from_object("chat485.config")
# 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 https://flask.palletsprojects.com/en/latest/patterns/packages/)
import chat485.views # noqa: E402
import chat485.model # noqa: E402
Config
Next, put app configuration variables in chat485/config.py. More about configuration in the Flask config docs.
"""Chat485 development configuration."""
import pathlib
# Root of this application, useful if it doesn't occupy an entire domain
APPLICATION_ROOT = "/"
# Database file is var/chat485.sqlite3
CHAT485_ROOT = pathlib.Path(__file__).resolve().parent.parent
DATABASE_FILENAME = CHAT485_ROOT / "var" / "chat485.sqlite3"
Model
The database connection code will live in chat485/model.py. For now, we’ll just create an empty placeholder file. Later, in the Database connection section, we’ll add code.
$ touch chat485/model.py
Static assets
Flask serves any file under chat485/static/ at http://localhost:8000/static/.... Create one subdirectory for stylesheets and another for images, like a logo.
$ mkdir -p chat485/static/css
$ mkdir -p chat485/static/images
$ tree chat485/static/
chat485/static/
├── css
└── images
Put your custom stylesheet rules in chat485/static/css/style.css. Optionally, drop a logo image into chat485/static/images/logo.png. Styling is a reach goal, so the logo and CSS are optional. You can add more CSS files and images here later.
Server-side dynamic pages
For Project 2. Server-side dynamic pages render HTML on every request using Jinja templates.
Create the templates directory and a minimal index.html. The {{ name }} below is a Jinja placeholder; Flask substitutes it with the value of the name variable passed in from the view.
$ mkdir -p chat485/templates
<!DOCTYPE html>
<html lang="en">
<body>
Hello {{ name }}!
</body>
</html>
Next we’ll create a views module. This Python module contains functions that are executed when a user visits a URL. Each view renders a Jinja template, passing data through a context dict. Later, you’ll access the database and add data to context.
"""
Chat485 index (main) view.
URLs include:
/
"""
import flask
import chat485
@chat485.app.route("/")
def show_index():
"""Display / route."""
context = {"name": "world"}
return flask.render_template("index.html", **context)
To make views a proper Python package, it needs a chat485/views/__init__.py file.
"""Views, one for each Chat485 page."""
from chat485.views.index import show_index
Pro-tip: When a template links to one of your routes or to a static file, build the URL with Jinja’s url_for() instead of hardcoding the path. Pass the view function’s name, not the URL: url_for('show_index') returns /, because show_index is the function decorated with @chat485.app.route("/"). For a static file, use the special static endpoint with a filename.
<a href="{{ url_for('show_index') }}">Home</a>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
Then, if you change a route’s URL later, every url_for() reference updates with it, and Flask always resolves the correct static path.
Client-side dynamic pages
For Project 3. Client-side dynamic pages use Flask to serve one static HTML shell; everything else the user sees is rendered by React in the browser.
Create a minimal chat485/static/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chat485</title>
<meta charset="UTF-8">
</head>
<body>
Hello world!
</body>
</html>
You will replace the body with a React entry point in the React/JS Tutorial.
Next we’ll create a views module. This Python module contains functions that are executed when a user visits a URL. The only server-rendered view serves the static index.html shell.
"""
Chat485 index (main) view.
URLs include:
/
"""
import chat485
@chat485.app.route("/")
def show_index():
"""Serve the React single-page app shell."""
return chat485.app.send_static_file("index.html")
To make views a proper Python package, it needs a chat485/views/__init__.py file.
"""Views, one for each Chat485 page."""
from chat485.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 = "chat485"
version = "1.0.0"
dependencies = [
"Flask",
"pytest",
"requests",
"ruff",
]
requires-python = ">=3.12"
[tool.setuptools]
packages = ["chat485"]
You should already have a virtual environment installed from the Python virtual environment tutorial. 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-chat485-serverside/env/bin/pip
$ pip install -r requirements.txt
$ pip install -e .
...
Successfully installed ... chat485 ...
Run
We’re finally ready to run our app! We’ll use environment variables to put the development server in debug mode and specify the name of our app’s Python module. These commands assume you’re using the bash shell.
$ flask --app chat485 --debug run --host 0.0.0.0 --port 8000
* Serving Flask app "chat485"
* 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 created a Python module called chat485. The module is a directory containing Python source code.
For Project 3, your files should look like this:
$ tree chat485 -I '__pycache__'
chat485
├── __init__.py
├── config.py
├── model.py
├── static
│ ├── css
│ ├── images
│ └── index.html
└── views
├── __init__.py
└── index.py
$ ls pyproject.toml
pyproject.toml
For Project 2, your files should look like this:
$ tree chat485 -I '__pycache__'
chat485
├── __init__.py
├── config.py
├── model.py
├── static
│ ├── css
│ └── images
├── templates
│ └── index.html
└── views
├── __init__.py
└── index.py
$ ls pyproject.toml
pyproject.toml
Run Script
Write a script called bin/chat485run that runs the development server.
If var/chat485.sqlite3 does not exist, print an error and exit non-zero.
$ ./bin/chat485run
Error: can't find database var/chat485.sqlite3
Try: ./bin/chat485db create
Run the development server on port 8000.
$ ./bin/chat485run
+ flask --app chat485 --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 REST API code.
First, make sure you’ve completed the SQLite Tutorial. You should be able to reset your database and query it.
$ ./bin/chat485db reset
$ sqlite3 var/chat485.sqlite3 "SELECT email, fullname FROM users;"
awdeorio@umich.edu|Andrew DeOrio
Model
Add the following code to chat485/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.
"""Chat485 model (database) API."""
import sqlite3
import flask
import chat485
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 serialized to
JSON for the REST API.
"""
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/latest/appcontext/#storing-data
"""
if "sqlite_db" not in flask.g:
db_filename = chat485.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
@chat485.app.teardown_appcontext
def close_db(_):
"""Close the database at the end of a request.
Flask docs:
https://flask.palletsprojects.com/en/latest/appcontext/#storing-data
"""
sqlite_db = flask.g.pop("sqlite_db", None)
if sqlite_db is not None:
sqlite_db.commit()
sqlite_db.close()
REST API
Chat485 talks to its React frontend over a JSON REST API. Every endpoint lives under /api/v1/.
Make sure you have a chat485 directory with an __init__.py and config.py. You can reuse the ones from the Python package section above.
Use pip to install the the chat485/ package.
$ pip install -e .
Create a Python module for the API.
$ mkdir chat485/api/
Add chat485/api/__init__.py:
"""Chat485 REST API."""
from chat485.api.models import list_models
Add chat485/api/models.py. This sample is hard coded; you will replace it with database-backed code later.
"""REST API for models."""
import flask
import chat485
@chat485.app.route("/api/v1/models")
def list_models():
"""List available chat models (OpenAI-compatible format)."""
data = [
{"id": "echo-1"},
{"id": "llama3.2:3b"},
]
return flask.jsonify(data=data)
Tell the chat485 module that it now has one more sub-module. Add one line to chat485/__init__.py.
...
import chat485.api # noqa: E402
import chat485.views # noqa: E402
import chat485.model # noqa: E402
Now, your files should look like this:
$ tree chat485 -I '__pycache__'
chat485
├── __init__.py
├── api
│ ├── __init__.py
│ └── models.py
├── config.py
├── model.py
├── static
│ ├── css
│ ├── images
│ └── index.html
└── views
├── __init__.py
└── index.py
Test your REST API.
$ flask --app chat485 --debug run --host 0.0.0.0 --port 8000
You can also use ./bin/chat485run.
Navigate to http://localhost:8000/api/v1/models. You should see this JSON response:
{
"data": [
{"id": "echo-1"},
{"id": "llama3.2:3b"}
]
}
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.