first comit
This commit is contained in:
407
README.md
Normal file
407
README.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user