File size: 3,832 Bytes
28a08e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""
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")