Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solution #30

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
56 changes: 56 additions & 0 deletions src/crud.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +33 to +38

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a check to see if a country with the given code already exists in the database before creating a new one. This can prevent duplicate entries and ensure data integrity.



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
150 changes: 148 additions & 2 deletions src/routes/movies.py
Original file line number Diff line number Diff line change
@@ -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.",
)
Comment on lines +71 to +75

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that the date field is validated to not be more than one year in the future, as per the task requirements. This check is correctly implemented here.


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.")
Comment on lines +149 to +156

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the edit_movie function, ensure that the score is validated to be between 0 and 100, and that budget and revenue are non-negative. This is crucial to meet the task's validation requirements.


return {"detail": "Movie updated successfully."}
125 changes: 124 additions & 1 deletion src/schemas/movies.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +81 to +82

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider allowing the budget and revenue fields to be zero by changing gt=0 to ge=0 in the MovieCreate schema. This would accommodate scenarios where a movie might have a budget or revenue of zero.

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)
Loading