feat: add response cache middleware with poisoning-safe keys

This commit is contained in:
2026-07-20 07:10:42 -04:00
parent c3d5079d39
commit 6286086551
4 changed files with 434 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
"""进程内缓存后端: dict + 过期时刻;测试与无 Redis 场景使用。"""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
class InMemoryCache:
"""笨 KV(CacheBackend 契约);容量不设限——库内默认 TTL 必填防无界增长。"""
def __init__(self, now: Callable[[], float] = time.monotonic) -> None:
self._now = now
self._store: dict[str, tuple[float, str]] = {}
async def get(self, key: str) -> str | None:
item = self._store.get(key)
if item is None:
return None
expires_at, value = item
if self._now() >= expires_at:
del self._store[key]
return None
return value
async def set(self, key: str, value: str, ttl_s: int) -> None:
self._store[key] = (self._now() + ttl_s, value)
+44
View File
@@ -0,0 +1,44 @@
"""Redis 缓存后端: 笨 KV(CacheBackend 契约);redis 是 optional extra。
降级不在这里做——本类忠实上抛异常,静默降级是 CacheMW 的职责(算法一份)。
"""
from __future__ import annotations
from typing import Any
try:
import redis.asyncio as aioredis
except ImportError as _exc: # pragma: no cover - 依赖缺失路径
aioredis = None
_IMPORT_ERROR = _exc
else:
_IMPORT_ERROR = None
class RedisCache:
"""基于 redis.asyncio 的 KV;value 为 JSON 字符串,TTL 由调用方传入。"""
def __init__(self, client: Any) -> None:
if aioredis is None:
raise ImportError(
"Redis 缓存后端需要 redis 包: pip install 'polygateway[redis]'"
) from _IMPORT_ERROR
self._client = client
@classmethod
def from_url(cls, url: str) -> RedisCache:
if aioredis is None:
raise ImportError(
"Redis 缓存后端需要 redis 包: pip install 'polygateway[redis]'"
) from _IMPORT_ERROR
return cls(aioredis.from_url(url, decode_responses=True))
async def get(self, key: str) -> str | None:
return await self._client.get(key)
async def set(self, key: str, value: str, ttl_s: int) -> None:
await self._client.set(key, value, ex=ttl_s)
async def aclose(self) -> None:
await self._client.aclose()