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

13 KiB

Démonstration d'API GraphQL

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 GraphQL, utilisez la commande :

# 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 :

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 :

    # 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 :

    # 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 :

    # 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 :

    # 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 :
    # 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:

    # 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:

    # 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 :

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 :
    # 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 :

    # 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 :

    # 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") :

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 :

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

query GetAllRecipes {
  recipes {
    id
    title
    description
  }
}
query GetRecipeById {
  recipe(id: "1") {
    id
    title
    description
    ingredients {
      name
      quantity
    }
  }
}
query GetNonExistentRecipe {
  recipe(id: "99") {
    id
    title
  }
}
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.