Files
xiaobai-review/xiaobai-datahub/datahub/admin_api.py
T
3498dd7a4b feat(HEL-382): 搭建 datahub 底座和盘后正式数据链路
新增独立 xiaobai-datahub 服务(SQLite WAL、Tushare 盘后发布、/v1 契约和管理后台),不改现站页面与数据链路。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-02 12:05:26 +08:00

163 lines
6.7 KiB
Python

from __future__ import annotations
import json
from typing import Any
from datahub.adapters import RESERVED
from datahub.auth import AuthService
from datahub.db import HubDB
from datahub.pipeline import Pipeline
from datahub.scheduler import Scheduler
from datahub.serving import ApiError
from datahub.timeutil import isoformat, now_shanghai, session_phase, yyyymmdd
class AdminAPI:
def __init__(self, db: HubDB, pipeline: Pipeline, scheduler: Scheduler, auth: AuthService) -> None:
self.db = db
self.pipeline = pipeline
self.scheduler = scheduler
self.auth = auth
def overview(self) -> dict[str, Any]:
today = yyyymmdd(now_shanghai())
cal = self.db.fetchone(
"SELECT is_open FROM trade_calendar WHERE exchange = 'SSE' AND cal_date = ?",
(today,),
)
is_open = bool(cal and int(cal["is_open"]) == 1)
pubs = self.db.fetchall("SELECT * FROM publications WHERE trade_date = ?", (today,))
failed = self.db.fetchall(
"SELECT * FROM batches WHERE trade_date = ? AND state IN ('failed','staged')",
(today,),
)
calls = self.db.fetchall(
"SELECT * FROM src_calls ORDER BY id DESC LIMIT 20",
)
return {
"trade_date": today,
"session_phase": session_phase(now_shanghai(), is_open),
"is_open_day": is_open,
"publications": pubs,
"anomalies": failed,
"recent_calls": _public_calls(calls),
"source_count": len(self.db.fetchall("SELECT provider FROM src_health")),
}
def sources(self) -> dict[str, Any]:
health = {f"{row['provider']}:{row['endpoint_class']}": row for row in self.db.fetchall("SELECT * FROM src_health")}
items = [
{
"provider": "tushare",
"role": "official",
"health": health.get("tushare:pro") or {"state": "unknown"},
"credential": self.auth.credential_status("tushare_token") or {"configured": bool(self.pipeline.adapter.token)},
}
]
for name, adapter in RESERVED.items():
items.append(
{
"provider": name,
"role": "reserved",
"health": adapter.probe(),
"credential": {"configured": False, "last4": "", "updated_at": ""},
}
)
# Prefer encrypted last4 if stored
cred = self.auth.credential_status("tushare_token")
if cred.get("configured"):
items[0]["credential"] = cred
elif self.pipeline.adapter.token:
from datahub.crypto import mask_secret
items[0]["credential"] = {"configured": True, "last4": mask_secret(self.pipeline.adapter.token), "updated_at": ""}
return {"items": items}
def probe(self, provider: str) -> dict[str, Any]:
if provider == "tushare":
return self.pipeline.adapter.probe()
adapter = RESERVED.get(provider)
if adapter is None:
raise ApiError("INVALID_ARGUMENT", f"unknown provider: {provider}")
return adapter.probe()
def jobs(self) -> dict[str, Any]:
runs = self.db.fetchall("SELECT * FROM job_runs ORDER BY id DESC LIMIT 100")
return {
"jobs": [
{"id": "precheck", "at": "08:45", "title": "盘前预检"},
{"id": "eod_a", "at": "15:05", "title": "盘后批 A daily/valuation/moneyflow/auction"},
{"id": "eod_b", "at": "15:10", "title": "盘后批 B index_daily"},
{"id": "cleanup", "at": "00:30", "title": "清理 staging / 日志"},
{"id": "backup", "at": "00:40", "title": "SQLite 备份"},
],
"runs": runs,
}
def run_job(self, job_id: str, trade_date: str) -> dict[str, Any]:
return self.scheduler.run_job(job_id, yyyymmdd(trade_date or now_shanghai()))
def batches(self, date: str, dataset: str = "") -> dict[str, Any]:
trade_date = yyyymmdd(date or now_shanghai())
if dataset:
rows = self.db.fetchall(
"SELECT * FROM batches WHERE trade_date = ? AND dataset = ? ORDER BY started_at",
(trade_date, dataset),
)
else:
rows = self.db.fetchall(
"SELECT * FROM batches WHERE trade_date = ? ORDER BY started_at",
(trade_date,),
)
pubs = self.db.fetchall("SELECT * FROM publications WHERE trade_date = ?", (trade_date,))
return {"trade_date": trade_date, "batches": rows, "publications": pubs}
def datasets(self, date: str) -> dict[str, Any]:
trade_date = yyyymmdd(date or now_shanghai())
pubs = self.db.fetchall("SELECT * FROM publications WHERE trade_date = ?", (trade_date,))
diffs = self.db.fetchall(
"SELECT * FROM diff_reports WHERE trade_date = ? ORDER BY id",
(trade_date,),
)
return {"trade_date": trade_date, "publications": pubs, "diff_reports": diffs}
def audit(self) -> dict[str, Any]:
return {"items": self.db.fetchall("SELECT * FROM audit_log ORDER BY id DESC LIMIT 200")}
def rollback(self, dataset: str, trade_date: str, password: str, confirm: str, actor: str) -> dict[str, Any]:
self._dangerous(password, confirm, f"{dataset}:{trade_date}")
result = self.pipeline.rollback(dataset, trade_date, actor=actor)
return result
def backfill(self, dataset: str, trade_date: str, password: str, confirm: str, actor: str) -> dict[str, Any]:
self._dangerous(password, confirm, f"{dataset}:{trade_date}")
if dataset == "reference":
result = self.pipeline.ingest_reference(trade_date)
else:
result = self.pipeline.run_dataset(dataset, trade_date)
self.pipeline.audit(actor, "backfill", f"{dataset}:{trade_date}", json.dumps({"ok": True}))
return result
def _dangerous(self, password: str, confirm: str, expected: str) -> None:
if not self.auth.confirm_password(password):
raise ApiError("UNAUTHORIZED", "二次确认密码错误")
if confirm.strip() != expected:
raise ApiError("INVALID_ARGUMENT", f"确认词必须为 {expected}")
def _public_calls(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
out = []
for row in rows:
out.append(
{
"id": row["id"],
"provider": row["provider"],
"endpoint": row["endpoint"],
"ok": bool(row["ok"]),
"latency_ms": row["latency_ms"],
"error": row["error"],
"created_at": row["created_at"],
}
)
return out