#!/usr/bin/env python3 """ patch_websocket_hub.py ────────────────────── Injects TradeLogParser (from hub_dashboard_service.py) and four API routes into /app/websocket_hub.py so that port 7860 serves: GET /api/trades → full open + closed state + stats GET /api/trades/open → open trades only GET /api/trades/closed → recent closed trades + stats (?limit=N, default 50) GET /api/health → service health including trade counts Usage: python3 patch_websocket_hub.py [--target /app/websocket_hub.py] [--dry-run] """ import argparse import shutil import sys from datetime import datetime from pathlib import Path # ── Snippet 1: Parser instantiation block ───────────────────────────────────── # Inserted BEFORE `_START_TIME = time.time()` PARSER_BLOCK = ''' # ── Trade log parser — injected so /api/trades is served on port 7860 ──────── import sys as _sys, os as _os _sys.path.insert(0, '/app') from hub_dashboard_service import TradeLogParser as _TradeLogParser _trade_parser = _TradeLogParser(log_dir=_os.environ.get("RANKER_LOG_DIR", "/app/ranker_logs")) _trade_parser.start_background() ''' # ── Snippet 2: FastAPI route definitions ────────────────────────────────────── # Appended AFTER `_START_TIME = time.time()` TRADE_ROUTES = ''' # ── /api/trades routes — injected by patch_websocket_hub.py ────────────────── @app.get("/api/trades") async def api_trades(): """Full trade state: open trades, recent closed trades, summary stats.""" return JSONResponse(_trade_parser.get_state()) @app.get("/api/trades/open") async def api_trades_open(): """Open trades only.""" state = _trade_parser.get_state() return JSONResponse({"open": state["open"]}) @app.get("/api/trades/closed") async def api_trades_closed(limit: int = 50): """Recent closed trades (newest first) + cumulative stats.""" state = _trade_parser.get_state() return JSONResponse({ "closed": state["closed"][:limit], "stats": state["stats"], }) @app.get("/api/health") async def api_health(): """Service health check — includes live trade counts and log-file inventory.""" import glob as _glob return JSONResponse({ "service": "websocket_hub", "version": "v2.0", "status": "running", "log_files": len(_glob.glob("/app/ranker_logs/*.log")), "trade_open": len(_trade_parser.get_state()["open"]), "trade_closed": len(_trade_parser.get_state()["closed"]), }) ''' ANCHOR = "_START_TIME = time.time()" def patch(source: str) -> str: """Apply both substitutions and return the patched source.""" if ANCHOR not in source: raise ValueError( f"Anchor '{ANCHOR}' not found in source — " "is this the right file?" ) # Count occurrences so we can give a clear warning if there are multiples. count = source.count(ANCHOR) if count > 1: print( f"[WARNING] Anchor appears {count} times; " "only the first occurrence will be modified.", file=sys.stderr, ) # ── Step 1: prepend the parser block before the anchor ──────────────────── src = source.replace(ANCHOR, PARSER_BLOCK + ANCHOR, 1) # ── Step 2: append trade routes after the (now-unique) anchor ──────────── # After step 1, ANCHOR still appears exactly once (at the end of the # inserted block), so a second replace(…, 1) is safe. src = src.replace(ANCHOR, ANCHOR + TRADE_ROUTES, 1) return src def main() -> None: ap = argparse.ArgumentParser(description="Patch websocket_hub.py with trade routes.") ap.add_argument( "--target", default="/app/websocket_hub.py", help="Path to websocket_hub.py (default: /app/websocket_hub.py)", ) ap.add_argument( "--dry-run", action="store_true", help="Print the patched source to stdout instead of writing to disk.", ) args = ap.parse_args() target = Path(args.target) if not target.exists(): print(f"[ERROR] Target file not found: {target}", file=sys.stderr) sys.exit(1) original = target.read_text(encoding="utf-8") # Guard: don't double-patch if "_trade_parser" in original: print( "[SKIP] Patch already applied (_trade_parser already present in file).", file=sys.stderr, ) sys.exit(0) patched = patch(original) if args.dry_run: print(patched) return # ── Backup the original ─────────────────────────────────────────────────── ts = datetime.now().strftime("%Y%m%d_%H%M%S") backup = target.with_suffix(f".bak_{ts}.py") shutil.copy2(target, backup) print(f"[INFO] Backup written → {backup}") target.write_text(patched, encoding="utf-8") print(f"[OK] Patch applied → {target}") print(f" Routes added: /api/trades /api/trades/open /api/trades/closed /api/health") if __name__ == "__main__": main()