feat(data): T4 backfill lane ①ann_meta ②news_meta 落地 [nas]
spec §18.7 T4(unit marker 断点+--limit 冒烟不落marker+墙钟rc3 全链复用): - ①ann_meta 回补: unit=(stock,year) 2000→今年逐年按池序, 空年=合法done (探测优先);今年窗口右端=today 相对;~15万 unit @1s≈3-6 跑日 - ②news_meta 回补: np-listapi unit=(stock,page) 翻到空页止;空页+page≥200 双止条件都落 news_end 哨兵(到头股零重扫);mkt前缀 6→1/其余→0 - 共用 _absorb_new(daily/backfill 同一条 id 去重+追加管道) - 测试 46 个;修 wiring 网络泄漏:_wire_backfill_ann 曾漏锁 fetch_news_backfill_page 致 ann 测试真打 np-listapi(40s/真出网, 零网络铁律违反)——补锁后套件 159s→1.3s;全套 263 绿
This commit is contained in:
@@ -532,3 +532,154 @@ def test_main_daily_exit0(root, monkeypatch):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
cd.main()
|
||||
assert e.value.code == 0
|
||||
|
||||
|
||||
# ---------- backfill lane ①ann_meta ②news_meta(T4) ----------
|
||||
|
||||
def _wire_backfill_ann(monkeypatch, rows_by_unit):
|
||||
"""rows_by_unit: {(code, start, end): rows} — 按 unit 窗口回放。
|
||||
必须同时锁死 news 段 fetch(零网络铁律——曾漏锁致真打 np-listapi 40s)。"""
|
||||
monkeypatch.setattr(cd, "load_stock_pool", lambda **kw: POOL)
|
||||
monkeypatch.setattr(cd, "fetch_news_backfill_page", MagicMock(return_value=[]))
|
||||
calls = []
|
||||
|
||||
def fake(client, code, org, start, end, category=""):
|
||||
calls.append((code, start, end))
|
||||
return rows_by_unit.get((code, start, end), [])
|
||||
|
||||
monkeypatch.setattr(cd, "fetch_cninfo_announcements", fake)
|
||||
return calls
|
||||
|
||||
|
||||
def test_backfill_ann_units_walk_years_asc_from_2000(root, monkeypatch):
|
||||
"""unit=(stock,year), 2000→今年逐年, 每年内按池序; 空年=合法 done。"""
|
||||
import pandas as pd
|
||||
calls = _wire_backfill_ann(monkeypatch, {("000001", "2001-01-01", "2001-12-31"):
|
||||
[_ann_raw(aid="b1")]})
|
||||
rc = cd.run_lane("backfill")
|
||||
assert rc == 0
|
||||
t = dt.date.today()
|
||||
n_years = t.year - 2000 + 1
|
||||
assert len(calls) == n_years * 2 # 每股每年 1 req(空窗 1 页即返)
|
||||
assert calls[0] == ("000001", "2000-01-01", "2000-12-31")
|
||||
assert calls[-1][1].startswith(t.isoformat()[:4]) # 今年
|
||||
# 今年窗口右端=今天(相对), 历年=12-31
|
||||
assert calls[-1][2] == t.isoformat()
|
||||
assert cd.is_done("backfill", "ann", "000001_2001")
|
||||
assert cd.is_done("backfill", "ann", "000001_2000") # 空年也 done
|
||||
today = t.isoformat()
|
||||
ann = pd.read_parquet(root / "ann_meta" / f"dt={today}" / "part-0.parquet")
|
||||
assert len(ann) == 1 and ann.iloc[0]["announcement_id"] == "b1"
|
||||
|
||||
|
||||
def test_backfill_ann_marker_skip_on_rerun(root, monkeypatch):
|
||||
"""done unit 零重拉(断点续传实证之 mock 版);数据幂等零重复。"""
|
||||
import pandas as pd
|
||||
_wire_backfill_ann(monkeypatch, {("000001", "2001-01-01", "2001-12-31"):
|
||||
[_ann_raw(aid="b1")]})
|
||||
cd.run_lane("backfill")
|
||||
calls2 = _wire_backfill_ann(monkeypatch, {("000001", "2001-01-01", "2001-12-31"):
|
||||
[_ann_raw(aid="b1")]})
|
||||
cd.run_lane("backfill")
|
||||
assert calls2 == [] # 全部 done → 零请求
|
||||
today = dt.date.today().isoformat()
|
||||
ann = pd.read_parquet(root / "ann_meta" / f"dt={today}" / "part-0.parquet")
|
||||
assert len(ann) == 1
|
||||
|
||||
|
||||
def test_backfill_ann_resume_from_oldest_undone(root, monkeypatch):
|
||||
"""手动标 2000/2001 done → 重跑只碰 2002 起。"""
|
||||
cd.mark_done("backfill", "ann", "000001_2000")
|
||||
cd.mark_done("backfill", "ann", "000001_2001")
|
||||
cd.mark_done("backfill", "ann", "600519_2000")
|
||||
cd.mark_done("backfill", "ann", "600519_2001")
|
||||
calls = _wire_backfill_ann(monkeypatch, {})
|
||||
cd.run_lane("backfill")
|
||||
starts = {c[1] for c in calls}
|
||||
assert "2000-01-01" not in starts and "2001-01-01" not in starts
|
||||
|
||||
|
||||
def test_backfill_ann_unit_fail_not_marked(root, monkeypatch):
|
||||
calls = _wire_backfill_ann(monkeypatch, {})
|
||||
monkeypatch.setattr(cd, "fetch_cninfo_announcements",
|
||||
MagicMock(side_effect=cd.TransportError("boom")))
|
||||
rc = cd.run_lane("backfill")
|
||||
assert rc == 1
|
||||
assert not cd.is_done("backfill", "ann", "000001_2000")
|
||||
|
||||
|
||||
def _wire_news_backfill(monkeypatch, pages):
|
||||
"""pages: {page_idx: rows} — 翻页回放。"""
|
||||
monkeypatch.setattr(cd, "load_stock_pool", lambda **kw: [("600519", "o1")])
|
||||
monkeypatch.setattr(cd, "fetch_cninfo_announcements",
|
||||
MagicMock(return_value=[]))
|
||||
urls = []
|
||||
|
||||
def fake(client, code, mkt, page):
|
||||
urls.append((code, mkt, page))
|
||||
return pages.get(page, [])
|
||||
|
||||
monkeypatch.setattr(cd, "fetch_news_backfill_page", fake)
|
||||
return urls
|
||||
|
||||
|
||||
def test_backfill_news_walks_pages_until_empty(root, monkeypatch):
|
||||
import pandas as pd
|
||||
np_rows = [{"Art_Code": f"a{i}", "Art_ShowTime": f"2024-01-0{i} 00:00:00",
|
||||
"Art_Title": f"t{i}", "Art_Url": f"http://x/a{i}.html",
|
||||
"Np_dst": "CMS"} for i in (1, 2, 3)]
|
||||
urls = _wire_news_backfill(monkeypatch, {1: np_rows[:2], 2: np_rows[2:]})
|
||||
rc = cd.run_lane("backfill")
|
||||
assert rc == 0
|
||||
assert urls[:2] == [("600519", "1", 1), ("600519", "1", 2)]
|
||||
assert urls[2] == ("600519", "1", 3) # 第3页空 → 止
|
||||
assert len(urls) == 3
|
||||
assert cd.is_done("backfill", "news", "600519_p1")
|
||||
assert cd.is_done("backfill", "news_end", "600519") # 空页=股票到头
|
||||
today = dt.date.today().isoformat()
|
||||
news = pd.read_parquet(root / "news_meta" / f"dt={today}" / "part-0.parquet")
|
||||
assert len(news) == 3
|
||||
assert news.iloc[0]["summary"] is None # np 段无摘要如实
|
||||
|
||||
|
||||
def test_backfill_news_end_sentinel_skips_stock(root, monkeypatch):
|
||||
cd.mark_done("backfill", "news_end", "600519")
|
||||
urls = _wire_news_backfill(monkeypatch, {1: ["x"]})
|
||||
cd.run_lane("backfill")
|
||||
assert urls == [] # 到头股零请求
|
||||
|
||||
|
||||
def test_backfill_news_page_cap_200(root, monkeypatch):
|
||||
"""page≥200 止(实测硬顶), 到 200 即算该股完结。"""
|
||||
row = {"Art_Code": "a", "Art_ShowTime": "2020-01-01 00:00:00",
|
||||
"Art_Title": "t", "Art_Url": "http://x/a.html", "Np_dst": "CMS"}
|
||||
urls = _wire_news_backfill(monkeypatch,
|
||||
{p: [dict(row)] for p in range(1, 201)})
|
||||
cd.run_lane("backfill")
|
||||
assert len(urls) == 200
|
||||
assert cd.is_done("backfill", "news_end", "600519")
|
||||
|
||||
|
||||
def test_backfill_news_mkt_prefix_mapping(root, monkeypatch):
|
||||
urls = _wire_news_backfill(monkeypatch, {})
|
||||
cd.run_lane("backfill")
|
||||
assert urls[0][1] == "1" # 600519 → SH=1
|
||||
urls2 = _wire_news_backfill(monkeypatch, {}) # 只换 fetch 窗口
|
||||
monkeypatch.setattr(cd, "load_stock_pool", lambda **kw: [("000001", "o")])
|
||||
cd.run_lane("backfill")
|
||||
assert urls2[0][1] == "0" # 000001 → SZ=0
|
||||
|
||||
|
||||
def test_backfill_limit_smoke_no_markers(root, monkeypatch):
|
||||
_wire_backfill_ann(monkeypatch, {})
|
||||
rc = cd.run_lane("backfill", limit=3)
|
||||
assert rc == 0
|
||||
assert not any((root / "state" / "markers" / "backfill").rglob("*.done"))
|
||||
|
||||
|
||||
def test_backfill_wallclock_rc3(root, monkeypatch):
|
||||
_wire_backfill_ann(monkeypatch, {})
|
||||
past = (dt.datetime.now() - dt.timedelta(minutes=1)).strftime("%H:%M")
|
||||
rc = cd.run_lane("backfill", until=past)
|
||||
assert rc == 3
|
||||
assert cd.is_done("backfill", "ann", "000001_2000") # 首 unit 完成后停
|
||||
|
||||
Reference in New Issue
Block a user