"""yfinance fallback 캐시 빌더. 로컬(yfinance 가 정상 동작하는 환경)에서 실행해 각 라우터 함수를 직접 호출하고, 그 결과 JSON 을 backend/cache/ 에 저장한다. Hugging Face Spaces 처럼 데이터센터 IP 에서 yfinance 가 401 로 차단되는 환경에서 이 캐시가 fallback 으로 제공된다. 실행: cd D:/OStock/backend .venv/Scripts/python.exe scripts/build_cache.py 주의: news/prediction 라우터는 transformers->torch 를 끌어와 import 순서 이슈가 있으므로, 여기서는 yfinance 기반 라우터(market/quotes/financial/technical)만 import 한다. """ import os import sys import json import datetime # scripts/ 에서 실행해도 backend/ 모듈을 import 할 수 있도록 backend 경로 추가 BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, BACKEND_DIR) from config import SECTOR_TICKERS # noqa: E402 from routers import market, quotes, financial, technical # noqa: E402 CACHE_DIR = os.path.join(BACKEND_DIR, "cache") def _save(relpath, data): path = os.path.join(CACHE_DIR, relpath) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def _is_error(result): """엔드포인트 결과가 에러/빈값이면 True (저장 스킵 + 실패 카운트용).""" if result is None: return True if isinstance(result, dict): if "error" in result: return True vals = list(result.values()) # sectors/bond 처럼 모든 하위 항목이 에러 dict 인 경우 if vals and all(isinstance(v, dict) and "error" in v for v in vals): return True return False if isinstance(result, list): return len(result) == 0 return False def main(): os.makedirs(CACHE_DIR, exist_ok=True) stats = {"ok": 0, "fail": 0, "failed": []} def run(label, relpath, fn): try: result = fn() except Exception as e: # noqa: BLE001 result = {"error": str(e)} if _is_error(result): stats["fail"] += 1 stats["failed"].append(label) print(f" [FAIL] {label}") else: _save(relpath, result) stats["ok"] += 1 print(f" [ OK ] {label}") print("== 무파라미터 엔드포인트 ==") run("indices", "indices.json", market.get_market_indices) run("sectors", "sectors.json", market.get_sector_data) run("get_bond_data", "get_bond_data.json", market.get_bond_data) print(f"== 종목별 엔드포인트 ({len(SECTOR_TICKERS)} 종목 x 4) ==") for _name, tk in SECTOR_TICKERS.items(): run(f"stocks/{tk}", f"stocks/{tk}.json", lambda tk=tk: quotes.get_stock_data(ticker=tk)) # period/interval 을 명시 전달해야 한다. 함수를 직접 호출하면 FastAPI 가 # 주입하지 않아 기본값이 Query(...) 객체로 남아 .lower() 에서 깨진다. run(f"stock_chart/{tk}", f"stock_chart/{tk}.json", lambda tk=tk: quotes.get_stock_chart(ticker=tk, period="1mo", interval="1d")) run(f"financial_info/{tk}", f"financial_info/{tk}.json", lambda tk=tk: financial.get_financial_info(ticker=tk)) run(f"technical_info/{tk}", f"technical_info/{tk}.json", lambda tk=tk: technical.get_technical_info(ticker=tk)) meta = { "generated_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"), "endpoints": [ "indices", "sectors", "get_bond_data", "stocks", "stock_chart", "financial_info", "technical_info", ], "ticker_count": len(SECTOR_TICKERS), "success": stats["ok"], "failed": stats["fail"], } with open(os.path.join(CACHE_DIR, ".meta.json"), "w", encoding="utf-8") as f: json.dump(meta, f, ensure_ascii=False, indent=2) print(f"\n완료: 성공 {stats['ok']} / 실패 {stats['fail']}") if stats["failed"]: print("실패 항목:", ", ".join(stats["failed"])) print("생성 시각:", meta["generated_at"]) if __name__ == "__main__": main()