"""WebSocket-поток метрик: GET /api/v1/metrics/stream.""" from __future__ import annotations import asyncio from fastapi import APIRouter, WebSocket, WebSocketDisconnect from awg_api.deps import SESSION_COOKIE_NAME, token_matches from awg_api.state import MockState router = APIRouter(tags=["metrics"]) WS_ACCESS_DENIED = 4401 MIN_INTERVAL_MS = 250 def _authorized(websocket: WebSocket, token: str | None) -> bool: expected: str = websocket.app.state.api_token header = websocket.headers.get("authorization", "") scheme, _, credentials = header.partition(" ") if scheme.lower() == "bearer" and token_matches(credentials.strip(), expected): return True if token_matches(token, expected): return True session_id = websocket.cookies.get(SESSION_COOKIE_NAME) state: MockState = websocket.app.state.mock_state return token_matches(session_id, state.session_token) @router.websocket("/api/v1/metrics/stream") async def stream_metrics( websocket: WebSocket, token: str | None = None, interval_ms: int = 1000 ) -> None: """Отправляет кадры ``WsFrame``: метрики до отключения клиента.""" if not _authorized(websocket, token): await websocket.close(code=WS_ACCESS_DENIED) return state: MockState = websocket.app.state.mock_state await websocket.accept() interval_s = max(MIN_INTERVAL_MS, interval_ms) / 1000 try: while True: await websocket.send_json(state.metrics_frame()) await asyncio.sleep(interval_s) except (WebSocketDisconnect, RuntimeError): return