import asyncio import time from abc import ABC, abstractmethod from datetime import datetime, timezone from app.models import ProviderUsage GENERIC_ERROR = 'Unable to retrieve usage information' def utc_now() -> datetime: return datetime.now(timezone.utc) class UsageProvider(ABC): def __init__(self, provider: str, cache_ttl: float, source: str | None = None): self.provider = provider self.cache_ttl = cache_ttl self.source = source self.last_successful_update: datetime | None = None self._cached: ProviderUsage | None = None self._cache_time = 0.0 self._lock = asyncio.Lock() async def get_usage(self) -> ProviderUsage: if self._cached is not None and time.monotonic() - self._cache_time < self.cache_ttl: return self._cached.model_copy(deep=True) async with self._lock: if self._cached is not None and time.monotonic() - self._cache_time < self.cache_ttl: return self._cached.model_copy(deep=True) try: result = await self._fetch_usage() if result.status == 'ok': self.last_successful_update = result.last_successful_update or utc_now() result.last_successful_update = self.last_successful_update self._cached = result.model_copy(deep=True) self._cache_time = time.monotonic() return result result.last_successful_update = self.last_successful_update result.error = GENERIC_ERROR return result except Exception: return ProviderUsage(provider=self.provider, status='error', error=GENERIC_ERROR, last_successful_update=self.last_successful_update, source=self.source) @abstractmethod async def _fetch_usage(self) -> ProviderUsage: raise NotImplementedError async def aclose(self) -> None: return None