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")asyncdefpong():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.
.png)
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
alembic upgrade head
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")
ifnot 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)
definit_db():
engine.connect()
defget_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
classTodo(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
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)
if config.config_file_name isnotNone:
fileConfig(config.config_file_name)
from app.models import Base
target_metadata = Base.metadata
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
Ceci indique qu'il y a un nouveau tableau todo, et tous les index, et que cela génère un fichier, nous pouvons regarder ce fichier, pas besoin de l'éditer, mais nous pouvons voir qu'il contient des fonctions de mise à niveau et de rétrogradation
upgrade et
downgrade.
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs"><span class="hljs-string">"""empty message
Revision ID: 1e4d3b4d51ca
Revise:
Date de création: 2023-08-22 07:08:01.586330
"""
from alembic import op
import sqlalchemy as sa
revision = '1e4d3b4d51ca'
down_revision = None
branch_labels = None
depends_on = Nonedefupgrade() -> None:
op.create_table('todo',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_todo_id'), 'todo', ['id'], unique=False)
op.create_index(op.f('ix_todo_title'), 'todo', ['title'], unique=False)
defdowngrade() -> None:
op.drop_index(op.f('ix_todo_title'), table_name='todo')
op.drop_index(op.f('ix_todo_id'), table_name='todo')
op.drop_table('todo')
<p>
</p>
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
Si, au cours de la mise à jour de l'application, nous découvrons que nous devons revenir en arrière, nous pouvons rétrograder la base de données, par exemple la dernière révision sera
head-1.
<pre>root@7bf903cd2721:/usr/src/app# alembic downgrade head-1
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 rétrogradation 1e4d3b4d51ca -> , message vide
<p>
et pour rétrograder complètement vers un état vide, utilisez le mot-clé <strong>base</strong>
</p>
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
classTodoCreate(BaseModel):
title: str
description: str
classTodo(TodoCreate):
id: int
classConfig:
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
defget_todos(db: Session, skip: int = 0, limit: int = 100):return db.query(models.Todo).offset(skip).limit(limit).all()
defcreate_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")defon_startup():
init_db()
@app.get("/ping")asyncdefpong():return {"ping": "pong!"}
@app.get("/todo", response_model=list[schemas.Todo])asyncdefread_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)asyncdefcreate_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.
.png)
Ajouter une nouvelle todo
<p>
<img src="/sites/default/files/inline/images/images/image(6813).png" />
</p>
<p>
Et vérifiez ce que nous avons ici
</p>
<p>
<img src="/sites/default/files/inline/images/images/image(6814).png" />
</p>
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