Spaces:
Running
Running
| import json | |
| import os | |
| import time | |
| import requests | |
| from .logger import get_logger | |
| logger = get_logger(__name__) | |
| SEEN_TICKERS_FILE = "seen_tickers.json" | |
| MEMORY_TTL_SECONDS = 30 * 24 * 60 * 60 # 30 days | |
| # VPS Data API (optional — falls back to local JSON if not set) | |
| VPS_API_URL = os.getenv("VPS_API_URL", "").rstrip("/") | |
| VPS_API_KEY = os.getenv("VPS_API_KEY", "") | |
| def _vps_headers() -> dict: | |
| return {"X-API-Key": VPS_API_KEY, "Content-Type": "application/json"} | |
| # --------------------------------------------------------------------------- | |
| # Public API | |
| # --------------------------------------------------------------------------- | |
| def load_seen_tickers() -> dict[str, float]: | |
| """Load the seen-tickers ledger, pruning entries older than 30 days. | |
| Values are Unix timestamps (float). | |
| Tries VPS API first, falls back to local JSON file. | |
| """ | |
| if VPS_API_URL: | |
| try: | |
| resp = requests.get(f"{VPS_API_URL}/seen-tickers", headers=_vps_headers(), timeout=5) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| logger.debug("Loaded %d seen tickers from VPS", len(data)) | |
| return data | |
| except Exception as exc: | |
| logger.warning("VPS seen-tickers unavailable, using local fallback: %s", exc) | |
| return _load_local() | |
| def mark_ticker_seen(ticker: str, region: str = "USA") -> None: | |
| """Record a ticker as recently analysed.""" | |
| if VPS_API_URL: | |
| try: | |
| resp = requests.post( | |
| f"{VPS_API_URL}/seen-tickers", | |
| headers=_vps_headers(), | |
| json={"ticker": ticker, "region": region}, | |
| timeout=5, | |
| ) | |
| resp.raise_for_status() | |
| logger.debug("Marked %s as seen on VPS", ticker) | |
| return | |
| except Exception as exc: | |
| logger.warning("VPS mark_ticker_seen failed, using local fallback: %s", exc) | |
| # Local fallback | |
| data = _load_local() | |
| data[ticker] = time.time() | |
| _save(data) | |
| # --------------------------------------------------------------------------- | |
| # Local JSON fallback (original behavior) | |
| # --------------------------------------------------------------------------- | |
| def _load_local() -> dict[str, float]: | |
| if not os.path.exists(SEEN_TICKERS_FILE): | |
| return {} | |
| try: | |
| with open(SEEN_TICKERS_FILE, "r") as f: | |
| raw: dict = json.load(f) | |
| now = time.time() | |
| cleaned: dict[str, float] = {} | |
| for ticker, ts in raw.items(): | |
| if isinstance(ts, str): | |
| try: | |
| from datetime import datetime, timezone | |
| ts = datetime.fromisoformat(ts).timestamp() | |
| except ValueError: | |
| continue | |
| if now - ts < MEMORY_TTL_SECONDS: | |
| cleaned[ticker] = ts | |
| _save(cleaned) | |
| return cleaned | |
| except (json.JSONDecodeError, OSError) as exc: | |
| logger.warning("Could not read %s: %s", SEEN_TICKERS_FILE, exc) | |
| return {} | |
| def _save(data: dict) -> None: | |
| try: | |
| with open(SEEN_TICKERS_FILE, "w") as f: | |
| json.dump(data, f, indent=2) | |
| except OSError as exc: | |
| logger.error("Failed to write %s: %s", SEEN_TICKERS_FILE, exc) | |