Terminal / redis.py
Pulka
sync: 1 changed, 0 deleted — 7b25b6a2 (2026-06-21 23:05)
6de2648 verified
Raw
History Blame Contribute Delete
3.83 kB
"""
redis.py — P46: Upstash Redis REST client centralizzato.
Centralizza l'accesso a Upstash Redis per tutti i moduli backend.
Ogni comando è una POST a UPSTASH_REDIS_REST_URL via HTTPS — zero SDK, zero dipendenze.
Configurazione: UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN (già presenti in env Railway).
Esportazioni:
redis_cmd(command) — comando raw (lista Redis)
redis_set(key, value, ttl) — SET [EX ttl]
redis_get(key) — GET
redis_del(*keys) — DEL
redis_mget(keys) — MGET
redis_scan(pattern) — SCAN paginato (P40-H fix: loop cursor != "0")
redis_expire(key, ttl) — EXPIRE
is_available() — PING health check
@module redis
"""
import os
import httpx
REDIS_URL = os.getenv("UPSTASH_REDIS_REST_URL", "")
REDIS_TOKEN = os.getenv("UPSTASH_REDIS_REST_TOKEN", "")
async def redis_cmd(command: list, timeout: float = 2.0) -> dict | None:
"""Esegue un comando Redis via Upstash REST API. Ritorna None su errore."""
if not REDIS_URL or not REDIS_TOKEN:
return None
try:
async with httpx.AsyncClient(timeout=timeout) as c:
r = await c.post(
REDIS_URL,
json=command,
headers={
"Authorization": f"Bearer {REDIS_TOKEN}",
"Content-Type": "application/json",
},
)
return r.json() if r.is_success else None
except Exception:
return None
async def redis_set(key: str, value: str, ttl: int | None = None) -> bool:
"""SET key value [EX ttl]. Ritorna True su successo."""
cmd: list = ["SET", key, value]
if ttl is not None:
cmd += ["EX", ttl]
return await redis_cmd(cmd) is not None
async def redis_get(key: str) -> str | None:
"""GET key. Ritorna None se chiave assente o errore."""
result = await redis_cmd(["GET", key])
if result and "result" in result:
return result["result"]
return None
async def redis_del(*keys: str) -> int:
"""DEL key [key ...]. Ritorna il numero di chiavi rimosse."""
if not keys:
return 0
result = await redis_cmd(["DEL"] + list(keys))
return result.get("result", 0) if result else 0
async def redis_mget(keys: list[str]) -> list[str | None]:
"""MGET key [key ...]. Ritorna lista di valori (None se assenti)."""
if not keys:
return []
result = await redis_cmd(["MGET"] + keys)
if result and "result" in result:
return result["result"]
return [None] * len(keys)
async def redis_scan(pattern: str) -> list[str]:
"""SCAN paginato. Ritorna tutte le chiavi che matchano il pattern.
Loop su cursor != "0" (P40-H fix: versioni precedenti usavano cursor fisso "0").
"""
all_keys: list[str] = []
cursor = "0"
while True:
result = await redis_cmd(["SCAN", cursor, "MATCH", pattern, "COUNT", "100"])
if not result or "result" not in result:
break
scan_result = result["result"]
if not isinstance(scan_result, list) or len(scan_result) < 2:
break
cursor = str(scan_result[0])
keys = scan_result[1]
if isinstance(keys, list):
all_keys.extend(keys)
if cursor == "0":
break
return all_keys
async def redis_expire(key: str, ttl: int) -> bool:
"""EXPIRE key ttl. Ritorna True se la chiave esiste."""
result = await redis_cmd(["EXPIRE", key, ttl])
return bool(result and result.get("result"))
async def is_available() -> bool:
"""Verifica se Upstash Redis è configurato e raggiungibile (PING)."""
if not REDIS_URL or not REDIS_TOKEN:
return False
result = await redis_cmd(["PING"])
return bool(result and result.get("result") == "PONG")