FastAPI con SQLAlchemy, PostgreSQL, Alembic e Docker – Parte 2 (versione asincrona)

scienzadeidati articoli - Database con FASTAPI [Parte2]

 

Introduzione

Lo scopo di questo articolo è creare una semplice guida su come utilizzare FastAPI con database relazionali in modo asincrono e utilizzare Alembic per le migrazioni.

Prima di iniziare con questo tutorial, leggi la parte 1 di questo tutorial.

Ecco il codice funzionante completo su github.

Iniziamo

Installa il pacchetto richiesto databases.

databases è un pacchetto leggero con supporto asyncio per molti database relazionali e utilizza le principali query di sqlalchemy.

Per lo scopo di questo tutorial userò pipenv , ma puoi usare pip o poetry o conda o qualsiasi altro gestore di pacchetti che preferisci.

            pipenv install databases
pipenv install databases[postgresql]
pipenv install asyncpg
        

useremo la stessa configurazione docker descritta nell’articolo precedente.

Dockerfile

            # Pull base image
FROM python:3.7

# Set environment varibles
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

WORKDIR /code/

# Install dependencies
RUN pip install pipenv
COPY Pipfile Pipfile.lock /code/
RUN pipenv install --system --dev

COPY . /code/

EXPOSE 8000
        

docker-compose.yml

            version: "3"

services:
  db:
    image: postgres:11
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=test_db
  web:
    build: .
    command: bash -c "uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    depends_on:
      - db

  pgadmin:
    container_name: pgadmin
    image: dpage/pgadmin4
    environment:
      - [email protected]
      - PGADMIN_DEFAULT_PASSWORD=admin
    ports:
      - "5050:80"
    depends_on:
      - db
        

manterremo schema.py così com’è

            # schema.py

from pydantic import BaseModel


class User(BaseModel):
    first_name: str
    last_name: str
    age: int

    class Config:
        orm_mode = True
        

Analogamente manteniamo lo stesso alembic.ini.

Modifichiamo il file .env come segue:

DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres

ed lo utilizziamo nel file db.py dove inizializzeremo il nostro database.

            # db.py

import os
from databases import Database
from dotenv import load_dotenv
import sqlalchemy

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(BASE_DIR, ".env"))

db = Database(os.environ["DATABASE_URL"])
metadata = sqlalchemy.MetaData()
        

A questo punto dobbiamo prevedere il file app.py, dove gestiremo l’inizializzazione dell’app con la connessione e la terminazione del database.

            # app.py

from db import db
from fastapi import FastAPI


app = FastAPI(title="Async FastAPI")


@app.on_event("startup")
async def startup():
    await db.connect()


@app.on_event("shutdown")
async def shutdown():
    await db.disconnect()
        
Di conseguenza dobbiamo modificare il file model.py.
            # model.py

from db import db

users = sqlalchemy.Table(
    "users",
    metadata,
    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
    sqlalchemy.Column("first_name", sqlalchemy.String),
    sqlalchemy.Column("last_name", sqlalchemy.String),
    sqlalchemy.Column("age", sqlalchemy.Integer),
)
        
miglioriamo il nostro model.py, creando una semplice classe di gestione del modello User
            # model.py

import sqlalchemy
from db import db, metadata, sqlalchemy


users = sqlalchemy.Table(
    "users",
    metadata,
    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
    sqlalchemy.Column("first_name", sqlalchemy.String),
    sqlalchemy.Column("last_name", sqlalchemy.String),
    sqlalchemy.Column("age", sqlalchemy.Integer),
)


class User:
    @classmethod
    async def get(cls, id):
        query = users.select().where(users.c.id == id)
        user = await db.fetch_one(query)
        return user

    @classmethod
    async def create(cls, **user):
        query = users.insert().values(**user)
        user_id = await db.execute(query)
        return user_id
        

Questa classe fornirà un’implementazione più semplice per i metodi get e create.

Di conseguenza dobbiamo modificare il file main.py come segue.

            # main.py

import uvicorn
from models import User as ModelUser
from schema import User as SchemaUser
from app import app
from db import db


@app.post("/user/")
async def create_user(user: SchemaUser):
    user_id = await ModelUser.create(**user.dict())
    return {"user_id": user_id}


@app.get("/user/{id}", response_model=SchemaUser)
async def get_user(id: int):
    user = await ModelUser.get(id)
    return SchemaUser(**user).dict()


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
        

Da notare come ora stiamo usando async/await per gestire le chiamate verso il database.

È tempo quindi di modificare la configurazione del nostro Alembic.

Bisogna modificare
import models

target_metadata = models.Base.metadata

in
import models
from db import metadata

target_metadata = metadata

 

NOTA: è importante importare i modelli prima dei metadati.

 

Quindi procediamo a ricompilare il nostro Docker:

  • Costruzione: docker-compose build
  • Creazioni migrazioni: docker-compose run web alembic revision --autogenerate
  • Migrazione: docker-compose run web alembic upgrade head
  • Esecuzione: docker-compose up

Ora aprendo il browser e collegarsi a http://localhost:8000

articoli - pgadmin localhost 7
richiesta POST per creare un utente
articoli - pgadmin localhost 8
risposta alla richiesta precedente
articoli - pgadmin localhost 9
richiesta GET dello stesso utente con risposta

Spero che questo tutorial sia stato abbastanza completo su come utilizzare FastAPI con PostgreSQL, SQLAlchemy e Alembic utilizzando la potenza di async .

Il codice completo per questo articolo è disponibile su github.

FastAPI con SQLAlchemy, PostgreSQL e Alembic e ovviamente Docker – Parte 1

scienzadeidati articoli - Database con FASTAPI

La guida completa (tutorial) all’utilizzo dei database relazionali con FastAPI

Introduzione

Lo scopo di questo articolo è creare una semplice guida su come utilizzare FastAPI con i database relazionali e utilizzare Alembic per le migrazioni. Un’implementazione che può essere utilizzata in produzione.

Installazione

Useremo pipenv per gestire sia i miei pacchetti che l’ambiente virtuale. Sentiti libero di gestire i tuoi pacchetti come preferisci.

