004 Add cache
This commit is contained in:
+96
-10
@@ -1,17 +1,103 @@
|
||||
"""Redis cache repository skeleton."""
|
||||
"""Redis-backed cache repository."""
|
||||
|
||||
from typing import Any
|
||||
from dataclasses import asdict, is_dataclass
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
import json
|
||||
from typing import Any, Callable, Protocol
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.config import RepositoryConfig
|
||||
|
||||
|
||||
class RedisClientProtocol(Protocol):
|
||||
async def get(self, key: str) -> bytes | str | None: ...
|
||||
|
||||
async def set(self, key: str, value: str, ex: int | None = None) -> Any: ...
|
||||
|
||||
async def delete(self, key: str) -> Any: ...
|
||||
|
||||
|
||||
class PriceCache:
|
||||
async def get(self, key: str) -> Any | None:
|
||||
_ = key
|
||||
raise NotImplementedError("Redis repository implementation is added in task 004.")
|
||||
"""Repository for cached provider payloads."""
|
||||
|
||||
async def set(self, key: str, value: Any, ttl: int) -> None:
|
||||
_ = (key, value, ttl)
|
||||
raise NotImplementedError("Redis repository implementation is added in task 004.")
|
||||
def __init__(self, redis_client: RedisClientProtocol, *, ttl_seconds: int) -> None:
|
||||
self._redis_client = redis_client
|
||||
self._ttl_seconds = ttl_seconds
|
||||
|
||||
@classmethod
|
||||
def from_repository_config(
|
||||
cls,
|
||||
repository_config: RepositoryConfig,
|
||||
*,
|
||||
client_factory: Callable[[str], RedisClientProtocol] | None = None,
|
||||
) -> "PriceCache":
|
||||
resolved_factory = client_factory or _default_redis_client_factory
|
||||
redis_client = resolved_factory(repository_config.redis_dsn)
|
||||
return cls(
|
||||
redis_client=redis_client,
|
||||
ttl_seconds=repository_config.price_cache_ttl_seconds,
|
||||
)
|
||||
|
||||
async def get(self, key: str) -> Any | None:
|
||||
payload = await self._redis_client.get(key)
|
||||
if payload is None:
|
||||
return None
|
||||
|
||||
if isinstance(payload, bytes):
|
||||
serialized_payload = payload.decode("utf-8")
|
||||
elif isinstance(payload, str):
|
||||
serialized_payload = payload
|
||||
else:
|
||||
raise TypeError("Redis cache payload must be bytes or str.")
|
||||
|
||||
return json.loads(serialized_payload)
|
||||
|
||||
async def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
||||
serialized_payload = _serialize(value)
|
||||
resolved_ttl = self._ttl_seconds if ttl is None else ttl
|
||||
await self._redis_client.set(key, serialized_payload, ex=resolved_ttl)
|
||||
|
||||
async def invalidate(self, key: str) -> None:
|
||||
_ = key
|
||||
raise NotImplementedError("Redis repository implementation is added in task 004.")
|
||||
await self._redis_client.delete(key)
|
||||
|
||||
|
||||
def _default_redis_client_factory(redis_dsn: str) -> RedisClientProtocol:
|
||||
"""Create async Redis client from DSN."""
|
||||
|
||||
try:
|
||||
import redis.asyncio as redis_asyncio # type: ignore[import-not-found]
|
||||
except ModuleNotFoundError:
|
||||
try:
|
||||
from aioredis import from_url as aioredis_from_url # type: ignore
|
||||
except ModuleNotFoundError as exc:
|
||||
raise RuntimeError(
|
||||
"No async Redis client implementation is available."
|
||||
) from exc
|
||||
return aioredis_from_url(redis_dsn, encoding="utf-8", decode_responses=False)
|
||||
|
||||
return redis_asyncio.from_url(redis_dsn, encoding="utf-8", decode_responses=False)
|
||||
|
||||
|
||||
def _serialize(value: Any) -> str:
|
||||
normalized = _normalize_for_json(value)
|
||||
return json.dumps(normalized, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def _normalize_for_json(value: Any) -> Any:
|
||||
if isinstance(value, BaseModel):
|
||||
return _normalize_for_json(value.model_dump(mode="json"))
|
||||
if is_dataclass(value):
|
||||
return _normalize_for_json(asdict(value))
|
||||
if isinstance(value, Enum):
|
||||
return _normalize_for_json(value.value)
|
||||
if isinstance(value, Decimal):
|
||||
return str(value)
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _normalize_for_json(item) for key, item in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [_normalize_for_json(item) for item in value]
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
raise TypeError(f"Unsupported cache payload type: {type(value)!r}")
|
||||
|
||||
Reference in New Issue
Block a user