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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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/*
2 changes: 1 addition & 1 deletion src/database/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from alembic import context

from database import models # noqa: F401
from database import models # noqa: F401
from database.models import Base
from database.session_postgresql import postgresql_engine

Expand Down
7 changes: 5 additions & 2 deletions src/database/populate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

from config import get_settings
from database import MovieModel, get_db_contextmanager
from database.models import CountryModel, GenreModel, ActorModel, MoviesGenresModel, ActorsMoviesModel, LanguageModel, \
MoviesLanguagesModel
from database.models import (
CountryModel, GenreModel, ActorModel, MoviesGenresModel,
ActorsMoviesModel, LanguageModel, MoviesLanguagesModel
)


class CSVDatabaseSeeder:
Expand Down Expand Up @@ -139,6 +141,7 @@ def seed(self):
print(f"Unexpected error: {e}")
raise


def main():
settings = get_settings()
with get_db_contextmanager() as db_session:
Expand Down
154 changes: 149 additions & 5 deletions src/routes/movies.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,156 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.exc import IntegrityError
import math
from datetime import timedelta, date
from typing import Type

from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import Response, JSONResponse

from sqlalchemy.exc import IntegrityError, SQLAlchemyError
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, Base
from routes.utils import get_or_404, extract
from schemas.movies import MovieDetailSchema, MovieListResponseSchema, MovieCreateSchema, MovieUpdateSchema

router = APIRouter()

DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 10
ROOT = "/theater"


@router.get("/movies/", response_model=MovieListResponseSchema)
def list_movies(
page: int = Query(DEFAULT_PAGE, ge=1),
per_page: int = Query(DEFAULT_PER_PAGE, ge=1, le=20),
db: Session = Depends(get_db),
):

offset = (page - 1) * per_page
total_items = db.query(MovieModel).count()
total_pages = math.ceil(total_items / per_page)

next_page = f"{ROOT}/movies/?page={page + 1}&per_page={per_page}" if page < total_pages else None
prev_page = f"{ROOT}/movies/?page={page - 1}&per_page={per_page}" if page > 1 else None

movies = db.query(MovieModel).order_by(MovieModel.id.desc()).limit(per_page).offset(offset).all()

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

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=MovieDetailSchema)
def get_movie(movie_id: int, db: Session = Depends(get_db)) -> Type[MovieModel]:
movie = get_or_404(movie_id, MovieModel, db)
return movie


@router.delete("/movies/{movie_id}/", response_model=None)
def delete_movie(movie_id: int, db: Session = Depends(get_db)) -> Response:
db_movie = get_or_404(movie_id, MovieModel, db)
try:
db.delete(db_movie)
db.commit()
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database Error. " + str(e))

return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.patch("/movies/{movie_id}/", response_model=None, status_code=status.HTTP_200_OK)
def update_movie(movie_id: int, movie_update: MovieUpdateSchema, db: Session = Depends(get_db)) -> dict:
db_movie = get_or_404(movie_id, MovieModel, db)

# for all schema fields: update model fields only if value is not none
for field_name, field_value in movie_update.model_dump(exclude_unset=True).items():
print(f"Field {field_name}: {field_value}")
if field_value is not None:
setattr(db_movie, field_name, field_value)

try:
db.commit()
db.refresh(db_movie)
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="SQLAlchemyError: " + str(e))

return {"detail": "Movie updated successfully."}


@router.post("/movies/", response_model=MovieDetailSchema, status_code=status.HTTP_201_CREATED)
def create_movie(
movie: MovieCreateSchema,
db: Session = Depends(get_db),
) -> MovieModel:

if not (0 < movie.score < 100) or movie.budget < 0 or movie.revenue < 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid input data. Movie score out of range (0,100) or budget or revenue must be positive."
)
if movie.date > date.today() + timedelta(days=365):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid input data. Date cannot be in so far the future."
)
if len(movie.name) > 255 or len(movie.country) > 3:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid input data. Movie name or country code too long."
)

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

country = db.query(CountryModel).filter(CountryModel.code == movie.country).first()
if not country:
country = CountryModel(code=movie.country)
try:
db.add(country)
db.commit()
db.refresh(country)
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database Error. " + str(e))

genres = extract(movie.genres, GenreModel, db)
actors = extract(movie.actors, ActorModel, db)
languages = extract(movie.languages, LanguageModel, db)

# add only movie data + country
db_movie = MovieModel(**movie.model_dump(exclude={"country", "genres", "actors", "languages"}), country=country)
try:
db.add(db_movie)
db.commit()
db.refresh(db_movie)
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"A movie with the name '{movie.name}' and release date '{movie.date}' already exists."
)
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database error. " + str(e))

# add relations with 'genres, 'actors, 'languages
db_movie.genres.extend(genres)
db_movie.actors.extend(actors)
db_movie.languages.extend(languages)
db.commit()

# Write your code here
return db_movie
41 changes: 41 additions & 0 deletions src/routes/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Type

from fastapi import HTTPException, status

from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session

from database.models import MovieModel, Base


def extract(field: list[str], model: Type[Base], db: Session) -> list[Base]:
"""
filed: list of names, model: Model of instances with name
extract all names from field: add | create in model & add it to list of model instances
Returns: list of model instances
"""
instances = []
for value in field:
db_model = db.query(model).filter(model.name == value).first()
if not db_model:
db_model = model(name=value)
try:
db.add(db_model)
db.commit()
db.refresh(db_model)
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database Error. " + str(e)
)
instances.append(db_model)
return instances


def get_or_404(id: int, model: Type[MovieModel], db: Session):
""" get instance by id or 404"""
db_movie = db.query(model).filter(model.id == id).first()
if not db_movie:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Movie with the given ID was not found.")
return db_movie
115 changes: 114 additions & 1 deletion src/schemas/movies.py
Original file line number Diff line number Diff line change
@@ -1 +1,114 @@
# Write your code here
from datetime import date
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field

from database.models import MovieStatusEnum, CountryModel, GenreModel, ActorModel, LanguageModel


class CountrySchema(BaseModel):
model_config = ConfigDict(from_attributes=True)

id: int
code: str
name: str | None


class GenreSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)

id: int
name: str


class ActorSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)

id: int
name: str


class LanguageSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)

id: int
name: str


class MovieDetailSchema(BaseModel):
model_config = ConfigDict(from_attributes=True, use_enum_values=True)

id: int
name: str
date: date
score: float
overview: str
status: MovieStatusEnum
budget: float
revenue: float

# country_id: int
country: CountrySchema
genres: list[GenreSchema]
actors: list[ActorSchema]
languages: list[LanguageSchema]


class MovieCreateSchema(BaseModel):
model_config = ConfigDict(from_attributes=True, use_enum_values=True)

name: str
date: date
score: float
overview: str
status: MovieStatusEnum
budget: float
revenue: float

country: str
genres: list[str]
actors: list[str]
languages: list[str]


class MovieUpdateSchema(BaseModel):
model_config = ConfigDict(from_attributes=True, use_enum_values=True)

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


class MovieListSchema(BaseModel):
model_config = ConfigDict(from_attributes=True, use_enum_values=True)

id: int
name: str
date: date
score: float
overview: str


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


# class FilmCreate(FilmBase):
# pass
#
# class FilmUpdate(FilmBase):
# pass
#
# class FilmRead(FilmBase):
# id: int
#
# class Config:
# from_attributes = True