Pacchetti Utilizzati
  • python ≥ 3.5
  • fastapi
  • pydantic
  • fastapi-sqlalchemy
  • alembic
  • psycopg2
  • uvicorn

Creiamo una nuova directory (puoi chiamarla come vuoi).

Ad esempio possiamo chiamarla fastapi_sqlalchemy_alembic

Apri il terminale e scrivi

cd fastapi_sqlalchemy_alembic

pipenv install --three fastapi fastapi-sqlalchemy pydantic alembic psycopg2 uvicorn

Utilizzerò docker compose per gestire il database, potresti ricevere alcuni errori relativi all’installazione di psycopg2 se stai utilizzando Mac-OS, ma dal momento che utilizzeremo Docker, questo non è molto importante.

Main.py

Iniziamo con un semplice file principale per Fastapi

            # main.py

import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.post("/user/", response_model=User)
def create_user(user: User):
    return user

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
    
        

Configurazione di Docker

            # Dockerfile

# Pull base image
FROM python:3.7

# Set environment varibles
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

WORKDIR /code/

# Install dependencies
RUN pip install pipenv
COPY Pipfile Pipfile.lock /code/
RUN pipenv install --system --dev

COPY . /code/

EXPOSE 8000
        
            # docker-compose.yml

version: "3"

services:
  db:
    image: postgres:11
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=test_db
  web:
    build: .
    command: bash -c "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    depends_on:
      - db

  pgadmin:
    container_name: pgadmin
    image: dpage/pgadmin4
    environment:
      - [email protected]
      - PGADMIN_DEFAULT_PASSWORD=admin
    ports:
      - "5050:80"
    depends_on:
      - db
        

La configurazione precedente creerà un cluster con 3 contenitori:

  1. contenitore web — dove verrà eseguito il codice effettivo
  2. contenitore db
  3. contenitore pgadmin

Nella tua directory corrente dovresti vedere 5 file:

  1. file pip
  2. Pipfile.lock
  3. Dockerfile
  4. docker-compose.yml
  5. main.py

Quindi costruiamo il cluster docker, eseguendo il seguente comando nel terminale:

docker-compose build

 

Alembic

Si inizializza Alembic eseguendo il seguente cmd nel terminale della stessa directory:

alembic init alembic

In questo modo si crea una directory chiamata alembic e un file di configurazione alembic.ini

articoli - alembic

Il prossimo passo è aprire il file alembic.ini con il tuo editor e modificare la riga 38 da:

sqlalchemy.url = driver://user:pass@localhost/dbname

a:

sqlalchemy.url =

e quindi aggiungere l’url db postgres nel file alembic/env.py.

Dato che stiamo creando una configurazione che dovrebbe funzionare in produzione, non possiamo scrivere in chiaro il nome utente e password del database all’interno del file alembic.ini. Dobbiamo invece leggerlo dalle variabili d’ambiente tramite lo script alembic/env.py.

 
Installiamo python-dotenv

pipenv install python-dotenv

Dal momento che abbiamo aggiunto un nuovo pacchetto, ricostruiamo il docker per includerlo:

docker-compose build

 
Creiamo un .envfile

e aggiungiamo quanto segue:

DATABASE_URL = postgresql+psycopg2://postgres:postgres@db:5432

Come abbiamo scoperto l’URL del database?

DATABASE_URL = postgresql+psycopg2://{utente}:{password}@{host}:{porta}

se controlliamo la configurazione docker-compose.yml per il database:

            ...
db:
    image: postgres:11
    ports:
        - "5432:5432"
    environment:
        - POSTGRES_USER=postgres
        - POSTGRES_PASSWORD=postgres
        - POSTGRES_DB=test_db

...

        

Vediamo come user=postgres, password=postgres e poiché siamo nel mondo docker, l’host del database non sarà localhost ma il nome del contenitore, nel nostro caso lo abbiamo chiamato db.

Quindi aggiungiamo questa riga al nostro .env:

DATABASE_URL = postgresql+psycopg2://postgres:postgres@db:5432

Apriamo alembic\env.py , che dovrebbe apparire come segue:

            from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
        

Dobbiamo quindi apportare le seguenti modifiche:

            from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import poolfrom alembic import context
# ---------------- added code here -------------------------#
import os, sys
from dotenv import load_dotenv

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
load_dotenv(os.path.join(BASE_DIR, ".env"))
sys.path.append(BASE_DIR)
#------------------------------------------------------------#
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# ---------------- added code here -------------------------#
# this will overwrite the ini-file sqlalchemy.url path
# with the path given in the config of the main code
config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])
#------------------------------------------------------------#
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# ---------------- added code here -------------------------#
import models
#------------------------------------------------------------#
# ---------------- changed code here -------------------------#
# here target_metadata was equal to None
target_metadata = models.Base.metadata
#------------------------------------------------------------#

        

Modelli

Ora creiamo i nostri modelli da migrare a PostgreSQL:

            # models.py

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    first_name = Column(String,)
    last_name = Column(String)
    age = Column(Integer)
        

Quindi è necessario generare la revisione per la nostra prima migrazione:

docker-compose run web alembic revision --autogenerate -m "First migration"

articoli - alembic update

Se il comando è stato eseguito correttamente dovresti vedere un nuovo file generato nella directory “versions”:

articoli - alembic2

Infine possiamo eseguire la migrazione:

docker-compose run web alembic upgrade head

articoli - alembic update2

Pgadmin

Per controllare le migrazioni create è sufficiente eseguire nel terminale il seguente comando:

docker-compose up

ed aspettare un po’, ci vuole un po’ di tempo per caricare. 

A caricamento concluso, si apre un browser all’indirizzo localhost:5050 e si può accedere con [email protected] e password=admin, come da impostazione definita nel nostro docker-compose.yml

            ...
pgadmin:
    container_name: pgadmin
    image: dpage/pgadmin4
    environment:
      - [email protected]
      - PGADMIN_DEFAULT_PASSWORD=admin
    ports:
      - "5050:80"
    depends_on:
      - db
...
        
articoli - pgadmin localhost

