"""# Summary Key-value store port with in-memory and Redis adaptors. --- Reserve infrastructure for caching small values by key (sessions, locks, result blobs, rate counters). The in-memory adaptor needs no services and is a safe default; the Redis adaptor is opt-in and imports ``redis`` lazily so it never becomes a hard dependency. Example: ```python from infra.adapters.kv import InMemoryKV kv = InMemoryKV() await kv.set("greeting", "hello", ttl=30) assert await kv.get("greeting") == "hello" ``` """ from __future__ import annotations from abc import ABC, abstractmethod class KVStore(ABC): """Abstract key-value store port.""" @abstractmethod async def get(self, key: str) -> str | None: """Return the value at ``key``, or ``None``.""" @abstractmethod async def set(self, key: str, value: str, ttl: int | None = None) -> None: """Store ``value`` at ``key`` with an optional TTL in seconds.""" @abstractmethod async def delete(self, key: str) -> None: """Delete ``key`` if present.""" @abstractmethod async def scan(self, pattern: str) -> list[str]: """Return keys matching a glob ``pattern``.""" async def clear(self, pattern: str = "*") -> None: """Delete all keys matching ``pattern``. Args: pattern (str): Glob-style key pattern. """ for key in await self.scan(pattern): await self.delete(key) async def close(self) -> None: """Release any underlying resources. Default is a no-op.""" class InMemoryKV(KVStore): """Process-local key-value store used as a dependency-free fallback. Attributes: _data (Dict[str, str]): In-memory value map. _expiry (Dict[str, float]): Absolute expiry timestamps by key; ``0.0`` means "no expiry". """ def __init__(self) -> None: """Initialize an empty store with no imports or connections.""" self._data: dict[str, str] = {} self._expiry: dict[str, float] = {} def _expired(self, key: str) -> bool: import time deadline = self._expiry.get(key, 0.0) if deadline and deadline < time.time(): self._data.pop(key, None) self._expiry.pop(key, None) return True return False async def get(self, key: str) -> str | None: """Return the value at ``key``, or ``None`` when absent/expired.""" if self._expired(key): return None return self._data.get(key) async def set(self, key: str, value: str, ttl: int | None = None) -> None: """Store ``value`` at ``key`` with an optional TTL in seconds.""" import time self._data[key] = value self._expiry[key] = (time.time() + ttl) if ttl else 0.0 async def delete(self, key: str) -> None: """Delete ``key`` if present.""" self._data.pop(key, None) self._expiry.pop(key, None) async def scan(self, pattern: str) -> list[str]: """Return live keys matching a glob ``pattern``.""" import fnmatch return [k for k in list(self._data) if fnmatch.fnmatch(k, pattern) and not self._expired(k)] class RedisKV(KVStore): """Redis-backed key-value store. Attributes: _redis: Asynchronous Redis client (imported lazily). """ def __init__(self, url: str = "redis://localhost:6379/0", decode_responses: bool = True): """Initialize the store with a Redis connection. Args: url (str): Redis connection URL. decode_responses (bool): Whether Redis returns decoded strings. Raises: ImportError: When the optional ``redis`` package is not installed. """ from redis.asyncio import Redis self._redis = Redis.from_url(url, decode_responses=decode_responses) async def get(self, key: str) -> str | None: """Return the value at ``key``, or ``None``.""" return await self._redis.get(key) async def set(self, key: str, value: str, ttl: int | None = None) -> None: """Store ``value`` at ``key`` with an optional TTL in seconds.""" await self._redis.set(key, value, ex=ttl) async def delete(self, key: str) -> None: """Delete ``key`` if present.""" await self._redis.delete(key) async def scan(self, pattern: str) -> list[str]: """Return keys matching a glob ``pattern``.""" return [key async for key in self._redis.scan_iter(match=pattern)] async def close(self) -> None: """Close the underlying Redis connection.""" await self._redis.aclose()