Skip to content

Commit

Permalink
Fix and refactor film endpoints (#29)
Browse files Browse the repository at this point in the history
* Fix CI

* Fix elasticsearch schema, modify etl

* Fix and refactor film endpoints

* Fix linters
  • Loading branch information
a1d4r authored Mar 25, 2024
1 parent 35244ed commit 8091a3c
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 79 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_async_api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches: [master]
paths: ["async_api/**"]
pull_request:
paths: ["etl/**"]
paths: ["async_api/**"]

jobs:
lint:
Expand Down
2 changes: 1 addition & 1 deletion async_api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ poetry-remove:
.PHONY: install
install:
poetry lock --no-interaction --no-update
poetry install --no-interaction
poetry install --no-interaction --sync

.PHONY: pre-commit-install
pre-commit-install:
Expand Down
84 changes: 61 additions & 23 deletions async_api/poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions async_api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pydantic = "^2.6.4"
pydantic-settings = "^2.2.1"
uvicorn = {extras = ["standart"], version = "^0.29.0"}
gunicorn = "^21.2.0"
orjson = "^3.9.15"

[tool.poetry.group.dev.dependencies] # https://python-poetry.org/docs/master/managing-dependencies/
black = "^24.3.0"
Expand Down
17 changes: 17 additions & 0 deletions async_api/src/api/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
from core.settings import settings
from models.value_objects import SortOrder
from pydantic import BaseModel, Field


class PaginationParams(BaseModel):
page_number: int = Field(1, ge=1)
page_size: int = Field(settings.default_page_size, ge=1)


class SortParams(BaseModel):
sort: str | None = Field(None, min_length=1)

@property
def sort_by(self) -> str | None:
if not self.sort:
return None
return self.sort.lstrip("-")

@property
def sort_order(self) -> SortOrder | None:
if not self.sort:
return None
return SortOrder.desc if self.sort.startswith("-") else SortOrder.asc
42 changes: 19 additions & 23 deletions async_api/src/api/v1/films.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from typing import Annotated

from api.dependencies import PaginationParams
from fastapi import APIRouter, Depends, HTTPException, Query, status
from models.genre import Genre
from models.person import Person
from api.dependencies import PaginationParams, SortParams
from fastapi import APIRouter, Depends, HTTPException, status
from models.value_objects import FilmID
from pydantic import BaseModel, Field
from services.film import FilmService
Expand All @@ -17,15 +15,20 @@ class FilmShort(BaseModel):
imdb_rating: float


class IdName(BaseModel):
uuid: FilmID = Field(..., validation_alias="id")
name: str


class FilmDetails(BaseModel):
uuid: FilmID = Field(..., validation_alias="id")
title: str
imdb_rating: float
description: str
genre: list[Genre]
actors: list[Person]
writers: list[Person]
directors: list[Person]
description: str | None = None
genres: list[IdName]
actors: list[IdName]
writers: list[IdName]
directors: list[IdName]


@router.get(
Expand All @@ -35,22 +38,17 @@ class FilmDetails(BaseModel):
status_code=status.HTTP_200_OK,
summary="Получить список всех фильмов",
)
async def film_list(
async def get_film_list(
film_service: Annotated[FilmService, Depends()],
pagination_params: Annotated[PaginationParams, Depends()],
sort_by: str = Query("id", description="Сортировка фильмов"),
genre: str = Query("Action", description="Фильтрация по жанру"),
sort_params: Annotated[SortParams, Depends()],
genre: str | None = None,
) -> list[FilmShort]:
if sort_by == "imdb_rating":
sort_order = "asc"
if sort_by == "-imdb_rating":
sort_order = "desc"
if sort_by == "id":
sort_order = "id"
films = await film_service.search_with_genre(
films = await film_service.get_list(
page=pagination_params.page_number,
size=pagination_params.page_size,
sort_by=sort_order,
sort_by=sort_params.sort_by,
sort_order=sort_params.sort_order,
genre=genre,
)
return [FilmShort.model_validate(film.model_dump()) for film in films]
Expand All @@ -63,17 +61,15 @@ async def film_list(
status_code=status.HTTP_200_OK,
summary="Поиск по фильмам",
)
async def film_search(
async def search_films(
film_service: Annotated[FilmService, Depends()],
pagination_params: Annotated[PaginationParams, Depends()],
sort_by: str = Query("id", description="Сортировка фильмов"),
query: str | None = None,
) -> list[FilmShort]:
films = await film_service.search(
query=query,
page=pagination_params.page_number,
size=pagination_params.page_size,
sort_by=sort_by,
)
return [FilmShort.model_validate(film.model_dump()) for film in films]

Expand Down
19 changes: 18 additions & 1 deletion async_api/src/models/film.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from enum import StrEnum, auto

from models.value_objects import FilmID, PersonID
from pydantic import BaseModel

Expand All @@ -9,15 +11,30 @@ class PersonIdName(BaseModel):
name: str


class GenreIdName(BaseModel):
"""Модель для хранения идентификатора и имени жанра."""

id: FilmID
name: str


class Film(BaseModel):
"""Модель для хранения информации о фильме."""

id: FilmID
title: str
imdb_rating: float | None
description: str | None
director: list[str] | None
genres: list[GenreIdName]
directors_names: list[str]
actors_names: list[str]
writers_names: list[str]
actors: list[PersonIdName]
writers: list[PersonIdName]
directors: list[PersonIdName]


class FilmSortFields(StrEnum):
id = auto()
imdb_rating = auto()
title = auto() # type: ignore[assignment]
5 changes: 5 additions & 0 deletions async_api/src/models/value_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ class Roles(StrEnum):
actor = auto()
writer = auto()
director = auto()


class SortOrder(StrEnum):
asc = auto()
desc = auto()
27 changes: 14 additions & 13 deletions async_api/src/services/film.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from elasticsearch import AsyncElasticsearch, NotFoundError
from fastapi import Depends
from models.film import Film
from models.value_objects import FilmID
from models.value_objects import FilmID, SortOrder

FILM_CACHE_EXPIRE_IN_SECONDS = 60 * 5

Expand All @@ -17,40 +17,41 @@
class FilmService:
elastic: Annotated[AsyncElasticsearch, Depends(get_elastic)]

async def search(
async def get_list(
self,
*,
query: str | None = None,
page: int = 1,
size: int = settings.default_page_size,
sort_order: str = "desc",
sort_by: str = "id",
sort_by: str | None = None,
sort_order: SortOrder | None = None,
genre: str | None = None,
) -> list[Film]:
result = await self.elastic.search(
index=settings.es_films_index,
from_=(page - 1) * size,
size=size,
query={"match": {"title": query}} if query else {"match_all": {}},
sort={sort_by: {"order": sort_order}},
query=(
{"bool": {"filter": {"term": {"genres.name.keyword": genre}}}}
if genre
else {"match_all": {}}
),
sort={sort_by: {"order": sort_order or SortOrder.asc}} if sort_by else None,
)

return [Film.model_validate(hit["_source"]) for hit in result["hits"]["hits"]]

async def search_with_genre(
async def search(
self,
*,
query: str | None = None,
page: int = 1,
size: int = settings.default_page_size,
sort_by: str = "imdb_rating",
sort_order: str = "desc",
genre: str | None = "Action",
) -> list[Film]:
result = await self.elastic.search(
index=settings.es_films_index,
from_=(page - 1) * size,
size=size,
query={"term": {"genre": genre}},
sort={sort_by: {"order": sort_order}},
query={"match": {"title": query}} if query else {"match_all": {}},
)

return [Film.model_validate(hit["_source"]) for hit in result["hits"]["hits"]]
Expand Down
8 changes: 4 additions & 4 deletions async_api/tests/test_services/test_film_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async def film_service(elastic):
return FilmService(elastic)


async def test_search_film_by_id(film_service):
async def test_search_film_by_id(film_service: FilmService):
# Arrange
star_wars_id = FilmID(UUID("c35dc09c-8ace-46be-8941-7e50b768ec33"))

Expand All @@ -27,7 +27,7 @@ async def test_search_film_by_id(film_service):
film.description
== "Luke Skywalker, a young farmer from the desert planet of Tattooine, must save Princess Leia from the evil Darth Vader."
)
assert film.director == []
assert film.directors == []
assert film.actors_names == []
assert film.writers_names == ["George Lucas"]
assert film.actors == []
Expand All @@ -48,9 +48,9 @@ async def test_search_films(film_service: FilmService):
assert all(isinstance(film, Film) for film in films)


async def test_search_films_by_genre(film_service: FilmService):
async def test_get_films_by_genre(film_service: FilmService):
# Act
films = await film_service.search_with_genre(genre="Action")
films = await film_service.get_list(genre="Action")

# Assert
assert len(films) is not None
13 changes: 11 additions & 2 deletions etl/etl/dto/elasticsearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"PersonMinimalElasticsearchRecord",
"PersonFilmWorkElasticsearchRecord",
"GenreElasticsearchRecord",
"GenreMinimalElasticsearchRecord",
"PersonElasticsearchRecord",
"FilmWorkElasticsearchRecord",
]
Expand Down Expand Up @@ -44,6 +45,13 @@ class GenreElasticsearchRecord(BaseElasticsearchRecord):
description: str


@dataclass
class GenreMinimalElasticsearchRecord(BaseElasticsearchRecord):
"""Модель для хранения краткой информации о жанре в индексе Elasticsearch."""

name: str


@dataclass
class PersonElasticsearchRecord(BaseElasticsearchRecord):
"""Модель для хранения информации о персоне в индексе Elasticsearch."""
Expand All @@ -57,11 +65,12 @@ class FilmWorkElasticsearchRecord(BaseElasticsearchRecord):
"""Модель для хранения информации о кинопроизведении в индексе Elasticsearch."""

imdb_rating: float
genre: list[str]
title: str
description: str
director: list[str]
genres: list[GenreMinimalElasticsearchRecord]
directors_names: list[str]
actors_names: list[str]
writers_names: list[str]
directors: list[PersonMinimalElasticsearchRecord]
actors: list[PersonMinimalElasticsearchRecord]
writers: list[PersonMinimalElasticsearchRecord]
12 changes: 10 additions & 2 deletions etl/etl/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ def build_film_works_elasticsearch_records(
dto.FilmWorkElasticsearchRecord(
id=film_work_info.id,
imdb_rating=film_work_info.rating,
genre=[genre.genre_name for genre in genres_by_film_work_id[film_work_info.id]],
genres=[
dto.GenreMinimalElasticsearchRecord(id=genre.genre_id, name=genre.genre_name)
for genre in genres_by_film_work_id[film_work_info.id]
],
title=film_work_info.title,
description=film_work_info.description,
director=[
directors_names=[
fwp.person_full_name
for fwp in persons_by_film_work_id[film_work_info.id]
if fwp.role == "director"
Expand All @@ -52,6 +55,11 @@ def build_film_works_elasticsearch_records(
for fwp in persons_by_film_work_id[film_work_info.id]
if fwp.role == "writer"
],
directors=[
dto.PersonMinimalElasticsearchRecord(id=fwp.person_id, name=fwp.person_full_name)
for fwp in persons_by_film_work_id[film_work_info.id]
if fwp.role == "director"
],
)
for film_work_info in film_works_info
]
Expand Down
Loading

0 comments on commit 8091a3c

Please sign in to comment.