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

Implement CRUD for Movies #40

Open
wants to merge 2 commits 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
174 changes: 173 additions & 1 deletion src/routes/movies.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,184 @@
from math import ceil

Choose a reason for hiding this comment

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

The import statement for ceil is incorrect. It should be from math import ceil instead of from math import ceil /.

from typing import Dict, Union, List, Optional, Annotated

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import status
from pydantic_core.core_schema import AnySchema
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import update

from database import get_db
from database.models import MovieModel, CountryModel, GenreModel, ActorModel, LanguageModel

from schemas.movies import (
MovieListResponseSchema,
MovieDetailsResponseSchema,
MovieAddSchema,
MovieUpdateSchema,
MovieUpdateResponseSchema
)


router = APIRouter()


# Write your code here
@router.get(
"/movies/",
response_model=Dict[str, Union[List[MovieListResponseSchema], Optional[str], int]]
)
def movies(
db: Session = Depends(get_db),
page: Annotated[int, Query(..., title="The page number to fetch", ge=1)] = 1,
per_page: Annotated[int, Query(
..., title="Number of movies to fetch per page", ge=1, le=20
)] = 10
) -> Dict[str, Union[List[MovieListResponseSchema], Optional[str], int]]:

data = db.query(MovieModel).order_by(MovieModel.id.desc()).all()

total_pages = ceil(len(data) / per_page)

start = (page - 1) * per_page
end = start + per_page

items = len(data)
if not items or page > total_pages:
raise HTTPException(status_code=404, detail="No movies found.")

if end > items:
next_page = None
else:
next_page = f"/theater/movies/?page={page + 1}&per_page={per_page}"
if page == 1:
prev_page = None
else:
prev_page = f"/theater/movies/?page={page - 1}&per_page={per_page}"

response = {
"movies": data[start:end],
"prev_page": prev_page,
"next_page": next_page,
"total_pages": total_pages,
"total_items": items
}

return response


def add_related_entities(entities: List[str], model, db) -> List[AnySchema]:
existing_entities = []
for entity in entities:
existing_entity = db.query(model).filter(model.name == entity).first()
if not existing_entity:
existing_entity = model(name=entity)
db.add(existing_entity)
db.commit()
db.refresh(existing_entity)
existing_entities.append(existing_entity)
return existing_entities


@router.post(
"/movies/",
response_model=MovieDetailsResponseSchema,
status_code=status.HTTP_201_CREATED
)
def add_movie(movie: MovieAddSchema, db: Session = Depends(get_db)):
required_fields = ["name", "date", "score", "overview", "status", "budget", "revenue", "country"]
for field in required_fields:
if not getattr(movie, field, None):
raise HTTPException(status_code=400, detail="Invalid input data.")

country = db.query(CountryModel).filter(CountryModel.code == movie.country).first()
if not country:
country = CountryModel(code=movie.country)

existing_movie = db.query(MovieModel).filter(
(MovieModel.name == movie.name) & (MovieModel.date == movie.date)
).first()
if existing_movie:
raise HTTPException(
status_code=409, detail=f"A movie with the name '{movie.name}' "
f"and 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,
)

db.add(db_movie)
db.commit()
db.refresh(db_movie)

if movie.genres:
db_movie.genres = add_related_entities(movie.genres, GenreModel, db)

if movie.actors:
db_movie.actors = add_related_entities(movie.actors, ActorModel, db)

if movie.languages:
db_movie.languages = add_related_entities(movie.languages, LanguageModel, db)

db.commit()
db.refresh(db_movie)

return db_movie


