二开闭环补齐三块: 数据进得来 (写入)、事件推得出 (SSE)、契约有人守 (快照+CI)。
- write:ext scope (第六档): POST /api/ext-data/{id}/ingest 对 Token 开放,
复用管理端同一校验/落盘/视图刷新路径; 结构配置/上传/拉取仍不开放,
外部只能写行数据, 不能改写表结构
- SSE 短期票据 (V2 收尾): POST /api/events/ticket 换 60s 一次性票据 →
GET /api/events?ticket=… 订阅事件流; 票据只继承 Token scope 不放大权限;
进程内事件总线 (services/events.py, 慢消费者丢旧保新), 告警落盘点已挂
(alert_store.append/append_many → bus), 事件按 scope 过滤双向验证
- 契约守护: test_openapi_contract.py 冻结 tier=a 开放面 (61 端点),
增删须显式更新快照; 新增 ci.yml (后端全量测试 + 前端构建) 把契约
变化拦在 PR 上
- examples/open-api: 四个零依赖可运行示例 (行情/写入/回测/事件流) + README
- 菜单设置架构标注: 每页核心/扩展徽标 + 悬停原因 + 图例 (按 open-platform-plan
§2 依赖方向分类, 核心 8 页/扩展 11 页); 顺带补上 MenuSettings 缺失的 模拟盘
- 测试: 网关 write矩阵/票据语义 + 票据生命周期/总线/SSE帧/告警联动 + 契约快照,
全量 2408 passed / 102s; 线上实测: ingest 200 / 票据一次性 / SSE hello+
alert 推送 / scope 过滤双向 / 契约 61 路径
112 lines
4.0 KiB
Python
112 lines
4.0 KiB
Python
"""Tier A 开放契约快照 — 开放面的任何变化必须是有意识的。
|
|
|
|
契约即承诺: /api/openapi.json?tier=a 列出的端点集合是二开方依赖的稳定面。
|
|
本测试把该集合冻结成常量 — 新增/移除/改方法都会失败, 失败者须同步更新
|
|
快照并在 PR 里说明契约变更 (对外破坏性变更需 major 版本)。
|
|
|
|
快照来源与 /api/openapi.json?tier=a 完全同源: app.openapi() 的 paths
|
|
逐 (method, path) 过 api_gateway.required_scope — 规则表是唯一源。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.services import api_gateway
|
|
|
|
# 冻结的开放面 (method + OpenAPI 路径模板), 按字母序。
|
|
# 变更此表 = 契约变更, 请在 commit message 与 changelog 中明示。
|
|
EXPECTED_OPEN_ENDPOINTS: set[str] = {
|
|
"GET /api/alerts",
|
|
"GET /api/backtest/candidates",
|
|
"POST /api/backtest/factor/batch",
|
|
"GET /api/backtest/factor/columns",
|
|
"POST /api/backtest/factor/run",
|
|
"POST /api/backtest/run",
|
|
"GET /api/backtest/status",
|
|
"POST /api/backtest/strategy/run",
|
|
"GET /api/events",
|
|
"POST /api/events/ticket",
|
|
"GET /api/ext-data",
|
|
"POST /api/ext-data/{config_id}/ingest",
|
|
"GET /api/ext-data/{config_id}/rows",
|
|
"GET /api/ext-data/{config_id}/values",
|
|
"GET /api/ext-data/schema-all",
|
|
"GET /api/ext-data/schema/{config_id}",
|
|
"GET /api/ext-data/{config_id}/dimension-intraday",
|
|
"GET /api/ext-data/{config_id}/dimension-members",
|
|
"GET /api/index/daily",
|
|
"GET /api/index/minute",
|
|
"GET /api/intraday/indices",
|
|
"GET /api/intraday/status",
|
|
"GET /api/kline/daily",
|
|
"GET /api/kline/daily/latest",
|
|
"GET /api/kline/instruments/search",
|
|
"GET /api/kline/minute",
|
|
"GET /api/kline/minute-range",
|
|
"GET /api/overview/market",
|
|
"GET /api/paper/account",
|
|
"POST /api/paper/account",
|
|
"GET /api/paper/accounts",
|
|
"POST /api/paper/auto_rules",
|
|
"DELETE /api/paper/auto_rules/{rule_id}",
|
|
"GET /api/paper/auto_rules",
|
|
"POST /api/paper/auto_rules/{rule_id}/enabled",
|
|
"GET /api/paper/compare",
|
|
"POST /api/paper/freeze",
|
|
"GET /api/paper/nav",
|
|
"POST /api/paper/orders",
|
|
"DELETE /api/paper/orders/{order_id}",
|
|
"GET /api/paper/orders",
|
|
"GET /api/paper/overview",
|
|
"GET /api/paper/positions",
|
|
"POST /api/paper/rebuild",
|
|
"POST /api/paper/settings",
|
|
"GET /api/paper/stats",
|
|
"GET /api/paper/trades",
|
|
"GET /api/regime/coverage",
|
|
"GET /api/regime/history",
|
|
"GET /api/regime/latest",
|
|
"GET /api/regime/mainline",
|
|
"GET /api/regime/phases",
|
|
"GET /api/regime/states",
|
|
"GET /api/screener/cached",
|
|
"GET /api/screener/limit-ladder",
|
|
"GET /api/screener/market-snapshot",
|
|
"POST /api/screener/run",
|
|
"POST /api/screener/run_all",
|
|
"POST /api/screener/run_preset",
|
|
"GET /api/screener/strategies",
|
|
"GET /api/strategies",
|
|
"GET /api/strategies/ai/status",
|
|
"GET /api/strategies/{strategy_id}",
|
|
"GET /api/strategies/{strategy_id}/source",
|
|
}
|
|
|
|
|
|
def _collect_open_endpoints() -> set[str]:
|
|
"""与 /api/openapi.json?tier=a 同源收集 (app.openapi paths 过 required_scope)。"""
|
|
from app.main import app
|
|
|
|
spec = app.openapi()
|
|
found: set[str] = set()
|
|
for path, ops in spec.get("paths", {}).items():
|
|
for method in ops:
|
|
if method in ("get", "post", "put", "delete", "patch") and api_gateway.required_scope(
|
|
method.upper(), path,
|
|
):
|
|
found.add(f"{method.upper()} {path}")
|
|
return found
|
|
|
|
|
|
def test_open_contract_snapshot():
|
|
found = _collect_open_endpoints()
|
|
added = found - EXPECTED_OPEN_ENDPOINTS
|
|
removed = EXPECTED_OPEN_ENDPOINTS - found
|
|
if added or removed:
|
|
pytest.fail(
|
|
"Tier A 开放契约发生变化 — 这是对外承诺面, 须有意识地更新快照:\n"
|
|
f" 新增: {sorted(added) or '无'}\n"
|
|
f" 移除: {sorted(removed) or '无'}\n"
|
|
"确认无误后, 同步更新本文件的 EXPECTED_OPEN_ENDPOINTS 并在 commit 中说明。",
|
|
)
|