Spaces:
Running
Running
| #!/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() | |