Una volta entrati, è necessario creare una nuova connessione. Alla voce “General” si deve scegliere un nome per la connessione.

Alla voce “Connection” bisogna inserire i riferimenti e le credenziali per connettersi al database (la password è postgres)

articoli - pgadmin localhost 2

Navigamio fino a trovare la nostra tabella User.

Servers > {your-connection-name} > Databases > postgres > Schemas > public > Tables > users

articoli - pgadmin localhost 3
articoli - pgadmin localhost 4

Ora possiamo sicuramente dire che la nostra migrazione si è conclusa con successo.

Finore abbiamo implementato completamente l’ORM con le migrazioni Alembic. Il prossimo passo è collegarlo allo schema Pydantic.

 

Schema — Modello Pydantic

            # schema.py

from pydantic import BaseModel

class User(BaseModel):
    first_name: str
    last_name: str = None
    age: int
    class Config:
        orm_mode = True
        

Si noti che abbiamo una classe Config in cui impostiamo orm_mode=True ed è tutto ciò di cui abbiamo bisogno per i modelli Pydantic, senza i quali gli oggetti del modello Sqlalchemy non verranno serializzati su JSON.

Connettiamo tutto all’interno di main.py

            import uvicorn
from fastapi import FastAPI

#--------------- added code ------------------------#
import os
from fastapi_sqlalchemy import DBSessionMiddleware
from fastapi_sqlalchemy import db
from models import User as ModelUser
from schema import User as SchemaUser
from dotenv import load_dotenv

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(BASE_DIR, ".env"))
#---------------------------------------------------#

app = FastAPI()
#--------------- added code ------------------------#
app.add_middleware(DBSessionMiddleware, 
                    db_url=os.environ["DATABASE_URL"])
#---------------------------------------------------#
#--------------- modified code ---------------------#
@app.post("/user/", response_model=SchemaUser)
def create_user(user: SchemaUser):
    db_user = ModelUser(first_name=user.first_name, 
                        last_name=user.last_name, 
                        age=user.age
    )
    db.session.add(db_user)
    db.session.commit()
    return db_user
#---------------------------------------------------#

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
        

Fantastico, eseguiamo di nuovo docker-compose up

Ora andiamo a \docs e invochiamo l’endpoint Create User

articoli - pgadmin localhost 5a

Possiamo quindi controllare su pgadmin se ha funzionato correttamente.

Colleghiamoci a localhost:5050

articoli - pgadmin localhost 6a

Spero che questo tutorial sia stato abbastanza completo su come utilizzare FastAPI con PostgreSQL, SQLAlchemy e Alembic.

Il codice completo di questo articolo è disponibile su github.

Nella Parte 2 discuteremo come lavorare con i database in modo asincrono.

Grafici delle Serie Temporali con Dash, Flask, TimescaleDB e Docker – Parte 3

scienzadeidati articoli - Grafici delle Serie Temporali con Dash, Flask, TimescaleDB, and Docker - Parte 3

Questo è il terzo articolo della serie su TimescaleDB, Flask e Dash.

Il primo articolo si è concentrato su come configurare il database TimescaleDB all’interno di un’istanza Docker, insieme a PgAdmin per gestire il database.

La seconda parte si è concentrata sul linguaggio Python, creando un sito Web con Flask e quindi integrandolo il framework Web Dash all’interno di Flask.

Questa terza parte si concentra sull’utilizzo di Dash per creare un’applicazione Web reattiva a pagina singola per visualizzare i dati all’interno del database TimescaleDB tramite grafici Plotly accattivanti e interattivi .

Tutto il codice per questo tutorial può essere trovato qui su GitHub.

Parte 3 - Grafici interattivi con Dash per creare un'applicazione di data science

Nella seconda parte di questa serie abbiamo creato un’istanza Dash con il file dash_setup.py, che prevede il seguente codice:

            # /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 Bootstrap CSS stylesheets
    external_stylesheets = [
        'https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'
    ]
    
    # external Bootstrap 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

        

In questo codice si utilizzano le funzioni get_layout e register_callback che sono importati da specifici moduli.

Iniziamo quindi a descrive i due moduli dashapp.layout e dashapp.callbacks

            # /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
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_sensor_types():
    """Get a list of different types of sensors"""
    sql = """
        --Get the labels and underlying values for the dropdown menu "children"
        SELECT 
            distinct 
            type as label, 
            type as value
        FROM sensors;
    """
    conn = get_conn()
    with conn.cursor(cursor_factory=RealDictCursor) as cursor:
        cursor.execute(sql)
        # types is a list of dictionaries that looks like this, for example:
        # [{'label': 'a', 'value': 'a'}]
        types = cursor.fetchall()
    
    return types


def get_body():
    """Get the body of the layout for our Dash SPA"""

    types = get_sensor_types()

    # The layout starts with a Bootstrap row, containing a Bootstrap column
    return dbc.Row(
        [
            # 1st column and dropdown (NOT empty at first)
            dbc.Col(
                [
                    html.Label('Types of Sensors', style={'margin-top': '1.5em'}),
                    dcc.Dropdown(
                        options=types,
                        value=types[0]['value'],
                        id="types_dropdown"
                    )
                ], xs=12, sm=6, md=4
            ),
            # 2nd column and dropdown (empty at first)
            dbc.Col(
                [
                    html.Label('Locations of Sensors', style={'margin-top': '1.5em'}),
                    dcc.Dropdown(
                        # options=None,
                        # value=None,
                        id="locations_dropdown"
                    )
                ], xs=12, sm=6, md=4
            ),
            # 3rd column and dropdown (empty at first)
            dbc.Col(
                [
                    html.Label('Sensors', style={'margin-top': '1.5em'}),
                    dcc.Dropdown(
                        # options=None,
                        # value=None,
                        id="sensors_dropdown"
                    )
                ], xs=12, sm=6, md=4
            ),
        ]
    )


def get_layout():
    """Function to get Dash's "HTML" layout"""

    # A Bootstrap 4 container holds the rest of the layout
    return dbc.Container(
        [
            get_navbar(),
            get_body(), 
        ], 
    )

        

