172 lines
5.9 KiB
Python
172 lines
5.9 KiB
Python
"""
|
|
FastAPI control interface for the executor.
|
|
|
|
Exposes endpoints for status inspection, configuration changes, execution
|
|
cancellation, and pause/resume. Intended for operator use; not used in the
|
|
normal signal flow.
|
|
"""
|
|
from typing import Optional
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from executor.executor import Executor
|
|
|
|
|
|
class ConfigResponse(BaseModel):
|
|
"""Current executor configuration."""
|
|
live_mode: bool
|
|
concurrent_slots: int
|
|
enforce_same_base_isolation: bool
|
|
socket_path: str
|
|
|
|
|
|
class ConfigPatchRequest(BaseModel):
|
|
"""Request body for configuration update endpoints."""
|
|
live_mode: Optional[bool] = None
|
|
concurrent_slots: Optional[int] = None
|
|
enforce_same_base_isolation: Optional[bool] = None
|
|
|
|
|
|
class ConfigPatchResponse(BaseModel):
|
|
"""Response after a configuration update, echoing the new values."""
|
|
ok: bool
|
|
live_mode: Optional[bool] = None
|
|
concurrent_slots: Optional[int] = None
|
|
enforce_same_base_isolation: Optional[bool] = None
|
|
|
|
|
|
class PauseResponse(BaseModel):
|
|
"""Response after a pause request."""
|
|
ok: bool
|
|
|
|
|
|
class ResumeResponse(BaseModel):
|
|
"""Response after a resume request."""
|
|
ok: bool
|
|
|
|
|
|
class CancelResponse(BaseModel):
|
|
"""Response after a cancellation request."""
|
|
ok: bool
|
|
message: str
|
|
|
|
|
|
class StatusResponse(BaseModel):
|
|
"""Runtime status of the executor."""
|
|
status: str
|
|
version: str
|
|
live_mode: bool
|
|
slots_used: int
|
|
slots_total: int
|
|
uptime_seconds: float
|
|
|
|
|
|
class ShutdownResponse(BaseModel):
|
|
"""Response after a shutdown request."""
|
|
ok: bool
|
|
|
|
|
|
def create_app(executor: Executor, shutdown_callback) -> FastAPI:
|
|
"""
|
|
Build the FastAPI application and wire all routes to the executor.
|
|
|
|
Parameters
|
|
----------
|
|
executor : Executor
|
|
The live Executor instance to control.
|
|
shutdown_callback : callable
|
|
Called when /api/v1/shutdown is hit to begin clean shutdown.
|
|
|
|
Returns
|
|
-------
|
|
FastAPI
|
|
Configured app ready to be passed to uvicorn.
|
|
"""
|
|
app = FastAPI(title="Executor API", description="Control interface for the triangular arbitrage executor")
|
|
|
|
@app.get("/api/v1/status", response_model=StatusResponse)
|
|
async def get_status() -> StatusResponse:
|
|
config = executor.get_config()
|
|
in_flight = executor.get_in_flight()
|
|
return StatusResponse(
|
|
status="ok",
|
|
version="0.1.0",
|
|
live_mode=config["live_mode"],
|
|
slots_used=len(in_flight),
|
|
slots_total=config["concurrent_slots"],
|
|
uptime_seconds=executor.get_uptime_seconds(),
|
|
)
|
|
|
|
@app.get("/api/v1/config", response_model=ConfigResponse)
|
|
async def get_config() -> ConfigResponse:
|
|
cfg = executor.get_config()
|
|
return ConfigResponse(
|
|
paper_mode=cfg["paper_mode"],
|
|
concurrent_slots=cfg["concurrent_slots"],
|
|
enforce_same_base_isolation=cfg["enforce_same_base_isolation"],
|
|
socket_path=cfg["socket_path"],
|
|
)
|
|
|
|
@app.patch("/api/v1/config/live_mode", response_model=ConfigPatchResponse)
|
|
async def patch_live_mode(req: ConfigPatchRequest) -> ConfigPatchResponse:
|
|
if req.live_mode is None:
|
|
raise HTTPException(status_code=400, detail="live_mode required")
|
|
executor._live_mode = req.live_mode
|
|
return ConfigPatchResponse(ok=True, live_mode=req.live_mode)
|
|
|
|
@app.patch("/api/v1/config/concurrent_slots", response_model=ConfigPatchResponse)
|
|
async def patch_concurrent_slots(req: ConfigPatchRequest) -> ConfigPatchResponse:
|
|
if req.concurrent_slots is None or req.concurrent_slots < 1:
|
|
raise HTTPException(status_code=400, detail="concurrent_slots must be >= 1")
|
|
executor.set_concurrent_slots(req.concurrent_slots)
|
|
return ConfigPatchResponse(ok=True, concurrent_slots=req.concurrent_slots)
|
|
|
|
@app.patch("/api/v1/config/enforce_same_base_isolation", response_model=ConfigPatchResponse)
|
|
async def patch_isolation(req: ConfigPatchRequest) -> ConfigPatchResponse:
|
|
if req.enforce_same_base_isolation is None:
|
|
raise HTTPException(status_code=400, detail="enforce_same_base_isolation required")
|
|
executor._settings.enforce_same_base_isolation = req.enforce_same_base_isolation
|
|
return ConfigPatchResponse(ok=True, enforce_same_base_isolation=req.enforce_same_base_isolation)
|
|
|
|
@app.get("/api/v1/executions")
|
|
async def get_executions() -> list[dict]:
|
|
return executor.get_in_flight()
|
|
|
|
@app.get("/api/v1/executions/{correlation_id}")
|
|
async def get_execution(correlation_id: str) -> dict:
|
|
in_flight = executor.get_in_flight()
|
|
for inf in in_flight:
|
|
if inf["correlation_id"] == correlation_id:
|
|
return inf
|
|
raise HTTPException(status_code=404, detail="Execution not found")
|
|
|
|
@app.post("/api/v1/cancel/{correlation_id}", response_model=CancelResponse)
|
|
async def cancel_execution(correlation_id: str) -> CancelResponse:
|
|
ok = await executor.cancel_execution(correlation_id)
|
|
if ok:
|
|
return CancelResponse(ok=True, message=f"Cancellation requested for {correlation_id}")
|
|
raise HTTPException(status_code=404, detail="Execution not found")
|
|
|
|
@app.post("/api/v1/pause", response_model=PauseResponse)
|
|
async def pause() -> PauseResponse:
|
|
await executor.pause()
|
|
return PauseResponse(ok=True)
|
|
|
|
@app.post("/api/v1/resume", response_model=ResumeResponse)
|
|
async def resume() -> ResumeResponse:
|
|
await executor.resume()
|
|
return ResumeResponse(ok=True)
|
|
|
|
@app.post("/api/v1/shutdown", response_model=ShutdownResponse)
|
|
async def shutdown() -> ShutdownResponse:
|
|
await executor.pause()
|
|
shutdown_callback()
|
|
return ShutdownResponse(ok=True)
|
|
|
|
@app.get("/api/v1/reports")
|
|
async def get_reports(limit: int = 50) -> list[dict]:
|
|
return executor.get_reports(limit=limit)
|
|
|
|
return app
|