Files
ENI-PythonAdvanced_08/README.md
2025-12-17 09:37:56 +01:00

408 lines
13 KiB
Markdown

# Démonstration d'API GraphQL
## 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 GraphQL, utilisez la commande :
```bash
# En étant à la racine du projet
cd src
uvicorn app.main:app --host 0.0.0.0 --port 8004
```
Ouvrez Appolo Studio à l'adresse suivante :git push -u origin main
https://studio.apollographql.com/sandbox/explorer/
Bien rentrer dans l'outil l'URL complète :
> http://127.0.0.1:8004/graphql
À noter que vous pouvez aussi utiliser l'interface incluse GraphiQL en vous rendant à l'adresse : http://127.0.0.1:8004/graphql
## Instructions
Vous allez devoir créer les fichiers manquants pour faire fonctionner l'application. Suivez attentivement les étapes.
### Vue d'ensemble de l'architecture
Avant de commencer, voici un schéma illustrant comment les différentes parties que vous allez créer interagissent :
```mermaid
graph TD
A["Client Apollo/GraphiQL"] -- "Requete GraphQL" --> B{"Serveur Uvicorn/Strawberry"};
B -- "Traite la requete" --> C[Query / Mutation];
C -- "Appelle le bon resolver" --> D["Resolver ex: resolve_recipe_by_id"];
D -- "Demande les donnees" --> E["Service recipe_service"];
E -- "Recupere/Ecrit les donnees" --> F["Repository recipe_repository"];
F -- "Interagit" --> G["Base de Donnees 'simulee'"];
```
-----
### Étape 1 : les Modèles Métier (Pydantic)
Ce sont les représentations internes de nos données. Elles utilisent Pydantic pour la validation.
1. Créez le fichier `app/models/ingredient.py` et ajoutez-y le code suivant :
```python
# Fichier: app/models/ingredient.py
from pydantic import BaseModel
class Ingredient(BaseModel):
name: str
quantity: str
```
2. Créez le fichier `app/models/recipe.py` et ajoutez-y ce code :
```python
# Fichier: app/models/recipe.py
from pydantic import BaseModel
from typing import List
from .ingredient import Ingredient
class Recipe(BaseModel):
id: int
title: str
description: str
ingredients: List[Ingredient]
```
---
### Étape 2 : les types GraphQL (Strawberry)
Ce sont les "schémas" que notre API va exposer aux clients. Ils décrivent la forme des données que l'on peut demander.
1. Créez le dossier `app/graphql/types/`.
2. Créez le fichier `app/graphql/types/ingredient.py` :
```python
# Fichier: app/graphql/types/ingredient.py
import strawberry
@strawberry.type
class Ingredient:
name: str
quantity: str
```
3. Créez le fichier `app/graphql/types/recipe.py` :
```python
# Fichier: app/graphql/types/recipe.py
import strawberry
from typing import List
from .ingredient import Ingredient
@strawberry.type
class Recipe:
id: strawberry.ID
title: str
description: str
ingredients: List[Ingredient]
```
---
### Étape 3 : la logique métier (BLL / Services)
Le service fait le lien entre la couche API (les resolvers que nous verrons après) et la couche de données (le repository qui est déjà fourni).
1. Créez le dossier `app/services/` si nécessaire.
2. Créez le fichier `app/services/recipe_service.py` et ajoutez-y les fonctions suivantes :
```python
# Fichier: app/services/recipe_service.py
from typing import List, Dict, Any
from app.models.recipe import Recipe
from app.repositories.recipe_repository import recipe_repository
from app.core.exceptions import NotFoundBLLException, ValidationBLLException
def get_all_recipes() -> List[Recipe]:
"""Récupère toutes les recettes."""
return recipe_repository.list()
def get_recipe_by_id(recipe_id: int) -> Recipe:
"""
Récupère une recette par son ID.
Lève une exception si la recette n'est pas trouvée.
"""
recipe = recipe_repository.find_by_id(recipe_id)
if not recipe:
raise NotFoundBLLException(resource_name="Recipe", resource_id=recipe_id)
return recipe
def add_new_recipe(title: str, description: str, ingredients: List[Dict[str, Any]]) -> Recipe:
"""
Ajoute une nouvelle recette après validation.
"""
if not title or not title.strip():
raise ValidationBLLException("Le titre de la recette ne peut pas être vide.")
if not ingredients:
raise ValidationBLLException("Une recette doit contenir au moins un ingrédient.")
recipe_data = {
"title": title,
"description": description,
"ingredients": ingredients
}
return recipe_repository.create(recipe_data)
```
---
### Étape 4 : les Inputs GraphQL (pour les Mutations)
Quand on veut envoyer des données complexes au serveur (comme pour créer une nouvelle recette), on utilise des `inputs`.
1. Créez le dossier `app/graphql/inputs/`.
2. Créez le fichier `app/graphql/inputs/ingredient_input.py`:
```python
# Fichier: app/graphql/inputs/ingredient_input.py
import strawberry
@strawberry.input
class IngredientInput:
name: str
quantity: str
```
3. Créez le fichier `app/graphql/inputs/recipe_input.py`:
```python
# Fichier: app/graphql/inputs/recipe_input.py
import strawberry
from typing import List
from .ingredient_input import IngredientInput
@strawberry.input
class AddRecipeInput:
title: str
description: str
ingredients: List[IngredientInput]
```
### Comprendre la différence : Model vs Type vs Input
Vous avez maintenant créé trois types de classes qui se ressemblent. Voici un schéma pour clarifier leurs rôles distincts :
```mermaid
graph TD
subgraph "Modele Metier 'Pydantic'"
A["class Recipe(BaseModel)<br><b>Role:</b> Validation et logique interne<br><b>Utilise par:</b> Services, Repository"]
end
subgraph "Schema GraphQL 'Strawberry'"
B["class Recipe(strawberry.type)<br><b>Role:</b> Definir ce qu'on peut lire 'Query'<br><b>Utilise pour:</b> Retourner des donnees"]
C["class AddRecipeInput(strawberry.input)<br><b>Role:</b> Definir ce qu'on peut ecrire 'Mutation'<br><b>Utilise pour:</b> Recevoir des donnees"]
end
E[Service] -- "Manipule et retourne un" --> A
A -- "Est converti en" --> B
B -- "Envoye au Client" --> D[Client GraphQL]
D -- "Envoie un" --> C
C -- "Est utilise par le" --> F[Resolver]
F -- "Pour appeler le" --> E
```
-----
### Étape 5 : les resolvers
Le **resolver** est la fonction qui "résout" une requête. C'est le cœur de notre API : il prend une demande GraphQL et appelle le service approprié pour obtenir les données.
1. Créez le dossier `app/graphql/resolvers/`.
2. Créez le fichier `app/graphql/resolvers/recipe_resolvers.py` et remplissez-le :
```python
# Fichier: app/graphql/resolvers/recipe_resolvers.py
import strawberry
from typing import List, Optional
from app.graphql.types.recipe import Recipe
from app.graphql.inputs.recipe_input import AddRecipeInput
import app.services.recipe_service as recipe_service
def resolve_recipes() -> List[Recipe]:
"""Resolver pour obtenir la liste de toutes les recettes."""
return recipe_service.get_all_recipes()
def resolve_recipe_by_id(id: strawberry.ID) -> Optional[Recipe]:
"""Resolver pour obtenir une recette par son ID."""
return recipe_service.get_recipe_by_id(recipe_id=int(id))
def resolve_add_recipe(input: AddRecipeInput) -> Recipe:
"""Resolver pour ajouter une nouvelle recette."""
ingredients_data = [
{"name": ing.name, "quantity": ing.quantity}
for ing in input.ingredients
]
return recipe_service.add_new_recipe(
title=input.title,
description=input.description,
ingredients=ingredients_data
)
```
---
### Étape 6 : assemblage final (Queries & Mutations)
Maintenant, nous allons déclarer les points d'entrée de notre API en les liant aux resolvers que nous venons de créer.
1. Créez le fichier `app/graphql/queries.py` :
```python
# Fichier: app/graphql/queries.py
import strawberry
from typing import List, Optional
from app.graphql.types.recipe import Recipe
from app.graphql.resolvers.recipe_resolvers import resolve_recipes, resolve_recipe_by_id
@strawberry.type
class Query:
recipes: List[Recipe] = strawberry.field(
resolver=resolve_recipes,
description="Récupère la liste de toutes les recettes de cuisine."
)
recipe: Optional[Recipe] = strawberry.field(
resolver=resolve_recipe_by_id,
description="Récupère une recette par son ID."
)
```
2. Créez le fichier `app/graphql/mutations.py` :
```python
# Fichier: app/graphql/mutations.py
import strawberry
from app.graphql.types.recipe import Recipe
from app.graphql.inputs.recipe_input import AddRecipeInput
from app.graphql.resolvers.recipe_resolvers import resolve_add_recipe
@strawberry.type
class Mutation:
addRecipe: Recipe = strawberry.field(
resolver=resolve_add_recipe,
description="Ajoute une nouvelle recette au livre de cuisine."
)
```
Vous avez créé tous les fichiers nécessaires. L'application est maintenant complète.
### 7. Requêtes GraphQL via Appolo Studio
En s'assurant que votre serveur est bien démarré, vous pouvez éxécuter les requêtes GraphQL suivantes pour tester l'API :
#### Flux d'une requête (Query)
Voici ce qui se passe lorsque vous exécutez la requête `recipe(id: "1")` :
```mermaid
sequenceDiagram
actor Client
participant API as "GraphQL 'main.py'"
participant Query as "Query 'queries.py'"
participant Resolver as "Resolver 'recipe_resolvers.py'"
participant Service as "Service 'recipe_service.py'"
participant Repo as "Repository 'recipe_repository.py'"
Client->>+API: query { recipe(id: "1") }
API->>+Query: Trouve le champ "recipe"
Query->>+Resolver: Appelle "resolve_recipe_by_id(id='1')"
Resolver->>+Service: Appelle "get_recipe_by_id(recipe_id=1)"
Service->>+Repo: Appelle "find_by_id(1)"
Repo-->>-Service: Retourne "Recipe Model Pydantic"
Service-->>-Resolver: Retourne "Recipe Model Pydantic"
Resolver-->>-Query: Retourne le modele
Query-->>-API: Construit la reponse JSON
API-->>-Client: Reponse JSON
```
#### Flux d'une requête (Mutation)
Et voici ce qui se passe lors d'une mutation `addRecipe` :
```mermaid
sequenceDiagram
actor Client
participant API as "GraphQL 'main.py'"
participant Mutation as "Mutation 'mutations.py'"
participant Resolver as "Resolver 'recipe_resolvers.py'"
participant Service as "Service 'recipe_service.py'"
participant Repo as "Repository 'recipe_repository.py'"
Client->>+API: mutation { addRecipe(input: {...}) }
API->>+Mutation: Trouve le champ "addRecipe"
Mutation->>+Resolver: Appelle "resolve_add_recipe(input=InputObject)"
Resolver->>Resolver: Convertit "InputObject" en "dict"
Resolver->>+Service: Appelle "add_new_recipe(title, ...)"
Service->>Service: Valide les donnees "ex: titre non vide"
Service->>+Repo: Appelle "create(recipe_data)"
Repo-->>-Service: Retourne nouvelle "Recipe Model Pydantic"
Service-->>-Resolver: Retourne "Recipe Model Pydantic"
Resolver-->>-Mutation: Retourne le modele
Mutation-->>-API: Construit la reponse JSON
API-->>-Client: Reponse JSON
```
#### Exemples de requêtes
```graphql
query GetAllRecipes {
recipes {
id
title
description
}
}
```
```graphql
query GetRecipeById {
recipe(id: "1") {
id
title
description
ingredients {
name
quantity
}
}
}
```
```graphql
query GetNonExistentRecipe {
recipe(id: "99") {
id
title
}
}
```
```graphql
mutation AddNewRecipe {
addRecipe(
input: {
title: "Salade César"
description: "Une salade fraîche et croquante."
ingredients: [
{ name: "Laitue romaine", quantity: "1" }
{ name: "Poulet grillé", quantity: "150g" }
{ name: "Croûtons", quantity: "1 tasse" }
{ name: "Parmesan", quantity: "50g" }
]
}
) {
id
title
description
}
}
```
Vous pouvez si vous le souhaitez ajouter d'autres recettes via la mutation `addRecipe` et vérifier qu'elles apparaissent bien dans la liste retournée par la requête `recipes`.