forked from cartsnitch/api
b4ad140796
Three categories of pre-existing CI failure on PR #42: 1. typecheck (mypy src/cartsnitch_api, 9 errors): - src/cartsnitch_api/config.py:89 — Settings() needs required secret args that only exist in env at runtime; suppress with type: ignore[call-arg] - src/cartsnitch_api/cache.py:38 — redis-py returns Any/bytes, normalize to str before returning from get() - src/cartsnitch_api/middleware/rate_limit.py:128,131,134 — three limiter globals were inferred as RedisSlidingWindow on the if branch then re-assigned InMemorySlidingWindow on else; declare them as RateLimitBackend up front - src/cartsnitch_api/middleware/rate_limit.py:181,187 — RateLimitBackend Protocol didn't declare max_requests even though both InMemorySlidingWindow and RedisSlidingWindow expose it; add max_requests: int to the Protocol 2. test (FK constraint on purchases.user_id): - tests/conftest.py:_create_test_user_and_session stored user_id as 32-char hex; test_e2e conftest reads it via raw SQL and wraps in uuid.UUID (36 chars) before passing to Purchase.user_id, so the FK never matched. Switch back to str(uuid.uuid4()) (36 chars) so the stored value and the FK bind value use the same format. 3. Verify lint + format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
"""Redis/DragonflyDB caching helpers."""
|
|
|
|
import redis.asyncio as redis
|
|
|
|
from cartsnitch_api.config import settings
|
|
|
|
|
|
class CacheClient:
|
|
"""Redis/DragonflyDB caching with connection pooling.
|
|
|
|
Will be used for expensive queries: price trends, product comparisons.
|
|
Cache invalidation via Redis pub/sub events from other services.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._pool: redis.ConnectionPool | None = None
|
|
self._client: redis.Redis | None = None
|
|
|
|
async def initialize(self) -> None:
|
|
"""Initialize the Redis connection pool."""
|
|
self._pool = redis.ConnectionPool.from_url(
|
|
settings.redis_url,
|
|
max_connections=20,
|
|
decode_responses=True,
|
|
)
|
|
self._client = redis.Redis(connection_pool=self._pool)
|
|
|
|
async def close(self) -> None:
|
|
"""Close the Redis connection pool."""
|
|
if self._client:
|
|
await self._client.aclose()
|
|
if self._pool:
|
|
await self._pool.aclose()
|
|
|
|
async def get(self, key: str) -> str | None:
|
|
if not self._client:
|
|
return None
|
|
result = await self._client.get(key)
|
|
if result is None:
|
|
return None
|
|
return str(result)
|
|
|
|
async def set(self, key: str, value: str, ttl_seconds: int = 300) -> None:
|
|
if not self._client:
|
|
return
|
|
await self._client.set(key, value, ex=ttl_seconds)
|
|
|
|
async def delete(self, key: str) -> None:
|
|
if not self._client:
|
|
return
|
|
await self._client.delete(key)
|
|
|
|
async def invalidate_price_cache(self, product_id: str) -> None:
|
|
"""Invalidate all price-related cache entries for a product."""
|
|
if not self._client:
|
|
return
|
|
pattern = f"price:*:{product_id}"
|
|
await self._delete_pattern(pattern)
|
|
|
|
async def invalidate_product_cache(self, product_id: str) -> None:
|
|
"""Invalidate the product detail cache entry."""
|
|
if not self._client:
|
|
return
|
|
await self._client.delete(f"product:{product_id}")
|
|
|
|
async def _delete_pattern(self, pattern: str) -> None:
|
|
"""Delete all keys matching a pattern using SCAN."""
|
|
if not self._client:
|
|
return
|
|
cursor = 0
|
|
while True:
|
|
cursor, keys = await self._client.scan(cursor=cursor, match=pattern, count=100)
|
|
if keys:
|
|
await self._client.delete(*keys)
|
|
if cursor == 0:
|
|
break
|
|
|
|
|
|
cache_client = CacheClient()
|