diff --git a/.flake8 b/.flake8 index cbd72d8..ae04aee 100644 --- a/.flake8 +++ b/.flake8 @@ -4,5 +4,5 @@ ignore = E203, E266, W503, ANN002, ANN003, ANN101, ANN102, ANN401, N807, N818, V max-line-length = 119 max-complexity = 18 select = B,C,E,F,W,T4,B9,ANN,Q0,N8,VNE -exclude = .venv +exclude = .venv, src/database/* extend-exclude = src/tests/* diff --git a/src/crud.py b/src/crud.py new file mode 100644 index 0000000..bd1df3d --- /dev/null +++ b/src/crud.py @@ -0,0 +1,56 @@ +from src.database.models import CountryModel + + +def get_movies_on_page(page, per_page, model, db): + return ( + db.query(model) + .order_by(model.id.desc()) + .offset((page - 1) * per_page) + .limit(per_page) + .all() + ) + + +def create_new_movie(movie, db): + db.add(movie) + db.commit() + db.refresh(movie) + return movie + + +def get_movie_by_id(movie_id, model, db): + return db.query(model).filter(model.id == movie_id).first() + + +def get_movie_by_name_and_date(name, date, model, db): + return db.query(model).filter(model.name == name).filter(model.date == date).first() + + +def get_instance_by_name(name, model, db): + return db.query(model).filter(model.name == name).first() + + +def create_country_by_code(code, model, db): + country = model(code=code) + db.add(country) + db.commit() + db.refresh(country) + return country + + +def create_instance_by_name(name, model, db): + instance = model(name=name) + db.add(instance) + db.commit() + db.refresh(instance) + return instance + + +def check_or_create_many_instances_by_name(names, model, db): + list_of_instances = [] + for name in names: + instance = get_instance_by_name(name=name, model=model, db=db) + if not instance: + instance = create_instance_by_name(name=name, model=model, db=db) + list_of_instances.append(instance) + return list_of_instances diff --git a/src/routes/movies.py b/src/routes/movies.py index e44678a..67b3190 100644 --- a/src/routes/movies.py +++ b/src/routes/movies.py @@ -1,12 +1,158 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, joinedload +import datetime 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 MovieListResponse, MovieDetail, MovieCreate, MovieUpdate + +from crud import ( + create_country_by_code, + create_instance_by_name, + check_or_create_many_instances_by_name, + get_movies_on_page, + get_movie_by_name_and_date, + get_movie_by_id, +) + +from src.crud import create_new_movie router = APIRouter() -# Write your code here +@router.get("/movies/", response_model=MovieListResponse) +def get_movies( + db: Session = Depends(get_db), + page: int = Query(1, ge=1), + per_page: int = Query(10, ge=1, le=20), +) -> MovieListResponse: + movies = get_movies_on_page(page, per_page, MovieModel, db) + + if not movies: + raise HTTPException(status_code=404, detail="No movies found.") + + prev_page = f"/theater/movies/?page={page - 1}&per_page={per_page}" + next_page = f"/theater/movies/?page={page + 1}&per_page={per_page}" + total_items = db.query(MovieModel).count() + total_pages = (total_items // per_page) + (1 if total_items % per_page > 0 else 0) + + if total_items == 0: + raise HTTPException(status_code=404, detail="No movies found") + + return { + "movies": movies, + "prev_page": prev_page if page > 1 else None, + "next_page": next_page if total_pages > page else None, + "total_items": total_items, + "total_pages": total_pages, + } + + +@router.post("/movies/", response_model=MovieDetail, status_code=201) +def create_movie(movie: MovieCreate, db: Session = Depends(get_db)) -> MovieDetail: + db_movie = get_movie_by_name_and_date( + name=movie.name, date=movie.date, model=MovieModel, db=db + ) + + if db_movie: + raise HTTPException( + status_code=409, + detail=f"A movie with the name '{db_movie.name}' and release date '{db_movie.date}' already exists.", + ) + + if movie.date > datetime.datetime.now().date() + datetime.timedelta(days=365): + raise HTTPException( + status_code=400, + detail="Invalid input data.", + ) + + country = db.query(CountryModel).filter(CountryModel.code == movie.country).first() + genres = [] + actors = [] + languages = [] + + if movie.languages: + languages.extend( + check_or_create_many_instances_by_name(movie.languages, LanguageModel, db) + ) + + if movie.actors: + actors.extend( + check_or_create_many_instances_by_name(movie.actors, ActorModel, db) + ) + + if movie.genres: + genres.extend( + check_or_create_many_instances_by_name(movie.genres, GenreModel, db) + ) + + if not country: + country = create_country_by_code(code=movie.country, model=CountryModel, db=db) + + try: + new_movie = MovieModel( + **movie.model_dump(exclude={"country", "genres", "actors", "languages"}), + country=country, + genres=genres, + actors=actors, + languages=languages, + ) + return create_new_movie(movie=new_movie, db=db) + + except IntegrityError: + raise HTTPException(status_code=400, detail="Invalid input data.") + + +@router.get("/movies/{movie_id}/", response_model=MovieDetail) +def get_movie(movie_id: int, db: Session = Depends(get_db)) -> MovieDetail: + movie = get_movie_by_id(movie_id, MovieModel, db) + + if not movie: + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) + return movie + + +@router.delete("/movies/{movie_id}/", status_code=204) +def delete_movie(movie_id: int, db: Session = Depends(get_db)) -> None: + movie = get_movie_by_id(movie_id, MovieModel, db) + + if not movie: + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) + db.delete(movie) + db.commit() + + +@router.patch("/movies/{movie_id}/", status_code=200) +def edit_movie( + movie_id: int, movie_data: MovieUpdate, db: Session = Depends(get_db) +) -> dict[str, str]: + movie = get_movie_by_id(movie_id, MovieModel, db) + + if not movie: + raise HTTPException( + status_code=404, detail="Movie with the given ID was not found." + ) + + try: + movie_date = movie_data.model_dump(exclude_unset=True) + for key, value in movie_date.items(): + if value: + setattr(movie, key, value) + db.commit() + db.refresh(movie) + except IntegrityError: + raise HTTPException(status_code=400, detail="Invalid input data.") + + return {"detail": "Movie updated successfully."} diff --git a/src/schemas/movies.py b/src/schemas/movies.py index fabb9be..57a4c6b 100644 --- a/src/schemas/movies.py +++ b/src/schemas/movies.py @@ -1 +1,124 @@ -# Write your code here +from pydantic import BaseModel, Field +import datetime +from typing import Optional +from enum import Enum + + +class MovieStatus(str, Enum): + RELEASED: str = "Released" + POST_PRODUCTION: str = "Post Production" + IN_PRODUCTION: str = "In Production" + + +class Movie(BaseModel): + name: str + date: datetime.date + score: float + overview: str + status: MovieStatus + budget: float + revenue: float + + +class Genre(BaseModel): + name: str + movies: list[Movie] + + model_config = {"from_attributes": True} + + +class GenreDetail(BaseModel): + id: int + name: str + + +class Actor(BaseModel): + name: str + movies: list[Movie] + + model_config = {"from_attributes": True} + + +class ActorDetail(BaseModel): + id: int + name: str + + +class Language(BaseModel): + name: str + movies: list[Movie] + + model_config = {"from_attributes": True} + + +class LanguageDetail(BaseModel): + id: int + name: str + + +class Country(BaseModel): + code: str + name: Optional[str] + movies: list[Movie] + + model_config = {"from_attributes": True} + + +class CountryDetail(BaseModel): + id: int + code: str + name: Optional[str] + + +class MovieCreate(BaseModel): + name: str = Field(max_length=255) + date: ( + datetime.date + ) + score: float = Field(ge=0, le=100) + overview: str + status: MovieStatus + budget: float = Field(gt=0) + revenue: float = Field(gt=0) + country: str + genres: list[str] + actors: list[str] + languages: list[str] + + model_config = {"from_attributes": True} + + +class MovieDetail(Movie): + id: int + country: Optional[CountryDetail] = None + genres: Optional[list[GenreDetail]] = None + actors: Optional[list[ActorDetail]] = None + languages: Optional[list[LanguageDetail]] = None + + +class MovieList(BaseModel): + id: int + name: str + date: datetime.date + score: float + overview: str + + +class MovieListResponse(BaseModel): + movies: list[MovieList] + prev_page: str | None + next_page: str | None + total_pages: int + total_items: int + + +class MovieUpdate(Movie): + name: Optional[str] = Field(max_length=255, default=None) + date: Optional[datetime.date] = Field( + lt=datetime.date.today() + datetime.timedelta(days=365), default=None + ) + overview: Optional[str] = None + status: Optional[MovieStatus] = None + score: Optional[float] = Field(ge=0, le=100, default=None) + budget: Optional[float] = Field(gt=0, default=None) + revenue: Optional[float] = Field(gt=0, default=None)