From 2767b1866a0a4d46dbf38494f6f91cece408a11f Mon Sep 17 00:00:00 2001 From: Tetyana Pavlyuk Date: Thu, 13 Feb 2025 16:02:27 +0200 Subject: [PATCH 1/2] Solution --- src/routes/movies.py | 158 +++++++++++++++++++++++++++++++++++++++++- src/schemas/movies.py | 115 +++++++++++++++++++++++++++++- 2 files changed, 270 insertions(+), 3 deletions(-) diff --git a/src/routes/movies.py b/src/routes/movies.py index e44678a..0a913ce 100644 --- a/src/routes/movies.py +++ b/src/routes/movies.py @@ -1,12 +1,166 @@ -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, Response +from sqlalchemy import desc from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, joinedload from database import get_db from database.models import MovieModel, CountryModel, GenreModel, ActorModel, LanguageModel +from schemas.movies import ( + MovieBaseSchema, + MovieListSchema, + MovieRetrieveSchema, + MovieCreateSchema, + MovieUpdateSchema +) router = APIRouter() -# Write your code here +@router.get("/movies/", response_model=MovieListSchema) +def get_movies( + page: int = Query(1, ge=1, description="Page number"), + per_page: int = Query(10, ge=1, le=20, description="Movies per page"), + db: Session = Depends(get_db) +): + total_items = db.query(MovieModel).count() + total_pages = total_items // per_page + 1 + 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 + + first_movie_id = (page - 1) * per_page + db_movies = db.query(MovieModel).order_by(desc(MovieModel.id)).offset(first_movie_id).limit(per_page).all() + if not db_movies: + raise HTTPException(status_code=404, detail="No movies found.") + + return MovieListSchema( + movies=db_movies, + prev_page=prev_page, + next_page=next_page, + total_pages=total_pages, + total_items=total_items + ) + + +@router.post("/movies/", response_model=MovieRetrieveSchema, status_code=201) +def create_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): + try: + country = get_or_create_country(db=db, country_code=movie.country) + genres = get_or_create_genre(db=db, genres_name=movie.genres) + actors = get_or_create_actors(db=db, actors_name=movie.actors) + languages = get_or_create_languages(db=db, languages_name=movie.languages) + + db_movie = db.query(MovieModel).filter_by(name=movie.name, date=movie.date).first() + if db_movie: + raise HTTPException( + status_code=409, + detail=f"A movie with the name '{movie.name}' and " + f"release date '{movie.date}' already exists." + ) + + 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, + genres=genres, + actors=actors, + languages=languages + ) + except IntegrityError: + raise HTTPException(status_code=400, detail="Invalid input data.") + else: + db.add(db_movie) + db.commit() + db.refresh(db_movie) + return db_movie + + +@router.get("/movies/{movie_id}/", response_model=MovieRetrieveSchema) +def get_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.") + return movie + + +@router.delete("/movies/{movie_id}/") +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.") + db.delete(movie) + db.commit() + return Response(status_code=204) + + +@router.patch("/movies/{movie_id}/") +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.") + + update_data = movie_data.dict(exclude_unset=True) + + try: + for field, value in update_data.items(): + setattr(movie, field, value) + except Exception as e: + print(e) + raise HTTPException(status_code=400, detail="Invalid input data.") + else: + db.add(movie) + db.commit() + db.refresh(movie) + return {"detail": "Movie updated successfully."} + + +def get_or_create_country(db: Session, country_code: str): + country = db.query(CountryModel).filter_by(code=country_code).first() + if not country: + country = CountryModel(code=country_code) + db.add(country) + db.commit() + db.refresh(country) + return country + +def get_or_create_genre(db: Session, genres_name: list): + genres: list = [] + for genre_name in genres_name: + genre = db.query(GenreModel).filter_by(name=genre_name).first() + if not genre: + genre = GenreModel(name=genre_name) + db.add(genre) + db.commit() + db.refresh(genre) + genres.append(genre) + return genres + +def get_or_create_actors(db: Session, actors_name: list): + actors: list = [] + for actor_name in actors_name: + actor = db.query(ActorModel).filter_by(name=actor_name).first() + if not actor: + actor = ActorModel(name=actor_name) + db.add(actor) + db.commit() + db.refresh(actor) + actors.append(actor) + return actors + +def get_or_create_languages(db: Session, languages_name: list): + languages = [] + for language_name in languages_name: + language = db.query(LanguageModel).filter_by(name=language_name).first() + if not language: + language = LanguageModel(name=language_name) + db.add(language) + db.commit() + db.refresh(language) + languages.append(language) + return languages diff --git a/src/schemas/movies.py b/src/schemas/movies.py index fabb9be..4d97eb6 100644 --- a/src/schemas/movies.py +++ b/src/schemas/movies.py @@ -1 +1,114 @@ -# Write your code here +import datetime +from typing import Optional + +from pydantic import BaseModel, Field, field_validator +from database.models import MovieStatusEnum + + +class GenreBaseSchema(BaseModel): + name: str + + +class GenreRetrieveSchema(GenreBaseSchema): + id: int + + class Config: + from_attributes = True + + +class ActorBaseSchema(BaseModel): + name: str + + +class ActorRetrieveSchema(ActorBaseSchema): + id: int + + class Config: + from_attributes = True + + +class CountryBaseSchema(BaseModel): + code: str + name: str | None + + +class CountryRetrieveSchema(CountryBaseSchema): + id: int + + class Config: + from_attributes = True + + +class LanguageBaseSchema(BaseModel): + name: str + + +class LanguageRetrieveSchema(LanguageBaseSchema): + id: int + + class Config: + from_attributes = True + + +class MovieBaseSchema(BaseModel): + name: str = Field(max_length=255) + date: datetime.date = Field( + le=datetime.date.today() + datetime.timedelta(days=365) + ) + score: float = Field(ge=0, le=100) + overview: str + status: MovieStatusEnum + budget: float = Field(ge=0) + revenue: float = Field(ge=0) + + +class MovieCreateSchema(MovieBaseSchema): + country: str = Field(pattern=r"^[A-Z]{2}$") + genres: list[str] + actors: list[str] + languages: list[str] + + +class MovieRetrieveSchema(MovieBaseSchema): + id: int + country: CountryRetrieveSchema + genres: list[GenreRetrieveSchema] + actors: list[ActorRetrieveSchema] + languages: list[LanguageRetrieveSchema] + + class Config: + from_attributes = True + + +class MovieShortSchema(BaseModel): + id: int + name: str = Field(max_length=255) + date: datetime.date = Field( + le=datetime.date.today() + datetime.timedelta(days=365) + ) + score: float = Field(ge=0, le=100) + overview: str + + class Config: + from_attributes = True + + +class MovieListSchema(BaseModel): + movies: list[MovieShortSchema] + prev_page: str | None + next_page: str | None + total_pages: int + total_items: int + + +class MovieUpdateSchema(BaseModel): + name: Optional[str] = Field(None, max_length=255) + date: Optional[datetime.date] = Field( + None, + le=datetime.date.today() + datetime.timedelta(days=365) + ) + score: Optional[float] = Field(None, ge=0, le=100) + overview: Optional[str] = None + status: Optional[MovieStatusEnum] = None + budget: Optional[float] = Field(None, ge=0) + revenue: Optional[float] = Field(None, ge=0) From 1d0560f6ebfeb33d0f6a0b227ff0ff7ad9827f41 Mon Sep 17 00:00:00 2001 From: Tetyana Pavlyuk Date: Thu, 13 Feb 2025 16:14:42 +0200 Subject: [PATCH 2/2] Solution with flake8 edits --- src/config/settings.py | 10 +- src/database/__init__.py | 9 +- src/database/migrations/env.py | 6 +- .../ea3a65568bd9_initial_migration.py | 142 +++++---- src/database/models.py | 72 +++-- src/database/populate.py | 139 ++++++--- src/database/session_postgresql.py | 10 +- src/database/session_sqlite.py | 8 +- src/main.py | 9 +- src/routes/movies.py | 63 +++- src/schemas/movies.py | 11 +- src/tests/conftest.py | 4 +- src/tests/test_integration/test_movies.py | 286 ++++++++++++------ 13 files changed, 487 insertions(+), 282 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/database/__init__.py b/src/database/__init__.py index 517d679..5fc75e0 100644 --- a/src/database/__init__.py +++ b/src/database/__init__.py @@ -1,9 +1,6 @@ import os -from database.models import ( - Base, - MovieModel -) +from database.models import Base, MovieModel from database.session_sqlite import reset_sqlite_database as reset_database environment = os.getenv("ENVIRONMENT", "developing") @@ -11,10 +8,10 @@ if environment == "testing": from database.session_sqlite import ( get_sqlite_db_contextmanager as get_db_contextmanager, - get_sqlite_db as get_db + get_sqlite_db as get_db, ) else: from database.session_postgresql import ( get_postgresql_db_contextmanager as get_db_contextmanager, - get_postgresql_db as get_db + get_postgresql_db as get_db, ) 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..9dcca54 100644 --- a/src/database/migrations/versions/ea3a65568bd9_initial_migration.py +++ b/src/database/migrations/versions/ea3a65568bd9_initial_migration.py @@ -1,10 +1,11 @@ """initial migration Revision ID: ea3a65568bd9 -Revises: +Revises: 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 f5fb895..df1053c 100644 --- a/src/database/populate.py +++ b/src/database/populate.py @@ -6,8 +6,15 @@ from config import get_settings from database import MovieModel, get_db_contextmanager -from database.models import CountryModel, GenreModel, ActorModel, MoviesGenresModel, ActorsMoviesModel, LanguageModel, \ - MoviesLanguagesModel +from database.models import ( + CountryModel, + GenreModel, + ActorModel, + MoviesGenresModel, + ActorsMoviesModel, + LanguageModel, + MoviesLanguagesModel, +) class CSVDatabaseSeeder: @@ -20,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) @@ -41,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] @@ -51,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 @@ -64,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: @@ -139,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 35ca2a7..92b4995 100644 --- a/src/main.py +++ b/src/main.py @@ -2,11 +2,10 @@ from routes import movie_router -app = FastAPI( - title="Movies homework", - description="Description of project" -) +app = FastAPI(title="Movies homework", description="Description of project") api_version_prefix = "/api/v1" -app.include_router(movie_router, prefix=f"{api_version_prefix}/theater", tags=["theater"]) +app.include_router( + movie_router, prefix=f"{api_version_prefix}/theater", tags=["theater"] +) diff --git a/src/routes/movies.py b/src/routes/movies.py index 0a913ce..ad8e694 100644 --- a/src/routes/movies.py +++ b/src/routes/movies.py @@ -4,13 +4,19 @@ from sqlalchemy.orm import Session, joinedload 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 ( MovieBaseSchema, MovieListSchema, MovieRetrieveSchema, MovieCreateSchema, - MovieUpdateSchema + MovieUpdateSchema, ) @@ -19,17 +25,29 @@ @router.get("/movies/", response_model=MovieListSchema) def get_movies( - page: int = Query(1, ge=1, description="Page number"), - per_page: int = Query(10, ge=1, le=20, description="Movies per page"), - db: Session = Depends(get_db) + page: int = Query(1, ge=1, description="Page number"), + per_page: int = Query(10, ge=1, le=20, description="Movies per page"), + db: Session = Depends(get_db), ): total_items = db.query(MovieModel).count() total_pages = total_items // per_page + 1 - 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 + ) first_movie_id = (page - 1) * per_page - db_movies = db.query(MovieModel).order_by(desc(MovieModel.id)).offset(first_movie_id).limit(per_page).all() + db_movies = ( + db.query(MovieModel) + .order_by(desc(MovieModel.id)) + .offset(first_movie_id) + .limit(per_page) + .all() + ) if not db_movies: raise HTTPException(status_code=404, detail="No movies found.") @@ -38,7 +56,7 @@ def get_movies( prev_page=prev_page, next_page=next_page, total_pages=total_pages, - total_items=total_items + total_items=total_items, ) @@ -50,12 +68,14 @@ def create_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): actors = get_or_create_actors(db=db, actors_name=movie.actors) languages = get_or_create_languages(db=db, languages_name=movie.languages) - db_movie = db.query(MovieModel).filter_by(name=movie.name, date=movie.date).first() + db_movie = ( + db.query(MovieModel).filter_by(name=movie.name, date=movie.date).first() + ) if db_movie: raise HTTPException( status_code=409, detail=f"A movie with the name '{movie.name}' and " - f"release date '{movie.date}' already exists." + f"release date '{movie.date}' already exists.", ) db_movie = MovieModel( @@ -69,7 +89,7 @@ def create_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): country=country, genres=genres, actors=actors, - languages=languages + languages=languages, ) except IntegrityError: raise HTTPException(status_code=400, detail="Invalid input data.") @@ -84,7 +104,9 @@ def create_movie(movie: MovieCreateSchema, db: Session = Depends(get_db)): def get_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.") + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) return movie @@ -92,18 +114,24 @@ def get_movie(movie_id: int, db: Session = Depends(get_db)): 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.") + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) db.delete(movie) db.commit() return Response(status_code=204) @router.patch("/movies/{movie_id}/") -def update_movie(movie_id: int, movie_data: MovieUpdateSchema, db: Session = Depends(get_db)): +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.") + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) update_data = movie_data.dict(exclude_unset=True) @@ -129,6 +157,7 @@ def get_or_create_country(db: Session, country_code: str): db.refresh(country) return country + def get_or_create_genre(db: Session, genres_name: list): genres: list = [] for genre_name in genres_name: @@ -141,6 +170,7 @@ def get_or_create_genre(db: Session, genres_name: list): genres.append(genre) return genres + def get_or_create_actors(db: Session, actors_name: list): actors: list = [] for actor_name in actors_name: @@ -153,6 +183,7 @@ def get_or_create_actors(db: Session, actors_name: list): actors.append(actor) return actors + def get_or_create_languages(db: Session, languages_name: list): languages = [] for language_name in languages_name: diff --git a/src/schemas/movies.py b/src/schemas/movies.py index 4d97eb6..9c975a2 100644 --- a/src/schemas/movies.py +++ b/src/schemas/movies.py @@ -52,9 +52,7 @@ class Config: class MovieBaseSchema(BaseModel): name: str = Field(max_length=255) - date: datetime.date = Field( - le=datetime.date.today() + datetime.timedelta(days=365) - ) + date: datetime.date = Field(le=datetime.date.today() + datetime.timedelta(days=365)) score: float = Field(ge=0, le=100) overview: str status: MovieStatusEnum @@ -83,9 +81,7 @@ class Config: class MovieShortSchema(BaseModel): id: int name: str = Field(max_length=255) - date: datetime.date = Field( - le=datetime.date.today() + datetime.timedelta(days=365) - ) + date: datetime.date = Field(le=datetime.date.today() + datetime.timedelta(days=365)) score: float = Field(ge=0, le=100) overview: str @@ -104,8 +100,7 @@ class MovieListSchema(BaseModel): class MovieUpdateSchema(BaseModel): name: Optional[str] = Field(None, max_length=255) date: Optional[datetime.date] = Field( - None, - le=datetime.date.today() + datetime.timedelta(days=365) + None, le=datetime.date.today() + datetime.timedelta(days=365) ) score: Optional[float] = Field(None, ge=0, le=100) overview: Optional[str] = None 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']}"