@router.get("/movies/{movie_id}/", response_model=MovieDetailsResponseSchema)
def get_movie(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.delete("/movies/{movie_id}/", status_code=status.HTTP_204_NO_CONTENT)
def delete_movie(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.")
db.delete(movie)
db.commit()


@router.patch(
"/movies/{movie_id}/",
response_model=MovieUpdateResponseSchema,
status_code=status.HTTP_200_OK,
)
def update_movie(movie_update: MovieUpdateSchema, movie_id: int, db: Session = Depends(get_db)):
existing_movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first()
if not existing_movie:
raise HTTPException(status_code=404, detail="Movie with the given ID was not found.")

new_attributes = movie_update.model_dump(exclude_unset=True)

if 'date' not in new_attributes:
new_attributes['date'] = existing_movie.date

try:
db.execute(
update(MovieModel)
.where(MovieModel.id == movie_id)
.values(new_attributes)
)

db.commit()
db.refresh(existing_movie)

except ValueError:
db.rollback()
existing_movie.detail = "Invalid input data."

Choose a reason for hiding this comment

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

Setting existing_movie.detail is not appropriate here as MovieModel likely does not have a detail attribute. Consider removing this line.

Choose a reason for hiding this comment

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

Setting existing_movie.detail is likely incorrect as MovieModel probably doesn't have a detail attribute. Ensure that you are updating the correct attributes of the MovieModel.

raise HTTPException(status_code=400, detail="Invalid input data.")
Comment on lines +178 to +181

Choose a reason for hiding this comment

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

Catching ValueError might not be appropriate here. Consider using a more specific exception like SQLAlchemyError for database operations.


existing_movie.detail = "Movie updated successfully."
return existing_movie
139 changes: 138 additions & 1 deletion src/schemas/movies.py
Original file line number Diff line number Diff line change
@@ -1 +1,138 @@
# Write your code here
from enum import Enum
from re import match

from dateutil.relativedelta import relativedelta
from pydantic import BaseModel, field_validator
from typing import List, Optional

Choose a reason for hiding this comment

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

Replace @field_validator with @validator in your Pydantic schemas. field_validator is not a valid decorator in Pydantic.

from datetime import date


class MovieStatusEnum(str, Enum):
released = "Released"
post_production = "Post Production"
in_production = "In Production"

@field_validator("status")
def validate_status_acceptable(cls, v):

Choose a reason for hiding this comment

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

The @field_validator decorator is not a valid Pydantic decorator. You should use @validator instead.

if v not in ["Released", "Post Production", "In Production"]:
raise ValueError("status must be one of (Released | Post Production | In Production)")
return v


class CountrySchema(BaseModel):
id: int
code: str
name: Optional[str] = None

# validator for checking format ISO 3166-1 alpha-2
@field_validator("code")
def validate_code_iso_3166_1(cls, v):

Choose a reason for hiding this comment

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

The @field_validator decorator is not a valid Pydantic decorator. You should use @validator instead.

if not match(r"^[A-Z]{2}$", v):
raise ValueError("code must be a valid ISO 3166-1 alpha-2 code")
return v


class GenreSchema(BaseModel):
id: int
name: str


class ActorSchema(BaseModel):
id: int
name: str


class LanguageSchema(BaseModel):
id: int
name: str


class MovieBase(BaseModel):
name: str
date: date
score: float
overview: str
status: MovieStatusEnum
budget: float
revenue: float

class Config:
from_attributes = True

@field_validator("name")
def validate_name_length(cls, v):

Choose a reason for hiding this comment

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

The @field_validator decorator is not a valid Pydantic decorator. You should use @validator instead.

if len(v) > 255:
raise ValueError("The name must not exceed 255 characters.")
return v

@field_validator("date")
def validate_date_not_more_1_year(cls, v):
if v > date.today() + relativedelta(years=1):
raise ValueError("The date must not be more than one year in the future.")
return v

@field_validator("score")
def validate_score_0_100(cls, v):
if not 0 <= v <= 100:
raise ValueError("The score must be between 0 and 100.")
return v

@field_validator("budget")
def validate_budget_positive(cls, v):
if not v >= 0:
raise ValueError("The budget must be non-negative.")
return v

@field_validator("revenue")
def validate_revenue_positive(cls, v):
if not v >= 0:
raise ValueError("The revenue must be non-negative.")
return v


class MovieListResponseSchema(BaseModel):
id: int
name: str
date: date
score: float
overview: str


class PageSchema(BaseModel):
items: List[MovieListResponseSchema]
total: int
prev_page: Optional[str] = None
next_page: Optional[str]


class MovieAddSchema(MovieBase):
country: str
genres: List[str] = None
actors: List[str] = None
languages: List[str] = None


class MovieDetailsResponseSchema(MovieBase):
id: int
country: CountrySchema
genres: List[GenreSchema]
actors: List[ActorSchema]
languages: List[LanguageSchema]


class MovieUpdateSchema(MovieBase):
name: Optional[str] = None
date: Optional[date] = None
score: Optional[float] = None

Choose a reason for hiding this comment

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

The default value for date in MovieUpdateSchema should be None instead of date. Ensure that optional fields are correctly handled.

overview: Optional[str] = None
status: Optional[MovieStatusEnum] = None
budget: Optional[float] = None
revenue: Optional[float] = None

class Config:
from_attributes = True
exclude = {"country", "genres", "actors", "languages"}


class MovieUpdateResponseSchema(MovieDetailsResponseSchema):
detail: str
Loading