Lavoriamo dal basso verso l’alto, iniziando con get_layout(). Abbiamo aggiunto get_body() sotto la funzione navbar.

Ecco come appare ora il semplice sito Flask nel browser:

timescale-dash-flask-click-here-to-see-the-main-dash-page

Fare clic sul collegamento per visualizzare il sito Dash su /dash/:

NOTA: In questa fase il secondo e il terzo menu a discesa saranno vuoti. È stato popolato solo il primo menu a discesa. Per il secondo e il terzo menu a discesa, utilizzeremo i callback di Dash invece di popolarli nel layout iniziale. Rimanete sintonizzati…

timescale-dash-flask-navbar-and-body

Visualizza il sito in development mode

Ho saltato un passaggio importante: come avviare un sito Flask / Dash in modalità “sviluppo” in modo da poterlo visualizzare nel browser.

Aggiungiamo un file chiamato .flaskenv accanto al file .env nella cartella principale del progetto, con le seguenti tre righe:

            FLASK_APP=wsgi.py
FLASK_RUN_HOST=0.0.0.0
FLASK_RUN_PORT=5002
        

Flask cerca il file .flaskenv quando si avvia il sito. Le variabili d’ambiente specificano il file di partenza e le coordinate (indirizzo web o IP) dove sarà pubblicato il sito (ad esempio si inizializza una  FLASK_APP che è stata importata e creata da wsgi.py ed pubblicata sull’host 0.0.0.0 e porta 5002).

Quindi, una volta creato il file .flaskenv, si digita flask run e sul terminale e il sito sarà pronto. Si può visualizzare il sito collegandosi a http:// localhost:5002 da qualsiasi browser.

Tornando al codice

Tornando al file layout.py, la funzione get_body() restituisce un Bootstrap row e tre Boostrap, columns uno per ciascuno dei menu a discesa di Dash.

Per ora concentriamoci sulla prima colonna, vediamo che è presente un html.Label Dash HTML Component “, seguito da un dcc.Dropdown Dash Core Component“. Stiamo creato creando un Bootstrap HTML/CSS/JS solamente usando Python! Questa è la bellezza di Dash, ed è molto conveniente per i data scientist che cercano di produrre i propri modelli e dati.

La variabile types proviene dalla funzione get_sensor_types(), che interroga il nostro database TimescaleDB e restituisce i tipi di sensori specifici/univoci in un “elenco di dizionari”. Questo è reso possibile tramite cursor_factory=RealDictCursor (cioè restituisce le righe del database come comodi dizionari Python).

Molti Pythonistas amano interrogare i loro database usando SQLAlchemy , e anch’io lo uso molto spesso, anche se a volte ho voglia di solo sporcarmi le mani e scrivere un po’ di SQL, come ai vecchi tempi. La libreria psycopg2 ci consente di farlo molto facilmente. Entrambe sono ottime librerie, ben mantenute e testate sul campo.

Le altre due colonne e query nel nostro layout sono sostanzialmente le stesse della prima, quindi non le descriverò individualmente.

Callback in Dash

È ora di descrivere la logica dei callback, così possiamo far effettivamente funzionare il secondo e il terzo menu a discesa. A questo punto, nel codice del file layout.py il secondo e il terzo elenco a discesa dovrebbero essere vuoti.

Finalmente, quello che stavamo tutti aspettando: un po’ di divertente interattività in Dash! Di seguito il codice del file callbacks.pyQui è dove popoleremo il 2 ° e il 3 ° menu a discesa, in base al valore del primo menu a discesa:

            # /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
from psycopg2.extras import RealDictCursor

# Local imports
from app.database import get_conn


def get_sensor_locations(type_):
    """Get a list of different locations of sensors"""
    sql = f"""
        --Get the labels and underlying values for the dropdown menu "children"
        SELECT 
            distinct 
            location as label, 
            location as value
        FROM sensors
        WHERE type = '{type_}';
    """
    conn = get_conn()
    with conn.cursor(cursor_factory=RealDictCursor) as cursor:
        cursor.execute(sql)
        # locations is a list of dictionaries that looks like this, for example:
        # [{'label': 'floor', 'value': 'floor'}]
        locations = cursor.fetchall()
    
    return locations


def get_sensors(type_, location):
    """
    Get a list of sensor dictionaries from our TimescaleDB database, 
    along with lists of distinct sensor types and locations
    """
    sql = f"""
        --Get the labels and underlying values for the dropdown menu "children"
        SELECT 
            location || ' - ' || type as label,
            id as value
        FROM sensors
        WHERE 
            type = '{type_}'
            and location = '{location}';
    """
    conn = get_conn()
    with conn.cursor(cursor_factory=RealDictCursor) as cursor:
        cursor.execute(sql)
        # sensors is a list of dictionaries that looks like this, for example:
        # [{'label': 'floor - a', 'value': 1}]
        sensors = cursor.fetchall()

    return sensors


def register_callbacks(dash_app):
    """Register the callback functions for the Dash app, within the Flask app""" 

    @dash_app.callback(
        [
            Output("locations_dropdown", "options"),
            Output("locations_dropdown", "value")
        ],
        [
            Input("types_dropdown", "value")
        ]
    )
    def get_locations_from_types(types_dropdown_value):
        """Get the location options, based on the type of sensor chosen"""

        # First get the location options (i.e. a list of dictionaries)
        location_options = get_sensor_locations(types_dropdown_value)

        # Default to the first item in the list, 
        # and get the "value" from the dictionary
        location_value = location_options[0]["value"]

        return location_options, location_value


    @dash_app.callback(
        [
            Output("sensors_dropdown", "options"),
            Output("sensors_dropdown", "value")
        ],
        [
            Input("types_dropdown", "value"),
            Input("locations_dropdown", "value")
        ]
    )
    def get_sensors_from_locations_and_types(types_dropdown_value, locations_dropdown_value):
        """Get the sensors available, based on both the location and type of sensor chosen"""

        # First get the sensor options (i.e. a list of dictionaries)
        sensor_options = get_sensors(types_dropdown_value, locations_dropdown_value)

        # Default to the first item in the list, 
        # and get the "value" from the dictionary
        sensor_value = sensor_options[0]["value"]

        return sensor_options, sensor_value


    return None

        

