Files
All-in-one-Financial-Analysis/atlas-terminal/server/core/cache.py
T

92 lines
3.5 KiB
Python

"""TTL cache wrapper for DataGateway implementations."""
from __future__ import annotations
import asyncio
import time
from collections.abc import Awaitable, Callable
from typing import Any, TypeVar
from server.core.data_gateway import Article, DataGateway, EarningEvent, Fundamentals, HoldersData, OHLCV, Profile, Quote, Segment
T = TypeVar("T")
class CachedGateway(DataGateway):
"""Process-local cache for expensive provider calls.
This is deliberately thin and replaceable. The contract gives us a single
seam where Redis/Vercel Runtime Cache/Supabase cache can later slot in
without rewriting routers.
"""
TTLS = {
"quote": 30,
"profile": 86_400,
"fundamentals": 43_200,
"financials": 43_200,
"segments": 604_800,
"history": 300,
"news": 300,
"peers": 86_400,
"holders": 43_200,
"earnings_calendar": 3_600,
}
def __init__(self, inner: DataGateway) -> None:
self.inner = inner
self._lock = asyncio.Lock()
self._store: dict[str, tuple[float, Any]] = {}
def _key(self, method: str, *parts: Any) -> str:
normalized = ":".join(str(part).strip().upper() for part in parts)
return f"{method}:{normalized}"
async def _cached(self, method: str, key_parts: tuple[Any, ...], fetcher: Callable[[], Awaitable[T]]) -> T:
key = self._key(method, *key_parts)
ttl = self.TTLS[method]
now = time.monotonic()
async with self._lock:
cached = self._store.get(key)
if cached and now - cached[0] < ttl:
return cached[1]
result = await fetcher()
async with self._lock:
self._store[key] = (time.monotonic(), result)
return result
async def quote(self, symbol: str) -> Quote:
return await self._cached("quote", (symbol,), lambda: self.inner.quote(symbol))
async def profile(self, symbol: str) -> Profile:
return await self._cached("profile", (symbol,), lambda: self.inner.profile(symbol))
async def fundamentals(self, symbol: str, period: str = "annual") -> Fundamentals:
return await self._cached("fundamentals", (symbol, period), lambda: self.inner.fundamentals(symbol, period))
async def financials(self, symbol: str, statement: str = "income", period: str = "annual") -> dict[str, Any]:
return await self._cached("financials", (symbol, statement, period), lambda: self.inner.financials(symbol, statement, period))
async def history(self, symbol: str, range: str = "1y") -> OHLCV:
return await self._cached("history", (symbol, range), lambda: self.inner.history(symbol, range))
async def news(self, symbols: list[str], limit: int = 20) -> list[Article]:
return await self._cached("news", (",".join(symbols), limit), lambda: self.inner.news(symbols, limit))
async def peers(self, symbol: str) -> list[str]:
return await self._cached("peers", (symbol,), lambda: self.inner.peers(symbol))
async def segments(self, symbol: str) -> list[Segment]:
return await self._cached("segments", (symbol,), lambda: self.inner.segments(symbol))
async def holders(self, symbol: str) -> HoldersData:
return await self._cached("holders", (symbol,), lambda: self.inner.holders(symbol))
async def earnings_calendar(self, symbol: str) -> list[EarningEvent]:
return await self._cached("earnings_calendar", (symbol,), lambda: self.inner.earnings_calendar(symbol))
async def clear(self) -> None:
async with self._lock:
self._store.clear()