-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into 6-get-film-information
- Loading branch information
Showing
15 changed files
with
257 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,24 @@ | ||
from pathlib import Path | ||
|
||
from pydantic import RedisDsn | ||
from pydantic_settings import BaseSettings | ||
|
||
PROJECT_ROOT = Path(__file__).parent.parent.parent | ||
|
||
|
||
class Settings(BaseSettings): | ||
api_project_name: str = "Movies" | ||
|
||
# cache | ||
redis_url: RedisDsn | ||
|
||
# elasticsearch | ||
elasticsearch_host: str | ||
es_genres_index: str = "genres" | ||
es_persons_index: str = "persons" | ||
|
||
# pagination | ||
default_page_size: int = 50 | ||
|
||
|
||
settings = Settings(_env_file=".env") | ||
settings = Settings(_env_file=PROJECT_ROOT / ".env") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
from uuid import UUID | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
class UUIDBase(BaseModel): | ||
"""Модель для базового класса.""" | ||
|
||
id: str | ||
id: UUID |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
from functools import lru_cache | ||
|
||
from db.elastic import get_elastic | ||
from db.redis import get_redis | ||
from elasticsearch import AsyncElasticsearch, NotFoundError | ||
from fastapi import Depends | ||
from models.film import Film | ||
from redis.asyncio import Redis | ||
|
||
FILM_CACHE_EXPIRE_IN_SECONDS = 60 * 5 | ||
|
||
|
||
class FilmService: | ||
def __init__(self, redis: Redis, elastic: AsyncElasticsearch): | ||
self.redis = redis | ||
self.elastic = elastic | ||
|
||
async def get_by_id(self, film_id: str) -> Film | None: | ||
film = await self._film_from_cache(film_id) | ||
if not film: | ||
film = await self._get_film_from_elastic(film_id) | ||
if not film: | ||
return None | ||
await self._put_film_to_cache(film) | ||
|
||
return film | ||
|
||
async def _get_film_from_elastic(self, film_id: str) -> Film | None: | ||
try: | ||
doc = await self.elastic.get(index="movies", id=film_id) | ||
except NotFoundError: | ||
return None | ||
return Film(**doc["_source"]) | ||
|
||
async def _film_from_cache(self, film_id: str) -> Film | None: | ||
data = await self.redis.get(film_id) | ||
if not data: | ||
return None | ||
|
||
return Film.parse_raw(data) | ||
|
||
async def _put_film_to_cache(self, film: Film) -> None: | ||
await self.redis.set(str(film.id), film.json(), FILM_CACHE_EXPIRE_IN_SECONDS) | ||
|
||
|
||
@lru_cache | ||
def get_film_service( | ||
redis: Redis = Depends(get_redis), | ||
elastic: AsyncElasticsearch = Depends(get_elastic), | ||
) -> FilmService: | ||
return FilmService(redis, elastic) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from typing import Annotated, NewType | ||
|
||
from dataclasses import dataclass | ||
from uuid import UUID | ||
|
||
from core.settings import settings | ||
from db.elastic import get_elastic | ||
from elasticsearch import AsyncElasticsearch, NotFoundError | ||
from fastapi import Depends | ||
from models.genre import Genre | ||
|
||
GenreID = NewType("GenreID", UUID) | ||
|
||
|
||
@dataclass | ||
class GenreService: | ||
elastic: Annotated[AsyncElasticsearch, Depends(get_elastic)] | ||
|
||
async def get_by_id(self, genre_id: GenreID) -> Genre | None: | ||
try: | ||
doc = await self.elastic.get(index=settings.es_genres_index, id=str(genre_id)) | ||
except NotFoundError: | ||
return None | ||
return Genre.model_validate(doc["_source"]) | ||
|
||
async def search( | ||
self, | ||
page: int = 1, | ||
size: int = settings.default_page_size, | ||
) -> list[Genre]: | ||
result = await self.elastic.search( | ||
index=settings.es_genres_index, | ||
from_=(page - 1) * size, | ||
size=size, | ||
query={"match_all": {}}, | ||
) | ||
return [Genre.model_validate(hit["_source"]) for hit in result["hits"]["hits"]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from typing import Annotated, NewType | ||
|
||
from dataclasses import dataclass | ||
from uuid import UUID | ||
|
||
from core.settings import settings | ||
from db.elastic import get_elastic | ||
from elasticsearch import AsyncElasticsearch, NotFoundError | ||
from fastapi import Depends | ||
from models.person import Person | ||
|
||
PersonID = NewType("PersonID", UUID) | ||
|
||
|
||
@dataclass | ||
class PersonService: | ||
elastic: Annotated[AsyncElasticsearch, Depends(get_elastic)] | ||
|
||
async def get_by_id(self, person_id: UUID) -> Person | None: | ||
try: | ||
doc = await self.elastic.get(index=settings.es_persons_index, id=str(person_id)) | ||
except NotFoundError: | ||
return None | ||
return Person.model_validate(doc["_source"]) | ||
|
||
async def search( | ||
self, | ||
*, | ||
query: str | None = None, | ||
page: int = 1, | ||
size: int = settings.default_page_size, | ||
) -> list[Person]: | ||
result = await self.elastic.search( | ||
index=settings.es_persons_index, | ||
from_=(page - 1) * size, | ||
size=size, | ||
query={"match": {"full_name": query}} if query else {"match_all": {}}, | ||
) | ||
return [Person.model_validate(hit["_source"]) for hit in result["hits"]["hits"]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import pytest | ||
|
||
from core.settings import settings | ||
from elasticsearch import AsyncElasticsearch | ||
|
||
|
||
@pytest.fixture() | ||
async def elastic(): | ||
es = AsyncElasticsearch(settings.elasticsearch_host) | ||
yield es | ||
await es.close() |
This file was deleted.
Oops, something went wrong.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from uuid import UUID | ||
|
||
import pytest | ||
|
||
from models.genre import Genre | ||
from services.genre import GenreService | ||
|
||
|
||
@pytest.fixture() | ||
async def genre_service(elastic): | ||
return GenreService(elastic) | ||
|
||
|
||
async def test_get_genre_by_id(genre_service): | ||
# Arrange | ||
action_genre_id = UUID("3d8d9bf5-0d90-4353-88ba-4ccc5d2c07ff") | ||
|
||
# Act | ||
genre = await genre_service.get_by_id(action_genre_id) | ||
|
||
# Assert | ||
assert genre is not None | ||
assert genre.id == action_genre_id | ||
assert genre.name == "Action" | ||
|
||
|
||
async def test_search_genres(genre_service): | ||
# Arrange | ||
page = 2 | ||
size = 5 | ||
|
||
# Act | ||
genres = await genre_service.search(page, size) | ||
|
||
# Assert | ||
assert len(genres) == size | ||
assert all(isinstance(genre, Genre) for genre in genres) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
from uuid import UUID | ||
|
||
import pytest | ||
|
||
from models.person import Person | ||
from services.person import PersonService | ||
|
||
|
||
@pytest.fixture() | ||
async def person_service(elastic): | ||
return PersonService(elastic) | ||
|
||
|
||
async def test_get_person_by_id(person_service: PersonService): | ||
# Arrange | ||
george_lucas_id = UUID("a5a8f573-3cee-4ccc-8a2b-91cb9f55250a") | ||
|
||
# Act | ||
person = await person_service.get_by_id(george_lucas_id) | ||
|
||
# Assert | ||
assert person is not None | ||
assert person.id == george_lucas_id | ||
assert person.full_name == "George Lucas" | ||
assert len(person.films) == 46 | ||
assert person.films[0].id == UUID("516f91da-bd70-4351-ba6d-25e16b7713b7") | ||
assert person.films[0].roles == ["director", "writer"] | ||
|
||
|
||
async def test_search_persons(person_service: PersonService): | ||
# Arrange | ||
page = 2 | ||
size = 5 | ||
|
||
# Act | ||
persons = await person_service.search(page=page, size=size) | ||
|
||
# Assert | ||
assert len(persons) == size | ||
assert all(isinstance(person, Person) for person in persons) | ||
|
||
|
||
async def test_search_persons_with_query(person_service: PersonService): | ||
# Arrange | ||
query = "George Lucas" | ||
george_lucas_id = UUID("a5a8f573-3cee-4ccc-8a2b-91cb9f55250a") | ||
|
||
# Act | ||
persons = await person_service.search(query=query) | ||
|
||
# Assert | ||
person = persons[0] | ||
assert person.id == george_lucas_id | ||
assert person.full_name == "George Lucas" | ||
assert len(person.films) == 46 | ||
assert person.films[0].id == UUID("516f91da-bd70-4351-ba6d-25e16b7713b7") | ||
assert person.films[0].roles == ["director", "writer"] |