from __future__ import annotations import json from collections.abc import Iterator from typing import Annotated, Literal from fastapi import APIRouter, Query, Request from fastapi.responses import StreamingResponse from backend.data.gateway import MarketDataUnavailable from backend.features.accounts.auth import SmartAccessPrincipal, SmartWritePrincipal from backend.features.heaven.schemas import ( DeleteResponse, FortuneInput, HeartCompleteInput, HeartLineInput, InterpretInput, TrendInput, ) from backend.features.heaven.service import HeavenError from backend.http.errors import AppError from backend.llm.gateway import LLMGatewayError router = APIRouter(prefix="/heaven", tags=["heaven"]) @router.get("/setup", response_model=dict) def setup( request: Request, principal: SmartAccessPrincipal, reading_date: Annotated[str, Query(alias="date", min_length=10, max_length=10)], ) -> dict: return _call(request, "setup", principal, reading_date) @router.post("/trend/load", response_model=dict) def load_trend(payload: TrendInput, request: Request, principal: SmartWritePrincipal) -> dict: return _call(request, "trend", principal, payload.query, payload.trade_date, payload.manual) @router.post("/fortune", response_model=dict) def create_fortune(payload: FortuneInput, request: Request, principal: SmartWritePrincipal) -> dict: return _call(request, "fortune", principal, payload.trade_date) @router.post("/heart/line", response_model=dict) def heart_line(payload: HeartLineInput, request: Request, principal: SmartWritePrincipal) -> dict: return _call(request, "heart_line", principal, payload.trade_date, payload.values) @router.post("/heart/complete", response_model=dict) def complete_heart( payload: HeartCompleteInput, request: Request, principal: SmartWritePrincipal ) -> dict: return _call( request, "complete_heart", principal, payload.trade_date, payload.values, payload.first_thought_confirmed, ) @router.get("/readings", response_model=list[dict]) def readings( request: Request, principal: SmartAccessPrincipal, mode: Annotated[Literal["trend", "fortune", "heart"] | None, Query()] = None, reading_date: Annotated[str | None, Query(alias="date")] = None, ) -> list[dict]: return _call(request, "readings", principal, mode, reading_date) @router.delete("/readings/{reading_id}", response_model=DeleteResponse) def delete_reading( reading_id: int, request: Request, principal: SmartWritePrincipal ) -> DeleteResponse: return DeleteResponse(deleted=_call(request, "delete", principal, reading_id)) @router.post("/interpret") def interpret( payload: InterpretInput, request: Request, principal: SmartWritePrincipal ) -> StreamingResponse: prepared = _call(request, "prepare_interpret", principal, payload.reading_id) def body() -> Iterator[bytes]: for event in request.app.state.container.heaven.stream_interpret(prepared): yield (json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n").encode( "utf-8" ) return StreamingResponse( body(), media_type="application/x-ndjson", headers={"Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no"}, ) def _call(request: Request, method: str, *args): try: return getattr(request.app.state.container.heaven, method)(*args) except HeavenError as exc: raise AppError("heaven_unavailable", str(exc), 409) from exc except MarketDataUnavailable as exc: raise AppError("market_data_unavailable", str(exc), 503) from exc except LLMGatewayError as exc: status = 403 if exc.code in {"membership_required", "quota_exhausted"} else 503 raise AppError(exc.code, str(exc), status) from exc