mirror of
https://github.com/flibusta-apps/book_library_server.git
synced 2025-12-06 15:15:36 +01:00
Refactor services
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
from app.models import Author
|
from app.models import Author
|
||||||
from app.services.common import TRGMSearchService, MeiliSearchService, GetRandomService
|
from app.services.common import TRGMSearchService, MeiliSearchService, GetRandomService
|
||||||
|
|
||||||
@@ -45,6 +47,7 @@ class AuthorTGRMSearchService(TRGMSearchService):
|
|||||||
MODEL_CLASS = Author
|
MODEL_CLASS = Author
|
||||||
PREFETCH_RELATED = ["source"]
|
PREFETCH_RELATED = ["source"]
|
||||||
SELECT_RELATED = ["annotations"]
|
SELECT_RELATED = ["annotations"]
|
||||||
|
|
||||||
GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY
|
GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY
|
||||||
|
|
||||||
|
|
||||||
@@ -62,8 +65,15 @@ SELECT id FROM filtered_authors;
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class GetRandomAuthorService(GetRandomService):
|
class RandomAuthorServiceQuery(TypedDict):
|
||||||
MODEL_CLASS = Author
|
allowed_langs: frozenset[str]
|
||||||
|
|
||||||
|
|
||||||
|
class GetRandomAuthorService(GetRandomService[Author, RandomAuthorServiceQuery]):
|
||||||
|
MODEL_CLASS = Author # type: ignore
|
||||||
|
PREFETCH_RELATED = ["source"]
|
||||||
|
SELECT_RELATED = ["annotations"]
|
||||||
|
|
||||||
GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY
|
GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
from app.models import Book as BookDB
|
from app.models import Book as BookDB
|
||||||
from app.services.common import (
|
from app.services.common import (
|
||||||
TRGMSearchService,
|
TRGMSearchService,
|
||||||
@@ -25,6 +27,7 @@ class BookTGRMSearchService(TRGMSearchService):
|
|||||||
MODEL_CLASS = BookDB
|
MODEL_CLASS = BookDB
|
||||||
PREFETCH_RELATED = ["source"]
|
PREFETCH_RELATED = ["source"]
|
||||||
SELECT_RELATED = ["authors", "translators", "annotations"]
|
SELECT_RELATED = ["authors", "translators", "annotations"]
|
||||||
|
|
||||||
GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY
|
GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY
|
||||||
|
|
||||||
|
|
||||||
@@ -43,8 +46,15 @@ SELECT id FROM filtered_books;
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class GetRandomBookService(GetRandomService):
|
class RandomBookServiceQuery(TypedDict):
|
||||||
MODEL_CLASS = BookDB
|
allowed_langs: frozenset[str]
|
||||||
|
|
||||||
|
|
||||||
|
class GetRandomBookService(GetRandomService[BookDB, RandomBookServiceQuery]):
|
||||||
|
MODEL_CLASS = BookDB # type: ignore
|
||||||
|
PREFETCH_RELATED = ["source"]
|
||||||
|
SELECT_RELATED = ["authors", "translators", "annotations"]
|
||||||
|
|
||||||
GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY
|
GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,21 +20,12 @@ MODEL = TypeVar("MODEL", bound=Model)
|
|||||||
QUERY = TypeVar("QUERY", bound=TypedDict)
|
QUERY = TypeVar("QUERY", bound=TypedDict)
|
||||||
|
|
||||||
|
|
||||||
class BaseSearchService(Generic[MODEL, QUERY], abc.ABC):
|
class BaseService(Generic[MODEL, QUERY], abc.ABC):
|
||||||
MODEL_CLASS: Optional[MODEL] = None
|
MODEL_CLASS: Optional[MODEL] = None
|
||||||
SELECT_RELATED: Optional[Union[list[str], str]] = None
|
CACHE_PREFIX: str = ""
|
||||||
PREFETCH_RELATED: Optional[Union[list[str], str]] = None
|
CUSTOM_MODEL_CACHE_NAME: Optional[str] = None
|
||||||
CUSTOM_CACHE_PREFIX: Optional[str] = None
|
|
||||||
CACHE_TTL = 6 * 60 * 60
|
CACHE_TTL = 6 * 60 * 60
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_params(cls) -> AbstractParams:
|
|
||||||
return resolve_params()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_raw_params(cls) -> RawParams:
|
|
||||||
return resolve_params().to_raw_params()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@property
|
@property
|
||||||
def model(cls) -> MODEL:
|
def model(cls) -> MODEL:
|
||||||
@@ -54,45 +45,18 @@ class BaseSearchService(Generic[MODEL, QUERY], abc.ABC):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@property
|
@property
|
||||||
def cache_prefix(cls) -> str:
|
def cache_prefix(cls) -> str:
|
||||||
return cls.CUSTOM_CACHE_PREFIX or cls.model.Meta.tablename
|
return cls.CUSTOM_MODEL_CACHE_NAME or cls.model.Meta.tablename
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_query_hash(query: QUERY):
|
def _get_query_hash(query: QUERY) -> int:
|
||||||
return hash(frozenset(query.items()))
|
return hash(frozenset(query.items()))
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def _get_object_ids(cls, query: QUERY) -> list[int]:
|
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_cache_key(cls, query: QUERY) -> str:
|
def get_cache_key(cls, query: QUERY) -> str:
|
||||||
model_class_name = cls.cache_prefix
|
model_class_name = cls.cache_prefix
|
||||||
query_hash = cls._get_query_hash(query)
|
query_hash = cls._get_query_hash(query)
|
||||||
return f"{model_class_name}_{query_hash}"
|
cache_key = f"{model_class_name}_{query_hash}"
|
||||||
|
return f"{cls.CACHE_PREFIX}_{cache_key}" if cls.CACHE_PREFIX else cache_key
|
||||||
@classmethod
|
|
||||||
async def get_cached_ids(
|
|
||||||
cls,
|
|
||||||
query: QUERY,
|
|
||||||
redis: aioredis.Redis,
|
|
||||||
params: RawParams,
|
|
||||||
) -> Optional[tuple[int, list[int]]]:
|
|
||||||
try:
|
|
||||||
key = cls.get_cache_key(query)
|
|
||||||
active_key = f"{key}_active"
|
|
||||||
|
|
||||||
if not await redis.exists(active_key):
|
|
||||||
return None
|
|
||||||
|
|
||||||
objects_count, objects = await asyncio.gather(
|
|
||||||
redis.llen(key),
|
|
||||||
redis.lrange(key, params.offset, params.offset + params.limit),
|
|
||||||
)
|
|
||||||
|
|
||||||
return objects_count, [int(item.decode()) for item in objects]
|
|
||||||
except aioredis.RedisError as e:
|
|
||||||
print(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def cache_object_ids(
|
async def cache_object_ids(
|
||||||
@@ -118,6 +82,47 @@ class BaseSearchService(Generic[MODEL, QUERY], abc.ABC):
|
|||||||
print(e)
|
print(e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSearchService(Generic[MODEL, QUERY], BaseService[MODEL, QUERY]):
|
||||||
|
SELECT_RELATED: Optional[Union[list[str], str]] = None
|
||||||
|
PREFETCH_RELATED: Optional[Union[list[str], str]] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_params(cls) -> AbstractParams:
|
||||||
|
return resolve_params()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_raw_params(cls) -> RawParams:
|
||||||
|
return resolve_params().to_raw_params()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _get_object_ids(cls, query: QUERY) -> list[int]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_cached_ids(
|
||||||
|
cls,
|
||||||
|
query: QUERY,
|
||||||
|
redis: aioredis.Redis,
|
||||||
|
params: RawParams,
|
||||||
|
) -> Optional[tuple[int, list[int]]]:
|
||||||
|
try:
|
||||||
|
key = cls.get_cache_key(query)
|
||||||
|
active_key = f"{key}_active"
|
||||||
|
|
||||||
|
if not await redis.exists(active_key):
|
||||||
|
return None
|
||||||
|
|
||||||
|
objects_count, objects = await asyncio.gather(
|
||||||
|
redis.llen(key),
|
||||||
|
redis.lrange(key, params.offset, params.offset + params.limit),
|
||||||
|
)
|
||||||
|
|
||||||
|
return objects_count, [int(item.decode()) for item in objects]
|
||||||
|
except aioredis.RedisError as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_object_ids(
|
async def get_object_ids(
|
||||||
cls, query: QUERY, redis: aioredis.Redis
|
cls, query: QUERY, redis: aioredis.Redis
|
||||||
@@ -201,13 +206,17 @@ class MeiliSearchService(Generic[MODEL], BaseSearchService[MODEL, SearchQuery]):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@property
|
@property
|
||||||
def lang_key(cls) -> str:
|
def lang_key(cls) -> str:
|
||||||
assert cls.MS_INDEX_LANG_KEY is not None, f"MODEL in {cls.__name__} don't set!"
|
assert (
|
||||||
|
cls.MS_INDEX_LANG_KEY is not None
|
||||||
|
), f"MS_INDEX_LANG_KEY in {cls.__name__} don't set!"
|
||||||
return cls.MS_INDEX_LANG_KEY
|
return cls.MS_INDEX_LANG_KEY
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@property
|
@property
|
||||||
def index_name(cls) -> str:
|
def index_name(cls) -> str:
|
||||||
assert cls.MS_INDEX_NAME is not None, f"MODEL in {cls.__name__} don't set!"
|
assert (
|
||||||
|
cls.MS_INDEX_NAME is not None
|
||||||
|
), f"MS_INDEX_NAME in {cls.__name__} don't set!"
|
||||||
return cls.MS_INDEX_NAME
|
return cls.MS_INDEX_NAME
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -251,37 +260,9 @@ class MeiliSearchService(Generic[MODEL], BaseSearchService[MODEL, SearchQuery]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GetRandomService(Generic[MODEL]):
|
class GetRandomService(Generic[MODEL, QUERY], BaseService[MODEL, QUERY]):
|
||||||
MODEL_CLASS: Optional[MODEL] = None
|
|
||||||
GET_OBJECTS_ID_QUERY: Optional[str] = None
|
GET_OBJECTS_ID_QUERY: Optional[str] = None
|
||||||
CUSTOM_CACHE_PREFIX: Optional[str] = None
|
CACHE_PREFIX: str = "random"
|
||||||
CACHE_TTL = 6 * 60 * 60
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@property
|
|
||||||
def model(cls) -> MODEL:
|
|
||||||
assert cls.MODEL_CLASS is not None, f"MODEL in {cls.__name__} don't set!"
|
|
||||||
return cls.MODEL_CLASS
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@property
|
|
||||||
def database(cls) -> Database:
|
|
||||||
return cls.model.Meta.database
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@property
|
|
||||||
def cache_prefix(cls) -> str:
|
|
||||||
return cls.CUSTOM_CACHE_PREFIX or cls.model.Meta.tablename
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_query_hash(query: frozenset[str]):
|
|
||||||
return hash(query)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_cache_key(cls, query: frozenset[str]) -> str:
|
|
||||||
model_class_name = cls.cache_prefix
|
|
||||||
query_hash = cls._get_query_hash(query)
|
|
||||||
return f"random_{model_class_name}_{query_hash}"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@property
|
@property
|
||||||
@@ -292,18 +273,18 @@ class GetRandomService(Generic[MODEL]):
|
|||||||
return cls.GET_OBJECTS_ID_QUERY
|
return cls.GET_OBJECTS_ID_QUERY
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _get_objects_from_db(cls, allowed_langs: frozenset[str]) -> list[int]:
|
async def _get_objects_from_db(cls, query: QUERY) -> list[int]:
|
||||||
objects = await cls.database.fetch_all(
|
objects = await cls.database.fetch_all(
|
||||||
cls.objects_id_query, {"langs": allowed_langs}
|
cls.objects_id_query, {"langs": query["allowed_langs"]}
|
||||||
)
|
)
|
||||||
return [obj["id"] for obj in objects]
|
return [obj["id"] for obj in objects]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _get_random_object_from_cache(
|
async def _get_random_object_from_cache(
|
||||||
cls, allowed_langs: frozenset[str], redis: aioredis.Redis
|
cls, query: QUERY, redis: aioredis.Redis
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
try:
|
try:
|
||||||
key = cls.get_cache_key(allowed_langs)
|
key = cls.get_cache_key(query)
|
||||||
active_key = f"{key}_active"
|
active_key = f"{key}_active"
|
||||||
|
|
||||||
if not await redis.exists(active_key):
|
if not await redis.exists(active_key):
|
||||||
@@ -316,41 +297,20 @@ class GetRandomService(Generic[MODEL]):
|
|||||||
print(e)
|
print(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def _cache_object_ids(
|
|
||||||
cls, object_ids: list[int], allowed_langs: frozenset[str], redis: aioredis.Redis
|
|
||||||
) -> bool:
|
|
||||||
try:
|
|
||||||
key = cls.get_cache_key(allowed_langs)
|
|
||||||
active_key = f"{key}_active"
|
|
||||||
|
|
||||||
p = redis.pipeline()
|
|
||||||
|
|
||||||
await p.set(active_key, 1, ex=cls.CACHE_TTL)
|
|
||||||
await p.delete(key)
|
|
||||||
await p.sadd(key, *object_ids)
|
|
||||||
|
|
||||||
await p.execute()
|
|
||||||
|
|
||||||
return True
|
|
||||||
except aioredis.RedisError as e:
|
|
||||||
print(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_random_id(
|
async def get_random_id(
|
||||||
cls,
|
cls,
|
||||||
allowed_langs: frozenset[str],
|
query: QUERY,
|
||||||
redis: aioredis.Redis,
|
redis: aioredis.Redis,
|
||||||
) -> int:
|
) -> int:
|
||||||
cached_object_id = await cls._get_random_object_from_cache(allowed_langs, redis)
|
cached_object_id = await cls._get_random_object_from_cache(query, redis)
|
||||||
|
|
||||||
if cached_object_id is not None:
|
if cached_object_id is not None:
|
||||||
return cached_object_id
|
return cached_object_id
|
||||||
|
|
||||||
object_ids = await cls._get_objects_from_db(allowed_langs)
|
object_ids = await cls._get_objects_from_db(query)
|
||||||
|
|
||||||
await cls._cache_object_ids(object_ids, allowed_langs, redis)
|
await cls.cache_object_ids(query, object_ids, redis)
|
||||||
|
|
||||||
return choice(object_ids)
|
return choice(object_ids)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
from app.models import Sequence
|
from app.models import Sequence
|
||||||
from app.services.common import TRGMSearchService, MeiliSearchService, GetRandomService
|
from app.services.common import TRGMSearchService, MeiliSearchService, GetRandomService
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ SELECT ARRAY (
|
|||||||
class SequenceTGRMSearchService(TRGMSearchService):
|
class SequenceTGRMSearchService(TRGMSearchService):
|
||||||
MODEL_CLASS = Sequence
|
MODEL_CLASS = Sequence
|
||||||
PREFETCH_RELATED = ["source"]
|
PREFETCH_RELATED = ["source"]
|
||||||
|
|
||||||
GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY
|
GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY
|
||||||
|
|
||||||
|
|
||||||
@@ -53,8 +56,14 @@ ORDER BY RANDOM() LIMIT 1;
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class GetRandomSequenceService(GetRandomService):
|
class RandomSequenceServiceQuery(TypedDict):
|
||||||
MODEL_CLASS = Sequence
|
allowed_langs: frozenset[str]
|
||||||
|
|
||||||
|
|
||||||
|
class GetRandomSequenceService(GetRandomService[Sequence, RandomSequenceServiceQuery]):
|
||||||
|
MODEL_CLASS = Sequence # type: ignore
|
||||||
|
PREFETCH_RELATED = ["source"]
|
||||||
|
|
||||||
GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY
|
GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class TranslatorTGRMSearchService(TRGMSearchService):
|
|||||||
CUSTOM_CACHE_PREFIX = "translator"
|
CUSTOM_CACHE_PREFIX = "translator"
|
||||||
PREFETCH_RELATED = ["source"]
|
PREFETCH_RELATED = ["source"]
|
||||||
SELECT_RELATED = ["annotations"]
|
SELECT_RELATED = ["annotations"]
|
||||||
|
|
||||||
GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY
|
GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ async def get_random_author(
|
|||||||
allowed_langs: frozenset[str] = Depends(get_allowed_langs),
|
allowed_langs: frozenset[str] = Depends(get_allowed_langs),
|
||||||
):
|
):
|
||||||
author_id = await GetRandomAuthorService.get_random_id(
|
author_id = await GetRandomAuthorService.get_random_id(
|
||||||
allowed_langs, request.app.state.redis
|
{"allowed_langs": allowed_langs}, request.app.state.redis
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ async def get_random_book(
|
|||||||
allowed_langs: frozenset[str] = Depends(get_allowed_langs),
|
allowed_langs: frozenset[str] = Depends(get_allowed_langs),
|
||||||
):
|
):
|
||||||
book_id = await GetRandomBookService.get_random_id(
|
book_id = await GetRandomBookService.get_random_id(
|
||||||
allowed_langs, request.app.state.redis
|
{"allowed_langs": allowed_langs}, request.app.state.redis
|
||||||
)
|
)
|
||||||
|
|
||||||
book = (
|
book = (
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async def get_random_sequence(
|
|||||||
allowed_langs: frozenset[str] = Depends(get_allowed_langs),
|
allowed_langs: frozenset[str] = Depends(get_allowed_langs),
|
||||||
):
|
):
|
||||||
sequence_id = await GetRandomSequenceService.get_random_id(
|
sequence_id = await GetRandomSequenceService.get_random_id(
|
||||||
allowed_langs,
|
{"allowed_langs": allowed_langs},
|
||||||
request.app.state.redis,
|
request.app.state.redis,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user