Skip to content

Commit 6dea9d6

Browse files
committed
추가: allFilings HF dataset 업/다운로드 파이프라인 — pushAllFilings + lazy _ensureFromHf + 일일 cron 업로드.
직전 PR 까지 allFilings 가 로컬 누적만 가능, HF dataset 동기화 부재. 본 PR 로 sectionsStorage / fieldIndexRebuild 와 동일 패턴 HF 업/다운로드 박음. 본 변경: - pushAllFilings(periods=None, *, token=None) — 일자 list 받아 huggingface_hub.HfApi upload_file 로 {HF_REPO}:dart/allFilings/{period}.parquet 경로에 업로드. periods=None 이면 로컬 전체. HF_TOKEN env 부재 시 즉시 0 반환 (graceful skip). - _ensureFromHf(period=None) — 로컬 부재 시 huggingface_hub.snapshot_download 로 lazy 다운로드. period 지정 시 단일 파일, None 이면 디렉토리 전체. DARTLAB_NO_HF_DOWNLOAD=1 env 로 offline 강제 skip + _HF_DOWNLOAD_ATTEMPTED set 으로 한 키 1 회만 시도 (실패 retry 회피). - loadDay / loadAll wire — 로컬 부재 시 _ensureFromHf 자동 호출 → 사용자가 별도 prebuild 없이 첫 호출 시 자동 다운로드. - .github/scripts/search/buildSearchDelta.py Phase 2.5 추가 — fillContent 직후 lookback 기간 (기본 30일) parquet 일괄 HF 업로드. HF_TOKEN 부재 시 skip. 설계 결정: - 분리 디렉토리 X — sectionsStorage 동일 패턴. {HF_REPO}:dart/allFilings/{YYYYMMDD}.parquet 단일 경로. - lookback 기간만 업로드 — 옛 immutable parquet 매일 재업로드 비용 회피. 일자당 ~6 MB × 30일 = 180 MB upload (HF Hub 의 diff-based commit 으로 변경 없는 파일 skip). - snapshot_download local_dir=dataDir — 옛 sectionsStorage 동일. 회귀 가드 5 종 신규: - test_push_all_filings_callable / test_ensure_from_hf_callable — import + callable smoke - test_push_all_filings_no_token — HF_TOKEN 부재 시 0 반환 (외부 호출 차단) - test_ensure_from_hf_env_skip — DARTLAB_NO_HF_DOWNLOAD=1 즉시 False - test_ensure_from_hf_local_exists_short_circuit — 로컬 .parquet 있으면 snapshot_download 호출 0 실측 검증: - 27일 재수집 (직전 백필 누락분): 390 rows - HF push (5/26 5.9 MB): 1.1초, eddmpython/dartlab-data:dart/allFilings/20260526.parquet 박힘 - Lazy download (5/26 로컬 삭제 → loadDay): 1.29초, 5.9 MB 복원 + schema 검증 (content_raw + fetch_status 포함) - 22 unit test 통과 (test-lock wrapper)
1 parent 3b3b56e commit 6dea9d6

3 files changed

Lines changed: 207 additions & 7 deletions

File tree

.github/scripts/search/buildSearchDelta.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
33
매일 실행:
44
1. 최근 N일 allFilings 수집 (collectMeta + fillContent)
5-
2. content delta 세그먼트 빌드 (rebuildContentDelta)
6-
3. HF `eddmpython/dartlab-data` 에 `dart/contentIndex/delta.*` 업로드
5+
2. allFilings parquet HF 업로드 (lookback 기간 — 신규/정정/error retry 모두 반영)
6+
3. content delta 세그먼트 빌드 (rebuildContentDelta)
7+
4. HF `eddmpython/dartlab-data` 에 `dart/contentIndex/delta.*` 업로드
78
89
main 풀리빌드는 별도 워크플로우 (월 1회).
910
@@ -49,6 +50,23 @@ def main() -> int:
4950
fillContent()
5051
print(f" content 채우기 완료, {time.perf_counter() - t0:.0f}초")
5152