Ricordiamoci che la funzione register_callbacks(dash_app) è usata all’interno di dash_setup.py.

Concentriamoci sulla prima funzione di callback. Tutti i callback di Dash hanno funzioni Input() che li attivano e funzioni Output(), che sono gli elementi HTML che sono modificati dai callback.

Il primo parametro nelle funzioni Input()Output() è sempre l’id= dell’elemento, all’interno del file layout.py. Il secondo parametro è sempre la proprietà che stiamo cercando di modificare. Quindi, nella funzione di callback stiamo utilizzando il “valore” del menu a discesa “types_dropdown” come input per la funzione, e stiamo modificando sia le “opzioni” che il “valore” selezionato del menu a discesa “locations_dropdown”.

La seconda callback è solo leggermente più complicata della prima. Utilizza i valori di entrambi i primi due menu a discesa (ovvero il menu a discesa dei tipi e il menu a discesa delle posizioni) per filtrare le opzioni del sensore e il valore selezionato.

Inoltre la funzione get_sensors(type_, location) prevede una delle nostre query al database, che acquisisce i sensori unici corrispondenti ai filtri sul tipo e sulla posizione dei sensori. L’altra query del database è quasi identica, ad eccezione che prevede un solo filtro in base al tipo di sensore.

Implementiamo ora un bel grafico delle serie temporali, in modo da visualizzare i dati nel tempo del sensore selezionato tramite i filtri.

Aggiungiamo la seguente funzione get_chart_row() al tuo file layout.py, nella parte inferiore. Questo ci fornisce una riga / colonna Bootstrap in cui posizionare un grafico Dash tramite una callback:

            ...

def get_chart_row(): # NEW
    """Create a row and column for our Plotly/Dash time series chart"""

    return dbc.Row(
        dbc.Col(
            id="time_series_chart_col"
        )
    )


def get_layout():
    """Function to get Dash's "HTML" layout"""

    # A Bootstrap 4 container holds the rest of the layout
    return dbc.Container(
        [
            get_navbar(),
            get_body(), 
            get_chart_row(), # NEW
        ], 
    )

        

Vediamo ora come creare il grafico tramite le callback.

Innanzitutto assicuriamoci di aggiungere i seguenti import nel file callbacks.py e installiamole nel nostro ambiente virtuale con il comando pip install pandas plotly:

            import pandas as pd
import plotly.graph_objs as go

        

A questo punto dobbiamo aggiungere la query per acquisire i dati delle serie temporali anche nel file callbacks.py. Da notare che questa volta inseriamo i dati estratti all’interno di un DataFrame Pandas. Pandas è una libreria essenziale per i data scientist che lavorano con Python.

In poche parole, un Pandas DataFrame è una tabella composta da un array NumPy per ogni colonna. Descrivere Pandas e Numpy non è lo scopo di questo articolo, ma è importante ricordare che queste librerie sono estremamente ottimizzati per il lavoro in ambito della data science.

            def get_sensor_time_series_data(sensor_id):
    """Get the time series data in a Pandas DataFrame, for the sensor chosen in the dropdown"""

    sql = f"""
        SELECT 
            --Get the 3-hour average instead of every single data point
            time_bucket('03:00:00'::interval, time) as time,
            sensor_id,
            avg(temperature) as temperature,
            avg(cpu) as cpu
        FROM sensor_data
        WHERE sensor_id = {sensor_id}
        GROUP BY 
            time_bucket('03:00:00'::interval, time), 
            sensor_id
        ORDER BY 
            time_bucket('03:00:00'::interval, time), 
            sensor_id;
    """

    conn = get_conn()
    with conn.cursor(cursor_factory=RealDictCursor) as cursor:
        cursor.execute(sql)
        rows = cursor.fetchall()
        columns = [str.lower(x[0]) for x in cursor.description]

    df = pd.DataFrame(rows, columns=columns)
    
    return df

        

Infine, dobbiamo aggiungere il grafico Dash / Plotly. 

A tale scopo dobbiamo aggiungere una callback alla funzione register_callbacks(dash_app)Da notare che la callback aggiorna la proprietà “children” della colonna Bootstrap con ID “time_series_chart_col”.

Il callback utilizza un Input() e due State() come parametri della funzione. La differenza tra Input() State() consiste che la funzione viene chiamata solo se ci sono modifiche a Input(). Se la State() cambia, la funzione non viene chiamata, ma abbiamo ancora ha disposizione i loro valori all’interno della funzione.

Successivamente, prendiamo i dati della serie temporale (in un DataFrame, come accennato in precedenza) e consideriamo la colonna time per l’asse x. Aggiungiamo due grafici a linee (grafici a dispersione collegati da linee), quindi preleviamo due colonne dal dataframe, “temperatura” e “cpu”. Aggiungi anche una variabile per il titolo dei grafici.

Quindi creiamo due oggetti grafici Plotly (ad esempio go.Scatter) per i due grafici e li passiamo in input alla funzione get_graph() (che descriviamo tra poco).

Infine, la funzione restituisce un semplice HTML div con due righe Bootstrap (una riga Bootstrap deve contenere sempre almeno una colonna).

              @dash_app.callback(
        Output("time_series_chart_col", "children"),
        [Input("sensors_dropdown", "value")],
        [
            State("types_dropdown", "value"),
            State("locations_dropdown", "value")
        ]
    )
    def get_time_series_chart(
        sensors_dropdown_value, 
        types_dropdown_value, 
        locations_dropdown_value
    ):
        """Get the sensors available, based on both the location and type of sensor chosen"""

        df = get_sensor_time_series_data(sensors_dropdown_value)
        x = df["time"]
        y1 = df["temperature"]
        y2 = df["cpu"]
        title = f"Location: {locations_dropdown_value} - Type: {types_dropdown_value}"

        trace1 = go.Scatter(
            x=x,
            y=y1,
            name="Temp"
        )
        trace2 = go.Scatter(
            x=x,
            y=y2,
            name="CPU"
        )

        # Create two graphs using the traces above
        graph1 = get_graph(trace1, f"Temperature for {title}")
        graph2 = get_graph(trace2, f"CPU for {title}")

        return html.Div(
            [
                dbc.Row(dbc.Col(graph1)),
                dbc.Row(dbc.Col(graph2)),
            ]
        )
        

