Article
· Oct 2, 2023 13m de lecture

Api REST avec IRIS sur Python et les migrations sql

Pour le prochain Concours Python, j'aimerais faire une petite démo, sur la création d'une simple application REST en Python, qui utilisera IRIS comme base de données. Et utiliser les outils suivants

  • Le cadre FastAPI, très performant, facile à apprendre, rapide à coder, prêt pour la production.
  • SQLAlchemy est la boîte à outils SQL et le Mapping objet-relationnel de Python qui donne aux développeurs en Python toute la puissance et la flexibilité de SQL.
  • Alembic est un outil léger de migration de base de données à utiliser avec le SQLAlchemy Database Toolkit pour Python.
  • Uvicorn est une implémentation de serveur web ASGI pour Python.

Préparation de l'environnement

En supposant que Python soit déjà installé, au moins en version 3.7., il faut créer un dossier de projet, et y créer un fichier requirements.txt avec le contenu suivant

fastapi==0.101.1
alembic==1.11.1
uvicorn==0.22.0
sqlalchemy==2.0.20
sqlalchemy-iris==0.10.5

 Je vous conseille d'utiliser l'environnement virtuel en python, nous allons créer un nouvel environnement et l'activer.

python -m venv env && source env/bin/activate

Et maintenant, nous pouvons installer nos dépendances

pip install -r requirements.txt

Démarrage rapide

Créons l'Api REST la plus simple avec FastAPI. Pour ce faire, créons app/main.py

from fastapi import FastAPI

app = FastAPI(
    title='TODO Application',
    version='1.0.0',
)

@app.get("/ping")
async def pong():
    return {"ping": "pong!"}

Pour l'instant, il suffit de démarrer notre application, et elle devrait déjà fonctionner. Pour démarrer le serveur, nous allons utiliser uvicorn

$ uvicorn app.main:app         
INFO:     Processus de serveur lancé [94936]
INFO:     En attente du lancement de l'application.
INFO:     Application startup compléte.
INFO:     Uvicorn fonctionne sur http://127.0.0.1:8000 ( Appuyez sur CTRL+C pour quitter)

Et nous pouvons soumettre une requête de ping.

$ curl http://localhost:8000/ping
{"ping":"pong!"}

FastAPI propose une interface utilisateur permettant de tester l'API.

Environnement Dockerisé

Pour ajouter IRIS à notre application, nous allons utiliser des conteneurs. L'image d'IRIS sera utilisée telle quelle, mais il nous faut construire une image Docker pour l'application python. Et nous aurons besoin de Dockerfile

FROM python:3.11-slim-buster

WORKDIR /usr/src/app

RUN --mount=type=bind,src=.,dst=. \
    pip install --upgrade pip && \
    pip install -r requirements.txt

COPY . .

ENTRYPOINT [ "/usr/src/app/entrypoint.sh" ]

Pour lancer l'application à l'intérieur du conteneur, il faut un simple entrypoint.sh.

#!/bin/sh

# Exécution des migrations SQL, pour mettre à jour le schéma de la base de données
alembic upgrade head

# Lancement de l'application Python
uvicorn app.main:app \
      --workers 1 \
      --host 0.0.0.0 \
      --port 8000 "$@"

N'oubliez pas d'ajouter un drapeau d'exécution

chmod +x entrypoint.sh

Et combinez avec IRIS dans docker-compose.yml

version: "3"
services:
  iris:
    image: intersystemsdc/iris-community
    ports:
      - 1972
    environment:
      - IRISUSERNAME=demo
      - IRISPASSWORD=demo
    healthcheck:
      test: /irisHealth.sh
      interval: 5s
  app:
    build: .
    ports:
      - 8000:8000
    environment:
      - DATABASE_URL=iris://demo:demo@iris:1972/USER
    volumes:
      - ./:/usr/src/app
    depends_on:
      iris:
        condition: service_healthy
    command:
      - --reload

Construisons-le

docker-compose build

Le premier modèle de données

Maintenant, déclarons l'accès à notre base de données IRIS à l'application, en ajoutant le fichier app/db.py, qui configurera SQLAlchemy pour accéder à notre base de données définie à travers l'URL passée par docker-compose.yml. Ce fichier contient également quelques gestionnaires que nous utiliserons plus tard dans l'application.

import os

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = os.environ.get("DATABASE_URL")
if not DATABASE_URL:
    DATABASE_URL = "iris://demo:demo@localhost:1972/USER"
engine = create_engine(DATABASE_URL, echo=True, future=True)

Base: DeclarativeMeta = declarative_base()

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def init_db():
    engine.connect()


def get_session():
    session = SessionLocal()
    yield session

Et nous sommes prêts à définir le premier et unique modèle de notre application. Nous créons et éditons le fichier app/models.py, il utilisera SQLAlchemy pour définir le modèle, nommé Todo, à trois colonnes, id, title, et description.

from sqlalchemy import Column, Integer, String, Text
from app.db import Base


