"""Shared cache abstractions and helpers."""
from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass
from typing import Any, Optional
from pydantic import BaseModel, Field
from accuralai_core.contracts.models import GenerateResponse
from accuralai_core.contracts.protocols import Cache
[docs]
class CacheConfig(BaseModel):
"""Base configuration shared across cache implementations."""
default_ttl_s: float | None = Field(default=None, ge=0)
copy_on_get: bool = True
stats_enabled: bool = False
[docs]
@dataclass(slots=True)
class CacheEntry:
"""Internal representation of a cached response."""
response: GenerateResponse
expires_at: float | None = None
[docs]
class BaseCache(Cache):
"""Helper base class that offers TTL resolution and cloning behaviour."""
[docs]
def __init__(self, *, options: CacheConfig | None = None) -> None:
self.options = options or CacheConfig()
self._lock = asyncio.Lock()
self._hits = 0
self._misses = 0
@property
def stats(self) -> dict[str, int]:
"""Return cache hit/miss counters."""
return {"hits": self._hits, "misses": self._misses}
def _resolve_ttl(self, ttl_s: Optional[int]) -> Optional[float]:
if ttl_s is not None:
return float(ttl_s)
return self.options.default_ttl_s
def _compute_expiry(self, ttl_s: Optional[int]) -> Optional[float]:
resolved = self._resolve_ttl(ttl_s)
if resolved is None:
return None
return time.time() + resolved
def _clone(self, response: GenerateResponse) -> GenerateResponse:
if self.options.copy_on_get:
return response.model_copy(deep=True)
return response
def _mark_hit(self) -> None:
if self.options.stats_enabled:
self._hits += 1
def _mark_miss(self) -> None:
if self.options.stats_enabled:
self._misses += 1
[docs]
async def invalidate_prefix(self, prefix: str) -> None:
"""Optional hook for derived caches; default routes to individual invalidation."""
raise NotImplementedError("invalidate_prefix is not implemented for this cache")