tp done
This commit is contained in:
0
src/app/__init__.py
Normal file
0
src/app/__init__.py
Normal file
0
src/app/api/__init__.py
Normal file
0
src/app/api/__init__.py
Normal file
10
src/app/api/deps.py
Normal file
10
src/app/api/deps.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import AsyncSessionLocal
|
||||
|
||||
# Injection de dépendances de la session SQLAlchemy (ORM)
|
||||
async def get_db() -> AsyncSession:
|
||||
"""
|
||||
Dependency that provides a database session for a single request.
|
||||
"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
43
src/app/api/exception_handlers.py
Normal file
43
src/app/api/exception_handlers.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from fastapi import Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.core.exceptions import NotFoundBLLException, ValidationBLLException, DALException, BLLException
|
||||
|
||||
async def not_found_bll_exception_handler(request: Request, exc: NotFoundBLLException):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
async def validation_bll_exception_handler(request: Request, exc: ValidationBLLException):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
async def bll_exception_handler(request: Request, exc: BLLException):
|
||||
"""Gestionnaire pour les autres erreurs métier non spécifiques."""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
async def dal_exception_handler(request: Request, exc: DALException):
|
||||
# En production, on devrait logger l'exception originale: exc.original_exception
|
||||
print(f"DAL Error: {exc.original_exception}") # Pour le debug
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={"detail": "Une erreur interne est survenue au niveau de la base de données."},
|
||||
)
|
||||
|
||||
# Note : cela évite de devoir gérer les exceptions de type DAL et BLL dans les routeurs FastAPI directement :
|
||||
#
|
||||
# @router.get("/movies/{movie_id}", response_model=movie_schemas.Movie)
|
||||
# async def read_movie(movie_id: int, db: AsyncSession = Depends(get_db)):
|
||||
# """Récupère les détails d'un film spécifique par son ID."""
|
||||
# try:
|
||||
# return await movie_service.get_movie_by_id(db=db, movie_id=movie_id)
|
||||
# except NotFoundError as e:
|
||||
# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
# except DALException as e:
|
||||
# # Logguer l'erreur originale (e.original_exception) serait une bonne pratique ici
|
||||
# raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Erreur interne du serveur.")
|
||||
0
src/app/api/routers/__init__.py
Normal file
0
src/app/api/routers/__init__.py
Normal file
12
src/app/api/routers/genres.py
Normal file
12
src/app/api/routers/genres.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import List
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import app.schemas.genre as genre_schemas
|
||||
from app.api.deps import get_db
|
||||
import app.services.genre as genre_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/genres/", response_model=List[genre_schemas.GenreRead])
|
||||
async def read_genres(db: AsyncSession = Depends(get_db)):
|
||||
return await genre_service.get_genres(db)
|
||||
24
src/app/api/routers/movies.py
Normal file
24
src/app/api/routers/movies.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import List
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import app.schemas.movie as movie_schemas
|
||||
from app.api.deps import get_db
|
||||
import app.services.movie as movie_service
|
||||
from fastapi import status
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/movies/", response_model=movie_schemas.MovieRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_movie(movie: movie_schemas.MovieCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""Crée un nouveau film."""
|
||||
return await movie_service.create_movie(db=db, movie=movie)
|
||||
|
||||
@router.get("/movies/", response_model=List[movie_schemas.MovieRead])
|
||||
async def read_movies(skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db)):
|
||||
"""Récupère une liste de films."""
|
||||
return await movie_service.get_movies(db, skip=skip, limit=limit)
|
||||
|
||||
@router.get("/movies/{movie_id}", response_model=movie_schemas.MovieRead)
|
||||
async def read_movie(movie_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Récupère les détails d'un film spécifique par son ID."""
|
||||
return await movie_service.get_movie_by_id(db=db, movie_id=movie_id)
|
||||
21
src/app/api/routers/opinions.py
Normal file
21
src/app/api/routers/opinions.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import app.schemas.opinion as opinion_schemas
|
||||
from app.api.deps import get_db
|
||||
import app.services.opinion as opinion_service
|
||||
from fastapi import Response, status
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/movies/{movie_id}/opinions/", response_model=opinion_schemas.OpinionRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_opinion_for_movie(
|
||||
movie_id: int, opinion: opinion_schemas.OpinionCreate, db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Ajoute un avis à un film spécifique."""
|
||||
return await opinion_service.create_opinion(db=db, movie_id=movie_id, opinion=opinion)
|
||||
|
||||
@router.delete("/opinions/{opinion_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_opinion(opinion_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Supprime un avis par son ID."""
|
||||
await opinion_service.delete_opinion(db=db, opinion_id=opinion_id)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
28
src/app/api/routers/participants.py
Normal file
28
src/app/api/routers/participants.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.api.deps import get_db
|
||||
from app.schemas.participant import ParticipantRead, ParticipantCreate, ParticipantUpdate
|
||||
from app.services import participant as participant_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/participants/", response_model=List[ParticipantRead])
|
||||
async def read_participants(db: AsyncSession = Depends(get_db)):
|
||||
"""Récupère une liste de tous les participants."""
|
||||
return await participant_service.get_participants(db)
|
||||
|
||||
@router.post("/participants/", response_model=ParticipantRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_participant(
|
||||
participant: ParticipantCreate, db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Crée un nouveau participant (acteur ou réalisateur)."""
|
||||
return await participant_service.create_participant(db=db, participant=participant)
|
||||
|
||||
|
||||
@router.patch("/participants/{participant_id}", response_model=ParticipantRead)
|
||||
async def update_participant(
|
||||
participant_id: int, participant_data: ParticipantUpdate, db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Met à jour un participant existant."""
|
||||
return await participant_service.update_participant(db, participant_id, participant_data)
|
||||
0
src/app/core/__init__.py
Normal file
0
src/app/core/__init__.py
Normal file
38
src/app/core/config.py
Normal file
38
src/app/core/config.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import AnyHttpUrl
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
Classe de configuration qui charge les variables d'environnement.
|
||||
"""
|
||||
# Configuration du modèle Pydantic
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=True # Respecte la casse des variables
|
||||
)
|
||||
|
||||
# Paramètres du projet
|
||||
PROJECT_NAME: str = "FastAPI Project"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
# Configuration de la base de données
|
||||
# Le type hint `str` est suffisant, mais des types plus stricts peuvent être utilisés
|
||||
DATABASE_URL: str = "sqlite+aiosqlite:///./local_dev.db"
|
||||
|
||||
# Configuration de la sécurité (JWT)
|
||||
# SECRET_KEY: str
|
||||
# ALGORITHM: str = "HS256"
|
||||
#ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
# Configuration CORS
|
||||
# Pydantic va automatiquement convertir la chaîne de caractères séparée par des virgules
|
||||
# en une liste de chaînes de caractères.
|
||||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||
|
||||
|
||||
# Création d'une instance unique des paramètres qui sera importée dans le reste de l'application
|
||||
settings = Settings()
|
||||
25
src/app/core/exceptions.py
Normal file
25
src/app/core/exceptions.py
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
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/db/__init__.py
Normal file
0
src/app/db/__init__.py
Normal file
71
src/app/db/seeding.py
Normal file
71
src/app/db/seeding.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from app.models.genre import Genre
|
||||
from app.models.participant import Participant
|
||||
from app.models.member import Member
|
||||
from app.models.movie import Movie
|
||||
from app.models.opinion import Opinion
|
||||
|
||||
|
||||
async def seed_db(session: AsyncSession):
|
||||
"""
|
||||
Préremplit la base de données avec des données initiales si elle est vide.
|
||||
"""
|
||||
|
||||
# 1. Vérifier si la BDD est déja remplie (ex: en comptant les genres)
|
||||
result = await session.execute(select(Genre))
|
||||
if result.scalars().first() is not None:
|
||||
print("La base de données contient déja des données. Seeding annulé.")
|
||||
return
|
||||
|
||||
print("Base de données vide. Début du seeding...")
|
||||
|
||||
# 2. Créer les Genres
|
||||
genre1 = Genre(label="Science-Fiction")
|
||||
genre2 = Genre(label="Comédie")
|
||||
genre3 = Genre(label="Drame")
|
||||
session.add_all([genre1, genre2, genre3])
|
||||
await session.commit() # Commit pour que les objets aient un ID
|
||||
|
||||
# 3. Créer les Participants (Acteurs/Réalisateurs)
|
||||
director1 = Participant(first_name="Christopher", last_name="Nolan")
|
||||
actor1 = Participant(first_name="Leonardo", last_name="DiCaprio")
|
||||
actor2 = Participant(first_name="Marion", last_name="Cotillard")
|
||||
session.add_all([director1, actor1, actor2])
|
||||
await session.commit()
|
||||
|
||||
# 4. Créer un Membre
|
||||
member1 = Member(
|
||||
login="testuser",
|
||||
password="hashed_password_here", # En réalité, il faudrait hasher ce mot de passe
|
||||
is_admin=False,
|
||||
first_name="Test",
|
||||
last_name="User"
|
||||
)
|
||||
session.add(member1)
|
||||
await session.commit()
|
||||
|
||||
# 5. Créer un Film
|
||||
movie1 = Movie(
|
||||
title="Inception",
|
||||
year=2010,
|
||||
duration=148,
|
||||
synopsis="Un voleur qui s'approprie des secrets... (etc)",
|
||||
director_id=director1.id,
|
||||
genre_id=genre1.id,
|
||||
actors=[actor1, actor2] # La relation M2M est gérée par SQLAlchemy
|
||||
)
|
||||
session.add(movie1)
|
||||
await session.commit()
|
||||
|
||||
# 6. Créer un Avis
|
||||
opinion1 = Opinion(
|
||||
note=5,
|
||||
comment="Incroyable !",
|
||||
member_id=member1.id,
|
||||
movie_id=movie1.id
|
||||
)
|
||||
session.add(opinion1)
|
||||
await session.commit()
|
||||
|
||||
print(f"Film '{movie1.title}' et ses relations ont été ajoutés.")
|
||||
21
src/app/db/session.py
Normal file
21
src/app/db/session.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
# Configuration conditionnelle de l'engine
|
||||
if "sqlite+aiosqlite" in settings.DATABASE_URL:
|
||||
# Configuration pour SQLite basé sur un fichier
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
echo=True,
|
||||
connect_args={"check_same_thread": False} # Requis pour SQLite
|
||||
)
|
||||
else:
|
||||
# Configuration par défaut pour les autres BDD (ex: mysql+asyncmy, postgresql+asyncpg, etc.)
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=True)
|
||||
|
||||
# SessionMaker pour créer des sessions asynchrones
|
||||
# expire_on_commit=False est important pour utiliser les objets après le commit dans un contexte async
|
||||
AsyncSessionLocal = sessionmaker(
|
||||
autocommit=False, autoflush=False, bind=engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
65
src/app/main.py
Normal file
65
src/app/main.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from fastapi import FastAPI
|
||||
from contextlib import asynccontextmanager
|
||||
from app.core.config import settings
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.db.session import engine, AsyncSessionLocal
|
||||
from app.api.routers.opinions import router as opinions_router
|
||||
from app.api.routers.movies import router as movies_router
|
||||
from app.api.routers.genres import router as genres_router
|
||||
from app.api.routers.participants import router as participants_router
|
||||
from app.models import Base
|
||||
from app.core.exceptions import NotFoundBLLException, ValidationBLLException, DALException, BLLException
|
||||
from app.api.exception_handlers import (
|
||||
not_found_bll_exception_handler,
|
||||
validation_bll_exception_handler,
|
||||
dal_exception_handler,
|
||||
bll_exception_handler,
|
||||
)
|
||||
from app.db.seeding import seed_db
|
||||
|
||||
# Crée les tables dans la BDD au démarrage (pour le développement)
|
||||
# En production, on utiliserait un outil de migration comme Alembic.
|
||||
@asynccontextmanager
|
||||
async def lifespan(myapp: FastAPI):
|
||||
async with engine.begin() as conn:
|
||||
#await conn.run_sync(Base.metadata.drop_all) Optionnel: pour repartir de zéro
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
async with AsyncSessionLocal() as session:
|
||||
await seed_db(session)
|
||||
yield
|
||||
|
||||
app = FastAPI(
|
||||
title="API Filmothèque",
|
||||
description="Une API pour gérer une collection de films, réalisée avec FastAPI et SQLAlchemy async.",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
|
||||
if settings.BACKEND_CORS_ORIGINS:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[str(origin).rstrip('/') for origin in settings.BACKEND_CORS_ORIGINS],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(opinions_router, prefix=settings.API_V1_STR, tags=["Opinions"])
|
||||
app.include_router(movies_router, prefix=settings.API_V1_STR, tags=["Movies"])
|
||||
app.include_router(genres_router, prefix=settings.API_V1_STR, tags=["Genres"])
|
||||
app.include_router(participants_router, prefix=settings.API_V1_STR, tags=["Participants"])
|
||||
|
||||
# Ajouter les gestionnaires d'exceptions
|
||||
app.add_exception_handler(NotFoundBLLException, not_found_bll_exception_handler)
|
||||
app.add_exception_handler(ValidationBLLException, validation_bll_exception_handler)
|
||||
app.add_exception_handler(BLLException, bll_exception_handler) # Gestionnaire plus générique
|
||||
app.add_exception_handler(DALException, dal_exception_handler)
|
||||
|
||||
@app.get("/", tags=["Root"])
|
||||
def read_root():
|
||||
"""
|
||||
Un endpoint simple pour vérifier que l'API est en ligne.
|
||||
"""
|
||||
return {"message": "Welcome to this fantastic API!"}
|
||||
|
||||
13
src/app/models/__init__.py
Normal file
13
src/app/models/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Eviter les erreurs liés à l'importation circulaire
|
||||
# Exemple : sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper[Movie(movies)], expression 'Genre' failed to locate a name ('Genre'). If this is a class name, consider adding this relationship() to the <class 'app.models.film.Film'> class after both dependent classes have been defined
|
||||
|
||||
from .base_class import Base
|
||||
from .genre import Genre
|
||||
from .member import Member
|
||||
from .movie import Movie
|
||||
from .opinion import Opinion
|
||||
from .participant import Participant
|
||||
from .person import Person
|
||||
|
||||
# L'import des associations (ex : personnefilm) n'est en général pas nécessaire ici car elles sont gérées dans les modèles eux-mêmes
|
||||
|
||||
3
src/app/models/base_class.py
Normal file
3
src/app/models/base_class.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from sqlalchemy.orm import declarative_base
|
||||
Base = declarative_base()
|
||||
|
||||
13
src/app/models/genre.py
Normal file
13
src/app/models/genre.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
)
|
||||
from .base_class import Base
|
||||
|
||||
class Genre(Base):
|
||||
__tablename__ = "genres"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
label: Mapped[str] = mapped_column(String(255))
|
||||
33
src/app/models/member.py
Normal file
33
src/app/models/member.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
Boolean,
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship
|
||||
)
|
||||
from typing import List
|
||||
from .person import Person
|
||||
|
||||
class Member(Person):
|
||||
__tablename__ = "members"
|
||||
|
||||
# La clé primaire est aussi une clé étrangère vers la table parente
|
||||
id: Mapped[int] = mapped_column(ForeignKey("persons.id"), primary_key=True)
|
||||
|
||||
# Champs spécifiques à Member
|
||||
login: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||
password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# La relation vers Opinion
|
||||
opinions: Mapped[List["Opinion"]] = relationship(back_populates="member")
|
||||
|
||||
__mapper_args__ = {
|
||||
"polymorphic_identity": "member",
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Member(id={self.id}, login='{self.login}')"
|
||||
36
src/app/models/movie.py
Normal file
36
src/app/models/movie.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
String,
|
||||
ForeignKey,
|
||||
Text
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship
|
||||
)
|
||||
from typing import List
|
||||
|
||||
from .base_class import Base
|
||||
from .movie_actors import movie_actors_association_table
|
||||
|
||||
class Movie(Base):
|
||||
__tablename__ = "movies"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(250), nullable=False)
|
||||
year: Mapped[int] = mapped_column(Integer)
|
||||
duration: Mapped[int] = mapped_column(Integer)
|
||||
synopsis: Mapped[str] = mapped_column(Text)
|
||||
|
||||
director_id: Mapped[int] = mapped_column(ForeignKey("participants.id"))
|
||||
genre_id: Mapped[int] = mapped_column(ForeignKey("genres.id"))
|
||||
|
||||
director: Mapped["Participant"] = relationship(foreign_keys=[director_id])
|
||||
genre: Mapped["Genre"] = relationship()
|
||||
actors: Mapped[List["Participant"]] = relationship(
|
||||
secondary=movie_actors_association_table
|
||||
)
|
||||
opinions: Mapped[List["Opinion"]] = relationship(
|
||||
back_populates="movie", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
10
src/app/models/movie_actors.py
Normal file
10
src/app/models/movie_actors.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from sqlalchemy import Column, ForeignKey, Table
|
||||
from .base_class import Base
|
||||
|
||||
# Table d'association pour la relation Many-to-Many entre Film et Acteur
|
||||
movie_actors_association_table = Table(
|
||||
"movie_actors_association",
|
||||
Base.metadata,
|
||||
Column("movie_id", ForeignKey("movies.id"), primary_key=True),
|
||||
Column("participant_id", ForeignKey("participants.id"), primary_key=True),
|
||||
)
|
||||
23
src/app/models/opinion.py
Normal file
23
src/app/models/opinion.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
ForeignKey,
|
||||
Text
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship
|
||||
)
|
||||
from .base_class import Base
|
||||
|
||||
class Opinion(Base):
|
||||
__tablename__ = "opinions"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
note: Mapped[int] = mapped_column(Integer)
|
||||
comment: Mapped[str] = mapped_column(Text)
|
||||
|
||||
member_id: Mapped[int] = mapped_column(ForeignKey("members.id"))
|
||||
movie_id: Mapped[int] = mapped_column(ForeignKey("movies.id"))
|
||||
|
||||
member: Mapped["Member"] = relationship(back_populates="opinions")
|
||||
movie: Mapped["Movie"] = relationship(back_populates="opinions")
|
||||
21
src/app/models/participant.py
Normal file
21
src/app/models/participant.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import (
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column
|
||||
)
|
||||
from .person import Person
|
||||
|
||||
class Participant(Person):
|
||||
__tablename__ = "participants"
|
||||
|
||||
# La clé primaire est aussi une clé étrangère vers la table parente
|
||||
id: Mapped[int] = mapped_column(ForeignKey("persons.id"), primary_key=True)
|
||||
|
||||
__mapper_args__ = {
|
||||
"polymorphic_identity": "participant",
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Participant(id={self.id}, name='{self.first_name} {self.last_name}')"
|
||||
21
src/app/models/person.py
Normal file
21
src/app/models/person.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column
|
||||
)
|
||||
from .base_class import Base
|
||||
|
||||
class Person(Base):
|
||||
__tablename__ = "persons"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
last_name: Mapped[str] = mapped_column(String(255))
|
||||
first_name: Mapped[str] = mapped_column(String(255))
|
||||
# colonne discriminante pour la hiérarchie d'héritage
|
||||
type: Mapped[str] = mapped_column(String(50))
|
||||
|
||||
__mapper_args__ = {
|
||||
"polymorphic_identity": "person",
|
||||
"polymorphic_on": "type",
|
||||
}
|
||||
0
src/app/repositories/__init__.py
Normal file
0
src/app/repositories/__init__.py
Normal file
14
src/app/repositories/genre.py
Normal file
14
src/app/repositories/genre.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
import app.models.genre as models
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from app.core.exceptions import DALException
|
||||
|
||||
async def get_genres(db: AsyncSession):
|
||||
"""Récupère tous les genres de la base de données."""
|
||||
try:
|
||||
stmt = select(models.Genre)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
except SQLAlchemyError as e:
|
||||
raise DALException("Erreur lors de la récupération des genres", original_exception=e)
|
||||
13
src/app/repositories/member.py
Normal file
13
src/app/repositories/member.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from app.models.member import Member
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from app.core.exceptions import DALException
|
||||
|
||||
async def get_member(db: AsyncSession, member_id: int):
|
||||
"""Récupère un membre par son ID."""
|
||||
try:
|
||||
result = await db.execute(select(Member).where(Member.id == member_id))
|
||||
return result.scalar_one_or_none()
|
||||
except SQLAlchemyError as e:
|
||||
raise DALException(f"Erreur lors de la récupération du membre {member_id}", original_exception=e)
|
||||
78
src/app/repositories/movie.py
Normal file
78
src/app/repositories/movie.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from app.core.exceptions import DALException
|
||||
|
||||
from app.models.movie import Movie
|
||||
from app.models.opinion import Opinion
|
||||
import app.schemas.movie as schemas
|
||||
import app.models.person as person_models
|
||||
|
||||
async def get_movie(db: AsyncSession, movie_id: int):
|
||||
"""Récupère un film par son ID avec ses relations."""
|
||||
try:
|
||||
stmt = (
|
||||
select(Movie)
|
||||
.where(Movie.id == movie_id)
|
||||
.options(
|
||||
selectinload(Movie.genre),
|
||||
selectinload(Movie.director),
|
||||
selectinload(Movie.actors),
|
||||
selectinload(Movie.opinions),
|
||||
selectinload(Movie.opinions).selectinload(Opinion.member)
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
except SQLAlchemyError as e:
|
||||
raise DALException(f"Erreur lors de la récupération du film {movie_id}", original_exception=e)
|
||||
|
||||
async def get_movies(db: AsyncSession, skip: int = 0, limit: int = 100):
|
||||
"""Récupère une liste de films avec leurs relations principales."""
|
||||
try:
|
||||
stmt = (
|
||||
select(Movie)
|
||||
.offset(skip).limit(limit)
|
||||
.options(
|
||||
selectinload(Movie.genre),
|
||||
selectinload(Movie.director),
|
||||
selectinload(Movie.actors),
|
||||
selectinload(Movie.opinions),
|
||||
selectinload(Movie.opinions).selectinload(Opinion.member)
|
||||
)
|
||||
.order_by(Movie.title)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
except SQLAlchemyError as e:
|
||||
raise DALException("Erreur lors de la récupération des films", original_exception=e)
|
||||
|
||||
async def create_movie(db: AsyncSession, movie: schemas.MovieCreate):
|
||||
"""Crée un nouveau film."""
|
||||
try:
|
||||
# Récupérer les objets acteurs à partir de leurs IDs
|
||||
actors_result = await db.execute(
|
||||
select(person_models.Person).where(person_models.Person.id.in_(movie.actors_ids))
|
||||
)
|
||||
actors = actors_result.scalars().all()
|
||||
|
||||
# Créer l'instance du film
|
||||
db_movie = Movie(
|
||||
title=movie.title,
|
||||
year=movie.year,
|
||||
duration=movie.duration,
|
||||
synopsis=movie.synopsis,
|
||||
genre_id=movie.genre_id,
|
||||
director_id=movie.director_id,
|
||||
actors=actors
|
||||
)
|
||||
db.add(db_movie)
|
||||
await db.commit()
|
||||
|
||||
# Recharger les relations pour les retourner dans la réponse
|
||||
await db.refresh(db_movie)
|
||||
return await get_movie(db, db_movie.id)
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback() # IMPORTANT: annuler la transaction en cas d'erreur
|
||||
raise DALException("Erreur lors de la création du film", original_exception=e)
|
||||
60
src/app/repositories/opinion.py
Normal file
60
src/app/repositories/opinion.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
import app.schemas.opinion as schemas
|
||||
import app.models.opinion as models
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from app.core.exceptions import DALException
|
||||
|
||||
async def create_opinion_for_movie(db: AsyncSession, opinion: schemas.OpinionCreate, movie_id: int):
|
||||
"""Crée un avis pour un film donné."""
|
||||
try:
|
||||
db_opinion = models.Opinion(**opinion.model_dump(), movie_id=movie_id)
|
||||
db.add(db_opinion)
|
||||
await db.commit()
|
||||
await db.refresh(db_opinion)
|
||||
|
||||
query = (
|
||||
select(models.Opinion)
|
||||
.where(models.Opinion.id == db_opinion.id)
|
||||
.options(
|
||||
selectinload(models.Opinion.member)
|
||||
)
|
||||
)
|
||||
result = await db.execute(query)
|
||||
return result.scalars().one()
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback() # IMPORTANT: annuler la transaction en cas d'erreur
|
||||
raise DALException("Erreur lors de la création de l'avis", original_exception=e)
|
||||
|
||||
async def get_opinion(db: AsyncSession, opinion_id: int):
|
||||
"""Récupère un avis par son ID."""
|
||||
try:
|
||||
stmt = select(models.Opinion).where(models.Opinion.id == opinion_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
except SQLAlchemyError as e:
|
||||
raise DALException(f"Erreur lors de la récupération de l'avis {opinion_id}", original_exception=e)
|
||||
|
||||
async def delete_opinion_by_id(db: AsyncSession, opinion_id: int):
|
||||
"""Supprime un avis de la base de données."""
|
||||
try:
|
||||
db_opinion = await get_opinion(db, opinion_id=opinion_id)
|
||||
if db_opinion:
|
||||
await db.delete(db_opinion)
|
||||
await db.commit()
|
||||
return db_opinion
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback() # IMPORTANT: annuler la transaction en cas d'erreur
|
||||
raise DALException(f"Erreur lors de la suppression de l'avis {opinion_id}", original_exception=e)
|
||||
|
||||
async def delete_opinion(db: AsyncSession, db_opinion: models.Opinion):
|
||||
"""Supprime un avis de la base de données."""
|
||||
try:
|
||||
if db_opinion:
|
||||
await db.delete(db_opinion)
|
||||
await db.commit()
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback() # IMPORTANT: annuler la transaction en cas d'erreur
|
||||
raise DALException(f"Erreur lors de la suppression de l'avis {db_opinion.id}", original_exception=e)
|
||||
60
src/app/repositories/participant.py
Normal file
60
src/app/repositories/participant.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from app.models.participant import Participant as ParticipantModel
|
||||
from app.schemas.participant import ParticipantCreate, ParticipantUpdate
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from app.core.exceptions import DALException
|
||||
|
||||
|
||||
async def get_participant(db: AsyncSession, participant_id: int):
|
||||
"""Récupère un participant par son ID."""
|
||||
try:
|
||||
result = await db.execute(select(ParticipantModel).where(ParticipantModel.id == participant_id))
|
||||
return result.scalar_one_or_none()
|
||||
except SQLAlchemyError as e:
|
||||
raise DALException(f"Erreur lors de la récupération du participant {participant_id}", original_exception=e)
|
||||
|
||||
async def get_participants(db: AsyncSession):
|
||||
"""Récupère tous les participants de la base de données."""
|
||||
try:
|
||||
result = await db.execute(select(ParticipantModel).order_by(ParticipantModel.last_name))
|
||||
return result.scalars().all()
|
||||
except SQLAlchemyError as e:
|
||||
raise DALException("Erreur lors de la récupération des participants", original_exception=e)
|
||||
|
||||
async def create_participant(db: AsyncSession, participant: ParticipantCreate) -> ParticipantModel:
|
||||
"""Crée un nouveau participant."""
|
||||
try:
|
||||
db_participant = ParticipantModel(
|
||||
first_name=participant.first_name,
|
||||
last_name=participant.last_name
|
||||
)
|
||||
db.add(db_participant)
|
||||
await db.commit()
|
||||
await db.refresh(db_participant)
|
||||
return db_participant
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback() # IMPORTANT: annuler la transaction en cas d'erreur
|
||||
raise DALException("Erreur lors de la création du participant", original_exception=e)
|
||||
|
||||
async def update_participant(
|
||||
db: AsyncSession, participant_id: int, participant_data: ParticipantUpdate
|
||||
) -> ParticipantModel:
|
||||
"""Met à jour les informations d'un participant existant."""
|
||||
try:
|
||||
db_participant = await get_participant(db, participant_id)
|
||||
if not db_participant:
|
||||
return None
|
||||
|
||||
# Met à jour les champs si la valeur n'est pas None
|
||||
update_data = participant_data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(db_participant, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_participant)
|
||||
return db_participant
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback()
|
||||
raise DALException(f"Erreur lors de la mise à jour du participant {participant_id}", original_exception=e)
|
||||
|
||||
0
src/app/schemas/__init__.py
Normal file
0
src/app/schemas/__init__.py
Normal file
13
src/app/schemas/genre.py
Normal file
13
src/app/schemas/genre.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
class GenreBase(BaseModel):
|
||||
label: str
|
||||
|
||||
|
||||
class GenreCreate(GenreBase):
|
||||
pass
|
||||
|
||||
|
||||
class GenreRead(GenreBase):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
9
src/app/schemas/member.py
Normal file
9
src/app/schemas/member.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
class MemberBase(BaseModel):
|
||||
login: str
|
||||
|
||||
class MemberRead(MemberBase):
|
||||
id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
29
src/app/schemas/movie.py
Normal file
29
src/app/schemas/movie.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Optional
|
||||
from .person import PersonRead
|
||||
from .genre import GenreRead
|
||||
from .opinion import OpinionRead
|
||||
|
||||
class MovieBase(BaseModel):
|
||||
title: str
|
||||
year: int
|
||||
duration: Optional[int] = None
|
||||
synopsis: Optional[str] = None
|
||||
|
||||
|
||||
# Schéma pour la création : on utilise les IDs pour les relations
|
||||
class MovieCreate(MovieBase):
|
||||
genre_id: int
|
||||
director_id: int
|
||||
actors_ids: List[int] = []
|
||||
|
||||
|
||||
# Schéma complet pour la lecture : on imbrique les objets complets
|
||||
class MovieRead(MovieBase):
|
||||
id: int
|
||||
genre: GenreRead
|
||||
director: PersonRead
|
||||
actors: List[PersonRead] = []
|
||||
opinions: List[OpinionRead] = []
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
22
src/app/schemas/opinion.py
Normal file
22
src/app/schemas/opinion.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.schemas.member import MemberRead
|
||||
|
||||
|
||||
class OpinionBase(BaseModel):
|
||||
note: int
|
||||
comment: str
|
||||
|
||||
|
||||
class OpinionCreate(OpinionBase):
|
||||
member_id: int
|
||||
# movie_id n'est pas nécessaire ici car il vient déjà de l'URL
|
||||
pass
|
||||
|
||||
|
||||
class OpinionRead(OpinionBase):
|
||||
id: int
|
||||
movie_id: int
|
||||
member: MemberRead
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
23
src/app/schemas/participant.py
Normal file
23
src/app/schemas/participant.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional
|
||||
from .person import PersonBase
|
||||
|
||||
|
||||
# Schéma pour la création d'un Participant
|
||||
class ParticipantCreate(PersonBase):
|
||||
pass
|
||||
|
||||
|
||||
# Schéma pour la mise à jour d'un Participant
|
||||
class ParticipantUpdate(BaseModel):
|
||||
# Les champs sont optionnels pour permettre des mises à jour partielles (PATCH)
|
||||
last_name: Optional[str] = None
|
||||
first_name: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # Empêche l'ajout de champs non définis
|
||||
|
||||
|
||||
# Schéma complet pour la lecture d'un Participant
|
||||
class ParticipantRead(PersonBase):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
15
src/app/schemas/person.py
Normal file
15
src/app/schemas/person.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional
|
||||
|
||||
class PersonBase(BaseModel):
|
||||
last_name: str
|
||||
first_name: Optional[str] = None
|
||||
|
||||
|
||||
class PersonCreate(PersonBase):
|
||||
pass
|
||||
|
||||
|
||||
class PersonRead(PersonBase):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
0
src/app/services/__init__.py
Normal file
0
src/app/services/__init__.py
Normal file
9
src/app/services/genre.py
Normal file
9
src/app/services/genre.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import List
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
import app.repositories.genre as genre_repository
|
||||
import app.models.genre as genre_models
|
||||
|
||||
async def get_genres(db: AsyncSession) -> List[genre_models.Genre]:
|
||||
"""Service pour récupérer une liste de genres."""
|
||||
return await genre_repository.get_genres(db)
|
||||
11
src/app/services/member.py
Normal file
11
src/app/services/member.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.repositories import member as member_repository
|
||||
from app.models.member import Member as MemberModel
|
||||
from app.core.exceptions import NotFoundBLLException
|
||||
|
||||
async def get_member_by_id(db: AsyncSession, member_id: int) -> MemberModel:
|
||||
"""Service pour récupérer un membre par son ID."""
|
||||
db_member = await member_repository.get_member(db, member_id=member_id)
|
||||
if db_member is None:
|
||||
raise NotFoundBLLException(resource_name="Membre", resource_id=member_id)
|
||||
return db_member
|
||||
43
src/app/services/movie.py
Normal file
43
src/app/services/movie.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import datetime
|
||||
from typing import List
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
import app.repositories.movie as movie_repository
|
||||
import app.schemas.movie as movie_schemas
|
||||
import app.models.movie as movie_models
|
||||
from app.core.exceptions import NotFoundBLLException, ValidationBLLException
|
||||
|
||||
async def get_movies(db: AsyncSession, skip: int, limit: int) -> List[movie_models.Movie]:
|
||||
"""Service pour récupérer une liste de films."""
|
||||
# Actuellement un simple relais, mais la logique complexe (filtres, etc.) irait ici.
|
||||
return await movie_repository.get_movies(db, skip=skip, limit=limit)
|
||||
|
||||
async def get_movie_by_id(db: AsyncSession, movie_id: int) -> movie_models.Movie:
|
||||
"""Service pour récupérer un film par son ID."""
|
||||
db_movie = await movie_repository.get_movie(db, movie_id=movie_id)
|
||||
if db_movie is None:
|
||||
# Utiliser notre exception métier, pas une exception HTTP, pour des raisons de séparation des préoccupations.
|
||||
raise NotFoundBLLException(resource_name="Film", resource_id=movie_id)
|
||||
return db_movie
|
||||
|
||||
async def create_movie(db: AsyncSession, movie: movie_schemas.MovieCreate) -> movie_models.Movie:
|
||||
"""Service pour créer un nouveau film."""
|
||||
|
||||
# Règle métier 1 : le titre ne peut pas être vide
|
||||
if not movie.title or not movie.title.strip():
|
||||
raise ValidationBLLException("Le titre du film ne peut pas être vide.")
|
||||
|
||||
# Règle métier 2 : l'année doit être réaliste
|
||||
current_year = datetime.date.today().year
|
||||
if not (1888 <= movie.year <= current_year + 5):
|
||||
raise ValidationBLLException(f"L'année du film doit être comprise entre 1888 et {current_year + 5}.")
|
||||
|
||||
# Règle métier 3 : valider l'existence des entités liées
|
||||
# Ici, on pourrait aussi vérifier que genre_id, director_id et actors_ids existent
|
||||
# en appelant leurs services/repositories respectifs pour une validation complète.
|
||||
# await genre_service.get_genre_by_id(db, movie.genre_id)
|
||||
# await participant_service.get_participant_by_id(db, movie.director_id)
|
||||
# await participant_service.check_participants_ids(db, movie.actors_ids)
|
||||
|
||||
return await movie_repository.create_movie(db=db, movie=movie)
|
||||
|
||||
40
src/app/services/opinion.py
Normal file
40
src/app/services/opinion.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
import app.services.movie as movie_service
|
||||
import app.services.member as member_service
|
||||
import app.repositories.opinion as opinion_repository
|
||||
import app.schemas.opinion as opinion_schemas
|
||||
import app.models.opinion as opinion_models
|
||||
|
||||
from app.core.exceptions import NotFoundBLLException, ValidationBLLException
|
||||
|
||||
async def create_opinion(
|
||||
db: AsyncSession, *, movie_id: int, opinion: opinion_schemas.OpinionCreate
|
||||
) -> opinion_models.Opinion:
|
||||
"""
|
||||
Service pour créer un avis pour un film.
|
||||
Contient la logique métier : vérifier que le film existe et que la note est valide.
|
||||
"""
|
||||
|
||||
# Règle métier 1 : On ne peut pas noter un film qui n'existe pas.
|
||||
# On utilise le service movie qui lève déjà une NotFoundError propre.
|
||||
await movie_service.get_movie_by_id(db, movie_id=movie_id)
|
||||
|
||||
# Règle métier 2 : L'auteur de l'avis (Membre) doit exister.
|
||||
await member_service.get_member_by_id(db, member_id=opinion.member_id)
|
||||
|
||||
# Règle métier 3 : La note doit être dans un intervalle valide (ex: 0 à 5)
|
||||
if not (0 <= opinion.note <= 5):
|
||||
raise ValidationBLLException("La note doit être comprise entre 0 et 5.")
|
||||
|
||||
# Appel au repositories pour la création pure
|
||||
return await opinion_repository.create_opinion_for_movie(db=db, opinion=opinion, movie_id=movie_id)
|
||||
|
||||
|
||||
async def delete_opinion(db: AsyncSession, opinion_id: int) -> opinion_models.Opinion:
|
||||
"""Service pour supprimer un avis."""
|
||||
db_opinion = await opinion_repository.get_opinion(db, opinion_id=opinion_id)
|
||||
if db_opinion is None:
|
||||
raise NotFoundBLLException(resource_name="Avis", resource_id=opinion_id)
|
||||
await opinion_repository.delete_opinion(db, db_opinion=db_opinion)
|
||||
return db_opinion
|
||||
23
src/app/services/participant.py
Normal file
23
src/app/services/participant.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from typing import List
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.repositories import participant as participant_repository
|
||||
from app.schemas.participant import ParticipantCreate, ParticipantUpdate
|
||||
from app.models.participant import Participant as ParticipantModel
|
||||
from app.core.exceptions import NotFoundBLLException
|
||||
|
||||
async def get_participants(db: AsyncSession) -> List[ParticipantModel]:
|
||||
"""Service pour récupérer une liste de participants."""
|
||||
return await participant_repository.get_participants(db)
|
||||
|
||||
async def create_participant(db: AsyncSession, participant: ParticipantCreate) -> ParticipantModel:
|
||||
"""Service pour créer un nouveau participant."""
|
||||
return await participant_repository.create_participant(db, participant=participant)
|
||||
|
||||
async def update_participant(
|
||||
db: AsyncSession, participant_id: int, participant_data: ParticipantUpdate
|
||||
) -> ParticipantModel:
|
||||
"""Service pour mettre à jour un participant."""
|
||||
db_participant = await participant_repository.update_participant(db, participant_id, participant_data)
|
||||
if db_participant is None:
|
||||
raise NotFoundBLLException(resource_name="Participant", resource_id=participant_id)
|
||||
return db_participant
|
||||
BIN
src/local_dev.db
Normal file
BIN
src/local_dev.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user