A questo punto non ci rimane che aggiungere la funzione la get_graph(). I grafici Plotly / Dash hanno molte opzioni, ma non lasciarti scoraggiare; sono facile da capire.

Il primo parametro disabilita il logo Plotly (totalmente opzionale). Successivamente nell’oggetto grafico c’è il parametro figure, che contiene due sottoparametri principali, datalayout. Ecco il riferimento completo alle API , che è una risorsa essenziale se stai lavorando con i grafici Plotly.

In layout, aggiungiamo alcune opzioni per il xaxis in modo da inserire alcuni rapidi filtri dato che stiamo costruendo un grafico di serie temporali. Di solito scelgo i pulsanti rangeselector sopra il grafico o il rangeslider sotto il grafico, ma in questo esempio li usiamo entrambi per mostrare le potenzialità di ploty. Preferisco i pulsanti rangeselector sopra il grafico. Se ti piace il trading di azioni, saprai che questi filtri temporali sono abbastanza comuni e intuitivi.

 

            def get_graph(trace, title):
    """Get a Plotly Graph object for Dash"""
    
    return dcc.Graph(
        # Disable the ModeBar with the Plotly logo and other buttons
        config=dict(
            displayModeBar=False
        ),
        figure=go.Figure(
            data=[trace],
            layout=go.Layout(
                title=title,
                plot_bgcolor="white",
                xaxis=dict(
                    autorange=True,
                    # Time-filtering buttons above chart
                    rangeselector=dict(
                        buttons=list([
                            dict(count=1,
                                label="1d",
                                step="day",
                                stepmode="backward"),
                            dict(count=7,
                                label="7d",
                                step="day",
                                stepmode="backward"),
                            dict(count=1,
                                label="1m",
                                step="month",
                                stepmode="backward"),
                            dict(step="all")
                        ])
                    ),
                    type = "date",
                    # Alternative time filter slider
                    rangeslider = dict(
                        visible = True
                    )
                )
            )
        )
    )

        

Abbiamo concluso l’implementazione delle callback e delle funzioni grafiche.

Avviamo la nostra applicazione, i grafici avranno il seguente aspetto:

timescale-dash-flask-finished-product

Questo è tutto! Abbiamo approfondito molti aspetti in questo tutorial diviso in tre parti.

Nella prima parte , abbiamo creato un database TimescaleDB e lo abbiamo popolato con i dati simulati delle serie temporali di sensori IoT. Abbiamo anche installato un’app Web PGAdmin per l’amministrazione del nostro database ed entrambe le applicazioni sono state distribuite utilizzando Docker-Compose, uno strumento fantastico per riprodurre facilmente ambienti e implementazioni.

Nella seconda parte, abbiamo combinato un’app Web Flask con un’app Web Dash in modo da poter avere il meglio da entrambe le librerie: Flask può praticamente implementare qualsiasi applicazione Web e Dash è ottimo per la produzione di app a pagina singola di data science senza bisogno di utilizzare JavaScript o React.

In questa terza parte, abbiamo descritto le funzioni di Dash, in modo particolare i callback interattivi del menu a discesa e i grafici Plotly per dare un assaggio di ciò che è possibile fare con Dash.

Spero che questa serie ti sia piaciuta.

A presto!

Grafici delle Serie Temporali con Dash, Flask, TimescaleDB e Docker – Parte 2

scienzadeidati articoli - Grafici delle Serie Temporali con Dash, Flask, TimescaleDB, and Docker - Parte 2

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:

  1. Un’applicazione Flask per un normale sito web
  2. Un’elegante applicazione Dash a pagina singola che utilizza il meglio di React JavaScript
  3. 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

        
Tornando al nostro file __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.pyall’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:

  1. __init__.py
  2. callbacks.py
  3. 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

        

Siamo arrivati alla conclusione di questa seconda parte. Spero che finora ti sia piaciuta. Dai un’occhiata alla Parte 3 per un’analisi approfondita dei callback di Dash e dei grafici di Plotly.

A presto!

Grafici delle Serie Temporali con Dash, Flask, TimescaleDB e Docker – Parte 1

scienzadeidati articoli - Grafici delle Serie Temporali con Dash, Flask, TimescaleDB, and Docker - Parte 01

In questo tutorial in tre parti, ti mostrerò come creare un’applicazione reattiva a pagina singola interamente in Python, con grafici di serie temporali dinamici tramite Dash / Plotly, il framework Flask per siti Web e un database specializzato per le serie temporali chiamato TimescaleDB, che a sua volta è basato su PostgreSQL. Un bel boccone, ma questo è uno stack tecnologico piuttosto interessante che è facile da imparare e da programmare, poiché utilizza solo Python e SQL standard (senza JavaScript). Quindi è uno stack ideale per distribuire rapidamente applicazioni di data science.

La prima parte del tutorial si concentrerà sull’utilizzo di Docker per configurare il database TimescaleDB specializzato e PGAdmin per gestirlo. Creeremo alcuni dati IoT simulati e mostreremo alcune delle fantastiche funzionalità di TimescaleDB, che non troverai nel PostgreSQL standard.

La seconda parte si concentrerà sulla configurazione di un sito Web Python Flask che si integra con la straordinaria libreria Dash per la creazione di applicazioni a pagina singola (SPA) basate su JavaScript React. Ti mostrerò come integrare correttamente Dash in Flask, in modo da poter ottenere il meglio da entrambi i framework web.

La terza parte si concentrerà sull’utilizzo di Dash per creare grafici di serie temporali interattivi per il monitoraggio dei dati IoT o per mostrare la tua applicazione di data science.

Tutto il codice per questo tutorial può essere trovato qui su GitHub.

Uso Docker laddove possibile, per ambienti riproducibili e una distribuzione estremamente semplice utilizzando Docker Swarm, quindi se non hai familiarità con Docker, consulta la documentazione qui.

