From fe458b49fe14deeed28de07b2c91c45175c848bb Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 11 Feb 2025 19:51:14 +0200 Subject: [PATCH 1/2] Solution --- src/crud/__init__.py | 0 src/crud/movies.py | 94 ++++++++++++++++++++++++++++++++++++++++ src/database/populate.py | 14 +++++- src/main.py | 92 ++++++++++++++++++++++++++++++++++++--- src/routes/movies.py | 73 +++++++++++++++++++++++++++++-- src/schemas/movies.py | 92 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 351 insertions(+), 14 deletions(-) create mode 100644 src/crud/__init__.py create mode 100644 src/crud/movies.py diff --git a/src/crud/__init__.py b/src/crud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/crud/movies.py b/src/crud/movies.py new file mode 100644 index 0000000..4df75f3 --- /dev/null +++ b/src/crud/movies.py @@ -0,0 +1,94 @@ +from typing import List + +from fastapi import HTTPException +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from database.models import MovieModel, CountryModel, GenreModel, ActorModel, LanguageModel + +from schemas.movies import MovieCreateSchema, MovieUpdateSchema + + +def get_movies(db: Session, offset, per_page): + return db.query(MovieModel).order_by(MovieModel.id.desc()).offset(offset).limit(per_page).all() + + +def get_movie(db: Session, movie_id): + return db.query(MovieModel).filter(MovieModel.id == movie_id).first() + + +def create_objects(data: List[str], model, db: Session): + objects = [model(name=element) for element in data] + db.add_all(objects) + db.commit() + + for obj in objects: + db.refresh(obj) + + return objects + + +def create_movie(db: Session, movie: MovieCreateSchema): + try: + + country_data = db.query(CountryModel).filter(CountryModel.code == movie.country).first() + if not country_data: + country_data = CountryModel(code=movie.country) + db.add(country_data) + db.commit() + db.refresh(country_data) + + genres_data = db.query(GenreModel).filter(GenreModel.name.in_(movie.genres)).all() + if not genres_data: + genres_data = create_objects(movie.genres, GenreModel, db) + + actors_data = db.query(ActorModel).filter(ActorModel.name.in_(movie.actors)).all() + if not actors_data: + actors_data = create_objects(movie.actors, ActorModel, db) + + languages_data = db.query(LanguageModel).filter(LanguageModel.name.in_(movie.languages)).all() + if not languages_data: + languages_data = create_objects(movie.languages, LanguageModel, db) + + db_movie = MovieModel( + name=movie.name, + date=movie.date, + score=movie.score, + overview=movie.overview, + status=movie.status, + budget=movie.budget, + revenue=movie.revenue, + country=country_data, + genres=genres_data, + actors=actors_data, + languages=languages_data + ) + + db.add(db_movie) + db.commit() + db.refresh(db_movie) + return db_movie + + except SQLAlchemyError: + db.rollback() + raise HTTPException(status_code=400, detail="Invalid input data.") + + +def delete_movie(db: Session, movie_id: int): + db_movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first() + if not db_movie: + raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") + db.delete(db_movie) + db.commit() + + +def update_movie(db: Session, movie_id: int, movie: MovieUpdateSchema): + db_movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first() + if not db_movie: + raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") + + for key, value in movie.dict(exclude_unset=True).items(): + setattr(db_movie, key, value) + + db.commit() + db.refresh(db_movie) diff --git a/src/database/populate.py b/src/database/populate.py index f5fb895..6d0f093 100644 --- a/src/database/populate.py +++ b/src/database/populate.py @@ -5,9 +5,19 @@ from tqdm import tqdm from config import get_settings -from database import MovieModel, get_db_contextmanager -from database.models import CountryModel, GenreModel, ActorModel, MoviesGenresModel, ActorsMoviesModel, LanguageModel, \ +from database import ( + MovieModel, + get_db_contextmanager +) +from database.models import ( + CountryModel, + GenreModel, + ActorModel, + MoviesGenresModel, + ActorsMoviesModel, + LanguageModel, MoviesLanguagesModel +) class CSVDatabaseSeeder: diff --git a/src/main.py b/src/main.py index 35ca2a7..8c4bed1 100644 --- a/src/main.py +++ b/src/main.py @@ -1,12 +1,90 @@ -from fastapi import FastAPI +import math -from routes import movie_router -app = FastAPI( - title="Movies homework", - description="Description of project" +from fastapi import APIRouter, Depends, HTTPException, Query, Path + +from sqlalchemy.orm import Session + +from crud.movies import get_movies, create_movie, get_movie, delete_movie, update_movie +from database import get_db +from database.models import ( + MovieModel, + CountryModel, + GenreModel, + ActorModel, + LanguageModel ) -api_version_prefix = "/api/v1" +from schemas.movies import ( + MovieListSchema, + MovieCreateSchema, + MovieDetailSchema, + MovieUpdateSchema, + MoviePageSchema +) + + +router = APIRouter() + + +@router.get("/movies/", response_model=MoviePageSchema) +async def list_movies( + page: int = Query(1, ge=1), + per_page: int = Query(10, ge=1, le=20), + db: Session = Depends(get_db) +): + offset = (page - 1) * per_page + movies_data = get_movies(db, offset, per_page) + + if movies_data: + + total_items = db.query(MovieModel).count() + total_pages = math.ceil(total_items / per_page) + + prev_page = f"/theater/movies/?page={page - 1}&per_page={per_page}" if page > 1 else None + next_page = f"/theater/movies/?page={page + 1}&per_page={per_page}" if page < total_pages else None + + return { + "movies": movies_data, + "prev_page": prev_page, + "next_page": next_page, + "total_pages": total_pages, + "total_items": total_items + } + raise HTTPException(status_code=404, detail="No movies found.") + + +@router.get("/movies/{movie_id}/", response_model=MovieDetailSchema) +async def get_movie_detail( + movie_id: int = Path(..., gt=0), + db: Session = Depends(get_db) +): + movie = get_movie(db, movie_id) + if movie: + return movie + raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") + + +@router.post("/movies/", response_model=MovieDetailSchema, status_code=201) +def add_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): + movies = db.query(MovieModel).filter( + MovieModel.name == movie.name, + MovieModel.date == movie.date + ).all() + if not movies: + return create_movie(db, movie) + raise HTTPException( + status_code=409, + detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists." + ) + + +@router.delete("/movies/{movie_id}/", status_code=204) +def remove_movie(movie_id: int, db: Session = Depends(get_db)): + delete_movie(db, movie_id) + -app.include_router(movie_router, prefix=f"{api_version_prefix}/theater", tags=["theater"]) +@router.patch("/movies/{movie_id}/") +def patch_movie(movie_id: int, movie: MovieUpdateSchema, db: Session = Depends(get_db)): + update_movie(db, movie_id, movie) + return HTTPException(status_code=200, detail="Movie updated successfully.") \ No newline at end of file diff --git a/src/routes/movies.py b/src/routes/movies.py index e44678a..ec7c3cd 100644 --- a/src/routes/movies.py +++ b/src/routes/movies.py @@ -1,12 +1,77 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session, joinedload +import math +from fastapi import APIRouter, Depends, HTTPException, Query, Path + +from sqlalchemy.orm import Session + +from crud.movies import get_movies, create_movie, get_movie, delete_movie, update_movie from database import get_db from database.models import MovieModel, CountryModel, GenreModel, ActorModel, LanguageModel +from schemas.movies import MovieListSchema, MovieCreateSchema, MovieDetailSchema, MovieUpdateSchema, MoviePageSchema + router = APIRouter() -# Write your code here +@router.get("/movies/", response_model=MoviePageSchema) +async def list_movies( + page: int = Query(1, ge=1), + per_page: int = Query(10, ge=1, le=20), + db: Session = Depends(get_db) +): + offset = (page - 1) * per_page + movies_data = get_movies(db, offset, per_page) + + if movies_data: + + total_items = db.query(MovieModel).count() + total_pages = math.ceil(total_items / per_page) + + prev_page = f"/theater/movies/?page={page - 1}&per_page={per_page}" if page > 1 else None + next_page = f"/theater/movies/?page={page + 1}&per_page={per_page}" if page < total_pages else None + + return { + "movies": movies_data, + "prev_page": prev_page, + "next_page": next_page, + "total_pages": total_pages, + "total_items": total_items + } + raise HTTPException(status_code=404, detail="No movies found.") + + +@router.get("/movies/{movie_id}/", response_model=MovieDetailSchema) +async def get_movie_detail( + movie_id: int = Path(..., gt=0), + db: Session = Depends(get_db) +): + movie = get_movie(db, movie_id) + if movie: + return movie + raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") + + +@router.post("/movies/", response_model=MovieDetailSchema, status_code=201) +def add_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): + movies = db.query(MovieModel).filter( + MovieModel.name == movie.name, + MovieModel.date == movie.date + ).all() + if not movies: + return create_movie(db, movie) + raise HTTPException( + status_code=409, + detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists." + ) + + +@router.delete("/movies/{movie_id}/", status_code=204) +def remove_movie(movie_id: int, db: Session = Depends(get_db)): + delete_movie(db, movie_id) + + +@router.patch("/movies/{movie_id}/") +def patch_movie(movie_id: int, movie: MovieUpdateSchema, db: Session = Depends(get_db)): + update_movie(db, movie_id, movie) + return HTTPException(status_code=200, detail="Movie updated successfully.") \ No newline at end of file diff --git a/src/schemas/movies.py b/src/schemas/movies.py index fabb9be..aaf7aba 100644 --- a/src/schemas/movies.py +++ b/src/schemas/movies.py @@ -1 +1,91 @@ -# Write your code here +from datetime import date as date_module, timedelta +from enum import Enum +from typing import Optional, List + +from pydantic import BaseModel, Field + + +class CountrySchema(BaseModel): + id: int + code: str + name: str | None + + +class GenresSchema(BaseModel): + id: int + name: str + + +class ActorsSchema(BaseModel): + id: int + name: str + + +class LanguagesSchema(BaseModel): + id: int + name: str + + +class MovieStatus(str, Enum): + released = "Released" + post_production = "Post Production" + in_production = "In Production" + + +class MovieListSchema(BaseModel): + id: int + name: str + date: date_module + score: float + overview: str + + +class MoviePageSchema(BaseModel): + movies: List[MovieListSchema] + prev_page: Optional[str] = None + next_page: Optional[str] = None + total_pages: int + total_items: int + + +class MovieDetailSchema(BaseModel): + id: int + name: str + date: date_module + score: float = Field(ge=0, le=100) + overview: str + status: MovieStatus + budget: float = Field(ge=0) + revenue: float = Field(ge=0) + country: CountrySchema + genres: List[GenresSchema] + actors: List[ActorsSchema] + languages: List[LanguagesSchema] + + +class MovieCreateSchema(BaseModel): + name: str = Field(..., max_length=255) + date: date_module = Field(..., le=(date_module.today() + timedelta(days=365))) + score: float = Field(ge=0, le=100) + overview: str + status: MovieStatus + budget: float = Field(ge=0) + revenue: float = Field(ge=0) + country: str + genres: List[str] + actors: List[str] + languages: List[str] + + +class MovieUpdateSchema(BaseModel): + name: Optional[str] = Field(None, max_length=255) + date: Optional[date_module] = Field(None, le=(date_module.today() + timedelta(days=365))) + score: Optional[float] = Field(None, ge=0, le=100) + overview: Optional[str] = None + status: Optional[MovieStatus] = None + budget: Optional[float] = Field(None, ge=0) + revenue: Optional[float] = Field(None, ge=0) + country: Optional[str] = None + genres: Optional[List[str]] = None + actors: Optional[List[str]] = None + languages: Optional[List[str]] = None \ No newline at end of file From 4884c6760368b03dded2a351818503bffda6f539 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 11 Feb 2025 20:05:08 +0200 Subject: [PATCH 2/2] Solution update --- src/config/settings.py | 10 +- src/crud/movies.py | 44 ++- src/database/migrations/env.py | 6 +- .../ea3a65568bd9_initial_migration.py | 140 +++++---- src/database/models.py | 72 +++-- src/database/populate.py | 135 ++++++--- src/database/session_postgresql.py | 10 +- src/database/session_sqlite.py | 8 +- src/main.py | 44 +-- src/routes/movies.py | 232 ++++++++++---- src/schemas/movies.py | 126 +++++--- src/tests/conftest.py | 4 +- src/tests/test_integration/test_movies.py | 286 ++++++++++++------ 13 files changed, 744 insertions(+), 373 deletions(-) diff --git a/src/config/settings.py b/src/config/settings.py index f49551d..23416ab 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -8,7 +8,9 @@ class BaseAppSettings(BaseSettings): BASE_DIR: Path = Path(__file__).parent.parent PATH_TO_DB: str = str(BASE_DIR / "database" / "source" / "theater.db") - PATH_TO_MOVIES_CSV: str = str(BASE_DIR / "database" / "seed_data" / "imdb_movies.csv") + PATH_TO_MOVIES_CSV: str = str( + BASE_DIR / "database" / "seed_data" / "imdb_movies.csv" + ) class Settings(BaseAppSettings): @@ -22,11 +24,11 @@ class Settings(BaseAppSettings): class TestingSettings(BaseAppSettings): def model_post_init(self, __context: dict[str, Any] | None = None) -> None: - object.__setattr__(self, 'PATH_TO_DB', ":memory:") + object.__setattr__(self, "PATH_TO_DB", ":memory:") object.__setattr__( self, - 'PATH_TO_MOVIES_CSV', - str(self.BASE_DIR / "database" / "seed_data" / "test_data.csv") + "PATH_TO_MOVIES_CSV", + str(self.BASE_DIR / "database" / "seed_data" / "test_data.csv"), ) diff --git a/src/crud/movies.py b/src/crud/movies.py index 4df75f3..859379b 100644 --- a/src/crud/movies.py +++ b/src/crud/movies.py @@ -4,13 +4,25 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from database.models import MovieModel, CountryModel, GenreModel, ActorModel, LanguageModel +from database.models import ( + MovieModel, + CountryModel, + GenreModel, + ActorModel, + LanguageModel, +) from schemas.movies import MovieCreateSchema, MovieUpdateSchema def get_movies(db: Session, offset, per_page): - return db.query(MovieModel).order_by(MovieModel.id.desc()).offset(offset).limit(per_page).all() + return ( + db.query(MovieModel) + .order_by(MovieModel.id.desc()) + .offset(offset) + .limit(per_page) + .all() + ) def get_movie(db: Session, movie_id): @@ -31,22 +43,32 @@ def create_objects(data: List[str], model, db: Session): def create_movie(db: Session, movie: MovieCreateSchema): try: - country_data = db.query(CountryModel).filter(CountryModel.code == movie.country).first() + country_data = ( + db.query(CountryModel).filter(CountryModel.code == movie.country).first() + ) if not country_data: country_data = CountryModel(code=movie.country) db.add(country_data) db.commit() db.refresh(country_data) - genres_data = db.query(GenreModel).filter(GenreModel.name.in_(movie.genres)).all() + genres_data = ( + db.query(GenreModel).filter(GenreModel.name.in_(movie.genres)).all() + ) if not genres_data: genres_data = create_objects(movie.genres, GenreModel, db) - actors_data = db.query(ActorModel).filter(ActorModel.name.in_(movie.actors)).all() + actors_data = ( + db.query(ActorModel).filter(ActorModel.name.in_(movie.actors)).all() + ) if not actors_data: actors_data = create_objects(movie.actors, ActorModel, db) - languages_data = db.query(LanguageModel).filter(LanguageModel.name.in_(movie.languages)).all() + languages_data = ( + db.query(LanguageModel) + .filter(LanguageModel.name.in_(movie.languages)) + .all() + ) if not languages_data: languages_data = create_objects(movie.languages, LanguageModel, db) @@ -61,7 +83,7 @@ def create_movie(db: Session, movie: MovieCreateSchema): country=country_data, genres=genres_data, actors=actors_data, - languages=languages_data + languages=languages_data, ) db.add(db_movie) @@ -77,7 +99,9 @@ def create_movie(db: Session, movie: MovieCreateSchema): def delete_movie(db: Session, movie_id: int): db_movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first() if not db_movie: - raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) db.delete(db_movie) db.commit() @@ -85,7 +109,9 @@ def delete_movie(db: Session, movie_id: int): def update_movie(db: Session, movie_id: int, movie: MovieUpdateSchema): db_movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first() if not db_movie: - raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) for key, value in movie.dict(exclude_unset=True).items(): setattr(db_movie, key, value) diff --git a/src/database/migrations/env.py b/src/database/migrations/env.py index d3101ac..ccd5a4b 100644 --- a/src/database/migrations/env.py +++ b/src/database/migrations/env.py @@ -2,7 +2,7 @@ from alembic import context -from database import models # noqa: F401 +from database import models # noqa: F401 from database.models import Base from database.session_postgresql import postgresql_engine @@ -47,7 +47,7 @@ def run_migrations_offline() -> None: connection=connection, target_metadata=target_metadata, compare_type=True, - compare_server_default=True + compare_server_default=True, ) with context.begin_transaction(): @@ -68,7 +68,7 @@ def run_migrations_online() -> None: connection=connection, target_metadata=target_metadata, compare_type=True, - compare_server_default=True + compare_server_default=True, ) with context.begin_transaction(): diff --git a/src/database/migrations/versions/ea3a65568bd9_initial_migration.py b/src/database/migrations/versions/ea3a65568bd9_initial_migration.py index b662282..da58fae 100644 --- a/src/database/migrations/versions/ea3a65568bd9_initial_migration.py +++ b/src/database/migrations/versions/ea3a65568bd9_initial_migration.py @@ -5,6 +5,7 @@ Create Date: 2025-01-02 21:07:13.284837 """ + from typing import Sequence, Union from alembic import op @@ -12,7 +13,7 @@ # revision identifiers, used by Alembic. -revision: str = 'ea3a65568bd9' +revision: str = "ea3a65568bd9" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -20,77 +21,94 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('actors', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') + op.create_table( + "actors", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), ) - op.create_table('countries', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('code', sa.String(length=3), nullable=False), - sa.Column('name', sa.String(length=255), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('code') + op.create_table( + "countries", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("code", sa.String(length=3), nullable=False), + sa.Column("name", sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("code"), ) - op.create_table('genres', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') + op.create_table( + "genres", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), ) - op.create_table('languages', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') + op.create_table( + "languages", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), ) - op.create_table('movies', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('date', sa.Date(), nullable=False), - sa.Column('score', sa.Float(), nullable=False), - sa.Column('overview', sa.Text(), nullable=False), - sa.Column('status', sa.Enum('RELEASED', 'POST_PRODUCTION', 'IN_PRODUCTION', name='moviestatusenum'), nullable=False), - sa.Column('budget', sa.DECIMAL(precision=15, scale=2), nullable=False), - sa.Column('revenue', sa.Float(), nullable=False), - sa.Column('country_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['country_id'], ['countries.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name', 'date', name='unique_movie_constraint') + op.create_table( + "movies", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("date", sa.Date(), nullable=False), + sa.Column("score", sa.Float(), nullable=False), + sa.Column("overview", sa.Text(), nullable=False), + sa.Column( + "status", + sa.Enum( + "RELEASED", "POST_PRODUCTION", "IN_PRODUCTION", name="moviestatusenum" + ), + nullable=False, + ), + sa.Column("budget", sa.DECIMAL(precision=15, scale=2), nullable=False), + sa.Column("revenue", sa.Float(), nullable=False), + sa.Column("country_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["country_id"], + ["countries.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name", "date", name="unique_movie_constraint"), ) - op.create_table('actors_movies', - sa.Column('movie_id', sa.Integer(), nullable=False), - sa.Column('actor_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['actor_id'], ['actors.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['movie_id'], ['movies.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('movie_id', 'actor_id') + op.create_table( + "actors_movies", + sa.Column("movie_id", sa.Integer(), nullable=False), + sa.Column("actor_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["actor_id"], ["actors.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["movie_id"], ["movies.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("movie_id", "actor_id"), ) - op.create_table('movies_genres', - sa.Column('movie_id', sa.Integer(), nullable=False), - sa.Column('genre_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['genre_id'], ['genres.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['movie_id'], ['movies.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('movie_id', 'genre_id') + op.create_table( + "movies_genres", + sa.Column("movie_id", sa.Integer(), nullable=False), + sa.Column("genre_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["genre_id"], ["genres.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["movie_id"], ["movies.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("movie_id", "genre_id"), ) - op.create_table('movies_languages', - sa.Column('movie_id', sa.Integer(), nullable=False), - sa.Column('language_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['language_id'], ['languages.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['movie_id'], ['movies.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('movie_id', 'language_id') + op.create_table( + "movies_languages", + sa.Column("movie_id", sa.Integer(), nullable=False), + sa.Column("language_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["language_id"], ["languages.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["movie_id"], ["movies.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("movie_id", "language_id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('movies_languages') - op.drop_table('movies_genres') - op.drop_table('actors_movies') - op.drop_table('movies') - op.drop_table('languages') - op.drop_table('genres') - op.drop_table('countries') - op.drop_table('actors') + op.drop_table("movies_languages") + op.drop_table("movies_genres") + op.drop_table("actors_movies") + op.drop_table("movies") + op.drop_table("languages") + op.drop_table("genres") + op.drop_table("countries") + op.drop_table("actors") # ### end Alembic commands ### diff --git a/src/database/models.py b/src/database/models.py index 942a3af..07612a6 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -2,7 +2,17 @@ from enum import Enum from typing import Optional -from sqlalchemy import String, Float, Text, DECIMAL, UniqueConstraint, Date, ForeignKey, Table, Column +from sqlalchemy import ( + String, + Float, + Text, + DECIMAL, + UniqueConstraint, + Date, + ForeignKey, + Table, + Column, +) from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped, relationship from sqlalchemy import Enum as SQLAlchemyEnum @@ -24,10 +34,16 @@ class MovieStatusEnum(str, Enum): Base.metadata, Column( "movie_id", - ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True, nullable=False), + ForeignKey("movies.id", ondelete="CASCADE"), + primary_key=True, + nullable=False, + ), Column( "genre_id", - ForeignKey("genres.id", ondelete="CASCADE"), primary_key=True, nullable=False), + ForeignKey("genres.id", ondelete="CASCADE"), + primary_key=True, + nullable=False, + ), ) ActorsMoviesModel = Table( @@ -35,17 +51,25 @@ class MovieStatusEnum(str, Enum): Base.metadata, Column( "movie_id", - ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True, nullable=False), + ForeignKey("movies.id", ondelete="CASCADE"), + primary_key=True, + nullable=False, + ), Column( "actor_id", - ForeignKey("actors.id", ondelete="CASCADE"), primary_key=True, nullable=False), + ForeignKey("actors.id", ondelete="CASCADE"), + primary_key=True, + nullable=False, + ), ) MoviesLanguagesModel = Table( "movies_languages", Base.metadata, Column("movie_id", ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True), - Column("language_id", ForeignKey("languages.id", ondelete="CASCADE"), primary_key=True), + Column( + "language_id", ForeignKey("languages.id", ondelete="CASCADE"), primary_key=True + ), ) @@ -56,9 +80,7 @@ class GenreModel(Base): name: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) movies: Mapped[list["MovieModel"]] = relationship( - "MovieModel", - secondary=MoviesGenresModel, - back_populates="genres" + "MovieModel", secondary=MoviesGenresModel, back_populates="genres" ) def __repr__(self): @@ -72,9 +94,7 @@ class ActorModel(Base): name: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) movies: Mapped[list["MovieModel"]] = relationship( - "MovieModel", - secondary=ActorsMoviesModel, - back_populates="actors" + "MovieModel", secondary=ActorsMoviesModel, back_populates="actors" ) def __repr__(self): @@ -88,7 +108,9 @@ class CountryModel(Base): code: Mapped[str] = mapped_column(String(3), unique=True, nullable=False) name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) - movies: Mapped[list["MovieModel"]] = relationship("MovieModel", back_populates="country") + movies: Mapped[list["MovieModel"]] = relationship( + "MovieModel", back_populates="country" + ) def __repr__(self): return f"" @@ -101,9 +123,7 @@ class LanguageModel(Base): name: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) movies: Mapped[list["MovieModel"]] = relationship( - "MovieModel", - secondary=MoviesLanguagesModel, - back_populates="languages" + "MovieModel", secondary=MoviesLanguagesModel, back_populates="languages" ) def __repr__(self): @@ -125,29 +145,23 @@ class MovieModel(Base): revenue: Mapped[float] = mapped_column(Float, nullable=False) country_id: Mapped[int] = mapped_column(ForeignKey("countries.id"), nullable=False) - country: Mapped["CountryModel"] = relationship("CountryModel", back_populates="movies") + country: Mapped["CountryModel"] = relationship( + "CountryModel", back_populates="movies" + ) genres: Mapped[list["GenreModel"]] = relationship( - "GenreModel", - secondary=MoviesGenresModel, - back_populates="movies" + "GenreModel", secondary=MoviesGenresModel, back_populates="movies" ) actors: Mapped[list["ActorModel"]] = relationship( - "ActorModel", - secondary=ActorsMoviesModel, - back_populates="movies" + "ActorModel", secondary=ActorsMoviesModel, back_populates="movies" ) languages: Mapped[list["LanguageModel"]] = relationship( - "LanguageModel", - secondary=MoviesLanguagesModel, - back_populates="movies" + "LanguageModel", secondary=MoviesLanguagesModel, back_populates="movies" ) - __table_args__ = ( - UniqueConstraint("name", "date", name="unique_movie_constraint"), - ) + __table_args__ = (UniqueConstraint("name", "date", name="unique_movie_constraint"),) @classmethod def default_order_by(cls): diff --git a/src/database/populate.py b/src/database/populate.py index 6d0f093..6e52bd3 100644 --- a/src/database/populate.py +++ b/src/database/populate.py @@ -5,10 +5,7 @@ from tqdm import tqdm from config import get_settings -from database import ( - MovieModel, - get_db_contextmanager -) +from database import MovieModel, get_db_contextmanager from database.models import ( CountryModel, GenreModel, @@ -16,7 +13,7 @@ MoviesGenresModel, ActorsMoviesModel, LanguageModel, - MoviesLanguagesModel + MoviesLanguagesModel, ) @@ -30,20 +27,24 @@ def is_db_populated(self) -> bool: def _preprocess_csv(self): data = pd.read_csv(self._csv_file_path) - data = data.drop_duplicates(subset=['names', 'date_x'], keep='first') + data = data.drop_duplicates(subset=["names", "date_x"], keep="first") - data['crew'] = data['crew'].fillna('Unknown') - data['crew'] = data['crew'].str.replace(r'\s+', '', regex=True) - data['crew'] = data['crew'].apply( - lambda crew: ','.join(sorted(set(crew.split(',')))) if crew != 'Unknown' else crew + data["crew"] = data["crew"].fillna("Unknown") + data["crew"] = data["crew"].str.replace(r"\s+", "", regex=True) + data["crew"] = data["crew"].apply( + lambda crew: ( + ",".join(sorted(set(crew.split(",")))) if crew != "Unknown" else crew + ) + ) + data["genre"] = data["genre"].fillna("Unknown") + data["genre"] = data["genre"].str.replace("\u00A0", "", regex=True) + data["date_x"] = data["date_x"].str.strip() + data["date_x"] = pd.to_datetime( + data["date_x"], format="%Y-%m-%d", errors="raise" ) - data['genre'] = data['genre'].fillna('Unknown') - data['genre'] = data['genre'].str.replace('\u00A0', '', regex=True) - data['date_x'] = data['date_x'].str.strip() - data['date_x'] = pd.to_datetime(data['date_x'], format='%Y-%m-%d', errors='raise') - data['date_x'] = data['date_x'].dt.date - data['orig_lang'] = data['orig_lang'].str.replace(r'\s+', '', regex=True) - data['status'] = data['status'].str.strip() + data["date_x"] = data["date_x"].dt.date + data["orig_lang"] = data["orig_lang"].str.replace(r"\s+", "", regex=True) + data["status"] = data["status"].str.strip() print("Preprocessing csv file") data.to_csv(self._csv_file_path, index=False) @@ -51,7 +52,11 @@ def _preprocess_csv(self): return data def _get_or_create_bulk(self, model, items: list, unique_field: str): - existing = self._db_session.query(model).filter(getattr(model, unique_field).in_(items)).all() + existing = ( + self._db_session.query(model) + .filter(getattr(model, unique_field).in_(items)) + .all() + ) existing_dict = {getattr(item, unique_field): item for item in existing} new_items = [item for item in items if item not in existing_dict] @@ -61,8 +66,14 @@ def _get_or_create_bulk(self, model, items: list, unique_field: str): self._db_session.execute(insert(model).values(new_records)) self._db_session.flush() - newly_inserted = self._db_session.query(model).filter(getattr(model, unique_field).in_(new_items)).all() - existing_dict.update({getattr(item, unique_field): item for item in newly_inserted}) + newly_inserted = ( + self._db_session.query(model) + .filter(getattr(model, unique_field).in_(new_items)) + .all() + ) + existing_dict.update( + {getattr(item, unique_field): item for item in newly_inserted} + ) return existing_dict @@ -74,72 +85,97 @@ def seed(self): data = self._preprocess_csv() - countries = data['country'].unique() + countries = data["country"].unique() genres = set( genre.strip() - for genres in data['genre'].dropna() for genre in genres.split(',') + for genres in data["genre"].dropna() + for genre in genres.split(",") if genre.strip() ) actors = set( actor.strip() - for crew in data['crew'].dropna() for actor in crew.split(',') + for crew in data["crew"].dropna() + for actor in crew.split(",") if actor.strip() ) languages = set( lang.strip() - for langs in data['orig_lang'].dropna() for lang in langs.split(',') + for langs in data["orig_lang"].dropna() + for lang in langs.split(",") if lang.strip() ) - country_map = self._get_or_create_bulk(CountryModel, countries, 'code') - genre_map = self._get_or_create_bulk(GenreModel, list(genres), 'name') - actor_map = self._get_or_create_bulk(ActorModel, list(actors), 'name') - language_map = self._get_or_create_bulk(LanguageModel, list(languages), 'name') + country_map = self._get_or_create_bulk(CountryModel, countries, "code") + genre_map = self._get_or_create_bulk(GenreModel, list(genres), "name") + actor_map = self._get_or_create_bulk(ActorModel, list(actors), "name") + language_map = self._get_or_create_bulk( + LanguageModel, list(languages), "name" + ) movies_data = [] movie_genres_data = [] movie_actors_data = [] movie_languages_data = [] - for _, row in tqdm(data.iterrows(), total=data.shape[0], desc="Processing movies"): - country = country_map[row['country']] + for _, row in tqdm( + data.iterrows(), total=data.shape[0], desc="Processing movies" + ): + country = country_map[row["country"]] movie = { - "name": row['names'], - "date": row['date_x'], - "score": float(row['score']), - "overview": row['overview'], - "status": row['status'], - "budget": float(row['budget_x']), - "revenue": float(row['revenue']), - "country_id": country.id + "name": row["names"], + "date": row["date_x"], + "score": float(row["score"]), + "overview": row["overview"], + "status": row["status"], + "budget": float(row["budget_x"]), + "revenue": float(row["revenue"]), + "country_id": country.id, } movies_data.append(movie) - result = self._db_session.execute(insert(MovieModel).returning(MovieModel.id), movies_data) + result = self._db_session.execute( + insert(MovieModel).returning(MovieModel.id), movies_data + ) movie_ids = result.scalars().all() - for i, (_, row) in enumerate(tqdm(data.iterrows(), total=data.shape[0], desc="Processing associations")): + for i, (_, row) in enumerate( + tqdm( + data.iterrows(), total=data.shape[0], desc="Processing associations" + ) + ): movie_id = movie_ids[i] - for genre_name in row['genre'].split(','): + for genre_name in row["genre"].split(","): if genre_name.strip(): genre = genre_map[genre_name.strip()] - movie_genres_data.append({"movie_id": movie_id, "genre_id": genre.id}) + movie_genres_data.append( + {"movie_id": movie_id, "genre_id": genre.id} + ) - for actor_name in row['crew'].split(','): + for actor_name in row["crew"].split(","): if actor_name.strip(): actor = actor_map[actor_name.strip()] - movie_actors_data.append({"movie_id": movie_id, "actor_id": actor.id}) + movie_actors_data.append( + {"movie_id": movie_id, "actor_id": actor.id} + ) - for lang_name in row['orig_lang'].split(','): + for lang_name in row["orig_lang"].split(","): if lang_name.strip(): language = language_map[lang_name.strip()] - movie_languages_data.append({"movie_id": movie_id, "language_id": language.id}) + movie_languages_data.append( + {"movie_id": movie_id, "language_id": language.id} + ) - self._db_session.execute(insert(MoviesGenresModel).values(movie_genres_data)) - self._db_session.execute(insert(ActorsMoviesModel).values(movie_actors_data)) - self._db_session.execute(insert(MoviesLanguagesModel).values(movie_languages_data)) + self._db_session.execute( + insert(MoviesGenresModel).values(movie_genres_data) + ) + self._db_session.execute( + insert(ActorsMoviesModel).values(movie_actors_data) + ) + self._db_session.execute( + insert(MoviesLanguagesModel).values(movie_languages_data) + ) self._db_session.commit() except SQLAlchemyError as e: @@ -149,6 +185,7 @@ def seed(self): print(f"Unexpected error: {e}") raise + def main(): settings = get_settings() with get_db_contextmanager() as db_session: diff --git a/src/database/session_postgresql.py b/src/database/session_postgresql.py index 0e85355..5f7ec2c 100644 --- a/src/database/session_postgresql.py +++ b/src/database/session_postgresql.py @@ -7,10 +7,14 @@ settings = get_settings() -POSTGRESQL_DATABASE_URL = (f"postgresql://{settings.POSTGRES_USER}:{settings.POSTGRES_PASSWORD}@" - f"{settings.POSTGRES_HOST}:{settings.POSTGRES_DB_PORT}/{settings.POSTGRES_DB}") +POSTGRESQL_DATABASE_URL = ( + f"postgresql://{settings.POSTGRES_USER}:{settings.POSTGRES_PASSWORD}@" + f"{settings.POSTGRES_HOST}:{settings.POSTGRES_DB_PORT}/{settings.POSTGRES_DB}" +) postgresql_engine = create_engine(POSTGRESQL_DATABASE_URL) -PostgresqlSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=postgresql_engine) +PostgresqlSessionLocal = sessionmaker( + autocommit=False, autoflush=False, bind=postgresql_engine +) def get_postgresql_db() -> Session: diff --git a/src/database/session_sqlite.py b/src/database/session_sqlite.py index 9724c98..9c1c13a 100644 --- a/src/database/session_sqlite.py +++ b/src/database/session_sqlite.py @@ -9,9 +9,13 @@ settings = get_settings() SQLITE_DATABASE_URL = f"sqlite:///{settings.PATH_TO_DB}" -sqlite_engine = create_engine(SQLITE_DATABASE_URL, connect_args={"check_same_thread": False}) +sqlite_engine = create_engine( + SQLITE_DATABASE_URL, connect_args={"check_same_thread": False} +) sqlite_connection = sqlite_engine.connect() -SqliteSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=sqlite_connection) +SqliteSessionLocal = sessionmaker( + autocommit=False, autoflush=False, bind=sqlite_connection +) def get_sqlite_db() -> Session: diff --git a/src/main.py b/src/main.py index 8c4bed1..faa7b5e 100644 --- a/src/main.py +++ b/src/main.py @@ -12,7 +12,7 @@ CountryModel, GenreModel, ActorModel, - LanguageModel + LanguageModel, ) from schemas.movies import ( @@ -20,7 +20,7 @@ MovieCreateSchema, MovieDetailSchema, MovieUpdateSchema, - MoviePageSchema + MoviePageSchema, ) @@ -29,9 +29,9 @@ @router.get("/movies/", response_model=MoviePageSchema) async def list_movies( - page: int = Query(1, ge=1), - per_page: int = Query(10, ge=1, le=20), - db: Session = Depends(get_db) + page: int = Query(1, ge=1), + per_page: int = Query(10, ge=1, le=20), + db: Session = Depends(get_db), ): offset = (page - 1) * per_page movies_data = get_movies(db, offset, per_page) @@ -41,41 +41,51 @@ async def list_movies( total_items = db.query(MovieModel).count() total_pages = math.ceil(total_items / per_page) - prev_page = f"/theater/movies/?page={page - 1}&per_page={per_page}" if page > 1 else None - next_page = f"/theater/movies/?page={page + 1}&per_page={per_page}" if page < total_pages else None + prev_page = ( + f"/theater/movies/?page={page - 1}&per_page={per_page}" + if page > 1 + else None + ) + next_page = ( + f"/theater/movies/?page={page + 1}&per_page={per_page}" + if page < total_pages + else None + ) return { "movies": movies_data, "prev_page": prev_page, "next_page": next_page, "total_pages": total_pages, - "total_items": total_items + "total_items": total_items, } raise HTTPException(status_code=404, detail="No movies found.") @router.get("/movies/{movie_id}/", response_model=MovieDetailSchema) async def get_movie_detail( - movie_id: int = Path(..., gt=0), - db: Session = Depends(get_db) + movie_id: int = Path(..., gt=0), db: Session = Depends(get_db) ): movie = get_movie(db, movie_id) if movie: return movie - raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) @router.post("/movies/", response_model=MovieDetailSchema, status_code=201) def add_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): - movies = db.query(MovieModel).filter( - MovieModel.name == movie.name, - MovieModel.date == movie.date - ).all() + movies = ( + db.query(MovieModel) + .filter(MovieModel.name == movie.name, MovieModel.date == movie.date) + .all() + ) if not movies: return create_movie(db, movie) raise HTTPException( status_code=409, - detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists." + detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists.", ) @@ -87,4 +97,4 @@ def remove_movie(movie_id: int, db: Session = Depends(get_db)): @router.patch("/movies/{movie_id}/") def patch_movie(movie_id: int, movie: MovieUpdateSchema, db: Session = Depends(get_db)): update_movie(db, movie_id, movie) - return HTTPException(status_code=200, detail="Movie updated successfully.") \ No newline at end of file + return HTTPException(status_code=200, detail="Movie updated successfully.") diff --git a/src/routes/movies.py b/src/routes/movies.py index ec7c3cd..cbd3029 100644 --- a/src/routes/movies.py +++ b/src/routes/movies.py @@ -6,72 +6,200 @@ from crud.movies import get_movies, create_movie, get_movie, delete_movie, update_movie from database import get_db -from database.models import MovieModel, CountryModel, GenreModel, ActorModel, LanguageModel +from database.models import ( + MovieModel, + CountryModel, + GenreModel, + ActorModel, + LanguageModel, +) + +from schemas.movies import ( + MovieListSchema, + MovieCreateSchema, + MovieDetailSchema, + MovieUpdateSchema, + MoviePageSchema, +) + +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session, joinedload -from schemas.movies import MovieListSchema, MovieCreateSchema, MovieDetailSchema, MovieUpdateSchema, MoviePageSchema +router = APIRouter() -router = APIRouter() +@router.post("/movies", response_model=MovieDetailSchema, status_code=201) +async def create_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): + existing_film = ( + db.query(MovieModel) + .filter(MovieModel.name == movie.name, MovieModel.date == movie.date) + .first() + ) -@router.get("/movies/", response_model=MoviePageSchema) -async def list_movies( - page: int = Query(1, ge=1), - per_page: int = Query(10, ge=1, le=20), - db: Session = Depends(get_db) + if existing_film: + raise HTTPException( + status_code=409, + detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists.", + ) + + try: + country = db.query(CountryModel).filter_by(code=movie.country).first() + + if not country: + country = CountryModel(code=movie.country) + db.add(country) + db.flush() + + genres = [] + for genre in movie.genres: + genre_exist = db.query(GenreModel).filter_by(name=genre).first() + if not genre_exist: + genre_new = GenreModel(name=genre) + db.add(genre_new) + db.flush() + genre_exist = genre_new + genres.append(genre_exist) + + actors = [] + for actor in movie.actors: + actor_exist = db.query(ActorModel).filter_by(name=actor).first() + if not actor_exist: + actor_new = ActorModel(name=actor) + db.add(actor_new) + db.flush() + actor_exist = actor_new + actors.append(actor_exist) + + languages = [] + for language in movie.languages: + language_exist = db.query(LanguageModel).filter_by(name=language).first() + if not language_exist: + language_new = LanguageModel(name=language) + db.add(language_new) + db.flush() + language_exist = language_new + languages.append(language_exist) + + movie = MovieModel( + name=movie.name, + date=movie.date, + score=movie.score, + overview=movie.overview, + status=movie.status, + budget=movie.budget, + revenue=movie.revenue, + country=country, + genres=genres, + actors=actors, + languages=languages, + ) + db.add(movie) + db.commit() + db.refresh(movie) + return MovieDetailSchema.model_validate(movie) + + except IntegrityError: + db.rollback() + raise HTTPException(status_code=400, detail="Invalid input data!") + + +@router.get("/movies", response_model=None) +async def get_list_movie( + page: int = Query(1, ge=1, description="Page must be more than 1"), + per_page: int = Query( + 10, ge=1, le=20, description="Items per page can be in diapason 1-20" + ), + db: Session = Depends(get_db), ): - offset = (page - 1) * per_page - movies_data = get_movies(db, offset, per_page) - - if movies_data: + sorted_movies = db.query(MovieModel).order_by(MovieModel.id.desc()) + total_items = sorted_movies.count() + total_pages = (total_items + per_page - 1) // per_page - total_items = db.query(MovieModel).count() - total_pages = math.ceil(total_items / per_page) + if page > total_pages: + raise HTTPException(status_code=404, detail="No movies found.") - prev_page = f"/theater/movies/?page={page - 1}&per_page={per_page}" if page > 1 else None - next_page = f"/theater/movies/?page={page + 1}&per_page={per_page}" if page < total_pages else None + movies = sorted_movies.offset((page - 1) * per_page).limit(per_page).all() - return { - "movies": movies_data, - "prev_page": prev_page, - "next_page": next_page, - "total_pages": total_pages, - "total_items": total_items - } - raise HTTPException(status_code=404, detail="No movies found.") + if not movies: + raise HTTPException(status_code=404, detail="No movies found.") + + movie_list = [MovieListItemSchema.model_validate(movie) for movie in movies] + + previous_page = None if page <= 1 else page - 1 + next_page = None if page >= total_pages else page + 1 + return MovieListResponseSchema( + movies=movie_list, + prev_page=( + f"/theater/movies/?page={previous_page}&per_page={per_page}" + if previous_page + else None + ), + next_page=( + f"/theater/movies/?page={next_page}&per_page={per_page}" + if next_page + else None + ), + total_pages=total_pages, + total_items=total_items, + ) @router.get("/movies/{movie_id}/", response_model=MovieDetailSchema) -async def get_movie_detail( - movie_id: int = Path(..., gt=0), - db: Session = Depends(get_db) -): - movie = get_movie(db, movie_id) - if movie: - return movie - raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") - - -@router.post("/movies/", response_model=MovieDetailSchema, status_code=201) -def add_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): - movies = db.query(MovieModel).filter( - MovieModel.name == movie.name, - MovieModel.date == movie.date - ).all() - if not movies: - return create_movie(db, movie) - raise HTTPException( - status_code=409, - detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists." +async def get_detail_movie(movie_id: int, db: Session = Depends(get_db)): + movie = ( + db.query(MovieModel) + .options( + joinedload(MovieModel.country), + joinedload(MovieModel.genres), + joinedload(MovieModel.actors), + joinedload(MovieModel.languages), + ) + .filter(MovieModel.id == movie_id) + .first() ) + if not movie: + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) + + return MovieDetailSchema.model_validate(movie) + -@router.delete("/movies/{movie_id}/", status_code=204) -def remove_movie(movie_id: int, db: Session = Depends(get_db)): - delete_movie(db, movie_id) +@router.delete("/movies/{movie_id}/", response_model=None, status_code=204) +async def delete_movie(movie_id: int, db: Session = Depends(get_db)): + movie = db.query(MovieModel).filter_by(id=movie_id).first() + if not movie: + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) -@router.patch("/movies/{movie_id}/") -def patch_movie(movie_id: int, movie: MovieUpdateSchema, db: Session = Depends(get_db)): - update_movie(db, movie_id, movie) - return HTTPException(status_code=200, detail="Movie updated successfully.") \ No newline at end of file + db.delete(movie) + db.commit() + return {"detail": "Movie deleted successfully!", "status_code": 204} + + +@router.patch("/movies/{movie_id}/", response_model=None) +async def update_movie( + movie_id: int, movie_data: MovieUpdateSchema, db: Session = Depends(get_db) +): + movie = db.query(MovieModel).filter_by(id=movie_id).first() + + if not movie: + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) + + for field, value in movie_data.model_dump(exclude_unset=True).items(): + setattr(movie, field, value) + + try: + db.commit() + db.refresh(movie) + except IntegrityError: + db.rollback() + raise HTTPException(status_code=400, detail="Invalid input data.") + else: + return {"detail": "Movie updated successfully."} diff --git a/src/schemas/movies.py b/src/schemas/movies.py index aaf7aba..b2633d5 100644 --- a/src/schemas/movies.py +++ b/src/schemas/movies.py @@ -1,91 +1,125 @@ -from datetime import date as date_module, timedelta -from enum import Enum +import datetime from typing import Optional, List -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator + +from src.database.models import MovieStatusEnum + + +class LanguageSchema(BaseModel): + id: int + name: str + + model_config = {"from_attributes": True} class CountrySchema(BaseModel): id: int code: str - name: str | None + name: Optional[str] + model_config = {"from_attributes": True} -class GenresSchema(BaseModel): + +class GenreSchema(BaseModel): id: int name: str + model_config = {"from_attributes": True} + -class ActorsSchema(BaseModel): +class ActorSchema(BaseModel): id: int name: str + model_config = {"from_attributes": True} -class LanguagesSchema(BaseModel): - id: int - name: str + +class MovieBaseSchema(BaseModel): + name: str = Field(..., max_length=255) + date: datetime.date + score: float = Field(..., ge=0, le=100) + overview: str + status: MovieStatusEnum + budget: float = Field(..., ge=0) + revenue: float = Field(..., ge=0) + + model_config = {"from_attributes": True} + + @field_validator("date") + @classmethod + def validate_date(cls, value): + current_year = datetime.datetime.now().year + if value.year > current_year + 1: + raise ValueError( + f"The year in 'date' cannot be greater than {current_year + 1}." + ) + return value -class MovieStatus(str, Enum): - released = "Released" - post_production = "Post Production" - in_production = "In Production" +class MovieDetailSchema(MovieBaseSchema): + id: int + country: CountrySchema + genres: List[GenreSchema] + actors: List[ActorSchema] + languages: List[LanguageSchema] + model_config = {"from_attributes": True} -class MovieListSchema(BaseModel): + +class MovieListItemSchema(BaseModel): id: int name: str - date: date_module + date: datetime.date score: float overview: str + model_config = {"from_attributes": True} + -class MoviePageSchema(BaseModel): - movies: List[MovieListSchema] - prev_page: Optional[str] = None - next_page: Optional[str] = None +class MovieListResponseSchema(BaseModel): + movies: List[MovieListItemSchema] + prev_page: Optional[str] + next_page: Optional[str] total_pages: int total_items: int - -class MovieDetailSchema(BaseModel): - id: int - name: str - date: date_module - score: float = Field(ge=0, le=100) - overview: str - status: MovieStatus - budget: float = Field(ge=0) - revenue: float = Field(ge=0) - country: CountrySchema - genres: List[GenresSchema] - actors: List[ActorsSchema] - languages: List[LanguagesSchema] + model_config = {"from_attributes": True} class MovieCreateSchema(BaseModel): - name: str = Field(..., max_length=255) - date: date_module = Field(..., le=(date_module.today() + timedelta(days=365))) - score: float = Field(ge=0, le=100) + name: str + date: datetime.date + score: float = Field(..., ge=0, le=100) overview: str - status: MovieStatus - budget: float = Field(ge=0) - revenue: float = Field(ge=0) + status: MovieStatusEnum + budget: float = Field(..., ge=0) + revenue: float = Field(..., ge=0) country: str genres: List[str] actors: List[str] languages: List[str] + model_config = {"from_attributes": True} + + @field_validator("country", mode="before") + @classmethod + def normalize_country(cls, value: str) -> str: + return value.upper() + + @field_validator("genres", "actors", "languages", mode="before") + @classmethod + def normalize_list_fields(cls, value: List[str]) -> List[str]: + return [item.title() for item in value] + class MovieUpdateSchema(BaseModel): - name: Optional[str] = Field(None, max_length=255) - date: Optional[date_module] = Field(None, le=(date_module.today() + timedelta(days=365))) + name: Optional[str] = None + date: Optional[datetime.date] = None score: Optional[float] = Field(None, ge=0, le=100) overview: Optional[str] = None - status: Optional[MovieStatus] = None + status: Optional[MovieStatusEnum] = None budget: Optional[float] = Field(None, ge=0) revenue: Optional[float] = Field(None, ge=0) - country: Optional[str] = None - genres: Optional[List[str]] = None - actors: Optional[List[str]] = None - languages: Optional[List[str]] = None \ No newline at end of file + + model_config = {"from_attributes": True} diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 3460bbd..b8593ab 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -30,7 +30,9 @@ def db_session(): @pytest.fixture(scope="function") def seed_database(db_session): settings = get_settings() - seeder = CSVDatabaseSeeder(csv_file_path=settings.PATH_TO_MOVIES_CSV, db_session=db_session) + seeder = CSVDatabaseSeeder( + csv_file_path=settings.PATH_TO_MOVIES_CSV, db_session=db_session + ) if not seeder.is_db_populated(): seeder.seed() yield db_session diff --git a/src/tests/test_integration/test_movies.py b/src/tests/test_integration/test_movies.py index cb26e3f..6244c6e 100644 --- a/src/tests/test_integration/test_movies.py +++ b/src/tests/test_integration/test_movies.py @@ -15,7 +15,9 @@ def test_get_movies_empty_database(client): assert response.status_code == 404, f"Expected 404, got {response.status_code}" expected_detail = {"detail": "No movies found."} - assert response.json() == expected_detail, f"Expected {expected_detail}, got {response.json()}" + assert ( + response.json() == expected_detail + ), f"Expected {expected_detail}, got {response.json()}" def test_get_movies_default_parameters(client, seed_database): @@ -24,21 +26,32 @@ def test_get_movies_default_parameters(client, seed_database): """ response = client.get("/api/v1/theater/movies/") - assert response.status_code == 200, "Expected status code 200, but got a different value" + assert ( + response.status_code == 200 + ), "Expected status code 200, but got a different value" response_data = response.json() - assert len(response_data["movies"]) == 10, "Expected 10 movies in the response, but got a different count" + assert ( + len(response_data["movies"]) == 10 + ), "Expected 10 movies in the response, but got a different count" - assert response_data["total_pages"] > 0, "Expected total_pages > 0, but got a non-positive value" + assert ( + response_data["total_pages"] > 0 + ), "Expected total_pages > 0, but got a non-positive value" - assert response_data["total_items"] > 0, "Expected total_items > 0, but got a non-positive value" + assert ( + response_data["total_items"] > 0 + ), "Expected total_items > 0, but got a non-positive value" - assert response_data["prev_page"] is None, "Expected prev_page to be None on the first page, but got a value" + assert ( + response_data["prev_page"] is None + ), "Expected prev_page to be None on the first page, but got a value" if response_data["total_pages"] > 1: - assert response_data[ - "next_page"] is not None, "Expected next_page to be present when total_pages > 1, but got None" + assert ( + response_data["next_page"] is not None + ), "Expected next_page to be present when total_pages > 1, but got None" def test_get_movies_with_custom_parameters(client, seed_database): @@ -50,55 +63,74 @@ def test_get_movies_with_custom_parameters(client, seed_database): response = client.get(f"/api/v1/theater/movies/?page={page}&per_page={per_page}") - assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}" + assert ( + response.status_code == 200 + ), f"Expected status code 200, but got {response.status_code}" response_data = response.json() - assert len(response_data["movies"]) == per_page, ( - f"Expected {per_page} movies in the response, but got {len(response_data['movies'])}" - ) + assert ( + len(response_data["movies"]) == per_page + ), f"Expected {per_page} movies in the response, but got {len(response_data['movies'])}" - assert response_data["total_pages"] > 0, "Expected total_pages > 0, but got a non-positive value" + assert ( + response_data["total_pages"] > 0 + ), "Expected total_pages > 0, but got a non-positive value" - assert response_data["total_items"] > 0, "Expected total_items > 0, but got a non-positive value" + assert ( + response_data["total_items"] > 0 + ), "Expected total_items > 0, but got a non-positive value" if page > 1: - assert response_data["prev_page"] == f"/theater/movies/?page={page - 1}&per_page={per_page}", ( + assert ( + response_data["prev_page"] + == f"/theater/movies/?page={page - 1}&per_page={per_page}" + ), ( f"Expected prev_page to be '/theater/movies/?page={page - 1}&per_page={per_page}', " f"but got {response_data['prev_page']}" ) if page < response_data["total_pages"]: - assert response_data["next_page"] == f"/theater/movies/?page={page + 1}&per_page={per_page}", ( + assert ( + response_data["next_page"] + == f"/theater/movies/?page={page + 1}&per_page={per_page}" + ), ( f"Expected next_page to be '/theater/movies/?page={page + 1}&per_page={per_page}', " f"but got {response_data['next_page']}" ) else: - assert response_data["next_page"] is None, "Expected next_page to be None on the last page, but got a value" - - -@pytest.mark.parametrize("page, per_page, expected_detail", [ - (0, 10, "Input should be greater than or equal to 1"), - (1, 0, "Input should be greater than or equal to 1"), - (0, 0, "Input should be greater than or equal to 1"), -]) + assert ( + response_data["next_page"] is None + ), "Expected next_page to be None on the last page, but got a value" + + +@pytest.mark.parametrize( + "page, per_page, expected_detail", + [ + (0, 10, "Input should be greater than or equal to 1"), + (1, 0, "Input should be greater than or equal to 1"), + (0, 0, "Input should be greater than or equal to 1"), + ], +) def test_invalid_page_and_per_page(client, page, per_page, expected_detail): """ Test the `/movies/` endpoint with invalid `page` and `per_page` parameters. """ response = client.get(f"/api/v1/theater/movies/?page={page}&per_page={per_page}") - assert response.status_code == 422, ( - f"Expected status code 422 for invalid parameters, but got {response.status_code}" - ) + assert ( + response.status_code == 422 + ), f"Expected status code 422 for invalid parameters, but got {response.status_code}" response_data = response.json() - assert "detail" in response_data, "Expected 'detail' in the response, but it was missing" + assert ( + "detail" in response_data + ), "Expected 'detail' in the response, but it was missing" - assert any(expected_detail in error["msg"] for error in response_data["detail"]), ( - f"Expected error message '{expected_detail}' in the response details, but got {response_data['detail']}" - ) + assert any( + expected_detail in error["msg"] for error in response_data["detail"] + ), f"Expected error message '{expected_detail}' in the response details, but got {response_data['detail']}" def test_per_page_maximum_allowed_value(client, seed_database): @@ -107,14 +139,16 @@ def test_per_page_maximum_allowed_value(client, seed_database): """ response = client.get("/api/v1/theater/movies/?page=1&per_page=20") - assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}" + assert ( + response.status_code == 200 + ), f"Expected status code 200, but got {response.status_code}" response_data = response.json() assert "movies" in response_data, "Response missing 'movies' field." - assert len(response_data["movies"]) <= 20, ( - f"Expected at most 20 movies, but got {len(response_data['movies'])}" - ) + assert ( + len(response_data["movies"]) <= 20 + ), f"Expected at most 20 movies, but got {len(response_data['movies'])}" def test_page_exceeds_maximum(client, db_session, seed_database): @@ -125,9 +159,13 @@ def test_page_exceeds_maximum(client, db_session, seed_database): total_movies = db_session.query(MovieModel).count() max_page = (total_movies + per_page - 1) // per_page - response = client.get(f"/api/v1/theater/movies/?page={max_page + 1}&per_page={per_page}") + response = client.get( + f"/api/v1/theater/movies/?page={max_page + 1}&per_page={per_page}" + ) - assert response.status_code == 404, f"Expected status code 404, but got {response.status_code}" + assert ( + response.status_code == 404 + ), f"Expected status code 404, but got {response.status_code}" response_data = response.json() @@ -141,15 +179,14 @@ def test_movies_sorted_by_id_desc(client, db_session, seed_database): """ response = client.get("/api/v1/theater/movies/?page=1&per_page=10") - assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}" + assert ( + response.status_code == 200 + ), f"Expected status code 200, but got {response.status_code}" response_data = response.json() expected_movies = ( - db_session.query(MovieModel) - .order_by(MovieModel.id.desc()) - .limit(10) - .all() + db_session.query(MovieModel).order_by(MovieModel.id.desc()).limit(10).all() ) expected_movie_ids = [movie.id for movie in expected_movies] @@ -177,7 +214,9 @@ def test_movie_list_with_pagination(client, db_session, seed_database): response = client.get(f"/api/v1/theater/movies/?page={page}&per_page={per_page}") - assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}" + assert ( + response.status_code == 200 + ), f"Expected status code 200, but got {response.status_code}" response_data = response.json() @@ -202,7 +241,9 @@ def test_movie_list_with_pagination(client, db_session, seed_database): f"/theater/movies/?page={page - 1}&per_page={per_page}" if page > 1 else None ), "Previous page link mismatch." assert response_data["next_page"] == ( - f"/theater/movies/?page={page + 1}&per_page={per_page}" if page < total_pages else None + f"/theater/movies/?page={page + 1}&per_page={per_page}" + if page < total_pages + else None ), "Next page link mismatch." @@ -212,7 +253,9 @@ def test_movies_fields_match_schema(client, db_session, seed_database): """ response = client.get("/api/v1/theater/movies/?page=1&per_page=10") - assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}" + assert ( + response.status_code == 200 + ), f"Expected status code 200, but got {response.status_code}" response_data = response.json() @@ -236,13 +279,15 @@ def test_get_movie_by_id_not_found(client): response = client.get(f"/api/v1/theater/movies/{movie_id}") - assert response.status_code == 404, f"Expected status code 404, but got {response.status_code}" + assert ( + response.status_code == 404 + ), f"Expected status code 404, but got {response.status_code}" response_data = response.json() - assert response_data == {"detail": "Movie with the given ID was not found."}, ( - f"Expected error message not found. Got: {response_data}" - ) + assert response_data == { + "detail": "Movie with the given ID was not found." + }, f"Expected error message not found. Got: {response_data}" def test_get_movie_by_id_valid(client, db_session, seed_database): @@ -260,18 +305,26 @@ def test_get_movie_by_id_valid(client, db_session, seed_database): random_id = random.randint(min_id, max_id) - expected_movie = db_session.query(MovieModel).filter(MovieModel.id == random_id).first() + expected_movie = ( + db_session.query(MovieModel).filter(MovieModel.id == random_id).first() + ) assert expected_movie is not None, "Movie not found in database." response = client.get(f"/api/v1/theater/movies/{random_id}") - assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}" + assert ( + response.status_code == 200 + ), f"Expected status code 200, but got {response.status_code}" response_data = response.json() - assert response_data["id"] == expected_movie.id, "Returned ID does not match the requested ID." + assert ( + response_data["id"] == expected_movie.id + ), "Returned ID does not match the requested ID." - assert response_data["name"] == expected_movie.name, "Returned name does not match the expected name." + assert ( + response_data["name"] == expected_movie.name + ), "Returned name does not match the expected name." def test_get_movie_by_id_fields_match_database(client, db_session, seed_database): @@ -283,30 +336,52 @@ def test_get_movie_by_id_fields_match_database(client, db_session, seed_database response = client.get(f"/api/v1/theater/movies/{random_movie.id}/") - assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}" + assert ( + response.status_code == 200 + ), f"Expected status code 200, but got {response.status_code}" response_data = response.json() assert response_data["id"] == random_movie.id, "ID does not match." assert response_data["name"] == random_movie.name, "Name does not match." - assert response_data["date"] == random_movie.date.isoformat(), "Date does not match." + assert ( + response_data["date"] == random_movie.date.isoformat() + ), "Date does not match." assert response_data["score"] == random_movie.score, "Score does not match." - assert response_data["overview"] == random_movie.overview, "Overview does not match." - assert response_data["status"] == random_movie.status.value, "Status does not match." - assert response_data["budget"] == float(random_movie.budget), "Budget does not match." + assert ( + response_data["overview"] == random_movie.overview + ), "Overview does not match." + assert ( + response_data["status"] == random_movie.status.value + ), "Status does not match." + assert response_data["budget"] == float( + random_movie.budget + ), "Budget does not match." assert response_data["revenue"] == random_movie.revenue, "Revenue does not match." - assert response_data["country"]["id"] == random_movie.country.id, "Country ID does not match." - assert response_data["country"]["code"] == random_movie.country.code, "Country code does not match." - assert response_data["country"]["name"] == random_movie.country.name, "Country name does not match." - - expected_genres = [{"id": genre.id, "name": genre.name} for genre in random_movie.genres] + assert ( + response_data["country"]["id"] == random_movie.country.id + ), "Country ID does not match." + assert ( + response_data["country"]["code"] == random_movie.country.code + ), "Country code does not match." + assert ( + response_data["country"]["name"] == random_movie.country.name + ), "Country name does not match." + + expected_genres = [ + {"id": genre.id, "name": genre.name} for genre in random_movie.genres + ] assert response_data["genres"] == expected_genres, "Genres do not match." - expected_actors = [{"id": actor.id, "name": actor.name} for actor in random_movie.actors] + expected_actors = [ + {"id": actor.id, "name": actor.name} for actor in random_movie.actors + ] assert response_data["actors"] == expected_actors, "Actors do not match." - expected_languages = [{"id": lang.id, "name": lang.name} for lang in random_movie.languages] + expected_languages = [ + {"id": lang.id, "name": lang.name} for lang in random_movie.languages + ] assert response_data["languages"] == expected_languages, "Languages do not match." @@ -326,19 +401,23 @@ def test_create_movie_and_related_models(client, db_session): "country": "US", "genres": ["Action", "Adventure"], "actors": ["John Doe", "Jane Doe"], - "languages": ["English", "French"] + "languages": ["English", "French"], } response = client.post("/api/v1/theater/movies/", json=movie_data) - assert response.status_code == 201, f"Expected status code 201, but got {response.status_code}" + assert ( + response.status_code == 201 + ), f"Expected status code 201, but got {response.status_code}" response_data = response.json() assert response_data["name"] == movie_data["name"], "Movie name does not match." assert response_data["date"] == movie_data["date"], "Movie date does not match." assert response_data["score"] == movie_data["score"], "Movie score does not match." - assert response_data["overview"] == movie_data["overview"], "Movie overview does not match." + assert ( + response_data["overview"] == movie_data["overview"] + ), "Movie overview does not match." for genre_name in movie_data["genres"]: genre = db_session.query(GenreModel).filter_by(name=genre_name).first() @@ -352,7 +431,9 @@ def test_create_movie_and_related_models(client, db_session): language = db_session.query(LanguageModel).filter_by(name=language_name).first() assert language is not None, f"Language '{language_name}' was not created." - country = db_session.query(CountryModel).filter_by(code=movie_data["country"]).first() + country = ( + db_session.query(CountryModel).filter_by(code=movie_data["country"]).first() + ) assert country is not None, f"Country '{movie_data['country']}' was not created." @@ -375,21 +456,21 @@ def test_create_movie_duplicate_error(client, db_session, seed_database): "country": "US", "genres": ["Drama"], "actors": ["New Actor"], - "languages": ["Spanish"] + "languages": ["Spanish"], } response = client.post("/api/v1/theater/movies/", json=movie_data) - assert response.status_code == 409, f"Expected status code 409, but got {response.status_code}" + assert ( + response.status_code == 409 + ), f"Expected status code 409, but got {response.status_code}" response_data = response.json() - expected_detail = ( - f"A movie with the name '{movie_data['name']}' and release date '{movie_data['date']}' already exists." - ) - assert response_data["detail"] == expected_detail, ( - f"Expected detail message: {expected_detail}, but got: {response_data['detail']}" - ) + expected_detail = f"A movie with the name '{movie_data['name']}' and release date '{movie_data['date']}' already exists." + assert ( + response_data["detail"] == expected_detail + ), f"Expected detail message: {expected_detail}, but got: {response_data['detail']}" def test_delete_movie_success(client, db_session, seed_database): @@ -403,9 +484,13 @@ def test_delete_movie_success(client, db_session, seed_database): response = client.delete(f"/api/v1/theater/movies/{movie_id}/") - assert response.status_code == 204, f"Expected status code 204, but got {response.status_code}" + assert ( + response.status_code == 204 + ), f"Expected status code 204, but got {response.status_code}" - deleted_movie = db_session.query(MovieModel).filter(MovieModel.id == movie_id).first() + deleted_movie = ( + db_session.query(MovieModel).filter(MovieModel.id == movie_id).first() + ) assert deleted_movie is None, f"Movie with ID {movie_id} was not deleted." @@ -417,13 +502,15 @@ def test_delete_movie_not_found(client): response = client.delete(f"/api/v1/theater/movies/{non_existent_id}/") - assert response.status_code == 404, f"Expected status code 404, but got {response.status_code}" + assert ( + response.status_code == 404 + ), f"Expected status code 404, but got {response.status_code}" response_data = response.json() expected_detail = "Movie with the given ID was not found." - assert response_data["detail"] == expected_detail, ( - f"Expected detail message: {expected_detail}, but got: {response_data['detail']}" - ) + assert ( + response_data["detail"] == expected_detail + ), f"Expected detail message: {expected_detail}, but got: {response_data['detail']}" def test_update_movie_success(client, db_session, seed_database): @@ -441,15 +528,19 @@ def test_update_movie_success(client, db_session, seed_database): response = client.patch(f"/api/v1/theater/movies/{movie_id}/", json=update_data) - assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}" + assert ( + response.status_code == 200 + ), f"Expected status code 200, but got {response.status_code}" response_data = response.json() - assert response_data["detail"] == "Movie updated successfully.", ( - f"Expected detail message: 'Movie updated successfully.', but got: {response_data['detail']}" - ) + assert ( + response_data["detail"] == "Movie updated successfully." + ), f"Expected detail message: 'Movie updated successfully.', but got: {response_data['detail']}" db_session.expire_all() - updated_movie = db_session.query(MovieModel).filter(MovieModel.id == movie_id).first() + updated_movie = ( + db_session.query(MovieModel).filter(MovieModel.id == movie_id).first() + ) assert updated_movie.name == update_data["name"], "Movie name was not updated." assert updated_movie.score == update_data["score"], "Movie score was not updated." @@ -460,17 +551,18 @@ def test_update_movie_not_found(client): Test the `/movies/{movie_id}/` endpoint with a non-existent movie ID. """ non_existent_id = 99999 - update_data = { - "name": "Non-existent Movie", - "score": 90.0 - } + update_data = {"name": "Non-existent Movie", "score": 90.0} - response = client.patch(f"/api/v1/theater/movies/{non_existent_id}/", json=update_data) + response = client.patch( + f"/api/v1/theater/movies/{non_existent_id}/", json=update_data + ) - assert response.status_code == 404, f"Expected status code 404, but got {response.status_code}" + assert ( + response.status_code == 404 + ), f"Expected status code 404, but got {response.status_code}" response_data = response.json() expected_detail = "Movie with the given ID was not found." - assert response_data["detail"] == expected_detail, ( - f"Expected detail message: {expected_detail}, but got: {response_data['detail']}" - ) + assert ( + response_data["detail"] == expected_detail + ), f"Expected detail message: {expected_detail}, but got: {response_data['detail']}"