11 KiB
Démonstration d'API REST
Installation
Ouvrir un terminal dans le dossier racine du projet :
poetry install # installer les dépendances nécessaires
Pensez également à marquer le dossier "src" comme "Source Root" (via clic droit "Mark Directory as ...") dans l'IDE PyCharm, pour éviter des problèmes d'importation.
Pour démarrer le serveur REST, utilisez la commande :
# En étant à la racine du projet
cd src
uvicorn app.main:app --reload --host 0.0.0.0 --port 8006
Architecture du projet
L'arborescence du projet vous est déjà donnée. Elle sépare clairement les responsabilités :
app/api/routers/: [couche API] contient les endpoints de l'API (la couche de présentation).app/services/: [couche BLL] contient la logique métier (validation, orchestration).app/repositories/: [couche DAL] contient la logique d'accès aux données (ici, nos dictionnaires).app/schemas/: [couche API] définit la structure des données de l'API avec Pydantic.app/db/: [BDD] simule notre base de données.app/core/: fichiers de configuration et exceptions.app/main.py: le point d'entrée de l'application.
Diagramme des couches
Ce diagramme montre comment les couches dépendent les unes des autres. Une couche ne doit communiquer qu'avec la couche directement inférieure.
graph TD
A["Couche API<br>(app/api/routers, app/schemas)"] --> B["Couche BLL<br>(app/services)"]
B --> C["Couche DAL<br>(app/repositories)"]
C --> D["Base de Données<br>(app/db)"]
Exemple de flux de données (Requête POST /recipes/)
Ce diagramme montre comment une requête HTTP traverse les différentes couches pour créer une nouvelle recette.
sequenceDiagram
participant Client as "Client (Postman)"
participant API as "Couche API (Router)"
participant BLL as "Couche BLL (Service)"
participant DAL as "Couche DAL (Repository)"
participant DB as "Base de Données (DB)"
Client->>+API: "POST /api/v1/recipes/ (JSON body)"
API->>+BLL: "Appel service.create_recipe(recipe_data)"
BLL->>BLL: 1. Validation des règles métier (temps > 0, etc.)
BLL->>+BLL: "2. Validation auteur (via author_service)"
BLL-->>-BLL: "(Auteur valide)"
BLL->>+DAL: "3. Appel repository.create_recipe(db_data)"
DAL->>+DB: Ajoute la recette au dictionnaire
DB-->>-DAL: "(OK)"
DAL-->>-BLL: "(Retourne la recette brute avec id)"
BLL->>+BLL: "4. Enrichissement (via get_recipe_by_id)"
BLL-->>-API: "(Retourne la recette complète)"
API-->>-Client: "201 CREATED (JSON recette complète)"
Instructions
Vous allez devoir créer les fichiers manquants pour faire fonctionner l'application, ou compléter les TODO.
Suivez attentivement les étapes.
Étape 1 : compléter la couche repository (DAL / accès aux données)
Les repositories sont responsables de la lecture et de l'écriture des données dans notre "fausse" base de données (local_db.py).
app/repositories/author.py
from app.db import local_db
from app.schemas.author import AuthorCreate
def get_author(author_id: int):
"""Récupère un auteur par son ID depuis notre DB locale."""
# TODO: Retourner l'auteur depuis le dictionnaire DB_AUTHORS en utilisant .get()
return # ... votre code ici ...
def get_authors():
"""Récupère tous les auteurs."""
# TODO: Retourner la liste de toutes les valeurs du dictionnaire DB_AUTHORS
return # ... votre code ici ...
def create_author(author: AuthorCreate):
"""Crée un nouvel auteur dans la DB locale."""
# TODO:
# 1. Obtenir un nouvel ID avec local_db.get_next_author_id()
# 2. Créer un dictionnaire pour le nouvel auteur
db_author = {"id": new_id, **author.model_dump()}
# 3. Ajouter ce dictionnaire à DB_AUTHORS
# 4. Retourner le dictionnaire du nouvel auteur
return db_author
app/repositories/recipe.py
from app.db import local_db
from app.schemas.recipe import RecipeCreate
def get_recipe(recipe_id: int):
"""Récupère une recette par son ID."""
# TODO: Retourner la recette depuis le dictionnaire DB_RECIPES
return # ... votre code ici ...
def get_recipes():
"""Récupère toutes les recettes."""
return list(local_db.DB_RECIPES.values())
def create_recipe(recipe: RecipeCreate):
"""Crée une nouvelle recette."""
# TODO:
# 1. Obtenir un nouvel ID
# 2. Créer un dictionnaire pour la nouvelle recette
db_recipe = {"id": new_id, **recipe.model_dump()}
# 3. Ajouter ce dictionnaire à DB_RECIPES
# 4. Retourner le dictionnaire créé
return db_recipe
Étape 2 : compléter la couche Service (Logique métier)
Les services orchestrent les appels aux repositories et appliquent les règles métier (validation, etc.).
app/services/author.py
from app.repositories import author as author_repo
from app.schemas.author import AuthorCreate
from app.core.exceptions import NotFoundBLLException
def get_author_by_id(author_id: int):
"""Service pour récupérer un auteur par son ID."""
# TODO:
# 1. Appeler le repository pour obtenir l'auteur
author = author_repo.get_author(author_id)
# 2. Si l'auteur n'est pas trouvé, lever une exception NotFoundBLLException
# 3. Sinon, retourner l'auteur
return author
def get_authors():
"""Service pour récupérer tous les auteurs."""
# TODO: Appeler simplement la fonction correspondante du repository
return # ... votre code ici ...
def create_author(author: AuthorCreate):
"""Service pour créer un auteur."""
# TODO: Appeler simplement la fonction correspondante du repository
return # ... votre code ici ...
app/services/recipe.py
from app.repositories import recipe as recipe_repo
from app.services import author as author_service
from app.schemas.recipe import RecipeCreate
from app.core.exceptions import NotFoundBLLException, ValidationBLLException
def get_recipe_by_id(recipe_id: int):
"""Service pour récupérer une recette par ID avec son auteur."""
# TODO:
# 1. Récupérer la recette "brute" depuis le repository
# ... votre code ici ...
# 2. Si elle n'existe pas, lever une NotFoundBLLException
if db_recipe is None:
raise NotFoundBLLException(resource_name="Recette", resource_id=recipe_id)
# 3. Récupérer les informations de l'auteur via le service des auteurs
author = # ... votre code ici ...
# 4. Retourner un dictionnaire combinant les infos de la recette et l'objet auteur
return {**db_recipe, "author": author}
def get_recipes():
"""Service pour récupérer la liste complète des recettes."""
# TODO:
# 1. Récupérer toutes les recettes "brutes" depuis le repository
all_recipes_raw = # ... votre code ici ...
# 2. Pour chaque recette, appeler get_recipe_by_id pour l'enrichir avec son auteur
return [get_recipe_by_id(recipe["id"]) for recipe in all_recipes_raw]
def create_recipe(recipe: RecipeCreate):
"""Service pour créer une nouvelle recette."""
# TODO: Implémenter les règles métier
# 1. Le nom de la recette ne doit pas être vide
if not recipe.name.strip():
raise ValidationBLLException("Le nom de la recette ne peut pas être vide.")
# 2. Le temps de cuisson doit être positif
if recipe.cooking_time_minutes <= 0:
raise ValidationBLLException("Le temps de cuisson doit être positif.")
# 3. Valider que l'auteur existe en appelant le service des auteurs
# ... votre code ici ...
# 4. Si tout est valide, appeler le repository pour créer la recette
# ... votre code ici ...
# 5. Retourner la version complète de la recette créée (en appelant get_recipe_by_id)
return get_recipe_by_id(new_recipe_raw["id"])
Étape 3 : compléter la couche API (Routers)
Les routers exposent les fonctionnalités de nos services via des endpoints HTTP.
app/api/routers/authors.py
from typing import List
from fastapi import APIRouter, status
from app.schemas.author import AuthorRead, AuthorCreate
from app.services import author as author_service
router = APIRouter()
@router.get("/authors/", response_model=List[AuthorRead])
def read_authors():
"""Récupère la liste de tous les auteurs."""
# TODO: Appeler le service pour obtenir tous les auteurs
# ... votre code ici ...
@router.post("/authors/", response_model=AuthorRead, status_code=status.HTTP_201_CREATED)
def create_author(author: AuthorCreate):
"""Crée un nouvel auteur."""
return author_service.create_author(author=author)
app/api/routers/recipes.py
from typing import List
from fastapi import APIRouter, status
from app.schemas.recipe import RecipeRead, RecipeCreate
from app.services import recipe as recipe_service
router = APIRouter()
@router.get("/recipes/", response_model=List[RecipeRead])
def read_recipes():
"""Récupère la liste de toutes les recettes."""
# TODO: Appeler le service pour obtenir toutes les recettes
# ... votre code ici ...
@router.get("/recipes/{recipe_id}", response_model=RecipeRead)
def read_recipe(recipe_id: int):
"""Récupère les détails d'une recette par son ID."""
return recipe_service.get_recipe_by_id(recipe_id=recipe_id)
@router.post("/recipes/", response_model=RecipeRead, status_code=status.HTTP_201_CREATED)
def create_recipe(recipe: RecipeCreate):
"""Crée une nouvelle recette."""
return recipe_service.create_recipe(recipe=recipe)
Étape 4 : lancer et tester
Avec PostMan, vous allez pouvoir lancer les requêtes contre votre API REST locale.
Exemple 1 : créer un auteur
- Méthode :
POST - URL :
http://127.0.0.1:8006/api/v1/authors/ - Body (JSON) :
{
"last_name": "Oliver",
"first_name": "Jamie"
}
Exemple 2 : créer une recette
- Méthode :
POST - URL :
http://127.0.0.1:8006/api/v1/recipes/ - Body (JSON) :
{
"name": "Poulet basquaise",
"instructions": "1. Faire dorer le poulet. 2. Ajouter les poivrons et les tomates. 3. Laisser mijoter.",
"cooking_time_minutes": 75,
"author_id": 2,
"ingredients": [
"poulet",
"poivrons",
"tomates",
"oignons",
"vin blanc"
]
}
Exemple 3 : créer une recette avec un auteur inexistant
- Méthode :
POST - URL :
http://127.0.0.1:8006/api/v1/recipes/ - Body (JSON) :
{
"name": "Recette test",
"instructions": "Instructions...",
"cooking_time_minutes": 20,
"author_id": 99,
"ingredients": ["ingrédient 1"]
}
Exemple 4 : créer une recette avec violation de règle métier
- Méthode :
POST - URL :
http://127.0.0.1:8006/api/v1/recipes/ - Body (JSON) :
{
"name": "Recette invalide",
"instructions": "Instructions...",
"cooking_time_minutes": -10,
"author_id": 1,
"ingredients": ["ingrédient 1"]
}
Autres exemples
Essayez par vous-même les autres cas de figure :
- Récupérer tous les auteurs
- Récupérer toutes les recettes
- Récupérer une recette par son ID
- Créer des recettes avec des données valides ou invalides
- etc.