"""Redis cache helper for equipment data and rate limiting.""" import json import logging from typing import Any import redis.asyncio as redis from app.config import settings logger = logging.getLogger(__name__) _cache_client: redis.Redis | None = None def _build_key(*parts: str) -> str: """Build a colon-separated Redis key.""" return ":".join(part.replace(":", "_") for part in parts) async def get_cache() -> redis.Redis: """Return the Redis client singleton, creating it on first use.""" global _cache_client if _cache_client is None: _cache_client = redis.from_url(settings.REDIS_URL, decode_responses=True) return _cache_client async def set_cache(key: str, value: Any, ttl: int = 3600) -> None: """Store a JSON-serialisable value in Redis with a TTL.""" try: client = await get_cache() await client.setex(key, ttl, json.dumps(value)) except Exception as exc: # noqa: BLE001 logger.warning("Cache set failed for key=%s: %s", key, exc) async def get_cached(key: str) -> Any | None: """Retrieve and JSON-deserialise a cached value.""" try: client = await get_cache() raw = await client.get(key) if raw is None: return None return json.loads(raw) except Exception as exc: # noqa: BLE001 logger.warning("Cache get failed for key=%s: %s", key, exc) return None async def delete_pattern(pattern: str) -> None: """Delete all keys matching a glob pattern.""" try: client = await get_cache() async for key in client.scan_iter(match=pattern, count=100): await client.delete(key) except Exception as exc: # noqa: BLE001 logger.warning("Cache delete_pattern failed for pattern=%s: %s", pattern, exc) async def check_rate_limit(identifier: str, max_requests: int = 5, window: int = 60) -> bool: """Return True if the identifier is within the rate limit.""" try: client = await get_cache() key = _build_key("rate", identifier) count = await client.incr(key) if count == 1: await client.expire(key, window) return count <= max_requests except Exception as exc: # noqa: BLE001 logger.warning("Rate limit check failed: %s", exc) return True async def ping_cache() -> bool: """Check if Redis is reachable.""" try: client = await get_cache() return bool(await client.ping()) except Exception: return False def equipment_list_key(page: int, category: str | None, sort: str | None, search: str | None) -> str: """Build the Redis cache key for an equipment list query.""" cat = category or "all" srt = sort or "default" srch = search or "all" return _build_key("equipment", "list", str(page), cat, srt, srch) def equipment_detail_key(item_id: int) -> str: """Build the Redis cache key for an equipment detail.""" return _build_key("equipment", "detail", str(item_id)) def equipment_categories_key() -> str: """Build the Redis cache key for the equipment categories list.""" return _build_key("equipment", "categories")