2026-04-03 15:00:08 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-03-09 04:53:15 +00:00
|
|
|
import json
|
|
|
|
|
from typing import Any, Optional
|
|
|
|
|
|
|
|
|
|
from core.settings import REDIS_DEFAULT_TTL, REDIS_ENABLED, REDIS_PREFIX, REDIS_URL
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from redis import Redis
|
|
|
|
|
except Exception: # pragma: no cover
|
|
|
|
|
Redis = None # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RedisCache:
|
2026-04-03 15:00:08 +00:00
|
|
|
def __init__(self, *, prefix_override: Optional[str] = None, default_ttl_override: Optional[int] = None):
|
2026-03-09 04:53:15 +00:00
|
|
|
self.enabled = bool(REDIS_ENABLED and REDIS_URL and Redis is not None)
|
2026-04-03 15:00:08 +00:00
|
|
|
self.prefix = str(prefix_override or REDIS_PREFIX).strip() or REDIS_PREFIX
|
|
|
|
|
self.default_ttl = int(default_ttl_override if default_ttl_override is not None else REDIS_DEFAULT_TTL)
|
2026-03-09 04:53:15 +00:00
|
|
|
self._client: Optional["Redis"] = None
|
|
|
|
|
if self.enabled:
|
|
|
|
|
try:
|
|
|
|
|
self._client = Redis.from_url(REDIS_URL, decode_responses=True)
|
|
|
|
|
self._client.ping()
|
|
|
|
|
except Exception:
|
|
|
|
|
self.enabled = False
|
|
|
|
|
self._client = None
|
|
|
|
|
|
|
|
|
|
def _full_key(self, key: str) -> str:
|
|
|
|
|
return f"{self.prefix}:{key}"
|
|
|
|
|
|
|
|
|
|
def ping(self) -> bool:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return False
|
|
|
|
|
try:
|
|
|
|
|
return bool(self._client.ping())
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
|
2026-04-03 15:00:08 +00:00
|
|
|
def get(self, key: str) -> Optional[str]:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return self._client.get(self._full_key(key))
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
ttl_seconds = int(ttl if ttl is not None else self.default_ttl)
|
|
|
|
|
self._client.setex(self._full_key(key), ttl_seconds, str(value))
|
|
|
|
|
except Exception:
|
|
|
|
|
return
|
|
|
|
|
|
2026-03-09 04:53:15 +00:00
|
|
|
def get_json(self, key: str) -> Any:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
2026-04-03 15:00:08 +00:00
|
|
|
raw = self.get(key)
|
2026-03-09 04:53:15 +00:00
|
|
|
if not raw:
|
|
|
|
|
return None
|
|
|
|
|
return json.loads(raw)
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def set_json(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
2026-04-03 15:00:08 +00:00
|
|
|
self.set(key, json.dumps(value, ensure_ascii=False, default=str), ttl=ttl)
|
|
|
|
|
except Exception:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def sadd(self, key: str, *members: str) -> None:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return
|
|
|
|
|
normalized = [str(member or "").strip() for member in members if str(member or "").strip()]
|
|
|
|
|
if not normalized:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
self._client.sadd(self._full_key(key), *normalized)
|
|
|
|
|
except Exception:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def srem(self, key: str, *members: str) -> None:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return
|
|
|
|
|
normalized = [str(member or "").strip() for member in members if str(member or "").strip()]
|
|
|
|
|
if not normalized:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
self._client.srem(self._full_key(key), *normalized)
|
|
|
|
|
except Exception:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def smembers(self, key: str) -> set[str]:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return set()
|
|
|
|
|
try:
|
|
|
|
|
rows = self._client.smembers(self._full_key(key))
|
|
|
|
|
return {str(row or "").strip() for row in rows if str(row or "").strip()}
|
|
|
|
|
except Exception:
|
|
|
|
|
return set()
|
|
|
|
|
|
|
|
|
|
def expire(self, key: str, ttl: int) -> None:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
self._client.expire(self._full_key(key), max(1, int(ttl)))
|
2026-03-09 04:53:15 +00:00
|
|
|
except Exception:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def delete(self, *keys: str) -> None:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return
|
|
|
|
|
full_keys = [self._full_key(key) for key in keys if str(key or "").strip()]
|
|
|
|
|
if not full_keys:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
self._client.delete(*full_keys)
|
|
|
|
|
except Exception:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def delete_prefix(self, prefix: str) -> None:
|
|
|
|
|
if not self.enabled or self._client is None:
|
|
|
|
|
return
|
|
|
|
|
pattern = self._full_key(f"{prefix}*")
|
|
|
|
|
try:
|
|
|
|
|
cursor = 0
|
|
|
|
|
while True:
|
|
|
|
|
cursor, rows = self._client.scan(cursor=cursor, match=pattern, count=200)
|
|
|
|
|
if rows:
|
|
|
|
|
self._client.delete(*rows)
|
|
|
|
|
if cursor == 0:
|
|
|
|
|
break
|
|
|
|
|
except Exception:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cache = RedisCache()
|
2026-04-03 15:00:08 +00:00
|
|
|
auth_cache = RedisCache(prefix_override=f"{REDIS_PREFIX}_auth")
|