serve 设为只读模式,禁用外部数据拉取
serve 端不再启动 pull_scheduler 和 financial_scheduler, 阻塞 ext_data 和 financials 的外部获取/同步 API 端点, 数据仅通过 local→serve 同步推送。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,6 @@ from app.services.ext_data import (
|
||||
write_ext_parquet,
|
||||
rows_to_parquet,
|
||||
)
|
||||
from app.services.ext_pull import fetch_and_ingest, pull_scheduler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/ext-data", tags=["ext-data"])
|
||||
@@ -330,20 +329,8 @@ async def fetch_preset_data(request: Request, config_id: str):
|
||||
"""手动触发内置预设 (概念/行业) 的数据拉取。
|
||||
|
||||
注意: 必须在 /{config_id}/... 动态路由之前声明, 否则 'presets' 会被当成 config_id。
|
||||
与通用 pull/run 不同: 走 ext_presets 的结构转换 (接口的 concepts/industries
|
||||
数组 → 拼接成字符串), 保证 schema 与现有数据一致。
|
||||
"""
|
||||
from app.services.ext_presets import fetch_preset
|
||||
|
||||
try:
|
||||
n = await fetch_preset(config_id, _data_dir(request))
|
||||
except ValueError as e:
|
||||
raise HTTPException(404, str(e)) from e
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"拉取失败: {e}") from e
|
||||
|
||||
_refresh_views(request)
|
||||
return {"status": "ok", "rows": n}
|
||||
raise HTTPException(403, "serve 端为只读模式,数据仅通过 local→serve 同步")
|
||||
|
||||
|
||||
@router.post("")
|
||||
@@ -548,115 +535,20 @@ def ingest_data(request: Request, config_id: str, body: IngestReq):
|
||||
|
||||
@router.put("/{config_id}/pull")
|
||||
def configure_pull(request: Request, config_id: str, body: PullConfigReq):
|
||||
"""配置(或更新)定时拉取。"""
|
||||
store = _store(request)
|
||||
config = store.get(config_id)
|
||||
if not config:
|
||||
raise HTTPException(404, f"配置 '{config_id}' 不存在")
|
||||
|
||||
# 保留历史状态字段
|
||||
old_pull = config.pull
|
||||
config.pull = PullConfig(
|
||||
url=body.url,
|
||||
method=body.method,
|
||||
headers=body.headers,
|
||||
body=body.body,
|
||||
response_path=body.response_path,
|
||||
field_map=body.field_map,
|
||||
schedule_minutes=body.schedule_minutes,
|
||||
enabled=body.enabled,
|
||||
last_run=old_pull.last_run if old_pull else None,
|
||||
last_status=old_pull.last_status if old_pull else None,
|
||||
last_message=old_pull.last_message if old_pull else None,
|
||||
last_rows=old_pull.last_rows if old_pull else None,
|
||||
)
|
||||
store.upsert(config)
|
||||
|
||||
# 刷新调度器
|
||||
pull_scheduler.refresh(_data_dir(request))
|
||||
|
||||
# 关闭定时拉取时清理残留的 next_run, 避免前端展示一个永不执行的"下次"
|
||||
if not config.pull.enabled:
|
||||
cleared = store.get(config_id)
|
||||
if cleared and cleared.pull and cleared.pull.next_run:
|
||||
cleared.pull.next_run = None
|
||||
store.upsert(cleared)
|
||||
|
||||
return {"status": "ok", "pull": config.pull.to_dict()}
|
||||
"""配置(或更新)定时拉取 — serve 端为只读模式,已禁用。"""
|
||||
raise HTTPException(403, "serve 端为只读模式,数据仅通过 local→serve 同步")
|
||||
|
||||
|
||||
@router.post("/{config_id}/pull/test")
|
||||
async def test_pull(request: Request, config_id: str):
|
||||
"""测试拉取:请求外部 API 并返回预览数据,不写入。"""
|
||||
store = _store(request)
|
||||
config = store.get(config_id)
|
||||
if not config:
|
||||
raise HTTPException(404, f"配置 '{config_id}' 不存在")
|
||||
if not config.pull or not config.pull.url:
|
||||
raise HTTPException(400, "拉取未配置或 URL 为空")
|
||||
|
||||
# 临时构建一个带新配置的 config 用于测试
|
||||
from app.services.ext_pull import _extract_rows, _apply_field_map
|
||||
import httpx
|
||||
|
||||
pull = config.pull
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
headers = pull.headers or {}
|
||||
kwargs: dict = {"headers": headers}
|
||||
if pull.method.upper() == "POST" and pull.body:
|
||||
kwargs["content"] = pull.body
|
||||
if "content-type" not in {k.lower() for k in headers}:
|
||||
kwargs["headers"]["Content-Type"] = "application/json"
|
||||
resp = await client.request(pull.method.upper(), pull.url, **kwargs)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
rows = _extract_rows(data, pull.response_path)
|
||||
preview = _apply_field_map(rows[:5], pull.field_map)
|
||||
return {
|
||||
"status": "ok",
|
||||
"total_rows": len(rows),
|
||||
"preview": preview,
|
||||
"has_symbol": bool(rows and "symbol" in rows[0]),
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"测试失败: {e}") from e
|
||||
"""测试拉取 — serve 端为只读模式,已禁用。"""
|
||||
raise HTTPException(403, "serve 端为只读模式,数据仅通过 local→serve 同步")
|
||||
|
||||
|
||||
@router.post("/{config_id}/pull/run")
|
||||
async def run_pull(request: Request, config_id: str):
|
||||
"""手动触发一次拉取并写入。"""
|
||||
store = _store(request)
|
||||
config = store.get(config_id)
|
||||
if not config:
|
||||
raise HTTPException(404, f"配置 '{config_id}' 不存在")
|
||||
if not config.pull or not config.pull.url:
|
||||
raise HTTPException(400, "拉取未配置或 URL 为空")
|
||||
|
||||
try:
|
||||
n, d = await fetch_and_ingest(config, _data_dir(request))
|
||||
_refresh_views(request)
|
||||
# 写回执行状态, 让前端"上次执行"面板立即反映
|
||||
updated = store.get(config_id)
|
||||
if updated and updated.pull:
|
||||
from datetime import datetime, timezone
|
||||
updated.pull.last_run = datetime.now(timezone.utc).isoformat()
|
||||
updated.pull.last_status = "success"
|
||||
updated.pull.last_message = f"{n} rows @ {d}"
|
||||
updated.pull.last_rows = n
|
||||
store.upsert(updated)
|
||||
return {"status": "ok", "rows": n, "date": d}
|
||||
except Exception as e:
|
||||
# 失败也写回状态, 记录错误信息
|
||||
failed = store.get(config_id)
|
||||
if failed and failed.pull:
|
||||
from datetime import datetime, timezone
|
||||
failed.pull.last_run = datetime.now(timezone.utc).isoformat()
|
||||
failed.pull.last_status = "error"
|
||||
failed.pull.last_message = str(e)[:200]
|
||||
store.upsert(failed)
|
||||
raise HTTPException(400, f"拉取失败: {e}") from e
|
||||
raise HTTPException(403, "serve 端为只读模式,数据仅通过 local→serve 同步")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user