Parte 1 - TimescaleDB, PGAdmin e Docker

Per prima cosa, creiamo una rete Docker in modo che i nostri prossimi contenitori possano parlare tra loro:

            docker network create --attachable --driver bridge timescale_network
        
Successivamente, creiamo un database TimescaleDB locale utilizzando Docker-Compose. Questo avvierà rapidamente un database PostgreSQL locale con l’estensione TimescaleDB configurata automaticamente. Si usa il seguente file docker-compose.timescale.yml:
            # docker-compose.timescale.yml

version: '3.7'
services:
  timescale:
    image: timescale/timescaledb:1.7.4-pg12
    container_name: flaskapp_timescale
    volumes: 
      - type: volume
        # source: timescale-db # the volume name
        source: timescale_volume
        # target: the location in the container where the data are stored
        target: /var/lib/postgresql/data 
        read_only: false
      # Custom postgresql.conf file will be mounted (see command: as well)
      - type: bind
        source: ./postgresql_custom.conf
        target: /postgresql_custom.conf
        read_only: false
    env_file: .env
    environment: 
      POSTGRES_HOST: timescale
    command: ["-c", "config_file=/postgresql_custom.conf"]
    ports: 
      - 0.0.0.0:5432:5432
    networks:
      timescale_network:
    deploy:
      restart_policy:
        condition: on-failure

# Creates a named volume to persist our database data
volumes:
  timescale_volume:

# Joins our external network
networks:
  timescale_network:
    external: true
        

Nel precedente file Docker-Compose possiamo notare i seguenti aspetti:

  • Si utilizza il  timescale_network che abbiamo creato nel passaggio precedente.
  • Si utilizza un volume per rendere persistenti i dati del database, anche se il contenitore Docker viene rimosso o sostituito. Questo è molto comune per i database “Dockerizzati”.
  • Si utilizza la porta 5432 (questo sarà importante quando proveremo ad accedere al database).
  • Si utilizza un file di configurazione personalizzato e un file  .env per memorizzare le informazioni sensibili per la connessione al database, come la password del database. E’ quindi necessario creare questi due file.

Ecco il file di configurazione personalizzato, nel caso in cui desideri / devi modificare una di queste impostazioni in futuro. Il file è troppo lungo per essere inserito in un blocco di codice in questo articolo, quindi fai clic su questo collegamento , quindi copia e incolla il testo in un file chiamato postgresql_custom.conf e mettilo nella radice della cartella del progetto.

Successivamente, ecco un modello per il file .env, che puoi lasciare nella root della cartella del tuo progetto, insieme ai file Docker-Compose e a quelli di configurazione del database:

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

Ora che abbiamo aggiunto la configurazione personalizzata e i file .env, puoi avviare il database TimescaleDB con il seguente comando. Il -d esegue il comando in background (--detached).

            docker-compose -f docker-compose.timescale.yml up -d
        
Controlla i tuoi contenitori in esecuzione con docker container ls o la vecchia scuola docker ps. Se il contenitore si sta riavviando, controlla i log con docker logs <container id> e assicurati di aver configurato il file .env, il file di configurazione e la rete Docker da cui dipende. Infine, creiamo un ambiente PGAdmin amichevole per l’amministrazione del nostro database e l’esecuzione di SQL. Crea un file chiamato docker-compose.pgadmin.yml e aggiungi quanto segue:
            # docker-compose.pgadmin.yml

version: '3.7'
services:
  pgadmin:
    # Name of the container this service creates. Otherwise it's prefixed with the git repo name
    image: "dpage/pgadmin4:latest"
    container_name: flaskapp_pgadmin4
    restart: unless-stopped
    env_file: .env
    environment: 
      PGADMIN_LISTEN_PORT: 9000
    ports: 
      - 0.0.0.0:9000:9000
    volumes: 
      # So the database server settings get saved and stored even if the container is replaced or deleted
      - pgadmin:/var/lib/pgadmin
    networks:
      timescale_network:

volumes:
  pgadmin:

networks:
  timescale_network:
    external: true

        
Aggiungi le seguenti righe al tuo .env file per PGAdmin. Avrai bisogno di queste informazioni di accesso quando tenterai di accedere a PGAdmin nel browser web.
            # .env

# For the PGAdmin web app
[email protected]
PGADMIN_DEFAULT_PASSWORD=password
        

Avvia l’applicazione web PGAdmin (PostgreSQL Admin) con il seguente comando Docker:

            docker-compose -f docker-compose.pgadmin.yml up -d

        

Esegui di docker container lsnuovo per verificare se il contenitore PGAdmin è in esecuzione. Nota che abbiamo specificato una porta di 9000, quindi ora puoi accedere a PGAdmin su http://localhost:9000 o http://127.0.0.1:9000 .Accedi con il nome utente e la password che hai impostato nel tuo file .env.

Ora che hai effettuato l’accesso a PGAdmin, fai clic con il pulsante destro del mouse su “Server” e “Crea / Server…”. Chiamalo “TimescaleDB Locale” nella scheda “Generale” e digita quanto segue nella scheda “Connessione”:

  • Host : timescale (questo è il nome host del “Servizio” Docker definito nel primo file docker-compose.yml per il contenitore del database TimescaleDB)
  • Porta : 5432
  • Database di manutenzione : postgres
  • Nome utente : postgres
  • Password : password

Fai clic su “Salva” e dovresti essere connesso. Ora puoi fare doppio clic su “TimescaleDB Local” e puoi accedere alle tabelle del tuo database su “/ Databases / postgres / Schemas / public / Tables”. Molto intuitivo, eh? Nel menu “Strumenti”, fai clic su “Strumento di query” e sei pronto per iniziare a scrivere SQL.

Ora sei l’orgoglioso comandante di un database TimescaleDB, che è identico a un database PostgreSQL (“Il database open source più avanzato al mondo”, se credi nel loro marketing), tranne per il fatto che ora ha capacità speciali per gestire dati ad alta frequenza di serie temporali.

