Questo è il secondo di tre articoli su TimescaleDB, Flask e Dash. Il primo articolo si è concentrato su come installare e configurare il database TimescaleDB tramite l’esecuzione in Docker, inoltre abbiamo configurato PgAdmin per la gestione del database. Questo articolo si concentra sul linguaggio Python, sulla creazione di un sito Web con Flask e quindi sull’integrazione del framework Web Dash all’interno di Flask.
Flask è un framework web Python molto popolare, leggero ed estensibile, quindi è molto adatto per applicazioni di data science. Dash è un framework Web basato su Flask, che utilizza React JavaScript dietro le quinte per creare applicazioni reattive e interattive a pagina singola (SPA), utilizzando i grafici generati con la libreria Plotly . Direi che Dash sta per Python come Shiny sta per R , in quanto entrambi si concentrano sulla produzione di data science e modelli di machine learning, senza che un utente debba imparare molto HTML, CSS e JavaScript. In genere i data scientist non sono ingegneri del software e le complessità della creazione di un’applicazione web a pagina singola sono troppo complicate e non valgono il loro tempo.
Questo tutorial mostra come ottenere il meglio da alcuni mondi diversi:
- Un’applicazione Flask per un normale sito web
- Un’elegante applicazione Dash a pagina singola che utilizza il meglio di React JavaScript
- Un modo per produrre un’applicazione di data science
Cominciamo con una semplice applicazione web Flask e quindi vediamo come integrare Dash. La parte 3 di questa serie approfondirà la creazione di grafici interattivi con Dash.
Tutto il codice per questo tutorial può essere trovato qui su GitHub.
Parte 2 - Integrazione dei Framework Web Flask e Dash
Prima di iniziare un nuovo progetto con Python, dobbiamo sempre creare un ambiente virtuale Python3 dedicato. Usiamo solo python3 -m venv venv
per creare un ambiente virtuale chiamato “venv” nella cartella principale del progetto. In questi giorni preferisco usare Poetry rispetto a Pip , ma Poetry non è al centro di questo articolo. Ora attiviamo l’ambiente virtuale con source venv/bin/activate
su Linux / Mac o venv\Scripts\activate.bat
su Windows. Dopo aver attivato l’ambiente virtuale, installiamo Flask, Dash, Dash Bootstrap Components e la libreria PostgreSQL psycopg2, con pip install flask dash dash-bootstrap-components psycopg2-binary
.
Per creare un’applicazione Flask, iniziamo dal punto di ingresso più esterno o dal punto di partenza dell’applicazione. Nella cartella di primo livello, creiamo un wsgi.py
file come segue. Questa è la pratica migliore, usando factory pattern
per inizializzare Flask.
# wsgi.py
from app import create_app
app = create_app()
Nel file wsgi.py
si importando la funzione chiamata create_app
presente all’interno di un pacchetto/libreria chiamato app
, quindi creiamo una cartella app
per ospitare la nostra applicazione Flask. All’interno della cartella dell’app, come per tutti i pacchetti Python, creiamo un file __init__.py
:
# /app/__init__.py
import os
import logging
# Third-party imports
from flask import Flask, render_template
# Local imports
from app import database
from app.dash_setup import register_dashapps
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
# For the Dash app
register_dashapps(app)
return app
Il file contiene la funzione create_app()
necessaria al file precedente wsgi.py
. Per ora possiamo ignorare le altre importazioni locali: ci ritorneremo tra poco.
All’interno della fuzione create_app()
, iniziamo con le basi, istanziando l’istanza Flask() passandole il __name__
del file e impostando la SECRET_KEY
… Chiave segreta?
Apriamo il file .env
e aggiungiamo una variabile di ambiente SECRET_KEY
in fondo al file, insieme alle altre variabili di ambiente:
# .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
[email protected]
PGADMIN_DEFAULT_PASSWORD=password
# For Flask
SECRET_KEY=long-random-string-of-characters-numbers-etc-must-be-unique # NEW
__init__.py
, impostiamo la registrazione di base e quindi aggiungiamo la prima “route” di Flask (la homepage principale), solo per dimostrare che abbiamo una normale applicazione Flask funzionante.
Creiamo una cartella /app/templates
per i modelli HTML e aggiungiamo un file index.html
con i seguenti contenuti per la nostra home page:
<html>
<body>
<h1 style="text-align: center;">
Click <a href="/dash/">here</a> to see the Dash single-page application (SPA)
</h1>
</body>
</html>
Successivamente, inizializziamo il nostro database con database.init_app(app)
. “Database” è un modulo locale che abbiamo importato all’inizio, quindi vediamo come implementarlo.
Infine, in fondo alla funzione create_app()
, richiamiamo la funzione register_dashapps(app)
dal modulo dash_setup.py
. Qui è dove inizializzeremo l’applicazione web Dash che utilizza il motore React JavaScript sotto il cofano.
Connessione al Database
Creiamo il file database.py
, accanto al file __init__.py
. Seguiamo le best practice consigliate qui dal team di Flask. Lo scopo di questo modulo è creare alcune funzioni per aprire e chiudere una connessione al database TimescaleDB e garantire che la connessione venga chiusa da Flask alla fine della richiesta HTTP. La funzione “init_app (app)” è quella che viene richiamata nel file __init__.py
attraverso la funzione create_app()
. Da notare che teardown_appcontext(close_db)
assicura che la connessione venga chiusa durante lo “smontaggio”. In futuro, quando avremo bisogno di dati dal database, richiameremo semplicemente get_conn()
per ottenere una connessione al database ed eseguire le query SQL.
Nel caso ve lo stiate chiedendo, g
è fondamentalmente un oggetto globale in cui memorizzate la connessione al database. È complicato, quindi non me ne parlerò, sappi solo che questa è la pratica migliore e goditi la vita. 😉 Va bene, ecco un link per ulteriori letture…
Se stai cercando un ottimo corso su Flask, che includa un’immersione profonda nella meccanica di Flask, consiglio vivamente questo corso di Patrick Kennedy.
# /app/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)
Integrazione con Dash
Passiamo ora a descrivere il modulo il dash_setup
, dove è definita la funzione register_dashapps
. Creiamo un file chiamato dash_setup.py
all’interno della cartella “/app”, accanto a __init__.py
:
# /app/dash_setup.py
import dash
from flask.helpers import get_root_path
def register_dashapps(app):
"""
Register Dash apps with the Flask app
"""
# external CSS stylesheets
external_stylesheets = [
'https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'
]
# external JavaScript files
external_scripts = [
"https://code.jquery.com/jquery-3.5.1.slim.min.js",
"https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js",
"https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js",
]
# To ensure proper rendering and touch zooming for all devices, add the responsive viewport meta tag
meta_viewport = [{
"name": "viewport",
"content": "width=device-width, initial-scale=1, shrink-to-fit=no"
}]
dashapp = dash.Dash(
__name__,
# This is where the Flask app gets appointed as the server for the Dash app
server = app,
url_base_pathname = '/dash/',
# Separate assets folder in "static_dash" (optional)
assets_folder = get_root_path(__name__) + '/static_dash/',
meta_tags = meta_viewport,
external_scripts = external_scripts,
external_stylesheets = external_stylesheets
)
dashapp.title = 'Dash Charts in Single-Page Application'
# Some of these imports should be inside this function so that other Flask
# stuff gets loaded first, since some of the below imports reference the other
# Flask stuff, creating circular references
from app.dashapp.layout import get_layout
from app.dashapp.callbacks import register_callbacks
with app.app_context():
# Assign the get_layout function without calling it yet
dashapp.layout = get_layout
# Register callbacks
# Layout must be assigned above, before callbacks
register_callbacks(dashapp)
return None
La funzione register_dashapps(app)
viene passata all’istanza dell’applicazione Flask, che Dash assegnerà come “server”.
Per prima cosa, creeremo una sorta di template HTML passando alla classe dash.Dash()
alcuni fogli di stile e script. Questi file .js e .css si trovano nella sezione “head” dei file HTML, quindi Dash li metterà lì per noi.
Useremo Bootstrap CSS per rendere eccezionale la nostra applicazione a pagina singola e funzionare alla grande sui telefoni cellulari. Bootstrap 4 richiede anche Popper e jQuery, quindi li includiamo secondo le linee guida per l’installazione di Bootstrap, disponibile qui.
Dopo aver inizializzato l’istanza Dash (dashapp), creeremo il suo layout HTML / CSS e i callback JavaScript (abbiamo React dietro le quinte).
Per evitare riferimenti circolari, importiamo il layout e i moduli di callback all’interno della funzione register_dashapps(app)
.
Per fare questo dobbiamo creare una cartella “dashapp” all’interno della cartella “app”, contenente tre nuovi file:
__init__.py
callbacks.py
layout.py
Non preoccupiamoci per il file __init__.py
: è lì, quindi Python (e tu) sa che la cartella fa parte del pacchetto del nostro progetto.
La parte 3 approfondisce il layout e i callback di Dash, quindi per ora impostiamo solo le basi.
Innanzitutto, layout.py
conterrà per ora solo una barra di navigazione Bootstrap all’interno di un contenitore Bootstrap:
# /app/dashapp/layout.py
import os
from flask import url_for
import dash_html_components as html
import dash_core_components as dcc
import dash_bootstrap_components as dbc
import psycopg2
from psycopg2.extras import RealDictCursor
# Local imports
from app.database import get_conn
def get_navbar():
"""Get a Bootstrap 4 navigation bar for our single-page application's HTML layout"""
return dbc.NavbarSimple(
children=[
dbc.NavItem(dbc.NavLink("Blog", href="https://mccarthysean.dev")),
dbc.NavItem(dbc.NavLink("IJACK", href="https://myijack.com")),
dbc.DropdownMenu(
children=[
dbc.DropdownMenuItem("References", header=True),
dbc.DropdownMenuItem("Dash", href="https://dash.plotly.com/"),
dbc.DropdownMenuItem("Dash Bootstrap Components", href="https://dash-bootstrap-components.opensource.faculty.ai/"),
dbc.DropdownMenuItem("Testdriven", href="https://testdriven.io/"),
],
nav=True,
in_navbar=True,
label="Links",
),
],
brand="Home",
brand_href="/",
color="dark",
dark=True,
)
def get_layout():
"""Function to get Dash's "HTML" layout"""
# A Bootstrap 4 container holds the rest of the layout
return dbc.Container(
[
# Just the navigation bar at the top for now...
# Stay tuned for part 3!
get_navbar(),
],
)
La funzione get_layout()
viene richiamata dal nostro modulo dash_setup.py
.
Non descriverò la funzione get_navbar()
perché penso sia autoesplicativa, ma se vuoi approfondire in questo link trovi la documentazione.
I callback di Dash sono il fulcro della parte 3 di questa serie, quindi per ora implementiamo il file callbacks.py
con quanto segue:
# /app/dashapp/callbacks.py
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
def register_callbacks(dash_app):
"""Register the callback functions for the Dash app, within the Flask app"""
return None