Files
decision/backend/app/services/cache_service.py
Yvv 403b94fa2c Sprint 5 : integration et production -- securite, performance, API publique, documentation
Backend: rate limiter, security headers, blockchain cache service avec RPC,
public API (7 endpoints read-only), WebSocket auth + heartbeat, DB connection
pooling, structured logging, health check DB. Frontend: API retry/timeout,
WebSocket auth + heartbeat + typed events, notifications toast, mobile hamburger
+ drawer, error boundary, offline banner, loading skeletons, dashboard enrichi.
Documentation: guides utilisateur complets (demarrage, vote, sanctuaire, FAQ 30+),
guide deploiement, politique securite. 123 tests, 155 fichiers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 15:12:50 +01:00

141 lines
3.4 KiB
Python

"""Cache service: blockchain data caching with TTL expiry.
Uses the BlockchainCache model (PostgreSQL/JSONB) to cache
on-chain data like WoT size, Smith size, and TechComm size.
Avoids repeated RPC calls to the Duniter node.
"""
from __future__ import annotations
import logging
from datetime import datetime, timedelta, timezone
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.cache import BlockchainCache
logger = logging.getLogger(__name__)
async def get_cached(key: str, db: AsyncSession) -> dict | None:
"""Retrieve a cached value by key if it has not expired.
Parameters
----------
key:
The cache key to look up.
db:
Async database session.
Returns
-------
dict | None
The cached value as a dict, or None if missing/expired.
"""
result = await db.execute(
select(BlockchainCache).where(
BlockchainCache.cache_key == key,
BlockchainCache.expires_at > datetime.now(timezone.utc),
)
)
entry = result.scalar_one_or_none()
if entry is None:
logger.debug("Cache miss pour la cle '%s'", key)
return None
logger.debug("Cache hit pour la cle '%s'", key)
return entry.cache_value
async def set_cached(
key: str,
value: dict,
db: AsyncSession,
ttl_seconds: int = 3600,
) -> None:
"""Store a value in the cache with the given TTL.
If the key already exists, it is replaced (upsert).
Parameters
----------
key:
The cache key.
value:
The value to store (must be JSON-serializable).
db:
Async database session.
ttl_seconds:
Time-to-live in seconds (default: 1 hour).
"""
now = datetime.now(timezone.utc)
expires_at = now + timedelta(seconds=ttl_seconds)
# Check if key already exists
result = await db.execute(
select(BlockchainCache).where(BlockchainCache.cache_key == key)
)
existing = result.scalar_one_or_none()
if existing is not None:
existing.cache_value = value
existing.fetched_at = now
existing.expires_at = expires_at
logger.debug("Cache mis a jour pour la cle '%s' (TTL=%ds)", key, ttl_seconds)
else:
entry = BlockchainCache(
cache_key=key,
cache_value=value,
fetched_at=now,
expires_at=expires_at,
)
db.add(entry)
logger.debug("Cache cree pour la cle '%s' (TTL=%ds)", key, ttl_seconds)
await db.commit()
async def invalidate(key: str, db: AsyncSession) -> None:
"""Remove a specific cache entry by key.
Parameters
----------
key:
The cache key to invalidate.
db:
Async database session.
"""
await db.execute(
delete(BlockchainCache).where(BlockchainCache.cache_key == key)
)
await db.commit()
logger.debug("Cache invalide pour la cle '%s'", key)
async def cleanup_expired(db: AsyncSession) -> int:
"""Remove all expired cache entries.
Parameters
----------
db:
Async database session.
Returns
-------
int
Number of entries removed.
"""
result = await db.execute(
delete(BlockchainCache).where(
BlockchainCache.expires_at <= datetime.now(timezone.utc)
)
)
await db.commit()
count = result.rowcount
if count > 0:
logger.info("Nettoyage cache: %d entrees expirees supprimees", count)
return count