Course Table of Contents

In this chapter, we’ll fire up a Flask web app for hosting our stock price forecasting, machine learning model.

Flask vs Dash

Flask is a popular Python web framework. It’s lightweight and extensible, so it’s very well-suited for data science applications. Dash is a web framework built on top of Flask, which uses React behind-the-scenes to create reactive, interactive Single-page Applications (SPAs) featuring Plotly charts.

I would say Dash is to Python as Shiny is to R, in that both focus on productionalizing data science and machine learning models, without a user having to learn much HTML, CSS, or JavaScript. Typically data scientists are not software engineers, and the intricacies of making a single-page web application are far too complicated and not the best use of their time.

This tutorial shows you how to get the best of a few different worlds:

  1. A Flask application for your normal website
  2. A fancy Dash Single-page Application that employs the best of React and JavaScript
  3. A way to productionalize your data science application

Let’s start with a simple Flask web application, and I’ll show you how to integrate Dash. Part 2 of this course will dive deeper into making interactive charts in Dash.

Flask Setup

To create a Flask application, let’s start from the outermost entrypoint, or the starting point for the application. In your top-level folder, create a wsgi.py file as follows. This is best practice using the factory pattern for initializing Flask.

# wsgi.py

from app import create_app

app = create_app()

The wsgi.py file is looking in a folder called “app” for a function called create_app, so let’s create an “app” folder to house our Flask application. Inside the app folder, as for all Python packages, create an __init__.py file:

# app/__init__.py

import logging
import os

# Third-party imports
from flask import Flask, render_template
from dotenv import load_dotenv

# Local imports
from app import database

# Load the secret environment variables using python-dotenv
# Flask actually does this automatically if python-dotenv is installed,
# but we're making it explicit so you know how the environment variables are loaded
load_dotenv()


def create_app():
    """Factory function that creates the Flask app"""

    app = Flask(__name__)
    app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
    logging.basicConfig(level=logging.DEBUG)

    @app.route("/")
    def home():
        """Our only non-Dash route, to demonstrate that Flask can be used normally"""
        return render_template("index.html")

    # Initialize extensions
    database.init_app(app) # PostgreSQL db with psycopg2

    return app

The above file contains the create_app() factory function that the previous wsgi.py file needs. Ignore the local imports for now – we’ll get back to those.

Inside the create_app() factory function, we start off with the basics, instantiating the Flask() instance by passing it the __name__ of the file and setting the SECRET_KEY… secret key?

def create_app():
    """Factory function that creates the Flask app"""

    app = Flask(__name__)
    app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")

Open the .env file and add a SECRET_KEY environment variable to the bottom of the file:

# .env

# For the Postgres/TimescaleDB database.
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_HOST=timescale
POSTGRES_PORT=5432
POSTGRES_DB=postgres
PGDATA=/var/lib/postgresql/data

# For the PGAdmin web app
PGADMIN_DEFAULT_EMAIL=your@email.com
PGADMIN_DEFAULT_PASSWORD=password

# For Flask
SECRET_KEY=long-random-string-of-characters-numbers-etc-must-be-unique # NEW

Back to our __init__.py file, we set up the basic logging config and then added our first Flask “route” (the main homepage), just to demonstrate that we’ve got a normal, working Flask application.

def create_app():
    """Factory function that creates the Flask app"""

    app = Flask(__name__)
    app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
    logging.basicConfig(level=logging.DEBUG)

    @app.route("/")
    def home():
        """Our only non-Dash route, to demonstrate that Flask can be used normally"""
        return render_template("index.html")

Create a new folder in “app” called “templates” for our HTML templates, and add an index.html file with the following contents for the homepage route:

<html>
    <body>
        <h1 style="text-align: center;">
            Click <a href="/dash/">here</a> to see the Dash single-page application (SPA)
        </h1>
    </body>
</html>

Next up in __init__.py, we initialized our database with database.init_app(app):

# Local imports
from app import database # we're dealing with this import now

# Load the secret environment variables using python-dotenv
# Flask actually does this automatically if python-dotenv is installed,
# but we're making it explicit so you know how the environment variables are loaded
load_dotenv()

