Files
devplacepy/devplacepy/services/nodes/cache.py
T
retoor 6f80a820da
DevPlace CI / test (push) Failing after 3m13s
Zo doen wijdat in oost
2026-10-16 07:20:44 +02:00

91 lines
2.6 KiB
Python

# retoor <retoor@molodetz.nl>
from __future__ import annotations
import hashlib
import time
from collections import OrderedDict
from dataclasses import dataclass
from typing import Callable
from devplacepy.services.nodes.spec import SPEC
LIMITS = SPEC.limits
@dataclass(frozen=True)
class CachedResult:
status: int
headers: dict
body: bytes
truncated: bool
stored_at: float
def cache_key(method: str, url: str) -> str:
return hashlib.sha256(f"{method.upper()} {url}".encode()).hexdigest()
class ResultCache:
def __init__(
self,
ttl_seconds: int = LIMITS.cache_ttl_seconds,
max_bytes: int = LIMITS.cache_max_bytes,
entry_max_bytes: int = LIMITS.cache_entry_max_bytes,
clock: Callable[[], float] = time.monotonic,
) -> None:
self.ttl_seconds = max(0, min(ttl_seconds, LIMITS.cache_ttl_seconds))
self.max_bytes = max_bytes
self.entry_max_bytes = min(entry_max_bytes, LIMITS.cache_entry_max_bytes)
self._clock = clock
self._entries: OrderedDict[str, CachedResult] = OrderedDict()
self._bytes = 0
self.hits = 0
self.misses = 0
@property
def size_bytes(self) -> int:
return self._bytes
def __len__(self) -> int:
return len(self._entries)
def _drop(self, key: str) -> None:
entry = self._entries.pop(key, None)
if entry is not None:
self._bytes -= len(entry.body)
def get(self, method: str, url: str) -> CachedResult | None:
key = cache_key(method, url)
entry = self._entries.get(key)
if entry is None:
self.misses += 1
return None
if self._clock() - entry.stored_at >= self.ttl_seconds:
self._drop(key)
self.misses += 1
return None
self._entries.move_to_end(key)
self.hits += 1
return entry
def put(
self, method: str, url: str, status: int, headers: dict, body: bytes, truncated: bool
) -> bool:
if self.ttl_seconds <= 0 or status >= 500 or truncated:
return False
if len(body) > self.entry_max_bytes or len(body) > self.max_bytes:
return False
key = cache_key(method, url)
self._drop(key)
self._entries[key] = CachedResult(status, dict(headers), body, truncated, self._clock())
self._bytes += len(body)
while self._bytes > self.max_bytes and self._entries:
oldest = next(iter(self._entries))
self._drop(oldest)
return True
def clear(self) -> None:
self._entries.clear()
self._bytes = 0