53+
# Phase 2.5: allFilings parquet HF 업로드 (lookback 기간 신규/정정/retry 반영)
54+
if hfToken:
55+
print("[delta] Phase 2.5: allFilings HF 업로드")
56+
from datetime import datetime as _dt
57+
from datetime import timedelta as _td
58+
59+
from dartlab.providers.dart.openapi.allFilingsCollector import pushAllFilings
60+
61+
# lookback 기간의 일자만 — 옛 immutable parquet 재업로드 비용 회피.
62+
_today = _dt.now()
63+
_lookbackDates = [(_today - _td(days=i)).strftime("%Y%m%d") for i in range(lookback)]
64+
t0 = time.perf_counter()
65+
nUp = pushAllFilings(_lookbackDates, token=hfToken)
66+
print(f" allFilings 업로드: {nUp} 파일, {time.perf_counter() - t0:.0f}초")
67+
else:
68+
print("[delta] Phase 2.5: HF_TOKEN 없음 — allFilings 업로드 skip")
69+
5270
# Phase 3: delta 인덱스 빌드
5371
print("[delta] Phase 3: content delta 세그먼트 빌드")
5472
t0 = time.perf_counter()

src/dartlab/providers/dart/openapi/allFilingsCollector.py

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -562,30 +562,154 @@ def pendingDates() -> list[str]:
562562
return dates
563563

564564