class Todo(Base):
    __tablename__ = 'todo'
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(200), index=True, nullable=False)
    description = Column(Text, nullable=False)

Préparation de la migration SQL

Dans ce monde changeant, nous savons que notre application sera améliorée à l'avenir, que la structure de nos tableaux n'est pas définitive, que nous pouvons ajouter des tableaux, des colonnes, des index, etc. Dans ce cas, le meilleur scénario consiste à utiliser des outils de migration SQL, qui permettent de mettre à jour la structure actuelle de la base de données en fonction de la version de notre application, et grâce à ces outils, il est également possible de la rétrograder, au cas où quelque chose ne fonctionnerait pas. Bien que dans ce projet nous utilisions Python et SQLAlchemy, l'auteur de SQLAlchemy propose son outil nommé Alembic, et nous allons l'utiliser ici.

We need to start IRIS and container with our application, at this moment we need bash, to be able to run commands

$ docker-compose run --entrypoint bash app
[+] Creating 2/0
 ✔ Réseau  fastapi-iris-demo_default   Crée                                                                                                                                                        0.0s 
 ✔ Conteneur fastapi-iris-demo-iris-1  Crée                                                                                                                                                        0.0s 
[+] Exécution 1/1
 ✔ Conteneur fastapi-iris-demo-iris-1  Lancé                                                                                                                                                        0.1s 
root@7bf903cd2721:/usr/src/app# 

Exécution de la commande alembic init app/migrations

root@7bf903cd2721:/usr/src/app# alembic init app/migrations
  Création du répertoire '/usr/src/app/app/migrations' ...  exécuté
  Création du répertoire '/usr/src/app/app/migrations/versions' ...  exécuté
  Génération de /usr/src/app/app/migrations/README ...  exécuté
  Génération de /usr/src/app/app/migrations/script.py.mako ...  exécuté
  Génération de /usr/src/app/app/migrations/env.py ...  exécuté
  Génération de /usr/src/app/alembic.ini ...  exécuté
  Veuillez modifier les paramètres de configuration/connexion/logging dans '/usr/src/app/alembic.ini' avant de continuer.
root@7bf903cd2721:/usr/src/app#

Cette configuration alembic a été préalablement configurée, et nous devons la corriger pour qu'elle corresponde aux besoins de notre application. Pour ce faire, il faut éditer le fichier app/migrations/env.py. Ce n'est que le début du fichier, qui doit être mis à jour, en se concentrant sur la mise à jour de sqlalchemy.url et target_metadata. Ce qui se trouve en dessous reste inchangé

import os
import urllib.parse
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

# il s'agit de l'objet Alembic Config, qui permet
# d'accéder aux valeurs du fichier .ini utilisé.
config = context.config

DATABASE_URL = os.environ.get("DATABASE_URL")

decoded_uri = urllib.parse.unquote(DATABASE_URL)
config.set_main_option("sqlalchemy.url", decoded_uri)

# Interprétation du fichier de configuration pour l'enregistrement Python.
# Cette ligne met en place les enregistreurs de façon basique.
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# ajoutez ici l'objet MetaData de votre modèle
# pour la prise en charge de l'autogénération
from app.models import Base
target_metadata = Base.metadata
# target_metadata = non applicable

Nous avons déjà un modèle, maintenant il faut créer une migration, avec la commande alembic revision --autogenerate (alembic revision ---autogénérer).

root@7bf903cd2721:/usr/src/app# alembic revision --autogenerate
INFO  [alembic.runtime.migration] Contexte impl IRISImpl.
INFO  [alembic.runtime.migration] Cela suppose un DDL non transactionnel.
INFO  [alembic.autogenerate.compare] Détection du tableau "todo" ajouté
INFO  [alembic.autogenerate.compare] Détection d'un index ajouté 'ix_todo_id' sur '['id']'
INFO  [alembic.autogenerate.compare] Détection d'un index ajouté 'ix_todo_title' sur '['title']'
  Generating /usr/src/app/app/migrations/versions/1e4d3b4d51ca_.py ... exécuté
root@7bf903cd2721:/usr/src/app# 
 

Let's see generated migration

Maintenant il faut appliquer ceci à la base de données, avec la commande alembic upgrade head, où "head" est un mot-clé pour mettre à jour vers la dernière version.

root@7bf903cd2721:/usr/src/app# alembic upgrade head
INFO  [alembic.runtime.migration] Contexte impl IRISImpl.
INFO  [alembic.runtime.migration] Cela suppose un DDL non transactionnel.
INFO  [alembic.runtime.migration] Exécution de la mise à jour -> 1e4d3b4d51ca, message vide
 

Rétrogradation

Vérifiez l'état actuel à tout moment, ce qui vous donnera des informations sur les migrations manquantes.

root@7bf903cd2721:/usr/src/app# alembic check
INFO  [alembic.runtime.migration] Contexte impl IRISImpl.
INFO  [alembic.runtime.migration] Cela suppose un DDL non transactionnel.
Aucune nouvelle opération de mise à jour détectée.

