356 lines
11 KiB
Markdown
356 lines
11 KiB
Markdown
# Démonstration d'API REST
|
|
|
|
## Installation
|
|
|
|
Ouvrir un terminal dans le dossier racine du projet :
|
|
|
|
```bash
|
|
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 :
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```mermaid
|
|
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`
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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) :
|
|
```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) :
|
|
```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) :
|
|
```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) :
|
|
```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.
|