def create_app():
    """Factory function that creates the Flask app"""

    app = Flask(__name__)
    app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
    logging.basicConfig(level=logging.DEBUG)

    @app.route("/")
    def home():
        """Our only non-Dash route, to demonstrate that Flask can be used normally"""
        return render_template("index.html")

    # Initialize extensions
    database.init_app(app) # PostgreSQL db with psycopg2

“database” is a local module we imported at the top, so we’ll deal with that next.

Database

Let’s create the database.py module next, beside the __init__.py file, following Flask’s recommended best-practices:

# database.py

import os

import psycopg2
from flask import g


def get_conn():
    """
    Connect to the application"s configured database. The connection
    is unique for each request and will be reused if this is called
    again.
    """
    if "conn" not in g:
        g.conn = psycopg2.connect(
            host=os.getenv("POSTGRES_HOST"),
            port=os.getenv("POSTGRES_PORT"),
            dbname=os.getenv("POSTGRES_DB"),
            user=os.getenv("POSTGRES_USER"),
            password=os.getenv("POSTGRES_PASSWORD"),
            connect_timeout=5
        )

    return g.conn


def close_db(e=None):
    """
    If this request connected to the database, close the
    connection.
    """
    conn = g.pop("conn", None)

    if conn is not None:
        conn.close()

    return None


def init_app(app):
    """
    Register database functions with the Flask app. This is called by
    the application factory.
    """
    app.teardown_appcontext(close_db)

The point of this database.py module is to make some functions for getting and closing a TimescaleDB database connection and ensuring that the connection is closed by Flask at the end of the HTTP request. The init_app(app) function is what gets called from our create_app() factory function. Notice the teardown_appcontext(close_db) that ensures the connection is closed on “teardown”. Pretty slick. In the future, when we need data from the database, we’ll just call get_conn() to get a database connection for running our SQL.

In case you’re wondering, g is basically a global object where you store your database connection. It’s complicated so I won’t get into it – just know this is best-practice and enjoy your life. ;) Don’t believe me? Okay fine. Here’s a a few links for further reading:

  1. The Application Context - Storing Data
  2. Understanding the Application and Request Contexts in Flask

If you’re looking for great course on Flask, including a deep-dive into Flask’s mechanics, I highly recommend this course by Patrick Kennedy.

Before we run our Flask app for the first time, let’s create a .flaskenv file beside the .env file, which Flask automatically searches for find when the app starts:

# .flaskenv

FLASK_APP=wsgi.py
FLASK_RUN_HOST=0.0.0.0

To run our Flask app for the first time, recall that our docker-compose.yml file in our “.devcontainer” folder contains our Python container, which looks something like the following. Note that our dev_container is going to forward all outside requests from port 5006 to port 5000 inside the container.

  # Our Python development container, for running our Flask/Dash app, and our Jupyter Notebooks
  dev_container:
    build:
      # context: where should docker-compose look for the Dockerfile?
      # i.e. either a path to a directory containing a Dockerfile, or a url to a git repository
      context: ..
      dockerfile: Dockerfile.dev
    env_file: 
      - ../.env
    environment:
      FLASK_CONFIG: development
      FLASK_ENV: development
    # Forwards port 0.0.0.0:5006 from the Docker host (e.g. your computer) 
    # to the dev environment container's port 5000
    ports:
      - 0.0.0.0:5006:5000

Recall also that our launch.json file has the following configuration, for launching Flask apps in debugging mode just by pressing “F5”:

        // Debug with Flask
        {
            "name": "flask run --no-debugger --no-reload",
            "type": "python",
            "request": "launch",
            "module": "flask",
            "env": {
                "FLASK_APP": "wsgi:app",
                "FLASK_ENV": "development",
                "FLASK_DEBUG": "0"
            },
            "args": [
                "run",
                "--no-debugger",
                "--no-reload",
            ],
            "jinja": true, 
            "justMyCode": false
        }

So to launch our Flask app, just go to the “Run and Debug” menu on the left side of VS Code, select the “flask run –no-debugger –no-reload” configuration, and either press the “play” button, or press “F5”. Once done, you should see our very simple landing page in your browser at http://localhost:5006 or http://127.0.0.1:5006.

Conclusion

Now that we’ve set up a basic Flask web app, let’s add a few other Flask essentials:

  1. Flask-SQLAlchemy for working with our database
  2. Registration / login pages for controlling access to our site

See you in the next chapter.

Course Table of Contents