Accessibilité des données

Donc, nous pouvons maintenant retourner au REST, et il nous faut le faire fonctionner, quitter le conteneur actuel et lancer le service d'application comme d'habitude maintenant, uvicorn a un drapeau --reload, donc, il vérifiera les changements dans les fichiers python et redémarrera lorsque nous les changerons.

$ docker-compose up app
[+] Running 2/0
 ✔ Conteneur fastapi-iris-demo-iris-1 Lancé                                                                                                                                                        0.0s 
 ✔ Conteneur fastapi-iris-demo-app-1   Crée                                                                                                                                                       0.0s 
Attaching to fastapi-iris-demo-app-1, fastapi-iris-demo-iris-1
fastapi-iris-demo-app-1   | INFO  [alembic.runtime.migration] Contexte impl IRISImpl.
fastapi-iris-demo-app-1   | INFO  [alembic.runtime.migration] Cela suppose un DDL non transactionnel.
fastapi-iris-demo-app-1   | INFO:     Surveillance des modifications apportées aux répertoires : ['/usr/src/app']
fastapi-iris-demo-app-1   | INFO:     Uvicorn lancé sur http://0.0.0.0:8000 (Appuyez sur CTRL+C pour quitter)
fastapi-iris-demo-app-1   | INFO:     Lancement du processus de rechargement [8] à l'aide de StatReload
fastapi-iris-demo-app-1   | INFO:     Lancement du processus de serveur [10]
fastapi-iris-demo-app-1   | INFO:     En attente du démarrage de l'application.
fastapi-iris-demo-app-1   | INFO:     Démarrage de l'application achevé.

FastAPI utilise le projet pydantic, pour déclarer le schéma de données, et nous en avons besoin aussi, créons app/schemas.py, les mêmes colonnes que dans models.py mais sous une forme simple en Python

from pydantic import BaseModel


class TodoCreate(BaseModel):
    title: str
    description: str


class Todo(TodoCreate):
    id: int

    class Config:
        from_attributes = True

Déclaration des opérations crud dans app/crud.py, où nous travaillons avec la base de données en utilisant l'ORM de SQLAlchemy.

from sqlalchemy.orm import Session
from . import models, schemas


def get_todos(db: Session, skip: int = 0, limit: int = 100):
    return db.query(models.Todo).offset(skip).limit(limit).all()


def create_todo(db: Session, todo: schemas.TodoCreate):
    db_todo = models.Todo(**todo.dict())
    db.add(db_todo)
    db.commit()
    db.refresh(db_todo)
    return db_todo

Pour finir, nous pouvons mettre à jour notre app/main.py, et ajouter des itinéraires pour lire et créer des todos.

from fastapi import FastAPI, Depends
from .db import init_db, get_session
from . import crud, schemas

app = FastAPI(
    title='TODO Application',
    version='1.0.0',
)


@app.on_event("startup")
def on_startup():
    init_db()


@app.get("/ping")
async def pong():
    return {"ping": "pong!"}


@app.get("/todo", response_model=list[schemas.Todo])
async def read_todos(skip: int = 0, limit: int = 100, session=Depends(get_session)):
    todos = crud.get_todos(session, skip=skip, limit=limit)
    return todos


@app.post("/todo", response_model=schemas.Todo)
async def create_todo(todo: schemas.TodoCreate, session=Depends(get_session)):
    return crud.create_todo(db=session, todo=todo)

La page de documentation "docs" a été mise à jour en conséquence, et nous pouvons maintenant jouer avec.

 

Essayez !

Vérifions-le dans IRIS

─$ docker-compose exec iris irissqlcli iris+emb:///
Serveur:  IRIS pour UNIX (Ubuntu Server LTS pour les conteneurs "ARM64 Containers") 2023.2 (Build 227U) Mon Jul 31 2023 17:43:25 EDT
Version: 0.5.4
[SQL]irisowner@/usr/irissys/:USER> .tables
+-------------------------+
| TABLE_NAME              |
+-------------------------+
| SQLUser.alembic_version |
| SQLUser.todo            |
+-------------------------+
Temps: 0.043s
[SQL]irisowner@/usr/irissys/:USER> select * from todo
+----+-------+---------------------+
| id | titre | description         |
+----+-------+---------------------+
| 1  | démo  | cela marche vraiment |
+----+-------+---------------------+
1 rang dans le jeu
Temps: 0.004s
[SQL]irisowner@/usr/irissys/:USER> select * from alembic_version
+--------------+
| version_num  |
+--------------+
| 1e4d3b4d51ca |
+--------------+
1 rang dans le jeu
Temps: 0.045s
[SQL]irisowner@/usr/irissys/:USER>

 

J'espère que vous avez apprécié la facilité d'utilisation de Python et de FastAPI pour la création de REST. Le code source de ce projet est disponible sur github https://github.com/caretdev/fastapi-iris-demo

Discussion (0)1
Connectez-vous ou inscrivez-vous pour continuer