I dati delle serie temporali sono leggermente diversi dai normali dati relazionali per descrivere utenti e cose. I dati delle serie temporali possono arrivare in qualsiasi secondo o anche più volte al secondo, a seconda di ciò che si sta archiviando, quindi il database deve essere in grado di gestire molti inserimenti. Alcuni esempi sono i dati finanziari, come i prezzi di negoziazione del mercato azionario o dati di Internet of Things (IoT), solitamente per il monitoraggio di metriche ambientali come temperatura, pressione, umidità o qualsiasi altra cosa tu possa pensare. Di solito quando esegui query sui dati di serie temporali, sei interessato ai dati più recenti e di solito stai filtrando sulla colonna timestamp, quindi è assolutamente necessario indicizzarli. TimescaleDB è specializzato in questo genere di cose.

Creiamo uno speciale “Hypertable” di TimescaleDB e inseriamo alcuni dati con cui giocare. Ecco la documentazione . Ed ecco il tutorial da cui ottengo i dati SQL simulati.

In PGAdmin, se non ci sei già, nel menu “Strumenti”, fai clic su “Strumento di query” e digita il seguente SQL per creare due tabelle di dati IoT:

            CREATE TABLE sensors(
  id SERIAL PRIMARY KEY,
  type VARCHAR(50),
  location VARCHAR(50)
);

CREATE TABLE sensor_data (
  time TIMESTAMPTZ NOT NULL,
  sensor_id INTEGER,
  temperature DOUBLE PRECISION,
  cpu DOUBLE PRECISION,
  FOREIGN KEY (sensor_id) REFERENCES sensors (id)
);

CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE


        

Ora vediamo una funzionalità speciale di TimeScaleDB che non puoi fare in un normale database PostgreSQL. Trasformeremo la tabella sensor_data  in una “Hypertable”. Dietro le quinte, TimescaleDB partizionerà i dati sulla dimensione temporale, semplificando il filtraggio, l’indicizzazione e l’eliminazione dei vecchi dati delle serie temporali.

Se sei arrivato a questo tutorial per sfruttare le caratteristiche uniche di TimescaleDB, di seguito vedrai dove avviene la magia.

Eseguiamo la seguente query in PGAdmin per creare l’hypertable, partizionato automaticamente sulla dimensione “time”:

            SELECT create_hypertable('sensor_data', 'time');

        

Ora che è stata creata la nostra tabella di serie temporali specializzata, creiamo un indice speciale sull’ID del sensore, poiché è molto probabile che filtreremo sia l’ID del sensore che l’ora.

            create index on sensor_data (sensor_id, time desc);
        

Aggiungiamo ora alcuni dati di esempio nella tabella “sensori”:

            INSERT INTO sensors (type, location) VALUES
    ('a','floor'),
    ('a', 'ceiling'),
    ('b','floor'),
    ('b', 'ceiling');

        

Ora la parte divertente: creiamo alcuni dati simulati per le serie temporali:

            INSERT INTO sensor_data 
  (time, sensor_id, cpu, temperature)
SELECT
  time,
  sensor_id,
  random() AS cpu,
  random()*100 AS temperature
FROM 
  generate_series(
    now() - interval '31 days', 
    now(), interval '5 minute'
  ) AS g1(time), 
  generate_series(1,4,1) AS g2(sensor_id);

        

Eseguiamo una semplice query di selezione per vedere alcuni dei nostri dati appena simulati:

            SELECT * 
FROM sensor_data
WHERE time > (now() - interval '1 day')
ORDER BY time;
        

Ecco un altro esempio di selezione dei dati aggregati (ovvero la media ad 1 ora, invece di vedere ogni singolo punto dati):

            SELECT 
  sensor_id,
  time_bucket('1 hour', time) AS period, 
  AVG(temperature) AS avg_temp, 
  AVG(cpu) AS avg_cpu 
FROM sensor_data 
GROUP BY 
  sensor_id, 
  time_bucket('1 hour', time)
ORDER BY 
  sensor_id, 
  time_bucket('1 hour', time);

        

Dal tutorial ufficiale di TimescaleDB, mostriamo altre due query. Innanzitutto, invece di una cronologia delle serie temporali, potremmo semplicemente desiderare i dati più recenti . Per questo, possiamo utilizzare la funzione “last()”:

            SELECT 
  time_bucket('30 minutes', time) AS period, 
  AVG(temperature) AS avg_temp, 
  last(temperature, time) AS last_temp --the latest value
FROM sensor_data 
GROUP BY period;

        

E, naturalmente, spesso vogliamo unire i dati delle serie temporali con i metadati (cioè i dati sui dati). In altre parole, otteniamo una posizione per ogni sensore, piuttosto che un ID sensore:

            SELECT 
  t2.location, --from the second metadata table
  time_bucket('30 minutes', time) AS period, 
  AVG(temperature) AS avg_temp, 
  last(temperature, time) AS last_temp, 
  AVG(cpu) AS avg_cpu 
FROM sensor_data t1 
INNER JOIN sensors t2 
  on t1.sensor_id = t2.id
GROUP BY 
  period, 
  t2.location;


        

TimescaleDB ha un’altra funzione molto utile chiamata “aggregazioni continue” per aggiornare continuamente ed efficientemente le visualizzazioni aggregate dei dati delle nostre serie temporali. Se desideriamo creare rapporti / grafici sui dati aggregati, il seguente codice per la creazione di viste fa al nostro caso:

            CREATE VIEW sensor_data_1_hour_view
WITH (timescaledb.continuous) AS --TimescaleDB continuous aggregate
SELECT 
  sensor_id,
  time_bucket('01:00:00'::interval, sensor_data.time) AS time,
  AVG(temperature) AS avg_temp, 
  AVG(cpu) AS avg_cpu
FROM sensor_data
GROUP BY 
  sensor_id,
  time_bucket('01:00:00'::interval, sensor_data.time)


        

Questo è tutto per la parte 1 di questo tutorial in tre parti su TimescaleDB, Dash e Flask. Ecco la parte 2 sull’integrazione di Dash e Flask. La parte 3 si concentrerà sulla creazione di grafici di serie temporali reattivi e interattivi in ​​Dash per la tua applicazione a pagina singola (SPA).

A presto!