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
PythonPython
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
Shell SessionShell Session
Et maintenant, nous pouvons installer nos dépendances
pip install -r requirements.txt
Shell SessionShell Session
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!"}
PythonPython
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" ]
DockerfileDockerfile
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 "$@"
BashBash
N'oubliez pas d'ajouter un drapeau d'exécution
chmod +x entrypoint.sh
Shell SessionShell Session
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
YAMLYAML
Construisons-le
docker-compose build
Shell SessionShell Session
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
PythonPython
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)
PythonPython
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
PythonPython
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
PythonPython
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
PythonPython
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)
PythonPython
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