Files
FinDataHub/Scripts/bin/sync_bridge.py
T
FinDataHub 11d36d1c1e fix(repo): 修正 Scripts 目录索引大小写并补齐未入库脚本
问题
- 索引中该目录记为小写 `scripts/`,工作区实为 `Scripts/`。因 core.ignorecase=true,
  本地 `git diff -- Scripts/bin/x.py` 会**静默返回空**(看似无改动),
  且在大小写敏感环境(Linux CI / Docker 挂载)会检出 `scripts/`,
  与全仓所有 `Scripts/...` 引用不匹配。
- 该目录此前 10 个脚本(check_python_version.py、perf_snapshot.py、
  install_collector_launchd.py、mysql_toggle.sh 等)从未入库,
  而 CI 第 31 行正调用 `Scripts/bin/check_python_version.py --strict` ——
  全新检出必然失败。

改动
- 索引路径统一为 `Scripts/`(工作区文件未移动,仅索引改名)。
- 补齐上述 10 个未跟踪脚本及其测试。

影响
- 修正前索引 35 条(全为小写路径),修正后 45 条。
2026-09-18 22:37:32 +08:00

204 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
sync_bridge.py — 统一同步桥接层
统一入口: sync_bridge.py <source> <type> [workers]
source: akshare | tushare
type: daily | weekly | monthly
tushare_fetcher 批量同步策略:
daily → [daily, market_sentiment] — 日线、资金流、融资融券
weekly → [fundamental, static, event] — 财报、基础信息、事件
monthly → [index, extra, stock_driven] — 指数、补充数据、股东
"""
import os, subprocess, sys, traceback
from pathlib import Path
HOME = Path.home()
# 优先使用 App 内部路径,再回退到外部源码
def _find_project(name: str) -> Path:
candidates = []
# 1. App 内部(通过环境变量传入,或从脚本位置推断)
resources_dir = os.environ.get("APP_RESOURCES", "")
if resources_dir:
candidates.append(Path(resources_dir) / name)
# 2. 从脚本位置推断(脚本在 Resources/bin/ 下)
script_dir = Path(__file__).resolve().parent
if script_dir.name == "bin" and script_dir.parent.name == "Resources":
candidates.append(script_dir.parent / name)
# 3. 外部源码
candidates.append(HOME / "Projects/项目" / name)
candidates.append(HOME / "Projects" / name)
candidates.append(HOME / name)
for p in candidates:
if (p / "pyproject.toml").exists():
return p
return candidates[0] # 返回第一个作为默认
PROJ = {
"akshare": _find_project("akshare_fetcher"),
"tushare": _find_project("tushare_fetcher"),
}
# tushare 批量同步 — 每个 sync_type 对应一组 fetch type
TUSHARE_BATCH = {
"daily": ["daily", "market_sentiment"],
"weekly": ["fundamental", "static", "event"],
"monthly": ["index", "extra", "stock_driven"],
}
# 扩展的 fetch type → table list(含 CLI 里没有的 static/event/extra/stock_driven)
TYPE_TABLES = {
"daily": ["daily", "daily_basic", "weekly", "monthly", "adj_factor", "suspend"],
"market_sentiment": ["moneyflow", "margin", "margin_detail", "top_list", "block_trade", "stk_holdernumber", "moneyflow_hsgt"],
"fundamental": ["stock_basic", "income", "balancesheet", "cashflow", "fina_indicator", "fina_audit", "forecast", "express", "dividend"],
"static": ["trade_cal", "index_basic", "index_classify", "stock_company", "bak_basic", "hs_const"],
"event": ["new_share", "daily_info", "sz_daily_info", "fund_nav", "fund_share", "fund_manager"],
"index": ["index_daily", "index_basic", "index_weight", "index_classify", "index_member_all", "index_dailybasic", "index_weekly", "index_monthly"],
"extra": ["yearly", "stk_weekly_monthly", "stk_week_month_adj", "stk_limit", "report_rc", "cn_gdp", "cn_cpi", "cn_ppi", "cn_pmi", "cn_m"],
"stock_driven": ["stk_rewards", "stk_holdernumber", "stk_holdertrade"],
}
def find_python(project: Path) -> str:
for p in [project / ".venv/bin/python3", project / ".venv/bin/python",
Path("/usr/bin/python3")]:
if p.exists():
return str(p)
raise FileNotFoundError(f"找不到 python3 (from {project})")
def load_token(project: Path) -> str:
env_file = project / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
if line.startswith("TUSHARE_TOKEN="):
return line.split("=", 1)[1].strip().strip("'\"")
return os.environ.get("TUSHARE_TOKEN", "")
def _pump_stream(fd, sink):
"""实时转发子进程输出流(块级读取,兼容 tqdm 用 \r 原地刷新的进度行)。"""
while True:
try:
chunk = os.read(fd, 8192)
except OSError:
break
if not chunk:
break
sink.write(chunk)
sink.flush()
def run_akshare(project: Path, sync_type: str, workers: int, python: str) -> int:
import threading
env = os.environ.copy()
env.update({"PYTHONPATH": f"{project}/src", "AKSHARE_HOME": str(project), "PYTHONUNBUFFERED": "1"})
cmd = [python, "-m", "akshare_fetcher.cli", "sync", "--type", sync_type, "--workers", str(workers)]
print(f" {' '.join(cmd)}", flush=True)
# 实时转发 stdout/stderr,不能 capture_output=True:
# 子进程的 tqdm 进度写在 stderr,若被缓冲则 App 端收不到实时进度
proc = subprocess.Popen(
cmd, cwd=str(project), env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
t1 = threading.Thread(target=_pump_stream, args=(proc.stdout.fileno(), sys.stdout.buffer), daemon=True)
t2 = threading.Thread(target=_pump_stream, args=(proc.stderr.fileno(), sys.stderr.buffer), daemon=True)
t1.start(); t2.start()
rc = proc.wait()
t1.join(); t2.join()
return rc
def run_tushare(project: Path, sync_type: str, workers: int, python: str) -> int:
"""tushare 批量同步 — 按策略拉取多组表"""
token = load_token(project)
if not token:
print(" ❌ TUSHARE_TOKEN 未配置", flush=True)
return 1
fetch_types = TUSHARE_BATCH.get(sync_type, ["daily"])
overall_rc = 0
for ft in fetch_types:
tables = TYPE_TABLES.get(ft, [ft])
if not tables:
print(f" ⏭ {ft}: 无表定义", flush=True)
continue
print(f"\n 📡 [{ft}] → {len(tables)} 张表: {', '.join(tables[:5])}{'...' if len(tables) > 5 else ''}", flush=True)
# 直接调用 DataFetcher,避免多次启动 Python 的开销
env = os.environ.copy()
env.update({
"PYTHONPATH": f"{project}/src",
"TUSHARE_HOME": str(project),
"PYTHONUNBUFFERED": "1",
"TUSHARE_TOKEN": token,
})
code = f"""
import sys
sys.path.insert(0, "{project}/src")
from tushare_fetcher.core.config import load_config
from tushare_fetcher.core.fetcher import DataFetcher
cfg = load_config()
fetcher = DataFetcher(cfg, max_workers={workers})
try:
fetcher.run({tables})
except KeyboardInterrupt:
print("\\n⚠ 用户中断")
finally:
fetcher.db.dispose()
"""
cmd = [python, "-c", code]
proc = subprocess.Popen(
cmd, cwd=str(project), env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
import threading
t1 = threading.Thread(target=_pump_stream, args=(proc.stdout.fileno(), sys.stdout.buffer), daemon=True)
t2 = threading.Thread(target=_pump_stream, args=(proc.stderr.fileno(), sys.stderr.buffer), daemon=True)
t1.start(); t2.start()
rc = proc.wait()
t1.join(); t2.join()
if rc != 0:
print(f" ❌ [{ft}] 失败 (rc={rc})", flush=True)
overall_rc = rc
else:
print(f" ✅ [{ft}] 完成", flush=True)
return overall_rc
if __name__ == "__main__":
if len(sys.argv) < 3:
print("用法: sync_bridge.py <source:akshare|tushare> <type:daily|weekly|monthly> [workers]")
sys.exit(2)
source = sys.argv[1]
sync_type = sys.argv[2]
workers = int(sys.argv[3]) if len(sys.argv) > 3 else 4
proj = PROJ.get(source)
if not proj or not proj.exists():
print(f"❌ {source}_fetcher 项目不存在: {proj}")
sys.exit(1)
python = find_python(proj)
print(f" 项目: {proj}", flush=True)
print(f" Python: {python}", flush=True)
if source == "akshare":
rc = run_akshare(proj, sync_type, workers, python)
else:
rc = run_tushare(proj, sync_type, workers, python)
print(f"\n 退出码: {rc}", flush=True)
sys.exit(rc)