Spaces:
Configuration error
Configuration error
| """ | |
| 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") | |