"""HTTP REST API (FastAPI).""" from __future__ import annotations from typing import Any import structlog from fastapi import Body, FastAPI, HTTPException from pydantic import BaseModel, TypeAdapter from .config import Config from .dispatch import CommandDispatcher from .layouts import PREDEFINED_LAYOUTS from .overlays import Overlay from .state import ControllerState log = structlog.get_logger() class LayoutSetReq(BaseModel): layout: str def create_app( cfg: Config, state: ControllerState, dispatcher: CommandDispatcher ) -> FastAPI: app = FastAPI( title="cuda-grid-controller", version="0.1.0", description="Control plane для vf_cuda_grid FFmpeg filter", ) def _check_instance(name: str): inst = next((i for i in cfg.instances if i.name == name), None) if inst is None: raise HTTPException(404, f"unknown instance '{name}'") return inst @app.get("/health") async def health() -> dict[str, Any]: return {"status": "ok"} @app.get("/layouts") async def layouts() -> dict[str, Any]: return {"predefined": PREDEFINED_LAYOUTS} @app.get("/state") async def get_state() -> dict[str, Any]: out = {} for inst in cfg.instances: overlays = await state.get_overlays(inst.name) out[inst.name] = { "active_layout": await state.get_layout(inst.name), "zmq_endpoint": inst.zmq_endpoint, "overlays_count": len(overlays), } return {"instances": out} @app.post("/layout/{instance}/set") async def set_layout(instance: str, req: LayoutSetReq) -> dict[str, Any]: _check_instance(instance) if req.layout not in PREDEFINED_LAYOUTS: raise HTTPException( 400, f"unknown layout '{req.layout}'. Доступны: {PREDEFINED_LAYOUTS}" ) await dispatcher.handle(instance, "layout.set", req.layout) return {"ok": True, "instance": instance, "layout": req.layout} # ─── Overlays ────────────────────────────────────────────────── @app.post("/overlay/{instance}/add") async def overlay_add( instance: str, overlay: Overlay = Body(...) ) -> dict[str, Any]: _check_instance(instance) await dispatcher.handle( instance, "overlay.add", overlay.model_dump_json() ) return {"ok": True, "id": overlay.id, "type": overlay.type} @app.get("/overlay/{instance}") async def overlay_list(instance: str) -> dict[str, Any]: _check_instance(instance) overlays = await state.get_overlays(instance) return { "instance": instance, "count": len(overlays), "overlays": [o.model_dump() for o in overlays], } @app.delete("/overlay/{instance}/{overlay_id}") async def overlay_remove(instance: str, overlay_id: str) -> dict[str, Any]: _check_instance(instance) await dispatcher.handle(instance, "overlay.remove", overlay_id) return {"ok": True} @app.delete("/overlay/{instance}") async def overlay_clear(instance: str) -> dict[str, Any]: _check_instance(instance) await dispatcher.handle(instance, "overlay.clear", "") return {"ok": True} @app.patch("/overlay/{instance}/{overlay_id}") async def overlay_update( instance: str, overlay_id: str, overlay: Overlay = Body(...) ) -> dict[str, Any]: _check_instance(instance) # Фиксируем id из URL — игнорируем body's id если отличается overlay.id = overlay_id await state.update_overlay(instance, overlay) await dispatcher.handle( instance, "overlay.add", overlay.model_dump_json() ) return {"ok": True, "id": overlay_id} return app