565+
# ═══════════════════════════════════════════
566+
# HF 동기화 (push / lazy pull)
567+
# ═══════════════════════════════════════════
568+
569+
# 다운로드 1 회 시도 가드 (period 또는 "_ALL_") — 실패 시 무한 retry 회피.
570+
_HF_DOWNLOAD_ATTEMPTED: set[str] = set()
571+
572+
573+
def pushAllFilings(periods: list[str] | None = None, *, token: str | None = None) -> int:
574+
"""allFilings parquet 을 HF dataset 에 업로드.
575+
576+
sectionsStorage / fieldIndexRebuild 의 push 패턴과 동일 — `huggingface_hub.HfApi`
577+
`upload_file` 로 일자별 `.parquet` 을 `{HF_REPO}:dart/allFilings/{period}.parquet`
578+
경로에 저장. 옛 파일 같은 경로면 자동 덮어쓰기 (HF Hub 의 commit-based 업로드).
579+
580+
Args:
581+
periods: 업로드 일자 list (YYYYMMDD). None 이면 로컬 `data/dart/allFilings/`
582+
에 있는 모든 `.parquet` (정기 `*_meta.parquet` 제외).
583+
token: HF token. None 이면 env `HF_TOKEN`.
584+
585+
Returns:
586+
int — 업로드 성공 파일 수.
587+
588+
Raises:
589+
없음 — HF 호출 실패는 warning 로그 후 다음 파일 진행.
590+
591+
Example:
592+
>>> pushAllFilings(["20260527", "20260528"], token=os.environ["HF_TOKEN"]) # doctest: +SKIP
593+
"""
594+
import os as _os
595+
596+
hfToken = token or _os.environ.get("HF_TOKEN", "")
597+
if not hfToken:
598+
_log.warning("[HF↑] HF_TOKEN 없음 — 업로드 skip")
599+
return 0
600+
601+
outDir = _allFilingsDir()
602+
if periods is None:
603+
files = sorted(f for f in outDir.glob("*.parquet") if _META_SUFFIX not in f.stem)
604+
else:
605+
files = [outDir / f"{p}.parquet" for p in periods]
606+
files = [f for f in files if f.exists()]
607+
608+
if not files:
609+
_log.info("[HF↑] 업로드 대상 0")
610+
return 0
611+
612+
from huggingface_hub import HfApi
613+
614+
from dartlab.core.dataConfig import HF_REPO
615+
616+
relDir = DATA_RELEASES[_ALLFILINGS_DIR_KEY]["dir"]
617+
api = HfApi(token=hfToken)
618+
ok = 0
619+
for f in files:
620+
dst = f"{relDir}/{f.name}"
621+
try:
622+
api.upload_file(
623+
path_or_fileobj=str(f),
624+
path_in_repo=dst,
625+
repo_id=HF_REPO,
626+
repo_type="dataset",
627+
)
628+
ok += 1
629+
_log.info("[HF↑] %s (%.1f MB)", dst, f.stat().st_size / 1024 / 1024)
630+
except Exception as exc: # noqa: BLE001
631+
_log.warning("[HF↑] %s 실패: %s", dst, exc)
632+
_log.info("[HF↑] 완료: %d/%d 파일", ok, len(files))
633+
return ok
634+
635+
636+
def _ensureFromHf(period: str | None = None) -> bool:
637+
"""artifact 부재 시 HF dataset 에서 lazy 다운로드.
638+
639+
sectionsStorage `_ensureFromHf` 동일 패턴 — `huggingface_hub.snapshot_download`
640+
로 `{HF_REPO}:dart/allFilings/` 의 parquet 받음.
641+
642+
Args:
643+
period: 특정 일자만 (YYYYMMDD) 받기. None 이면 디렉토리 전체.
644+
645+
Returns:
646+
bool — 다운로드 성공 (또는 이미 로컬에 있음).
647+
648+
Raises:
649+
없음 — 네트워크 / 인증 / 부재 실패는 warning 로그 후 False.
650+
651+
환경변수 `DARTLAB_NO_HF_DOWNLOAD=1` 시 즉시 skip. 한 (period or "_ALL_") 1 회만 시도.
652+
"""
653+
import os as _os
654+
655+
outDir = _allFilingsDir()
656+
if period is not None:
657+
if (outDir / f"{period}.parquet").exists():
658+
return True
659+
660+
if _os.environ.get("DARTLAB_NO_HF_DOWNLOAD", "").strip() in ("1", "true", "True"):
661+
return False
662+
663+
key = period or "_ALL_"
664+
if key in _HF_DOWNLOAD_ATTEMPTED:
665+
return False
666+
_HF_DOWNLOAD_ATTEMPTED.add(key)
667+
668+
try:
669+
from huggingface_hub import snapshot_download
670+
671+
from dartlab.core.dataConfig import HF_REPO
672+
673+
relDir = DATA_RELEASES[_ALLFILINGS_DIR_KEY]["dir"]
674+
pattern = f"{relDir}/{period}.parquet" if period else f"{relDir}/*.parquet"
675+
snapshot_download(
676+
repo_id=HF_REPO,
677+
repo_type="dataset",
678+
allow_patterns=[pattern],
679+
local_dir=str(Path(_cfg.dataDir)),
680+
)
681+
return True
682+
except Exception as exc: # noqa: BLE001
683+
_log.warning("[HF↓] allFilings 다운로드 실패 (%s): %s", period or "ALL", exc)
684+
return False
685+
686+
565687
def loadDay(period: str) -> pl.DataFrame | None:
566-
"""수집된 하루치 데이터 로드.
688+
"""수집된 하루치 데이터 로드. 로컬 부재 시 HF 에서 lazy 다운로드.
567689
568690
Args:
569-
period: 인자.
691+
period: YYYYMMDD.
570692
571693
Raises:
572694
없음.
573695
574696
Example:
575-
>>> loadDay(...)
697+
>>> loadDay("20260527") # doctest: +SKIP
576698
577699
Returns:
578700
pl.DataFrame 또는 None — 수집 결과.
579701
"""
580702
path = _allFilingsDir() / f"{period}.parquet"
703+
if not path.exists():
704+
_ensureFromHf(period)
581705
if not path.exists():
582706
return None
583707
return pl.read_parquet(path)
584708

585709

586710
@withMemoryBudget(limitMb=500)
587711
def loadAll() -> pl.DataFrame:
588-
"""원문 수집 완료된 전체 데이터 로드.
712+
"""원문 수집 완료된 전체 데이터 로드. 로컬 디렉토리 비어있으면 HF 에서 lazy 다운로드.
589713
590714
Args:
591715
(인자 자동 생성).
@@ -594,13 +718,16 @@ def loadAll() -> pl.DataFrame:
594718
없음.
595719
596720
Example:
597-
>>> loadAll(...)
721+
>>> loadAll() # doctest: +SKIP
598722
599723
Returns:
600724
pl.DataFrame — 결과.
601725
"""
602726
outDir = _allFilingsDir()
603727
files = sorted(f for f in outDir.glob("*.parquet") if _META_SUFFIX not in f.stem)
728+
if not files:
729+
_ensureFromHf()
730+
files = sorted(f for f in outDir.glob("*.parquet") if _META_SUFFIX not in f.stem)
604731
if not files:
605732
return pl.DataFrame()
606733
return pl.scan_parquet(files).collect(engine="streaming")

