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.