diff --git a/.flake8 b/.flake8 index cbd72d8..e65b142 100644 --- a/.flake8 +++ b/.flake8 @@ -5,4 +5,4 @@ max-line-length = 119 max-complexity = 18 select = B,C,E,F,W,T4,B9,ANN,Q0,N8,VNE exclude = .venv -extend-exclude = src/tests/* +extend-exclude = src/tests/*, src/database/migrations/* diff --git a/src/database/populate.py b/src/database/populate.py index f5fb895..e624239 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, \ +from database.models import ( + CountryModel, + GenreModel, + ActorModel, + MoviesGenresModel, + ActorsMoviesModel, + LanguageModel, MoviesLanguagesModel +) class CSVDatabaseSeeder: @@ -139,6 +146,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/routes/movies.py b/src/routes/movies.py index e44678a..889b7db 100644 --- a/src/routes/movies.py +++ b/src/routes/movies.py @@ -1,12 +1,221 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session, joinedload +import datetime +from typing import Annotated + +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Query, + status +) +from sqlalchemy.orm import Session 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 ( + MovieDetailResponseSchema, + MovieListResponseSchema, + MovieUpdateResponseSchema, + MovieCreateResponseSchema, + MovieListReadResponseSchema +) router = APIRouter() -# Write your code here +@router.get("/movies/", response_model=MovieListResponseSchema) +def get_movies( + page: Annotated[int, Query(ge=1, description="The page number to fetch.")] = 1, + per_page: Annotated[int, Query(ge=1, le=20, description="Number of movies per page.")] = 10, + db: Session = Depends(get_db) +): + total_items = db.query(MovieModel).count() + total_pages = (total_items + per_page - 1) // per_page + + if page > total_pages or total_items == 0: + raise HTTPException(status_code=404, detail="No movies found.") + + offset = (page - 1) * per_page + movies_query = db.query(MovieModel).order_by(MovieModel.id.desc()).offset(offset).limit(per_page).all() + movies = [MovieListReadResponseSchema.model_validate(movie) for movie in movies_query] + + 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 MovieListResponseSchema( + movies=movies, + prev_page=prev_page, + next_page=next_page, + total_pages=total_pages, + total_items=total_items, + ) + + +@router.get("/movies/{movie_id}", response_model=MovieDetailResponseSchema) +def get_movie_by_id(movie_id: int, db: Session = Depends(get_db)): + movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first() + if not movie: + raise HTTPException(status_code=404, detail="Movie with the given ID was not found.") + return movie + + +@router.post("/movies/", response_model=MovieDetailResponseSchema, status_code=status.HTTP_201_CREATED) +def create_movie(movie: MovieCreateResponseSchema, db: Session = Depends(get_db)): + if len(movie.name) > 255: + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + + if movie.date > datetime.datetime.now().date() + datetime.timedelta(days=365): + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + + if not (0 <= movie.score <= 100): + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + + if movie.budget < 0 or movie.revenue < 0: + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + + existing_movie = db.query(MovieModel).filter_by(name=movie.name, date=movie.date).first() + if existing_movie: + raise HTTPException( + status_code=409, + detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists." + ) + + country = db.query(CountryModel).filter_by(code=movie.country).first() + if not country: + country = CountryModel(code=movie.country) + db.add(country) + db.commit() + db.refresh(country) + + genres = [] + for genre in movie.genres: + genre_obj = db.query(GenreModel).filter_by(name=genre).first() + if not genre_obj: + genre_obj = GenreModel(name=genre) + db.add(genre_obj) + db.commit() + db.refresh(genre_obj) + genres.append(genre_obj) + + actors = [] + for actor in movie.actors: + actor_obj = db.query(ActorModel).filter_by(name=actor).first() + if not actor_obj: + actor_obj = ActorModel(name=actor) + db.add(actor_obj) + db.commit() + db.refresh(actor_obj) + actors.append(actor_obj) + + languages = [] + for language in movie.languages: + language_obj = db.query(LanguageModel).filter_by(name=language).first() + if not language_obj: + language_obj = LanguageModel(name=language) + db.add(language_obj) + db.commit() + db.refresh(language_obj) + languages.append(language_obj) + + 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, + ) + + db.add(db_movie) + db.commit() + db.refresh(db_movie) + return db_movie + + +@router.delete("/movies/{movie_id}", status_code=status.HTTP_204_NO_CONTENT) +def remove_movie(movie_id: int, db: Session = Depends(get_db)): + 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() + + +@router.patch("/movies/{movie_id}") +def edit_movie(movie_id: int, movie: MovieUpdateResponseSchema, db: Session = Depends(get_db)): + 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.") + + if movie.name: + if len(movie.name) > 255: + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + db_movie.name = movie.name + + if movie.date: + if movie.date > datetime.datetime.now().date() + datetime.timedelta(days=365): + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + db_movie.date = movie.date + + if movie.score: + if not (0 <= movie.score <= 100): + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + db_movie.score = movie.score + + if movie.overview: + db_movie.overview = movie.overview + if movie.status: + db_movie.status = movie.status + + if movie.budget: + if movie.budget < 0: + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + db_movie.budget = movie.budget + + if movie.revenue: + if movie.revenue < 0: + raise HTTPException( + status_code=400, + detail="Invalid input data." + ) + db_movie.revenue = movie.revenue + + db.commit() + db.refresh(db_movie) + return {"detail": "Movie updated successfully.", "movie": db_movie} diff --git a/src/schemas/movies.py b/src/schemas/movies.py index fabb9be..363c987 100644 --- a/src/schemas/movies.py +++ b/src/schemas/movies.py @@ -1 +1,97 @@ -# Write your code here +import datetime + +from pydantic import BaseModel + +from database.models import MovieStatusEnum + + +class CountryResponseSchema(BaseModel): + id: int + code: str + name: str | None + + model_config = {"from_attributes": True} + + +class GenreResponseSchema(BaseModel): + id: int + name: str + + model_config = {"from_attributes": True} + + +class ActorResponseSchema(BaseModel): + id: int + name: str + + model_config = {"from_attributes": True} + + +class LanguageResponseSchema(BaseModel): + id: int + name: str + + model_config = {"from_attributes": True} + + +class MovieListReadResponseSchema(BaseModel): + id: int + name: str + date: datetime.date + score: float + overview: str + + model_config = {"from_attributes": True} + + +class MovieListResponseSchema(BaseModel): + movies: list[MovieListReadResponseSchema] + prev_page: str | None + next_page: str | None + total_pages: int + total_items: int + + +class MovieDetailResponseSchema(BaseModel): + id: int + name: str + date: datetime.date + score: float + overview: str + status: MovieStatusEnum + budget: float + revenue: float + country: CountryResponseSchema + genres: list[GenreResponseSchema] + actors: list[ActorResponseSchema] + languages: list[LanguageResponseSchema] + + model_config = {"from_attributes": True} + + +class MovieCreateResponseSchema(BaseModel): + name: str + date: datetime.date + score: float + overview: str + status: MovieStatusEnum + budget: float + revenue: float + country: str + genres: list[str] + actors: list[str] + languages: list[str] + + model_config = {"from_attributes": True} + + +class MovieUpdateResponseSchema(BaseModel): + name: str | None = None + date: datetime.date | None = None + score: float | None = None + overview: str | None = None + status: MovieStatusEnum | None = None + budget: float | None = None + revenue: float | None = None + + model_config = {"from_attributes": True}