tests/providers/dart/openapi/test_allFilingsCollector.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,61 @@ def test_stats_callable() -> None:
7676
assert callable(stats)
7777

7878

79+
def test_push_all_filings_callable() -> None:
80+
"""pushAllFilings() callable smoke."""
81+
from dartlab.providers.dart.openapi.allFilingsCollector import pushAllFilings
82+
83+
assert callable(pushAllFilings)
84+
85+
86+
def test_ensure_from_hf_callable() -> None:
87+
"""_ensureFromHf() callable smoke."""
88+
from dartlab.providers.dart.openapi.allFilingsCollector import _ensureFromHf
89+
90+
assert callable(_ensureFromHf)
91+
92+
93+
def test_push_all_filings_no_token(monkeypatch, tmp_path) -> None:
94+
"""HF_TOKEN 없으면 즉시 0 반환 — 외부 호출 없음."""
95+
import dartlab.config as _cfg
96+
from dartlab.providers.dart.openapi import allFilingsCollector as mod
97+
98+
monkeypatch.setattr(_cfg, "dataDir", str(tmp_path))
99+
monkeypatch.delenv("HF_TOKEN", raising=False)
100+
n = mod.pushAllFilings()
101+
assert n == 0
102+
103+
104+
def test_ensure_from_hf_env_skip(monkeypatch) -> None:
105+
"""DARTLAB_NO_HF_DOWNLOAD=1 환경에서 즉시 False — 외부 호출 없음."""
106+
from dartlab.providers.dart.openapi import allFilingsCollector as mod
107+
108+
monkeypatch.setenv("DARTLAB_NO_HF_DOWNLOAD", "1")
109+
mod._HF_DOWNLOAD_ATTEMPTED.clear()
110+
result = mod._ensureFromHf("20990101")
111+
assert result is False
112+
113+
114+
def test_ensure_from_hf_local_exists_short_circuit(monkeypatch, tmp_path) -> None:
115+
"""로컬에 이미 .parquet 있으면 HF 호출 없이 True."""
116+
import dartlab.config as _cfg
117+
from dartlab.providers.dart.openapi import allFilingsCollector as mod
118+
119+
monkeypatch.setattr(_cfg, "dataDir", str(tmp_path))
120+
outDir = mod._allFilingsDir()
121+
(outDir / "20260527.parquet").write_bytes(b"stub")
122+
123+
# snapshot_download 가 호출되면 실패 — 호출 없음 검증.
124+
def shouldNotBeCalled(*args, **kwargs):
125+
raise AssertionError("snapshot_download 호출됨 — short-circuit 실패")
126+
127+
import huggingface_hub
128+
129+
monkeypatch.setattr(huggingface_hub, "snapshot_download", shouldNotBeCalled)
130+
mod._HF_DOWNLOAD_ATTEMPTED.clear()
131+
assert mod._ensureFromHf("20260527") is True
132+
133+
79134
_STUB_DART_014 = (
80135
b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
81136
b"<result><status>014</status><message>\xed\x8c\x8c\xec\x9d\xbc\xec\x9d\xb4 "

0 commit comments

Comments
 (0)