Initial commit
This commit is contained in:
0
src/app/__init__.py
Normal file
0
src/app/__init__.py
Normal file
BIN
src/app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/__pycache__/main.cpython-313.pyc
Normal file
BIN
src/app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
0
src/app/core/__init__.py
Normal file
0
src/app/core/__init__.py
Normal file
BIN
src/app/core/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/core/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/core/__pycache__/config.cpython-313.pyc
Normal file
BIN
src/app/core/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/core/__pycache__/exceptions.cpython-313.pyc
Normal file
BIN
src/app/core/__pycache__/exceptions.cpython-313.pyc
Normal file
Binary file not shown.
15
src/app/core/config.py
Normal file
15
src/app/core/config.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from typing import List
|
||||
from pydantic import AnyHttpUrl
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=True
|
||||
)
|
||||
|
||||
PROJECT_NAME: str = "Recipe Book GraphQL API"
|
||||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||
|
||||
settings = Settings()
|
||||
24
src/app/core/exceptions.py
Normal file
24
src/app/core/exceptions.py
Normal file
@@ -0,0 +1,24 @@
|
||||
class BaseAppException(Exception):
|
||||
"""Exception de base pour l'application."""
|
||||
pass
|
||||
|
||||
class DALException(BaseAppException):
|
||||
"""Exception levée pour les erreurs de la couche d'accès aux données (DAL)."""
|
||||
def __init__(self, message: str, original_exception: Exception = None):
|
||||
self.message = message
|
||||
self.original_exception = original_exception
|
||||
super().__init__(self.message)
|
||||
|
||||
class BLLException(BaseAppException):
|
||||
"""Exception de base pour les erreurs de la couche métier (BLL)."""
|
||||
pass
|
||||
|
||||
class NotFoundBLLException(BLLException):
|
||||
"""Levée lorsqu'une ressource n'est pas trouvée."""
|
||||
def __init__(self, resource_name: str, resource_id: int | str):
|
||||
message = f"{resource_name} avec l'ID '{resource_id}' non trouvé."
|
||||
super().__init__(message)
|
||||
|
||||
class ValidationBLLException(BLLException):
|
||||
"""Levée pour les erreurs de validation des règles métier."""
|
||||
pass
|
||||
0
src/app/graphql/__init__.py
Normal file
0
src/app/graphql/__init__.py
Normal file
BIN
src/app/graphql/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/graphql/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/graphql/__pycache__/extensions.cpython-313.pyc
Normal file
BIN
src/app/graphql/__pycache__/extensions.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/graphql/__pycache__/mutations.cpython-313.pyc
Normal file
BIN
src/app/graphql/__pycache__/mutations.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/graphql/__pycache__/queries.cpython-313.pyc
Normal file
BIN
src/app/graphql/__pycache__/queries.cpython-313.pyc
Normal file
Binary file not shown.
12
src/app/graphql/extensions.py
Normal file
12
src/app/graphql/extensions.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from strawberry.extensions import Extension
|
||||
from app.core.exceptions import BLLException
|
||||
|
||||
class BusinessLogicErrorExtension(Extension):
|
||||
def on_request_end(self):
|
||||
for error in self.execution_context.errors:
|
||||
original_error = error.original_error
|
||||
if isinstance(original_error, BLLException):
|
||||
error.message = f"[Business Error] {original_error}"
|
||||
if not error.extensions:
|
||||
error.extensions = {}
|
||||
error.extensions['code'] = original_error.__class__.__name__
|
||||
0
src/app/graphql/inputs/__init__.py
Normal file
0
src/app/graphql/inputs/__init__.py
Normal file
BIN
src/app/graphql/inputs/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/graphql/inputs/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/app/graphql/inputs/__pycache__/recipe_input.cpython-313.pyc
Normal file
BIN
src/app/graphql/inputs/__pycache__/recipe_input.cpython-313.pyc
Normal file
Binary file not shown.
6
src/app/graphql/inputs/ingredient_input.py
Normal file
6
src/app/graphql/inputs/ingredient_input.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import strawberry
|
||||
|
||||
@strawberry.input
|
||||
class IngredientInput:
|
||||
name: str
|
||||
quantity: str
|
||||
9
src/app/graphql/inputs/recipe_input.py
Normal file
9
src/app/graphql/inputs/recipe_input.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import strawberry
|
||||
from typing import List
|
||||
from .ingredient_input import IngredientInput
|
||||
|
||||
@strawberry.input
|
||||
class AddRecipeInput:
|
||||
title: str
|
||||
description: str
|
||||
ingredients: List[IngredientInput]
|
||||
16
src/app/graphql/mutations.py
Normal file
16
src/app/graphql/mutations.py
Normal file
@@ -0,0 +1,16 @@
|
||||
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:
|
||||
"""
|
||||
Point d'entrée pour toutes les requêtes d'écriture.
|
||||
"""
|
||||
|
||||
addRecipe: Recipe = strawberry.field(
|
||||
resolver=resolve_add_recipe,
|
||||
description="Ajoute une nouvelle recette au livre de cuisine."
|
||||
)
|
||||
21
src/app/graphql/queries.py
Normal file
21
src/app/graphql/queries.py
Normal file
@@ -0,0 +1,21 @@
|
||||
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:
|
||||
"""
|
||||
Point d'entrée pour toutes les requêtes de lecture.
|
||||
"""
|
||||
|
||||
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."
|
||||
)
|
||||
0
src/app/graphql/resolvers/__init__.py
Normal file
0
src/app/graphql/resolvers/__init__.py
Normal file
BIN
src/app/graphql/resolvers/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/graphql/resolvers/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
30
src/app/graphql/resolvers/recipe_resolvers.py
Normal file
30
src/app/graphql/resolvers/recipe_resolvers.py
Normal file
@@ -0,0 +1,30 @@
|
||||
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."""
|
||||
# Strawberry.ID est une chaîne, il faut la convertir en entier
|
||||
return recipe_service.get_recipe_by_id(recipe_id=int(id))
|
||||
|
||||
|
||||
def resolve_add_recipe(input: AddRecipeInput) -> Recipe:
|
||||
"""Resolver pour ajouter une nouvelle recette."""
|
||||
# Conversion des inputs strawberry en dictionnaires simples
|
||||
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
|
||||
)
|
||||
0
src/app/graphql/types/__init__.py
Normal file
0
src/app/graphql/types/__init__.py
Normal file
BIN
src/app/graphql/types/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/graphql/types/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/graphql/types/__pycache__/ingredient.cpython-313.pyc
Normal file
BIN
src/app/graphql/types/__pycache__/ingredient.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/graphql/types/__pycache__/recipe.cpython-313.pyc
Normal file
BIN
src/app/graphql/types/__pycache__/recipe.cpython-313.pyc
Normal file
Binary file not shown.
6
src/app/graphql/types/ingredient.py
Normal file
6
src/app/graphql/types/ingredient.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import strawberry
|
||||
|
||||
@strawberry.type
|
||||
class Ingredient:
|
||||
name: str
|
||||
quantity: str
|
||||
10
src/app/graphql/types/recipe.py
Normal file
10
src/app/graphql/types/recipe.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import strawberry
|
||||
from typing import List
|
||||
from .ingredient import Ingredient
|
||||
|
||||
@strawberry.type
|
||||
class Recipe:
|
||||
id: strawberry.ID
|
||||
title: str
|
||||
description: str
|
||||
ingredients: List[Ingredient]
|
||||
43
src/app/main.py
Normal file
43
src/app/main.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from fastapi import FastAPI
|
||||
from app.core.config import settings
|
||||
from strawberry.fastapi import GraphQLRouter
|
||||
import strawberry
|
||||
import uvicorn
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.graphql.extensions import BusinessLogicErrorExtension
|
||||
from app.graphql.mutations import Mutation
|
||||
from app.graphql.queries import Query
|
||||
|
||||
# Crée l'application FastAPI
|
||||
app = FastAPI(
|
||||
title="Recipe Book GraphQL API",
|
||||
description="API GraphQL pour un livre de recettes de cuisine.",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[str(origin).rstrip('/') for origin in settings.BACKEND_CORS_ORIGINS] if settings.BACKEND_CORS_ORIGINS else ["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Crée le schéma GraphQL
|
||||
schema = strawberry.Schema(
|
||||
query=Query,
|
||||
mutation=Mutation,
|
||||
extensions=[BusinessLogicErrorExtension]
|
||||
)
|
||||
|
||||
# Crée le routeur GraphQL
|
||||
graphql_app = GraphQLRouter(schema)
|
||||
app.include_router(graphql_app, prefix="/graphql")
|
||||
|
||||
@app.get("/", tags=["Root"])
|
||||
def read_root():
|
||||
return {"message": "Bienvenue sur l'API du livre de recettes. Rendez-vous sur /graphql"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||
0
src/app/models/__init__.py
Normal file
0
src/app/models/__init__.py
Normal file
BIN
src/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/models/__pycache__/ingredient.cpython-313.pyc
Normal file
BIN
src/app/models/__pycache__/ingredient.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/models/__pycache__/recipe.cpython-313.pyc
Normal file
BIN
src/app/models/__pycache__/recipe.cpython-313.pyc
Normal file
Binary file not shown.
5
src/app/models/ingredient.py
Normal file
5
src/app/models/ingredient.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Ingredient(BaseModel):
|
||||
name: str
|
||||
quantity: str
|
||||
9
src/app/models/recipe.py
Normal file
9
src/app/models/recipe.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
from .ingredient import Ingredient
|
||||
|
||||
class Recipe(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
description: str
|
||||
ingredients: List[Ingredient]
|
||||
0
src/app/repositories/__init__.py
Normal file
0
src/app/repositories/__init__.py
Normal file
BIN
src/app/repositories/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/repositories/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
61
src/app/repositories/recipe_repository.py
Normal file
61
src/app/repositories/recipe_repository.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from typing import List, Optional, Dict, Any
|
||||
from app.models.recipe import Recipe
|
||||
|
||||
# --- Base de données en mémoire ---
|
||||
_RECIPES_DB = [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Crêpes simples",
|
||||
"description": "Une recette facile pour des crêpes délicieuses.",
|
||||
"ingredients": [
|
||||
{"name": "Farine", "quantity": "250g"},
|
||||
{"name": "Oeufs", "quantity": "4"},
|
||||
{"name": "Lait", "quantity": "500ml"},
|
||||
{"name": "Sucre", "quantity": "2 cuillères à soupe"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Gâteau au chocolat",
|
||||
"description": "Un classique qui plaît à tout le monde.",
|
||||
"ingredients": [
|
||||
{"name": "Chocolat noir", "quantity": "200g"},
|
||||
{"name": "Beurre", "quantity": "150g"},
|
||||
{"name": "Sucre", "quantity": "100g"},
|
||||
{"name": "Oeufs", "quantity": "3"},
|
||||
{"name": "Farine", "quantity": "50g"}
|
||||
],
|
||||
},
|
||||
]
|
||||
# Compteur pour simuler l'auto-incrémentation des IDs
|
||||
_next_id = 3
|
||||
# ---
|
||||
|
||||
class RecipeRepository:
|
||||
"""
|
||||
Simule l'accès à une base de données de recettes.
|
||||
"""
|
||||
def list(self) -> List[Recipe]:
|
||||
"""Retourne toutes les recettes."""
|
||||
return [Recipe.model_validate(r) for r in _RECIPES_DB]
|
||||
|
||||
def find_by_id(self, recipe_id: int) -> Optional[Recipe]:
|
||||
"""Trouve une recette par son ID."""
|
||||
for recipe_data in _RECIPES_DB:
|
||||
if recipe_data["id"] == recipe_id:
|
||||
return Recipe.model_validate(recipe_data)
|
||||
return None
|
||||
|
||||
def create(self, recipe_data: Dict[str, Any]) -> Recipe:
|
||||
"""Crée une nouvelle recette et l'ajoute à la DB."""
|
||||
global _next_id
|
||||
new_recipe = {
|
||||
"id": _next_id,
|
||||
**recipe_data
|
||||
}
|
||||
_RECIPES_DB.append(new_recipe)
|
||||
_next_id += 1
|
||||
return Recipe.model_validate(new_recipe)
|
||||
|
||||
# Instance unique du repository
|
||||
recipe_repository = RecipeRepository()
|
||||
0
src/app/services/__init__.py
Normal file
0
src/app/services/__init__.py
Normal file
BIN
src/app/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/app/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/app/services/__pycache__/recipe_service.cpython-313.pyc
Normal file
BIN
src/app/services/__pycache__/recipe_service.cpython-313.pyc
Normal file
Binary file not shown.
40
src/app/services/recipe_service.py
Normal file
40
src/app/services/recipe_service.py
Normal file
@@ -0,0 +1,40 @@
|
||||
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.
|
||||
"""
|
||||
# Règle métier simple : le titre ne doit pas être vide
|
||||
if not title or not title.strip():
|
||||
raise ValidationBLLException("Le titre de la recette ne peut pas être vide.")
|
||||
|
||||
# Règle métier simple : il faut au moins un ingrédient
|
||||
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)
|
||||
Reference in New Issue
Block a user