Files
sanguo_vnpy_v2/sanguo_api/main.py
T
claude_dev 3ef8c617f0 feat(deploy): VPS原生大脑部署—env覆盖配置+部署文档(Option B落地)
- sanguo_data/config.py: load_config 加 env 覆盖(SANGUO_DATA_ROOT 重映射
  daily/raw/qfq/15min/vnpy_db;SANGUO_LIVE_ENABLED/SANGUO_BRIDGE_URL 覆盖 live 段),
  三机共用一份 git config 零漂移
- sanguo_api/main.py: build_app 加 SANGUO_DB_PATH/SANGUO_FILE_PATH env 覆盖
- docs/deployment/vps-native-brain.md: VPS原生部署全记录(安装/配置/服务/验证/
  数据staging/Phase2暂缓理由)

实证:VPS vnpy4.4.0+vnpy_qmt0.3.3 连真实miniQMT读真实账户/持仓;
live_step全链路+影子transport(本机bridge)通过。详见文档。
2026-07-15 12:55:58 +08:00

104 lines
3.9 KiB
Python

"""Container uvicorn entrypoint.
Reads ``config/backtest.yaml`` to build the FastAPI app (db paths + auth +
workers) and exposes a module-level ``app`` for ``uvicorn sanguo_api.main:app``.
Optionally mounts the built Vue SPA at ``/`` (history fallback).
Task S0.1. Imported by uvicorn (sanguo_api.main:app) and tests/api/test_main.py.
"""
from __future__ import annotations
import os
from pathlib import Path
import yaml
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.responses import FileResponse
from .app import create_app as _create_app
def _load_config(config_path: str) -> dict:
p = Path(config_path)
if not p.exists():
return {}
with open(p, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def build_app(config_path: str = "config/backtest.yaml", static_dir: str | None = None) -> FastAPI:
"""Build the FastAPI app from a backtest.yaml config file.
Args:
config_path: Path to backtest.yaml (db_path/file_dir/auth/pool).
static_dir: Optional directory of built Vue SPA files; mounted at ``/``.
Returns:
A configured FastAPI application with /api/v1 routes and optional SPA.
"""
cfg = _load_config(config_path)
bt = cfg.get("backtest", {})
auth = cfg.get("auth", {})
pool = cfg.get("pool", {})
db_path = os.environ.get("SANGUO_DB_PATH") or bt.get("db_path", "/tmp/backtest_results.db")
file_dir = os.environ.get("SANGUO_FILE_DIR") or bt.get("file_dir", "/tmp/backtest_files")
auth_config = None
if auth:
auth_config = {
"username": auth.get("username", "admin"),
"password_hash": auth.get("password_hash", ""),
"jwt_secret": auth.get("jwt_secret", "change-me"),
"expire_minutes": auth.get("token_expire_minutes", 60),
}
max_workers = pool.get("max_workers", bt.get("max_workers", 2))
app = _create_app(
db_path=db_path,
file_dir=file_dir,
auth_config=auth_config,
max_workers=max_workers,
)
# SPA static mount with history fallback. Register the root route BEFORE
# mounting StaticFiles at "/" so the explicit handler wins for "/".
if static_dir and os.path.isdir(static_dir):
index_html = os.path.join(static_dir, "index.html")
@app.get("/", include_in_schema=False)
async def _spa_root() -> FileResponse: # noqa: D401
return FileResponse(index_html)
app.mount("/", StaticFiles(directory=static_dir, html=True), name="spa")
# SPA history fallback: deep links like /backtest/result/:id hit StaticFiles
# (no such file) → 404. For browser navigation (Accept: text/html) serve
# index.html so vue-router can take over; API 404s (Accept: application/json)
# still return JSON. This makes refresh/bookmark of any SPA route work.
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse
@app.exception_handler(StarletteHTTPException)
async def _spa_fallback(request, exc): # noqa: ANN001
if exc.status_code == 404 and "text/html" in request.headers.get("accept", ""):
return FileResponse(index_html)
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
return app
def create_app() -> FastAPI:
"""No-arg factory for ``uvicorn sanguo_api.main:create_app --factory``.
Building the app here (rather than at module import) avoids constructing
the Orchestrator's ProcessPoolExecutor at import time, which would recurse
under the ``spawn`` start method. uvicorn calls this factory once at server
startup in the main process.
"""
repo = Path(__file__).resolve().parent.parent
return build_app(
str(repo / "config" / "backtest.yaml"),
static_dir=os.environ.get("SPA_STATIC_DIR", str(repo / "frontend" / "dist")),
)