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

feat: Implement task #14

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
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
10 changes: 9 additions & 1 deletion src/database/populate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -139,6 +146,7 @@ def seed(self):
print(f"Unexpected error: {e}")

Choose a reason for hiding this comment

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

Similar to the SQLAlchemyError, consider logging unexpected errors to a file or a logging system for better traceability.

raise


def main():
settings = get_settings()
with get_db_contextmanager() as db_session:
Expand Down
219 changes: 214 additions & 5 deletions src/routes/movies.py
Original file line number Diff line number Diff line change
@@ -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.")

Choose a reason for hiding this comment

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

Consider providing more specific feedback in the HTTPException message, such as indicating whether the issue is with the page number or if there are no movies at all.

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."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is quite generic. Consider specifying which field is invalid to provide better feedback to the client.


if movie.date > datetime.datetime.now().date() + datetime.timedelta(days=365):
raise HTTPException(
status_code=400,
detail="Invalid input data."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is generic. Consider specifying that the 'date' field is invalid to provide better feedback to the client.


if not (0 <= movie.score <= 100):
raise HTTPException(
status_code=400,
detail="Invalid input data."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is generic. Consider specifying that the 'score' field is invalid to provide better feedback to the client.


if movie.budget < 0 or movie.revenue < 0:
raise HTTPException(
status_code=400,
detail="Invalid input data."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is generic. Consider specifying that the 'budget' or 'revenue' field is invalid to provide better feedback to the client.


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."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is generic. Consider specifying that the 'name' field is invalid to provide better feedback to the client.

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."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is generic. Consider specifying that the 'date' field is invalid to provide better feedback to the client.

db_movie.date = movie.date

if movie.score:
if not (0 <= movie.score <= 100):
raise HTTPException(
status_code=400,
detail="Invalid input data."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is generic. Consider specifying that the 'score' field is invalid to provide better feedback to the client.

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."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is generic. Consider specifying that the 'budget' field is invalid to provide better feedback to the client.

db_movie.budget = movie.budget

if movie.revenue:
if movie.revenue < 0:
raise HTTPException(
status_code=400,
detail="Invalid input data."
)

Choose a reason for hiding this comment

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

The error message 'Invalid input data.' is generic. Consider specifying that the 'revenue' field is invalid to provide better feedback to the client.

db_movie.revenue = movie.revenue

db.commit()
db.refresh(db_movie)
return {"detail": "Movie updated successfully.", "movie": db_movie}
98 changes: 97 additions & 1 deletion src/schemas/movies.py
Original file line number Diff line number Diff line change
@@ -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

Choose a reason for hiding this comment

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

Consider providing a default value for the 'name' field in the CountryResponseSchema to ensure consistency and avoid potential issues when the field is not provided.

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

Comment on lines +89 to +95

Choose a reason for hiding this comment

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

Ensure that the optional fields in MovieUpdateResponseSchema have appropriate default values. Currently, they are set to None, which is fine, but make sure this aligns with the application's logic and requirements.

model_config = {"from_attributes": True}
Loading