1"""Watch list endpoints. 2 3The watch list is the single source of truth for "what targets are 4interesting right now". Anything expensive (Kronos forecasts, MiroFish 5crowd-sims) keys off this set, so cold targets cost nothing. 6""" 7 8from __future__ import annotations 9 10from typing import Any 11 12from fastapi import APIRouter 13from pydantic import BaseModel 14 15from maf.dashboard import state 16 17router = APIRouter() 18 19 20class WatchAddRequest(BaseModel): 21 target_id: str 22 kind: str = "symbol" 23 ttl_seconds: int = 6 * 3600 24 attrs: dict[str, Any] = {} 25 26 27def _redis_url() -> str | None: 28 """Lazy resolution — only need it on actual watchlist operations.""" 29 app = state.get_maf_app() 30 return app.config.redis_url if app else None 31 32 33@router.get("/api/watch") 34async def list_watch(kind: str | None = None) -> dict[str, Any]: 35 """Return current watch-list entries; filter by kind if given.""" 36 from maf.watch.list import WatchList 37 38 wl = WatchList(redis_url=_redis_url()) 39 try: 40 members = await wl.members(kind=kind) 41 return { 42 "members": [ 43 { 44 "target_id": m.target_id, 45 "kind": m.kind, 46 "expires_at": m.expires_at, 47 "remaining_seconds": round(m.remaining_seconds, 1), 48 "attrs": m.attrs, 49 } 50 for m in members 51 ], 52 } 53 finally: 54 await wl.aclose() 55 56 57@router.post("/api/watch") 58async def add_watch(req: WatchAddRequest) -> dict[str, Any]: 59 from maf.watch.list import WatchList 60 61 wl = WatchList(redis_url=_redis_url()) 62 try: 63 e = await wl.add( 64 req.target_id, kind=req.kind, 65 ttl_seconds=req.ttl_seconds, attrs=req.attrs, 66 ) 67 return { 68 "ok": True, "target_id": e.target_id, 69 "kind": e.kind, "expires_at": e.expires_at, 70 } 71 finally: 72 await wl.aclose() 73 74 75@router.delete("/api/watch/{target_id}") 76async def remove_watch(target_id: str, kind: str | None = None) -> dict[str, Any]: 77 from maf.watch.list import WatchList 78 79 wl = WatchList(redis_url=_redis_url()) 80 try: 81 removed = await wl.remove(target_id, kind=kind) 82 return {"ok": True, "removed": removed} 83 finally: 84 await wl.aclose()