- FastAPI + NiceGUI web application - QuantLib-based Black-Scholes pricing with Greeks - Protective put, laddered, and LEAPS strategies - Real-time WebSocket updates - TradingView-style charts via Lightweight-Charts - Docker containerization - GitLab CI/CD pipeline for VPS deployment - VPN-only access configuration
73 lines
2.2 KiB
Python
73 lines
2.2 KiB
Python
"""Redis-backed caching utilities with graceful fallback support."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from redis.asyncio import Redis
|
|
except ImportError: # pragma: no cover - optional dependency
|
|
Redis = None
|
|
|
|
|
|
class CacheService:
|
|
"""Small async cache wrapper around Redis."""
|
|
|
|
def __init__(self, url: str | None, default_ttl: int = 300) -> None:
|
|
self.url = url
|
|
self.default_ttl = default_ttl
|
|
self._client: Redis | None = None
|
|
self._enabled = bool(url and Redis)
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return self._enabled and self._client is not None
|
|
|
|
async def connect(self) -> None:
|
|
if not self._enabled:
|
|
if self.url and Redis is None:
|
|
logger.warning("Redis URL configured but redis package is not installed; cache disabled")
|
|
return
|
|
|
|
try:
|
|
self._client = Redis.from_url(self.url, decode_responses=True)
|
|
await self._client.ping()
|
|
logger.info("Connected to Redis cache")
|
|
except Exception as exc: # pragma: no cover - network dependent
|
|
logger.warning("Redis unavailable, cache disabled: %s", exc)
|
|
self._client = None
|
|
|
|
async def close(self) -> None:
|
|
if self._client is None:
|
|
return
|
|
await self._client.aclose()
|
|
self._client = None
|
|
|
|
async def get_json(self, key: str) -> dict[str, Any] | list[Any] | None:
|
|
if self._client is None:
|
|
return None
|
|
|
|
value = await self._client.get(key)
|
|
if value is None:
|
|
return None
|
|
|
|
return json.loads(value)
|
|
|
|
async def set_json(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
if self._client is None:
|
|
return
|
|
|
|
payload = json.dumps(value, default=self._json_default)
|
|
await self._client.set(key, payload, ex=ttl or self.default_ttl)
|
|
|
|
@staticmethod
|
|
def _json_default(value: Any) -> str:
|
|
if isinstance(value, datetime):
|
|
return value.isoformat()
|
|
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
|