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 #34

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 @@ -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, **/migrations/
extend-exclude = src/tests/*
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}")
raise


def main():
settings = get_settings()
with get_db_contextmanager() as db_session:
Expand Down
106 changes: 102 additions & 4 deletions src/routes/movies.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,110 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, joinedload

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 (
PaginatedMovieResponseSchema,
CreateMovieSchema,
CreateResponseMovieSchema,
MovieDetailSchema,
UpdateMovieRequest
)
from services.crud import (
delete_movie,
get_movies,
create_movie,
get_movie_by_id,
update_movie,
)

router = APIRouter()


# Write your code here
@router.get(
"/movies/",
response_model=PaginatedMovieResponseSchema,
status_code=status.HTTP_200_OK
)

Choose a reason for hiding this comment

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

The status module is used here but not imported. Consider adding from fastapi import status to the import statements.

def read_movies(
db: Session = Depends(get_db),
page: Annotated[int, Query(ge=1)] = 1,
per_page: Annotated[int, Query(ge=1, le=20)] = 10
):
total_items = db.query(MovieModel).count()
total_pages = (total_items + per_page - 1) // per_page

offset = (page - 1) * per_page
limit = per_page

movies = get_movies(db, offset, limit)

if not movies:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No movies found."
)

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 PaginatedMovieResponseSchema(
movies=movies,
prev_page=prev_page,
next_page=next_page,
total_pages=total_pages,
total_items=total_items
)


@router.post(
"/movies/",
response_model=CreateResponseMovieSchema,
status_code=status.HTTP_201_CREATED
)
def add_movie(
movie: CreateMovieSchema,
db: Session = Depends(get_db)
):
movie_exists = db.query(MovieModel).filter_by(name=movie.name, date=movie.date).first()

if movie_exists:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists."
)

try:
new_movie = create_movie(movie, db)
return CreateResponseMovieSchema.model_validate(new_movie)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid input data."
)

Comment on lines +88 to +92

Choose a reason for hiding this comment

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

Catching a generic ValueError might not be sufficient for all cases. Consider catching specific exceptions related to the create_movie function to provide more precise error handling.

Comment on lines +88 to +92

Choose a reason for hiding this comment

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

Consider catching more specific exceptions related to the create_movie function instead of a generic ValueError for more precise error handling.


@router.get("/movies/{movie_id}/", response_model=MovieDetailSchema)
def get_movie_details(movie_id: int, db: Session = Depends(get_db)):
return get_movie_by_id(db, movie_id)


@router.delete("/movies/{movie_id}/", status_code=204)
def delete_movie_by_id(movie_id: int, db: Session = Depends(get_db)):

Choose a reason for hiding this comment

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

Consider using status_code=status.HTTP_204_NO_CONTENT for consistency with other status code definitions.

delete_movie(db, movie_id)
return {}

Comment on lines +100 to +103

Choose a reason for hiding this comment

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

Ensure that the delete operation returns a status code of 204 explicitly to indicate successful deletion.


@router.patch("/movies/{movie_id}/")
def update_movie_by_id(movie_id: int, updated_data: UpdateMovieRequest, db: Session = Depends(get_db)):
update_data_dict = updated_data.model_dump(exclude_unset=True)
update_movie(db, movie_id, update_data_dict)
return {"detail": "Movie updated successfully."}
97 changes: 96 additions & 1 deletion src/schemas/movies.py
Original file line number Diff line number Diff line change
@@ -1 +1,96 @@
# Write your code here
from datetime import date, timedelta
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator

from database.models import MovieStatusEnum


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

class Config:
from_attributes = True


class PaginatedMovieResponseSchema(BaseModel):
movies: list[MovieSchema]
prev_page: Optional[str]
next_page: Optional[str]
total_pages: int
total_items: int


class CreateMovieSchema(BaseModel):
name: str = Field(max_length=255)
date: date
score: float = Field(ge=0, le=100)
overview: str
status: MovieStatusEnum
budget: float = Field(ge=0)
revenue: float = Field(ge=0)
country: str
genres: list[str]
actors: list[str]
languages: list[str]


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

class Config:
from_attributes = True


class CountrySchema(BaseSchema):
code: str
name: Optional[str]


class GenreSchema(BaseSchema):
pass


class ActorSchema(BaseSchema):
pass


class LanguageSchema(BaseSchema):
pass


class CreateResponseMovieSchema(BaseModel):
id: int
name: str
date: date
score: float
overview: str
status: MovieStatusEnum
budget: float
revenue: float
country: CountrySchema
genres: list[GenreSchema]
actors: list[ActorSchema]
languages: list[LanguageSchema]

class Config:
from_attributes = True


class MovieDetailSchema(CreateResponseMovieSchema):
pass


class UpdateMovieRequest(BaseModel):
name: str = None
date: str = None
score: float = None

Choose a reason for hiding this comment

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

Consider changing the type of date from str to date in the UpdateMovieRequest class to maintain consistency with other schemas and ensure proper date handling.

Choose a reason for hiding this comment

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

Change the type of the date field from str to date for consistency and proper validation.

overview: str = None
status: str = None
budget: float = None

Choose a reason for hiding this comment

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

The status field in UpdateMovieRequest should be of type MovieStatusEnum to ensure that only valid status values are accepted.

Choose a reason for hiding this comment

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

Change the type of the status field from str to MovieStatusEnum to maintain consistency with other schemas.

revenue: float = None
Empty file added src/services/__init__.py
Empty file.
139 changes: 139 additions & 0 deletions src/services/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from typing import Annotated, Optional

from fastapi import Depends, Query, HTTPException
from pydantic import ValidationError
from sqlalchemy.orm import Session

from database import get_db
from database.models import MovieModel, CountryModel, GenreModel, ActorModel, LanguageModel
from schemas.movies import (
MovieSchema,
CreateMovieSchema,
MovieDetailSchema,
)


def get_movies(
db: Session = Depends(get_db),
offset: Optional[int] = None,
limit: Optional[int] = None
):
query = db.query(MovieModel).order_by(MovieModel.id.desc())

if offset:
query = query.offset(offset)
if limit:
query = query.limit(limit)

return query.all()


def create_movie(
movie: CreateMovieSchema,
db: Session = Depends(get_db)
):
try:
country = get_or_create_country(movie.country, db)

genres = [get_or_create_genre(genre, db) for genre in movie.genres]
actors = [get_or_create_actor(actor, db) for actor in movie.actors]
languages = [get_or_create_language(language, db) for language in movie.languages]

new_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(new_movie)
db.commit()
db.refresh(new_movie)

return new_movie
except Exception as e:
db.rollback()
raise e
Comment on lines +61 to +63

Choose a reason for hiding this comment

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

Consider logging the exception e before raising it to help with debugging and monitoring. This can provide more context when an error occurs.

Choose a reason for hiding this comment

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

Raising the exception without additional context might make it harder to diagnose issues. Consider wrapping the exception in a custom error message or logging it for better traceability.

Comment on lines +61 to +63

Choose a reason for hiding this comment

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

Consider catching more specific exceptions related to database operations instead of a generic Exception for better error handling.



def get_or_create_country(country_code: str, db: Session):
country = db.query(CountryModel).filter_by(code=country_code).first()
if not country:
country = CountryModel(code=country_code)
db.add(country)
db.flush()
return country


def get_or_create_genre(genre_name: str, db: Session):
genre = db.query(GenreModel).filter_by(name=genre_name).first()
if not genre:
genre = GenreModel(name=genre_name)
db.add(genre)
db.flush()
return genre


def get_or_create_actor(actor_name: str, db: Session):
actor = db.query(ActorModel).filter_by(name=actor_name).first()
if not actor:
actor = ActorModel(name=actor_name)
db.add(actor)
db.flush()
return actor


def get_or_create_language(language_name: str, db: Session):
language = db.query(LanguageModel).filter_by(name=language_name).first()
if not language:
language = LanguageModel(name=language_name)
db.add(language)
db.flush()
return language


def get_movie_by_id(db: Session, movie_id: int) -> MovieModel | None:
movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first()

if movie is None:
raise HTTPException(
status_code=404,
detail="Movie with the given ID was not found."
)
return movie


def delete_movie(db: Session, movie_id: int) -> None:
movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first()

if movie is None:
raise HTTPException(
status_code=404,
detail="Movie with the given ID was not found."
)

db.delete(movie)
db.commit()


def update_movie(db: Session, movie_id: int, updated_data: dict) -> None:
movie = db.query(MovieModel).filter(MovieModel.id == movie_id).first()

if movie is None:
raise HTTPException(
status_code=404,
detail="Movie with the given ID was not found."
)

for key, value in updated_data.items():
if hasattr(movie, key):
setattr(movie, key, value)

db.commit()
Loading