feat: Sprint-3 완결 + Sprint-4 착수 (WBS-3.2, 3.4, 5.2)
주요 변경: - [WBS-3.2] 리밸런싱 V2 신호 가중 목표배분 (signal_weighted_ss001_v1) * equal_weight -> SS001_Norm_Score 비례 버킷내 배분 * 하네스: 삼성(36.84%) > SK하이닉스(29.16%), Core=66.00% PASS - [WBS-3.4] logDailyAssetHistory_ SpreadsheetApp.getActiveSpreadsheet() -> getSpreadsheet_() 수정 * run_all 컨텍스트에서 null 반환 방지 - [WBS-5.2] deploy_gas.py 전면 재작성 * src/gas_adapter_parts/ + src/gas/ 양쪽 소스 탐색 * gdc_01+gdc_02 -> gas_data_collect.gs 번들링 * dry-run PASS: 17개 파일 WARN 0건 - src/gas/ 디렉토리 신규 추가 (CLASP 조직화 구조) - tools/automate_routine.py, download_trading_data.py 신규 추가 - .gitignore: .clasprc.json OAuth 토큰 제외 추가 - ROADMAP_WBS.md: Sprint-3 [x] 완료, Sprint-4 착수 목록 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+115
-64
@@ -1,91 +1,142 @@
|
||||
"""deploy_gas.py -- WBS-5.2 GAS auto-deploy script
|
||||
|
||||
Bundles src/gas_adapter_parts/ + src/gas/ -> Temp/gas_deploy/ then clasp push.
|
||||
Usage: python tools/deploy_gas.py [--dry-run] [--skip-push]
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# Resolve project root dynamically relative to this script's directory (tools/)
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC_PARTS = ROOT / "src" / "gas_adapter_parts"
|
||||
SRC_GAS = ROOT / "src" / "gas"
|
||||
DEPLOY_DIR = ROOT / "Temp" / "gas_deploy"
|
||||
|
||||
GAS_FILES = [
|
||||
"gas_data_feed.gs",
|
||||
"gas_data_collect.gs",
|
||||
"gas_lib.gs",
|
||||
"gas_harness_rows.gs",
|
||||
"gas_report.gs",
|
||||
"gas_event_calendar.gs",
|
||||
"gas_apex_alpha_watch.gs",
|
||||
"gas_apex_runtime_core.gs"
|
||||
]
|
||||
# Resolve a file from multiple candidate directories
|
||||
def _find(filename: str) -> Path | None:
|
||||
candidates = [
|
||||
SRC_PARTS / filename,
|
||||
SRC_GAS / "core" / filename,
|
||||
SRC_GAS / "collection" / filename,
|
||||
SRC_GAS / "engines" / filename,
|
||||
SRC_GAS / "reports" / filename,
|
||||
]
|
||||
for c in candidates:
|
||||
if c.exists():
|
||||
return c
|
||||
return None
|
||||
|
||||
def main():
|
||||
print(f"Preparing deployment directory: {DEPLOY_DIR}")
|
||||
if DEPLOY_DIR.exists():
|
||||
shutil.rmtree(DEPLOY_DIR)
|
||||
DEPLOY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
# dst_name -> [src_file, ...] (concatenated in order)
|
||||
BUNDLE_MAP: dict[str, list[str]] = {
|
||||
"appsscript.json": ["appsscript.json"],
|
||||
"gas_lib.gs": ["gas_lib.gs"],
|
||||
"data_feed_base.gs": ["data_feed_base.gs"],
|
||||
"gas_apex_runtime_core.gs":["gas_apex_runtime_core.gs"],
|
||||
# gdc_01 + gdc_02 bundled as single file (GAS project legacy name)
|
||||
"gas_data_collect.gs": [
|
||||
"gdc_01_fetch_fundamentals.gs",
|
||||
"gdc_02_account_satellite.gs",
|
||||
],
|
||||
"gdf_01_price_metrics.gs": ["gdf_01_price_metrics.gs"],
|
||||
"gdf_02_harness_assembly.gs": ["gdf_02_harness_assembly.gs"],
|
||||
"gdf_03_portfolio_gates.gs": ["gdf_03_portfolio_gates.gs"],
|
||||
"gdf_04_execution_quality.gs":["gdf_04_execution_quality.gs"],
|
||||
"gdf_05_alpha_engines.gs": ["gdf_05_alpha_engines.gs"],
|
||||
"gdf_06_rebalance.gs": ["gdf_06_rebalance.gs"],
|
||||
"gas_data_feed.gs": ["gas_data_feed.gs"],
|
||||
"gas_harness_rows.gs": ["gas_harness_rows.gs"],
|
||||
"gas_report.gs": ["gas_report.gs"],
|
||||
"gas_event_calendar.gs": ["gas_event_calendar.gs"],
|
||||
"gas_apex_alpha_watch.gs": ["gas_apex_alpha_watch.gs"],
|
||||
}
|
||||
|
||||
# 1. Copy appsscript.json manifest
|
||||
manifest_src = ROOT / "src" / "gas_adapter_parts" / "appsscript.json"
|
||||
if manifest_src.exists():
|
||||
shutil.copy(manifest_src, DEPLOY_DIR / "appsscript.json")
|
||||
print(f"Copied appsscript.json from {manifest_src}")
|
||||
else:
|
||||
# Create default manifest
|
||||
manifest = {
|
||||
"timeZone": "Asia/Seoul",
|
||||
"dependencies": {},
|
||||
"exceptionLogging": "STACKDRIVER",
|
||||
"runtimeVersion": "V8"
|
||||
SCRIPT_ID = "1xfeBAeeknmnBtSvrIqWXO_2hc3ByeriLUOSuOOB4YxLLHhN3zdnL7tVh"
|
||||
|
||||
|
||||
def build_deploy(dry_run: bool = False) -> bool:
|
||||
print("[deploy_gas] src_parts=" + str(SRC_PARTS))
|
||||
print("[deploy_gas] src_gas= " + str(SRC_GAS))
|
||||
print("[deploy_gas] dst= " + str(DEPLOY_DIR))
|
||||
if not dry_run:
|
||||
if DEPLOY_DIR.exists():
|
||||
shutil.rmtree(DEPLOY_DIR)
|
||||
DEPLOY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ok = True
|
||||
for dst_name, src_files in BUNDLE_MAP.items():
|
||||
dst_path = DEPLOY_DIR / dst_name
|
||||
parts: list[str] = []
|
||||
for sf in src_files:
|
||||
src_path = _find(sf)
|
||||
if src_path is None:
|
||||
print(" WARN: " + sf + " not found")
|
||||
ok = False
|
||||
continue
|
||||
parts.append(src_path.read_text(encoding="utf-8"))
|
||||
if not parts:
|
||||
continue
|
||||
content = "\n".join(parts)
|
||||
tag = "[DRY]" if dry_run else "write"
|
||||
print(" " + tag + " " + dst_name + " (" + str(len(content)) + " chars)")
|
||||
if not dry_run:
|
||||
dst_path.write_text(content, encoding="utf-8")
|
||||
|
||||
if not dry_run:
|
||||
clasp_cfg = {
|
||||
"scriptId": SCRIPT_ID,
|
||||
"rootDir": str(DEPLOY_DIR.relative_to(ROOT)).replace("\\", "/"),
|
||||
}
|
||||
with open(DEPLOY_DIR / "appsscript.json", "w", encoding="utf-8") as f:
|
||||
json.dump(manifest, f, ensure_ascii=False, indent=2)
|
||||
print("Created default appsscript.json")
|
||||
(ROOT / ".clasp.json").write_text(
|
||||
json.dumps(clasp_cfg, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
print(" write .clasp.json -> rootDir=" + clasp_cfg["rootDir"])
|
||||
return ok
|
||||
|
||||
# 2. Copy GAS files from ROOT to DEPLOY_DIR
|
||||
copied_count = 0
|
||||
for gf in GAS_FILES:
|
||||
src = ROOT / gf
|
||||
if src.exists():
|
||||
shutil.copy(src, DEPLOY_DIR / gf)
|
||||
print(f"Copied {gf}")
|
||||
copied_count += 1
|
||||
else:
|
||||
print(f"WARNING: Source file not found: {gf}")
|
||||
|
||||
# 3. Create/Overwrite .clasp.json with DEPLOY_DIR as rootDir
|
||||
clasp_cfg = {
|
||||
"scriptId": "1xfeBAeeknmnBtSvrIqWXO_2hc3ByeriLUOSuOOB4YxLLHhN3zdnL7tVh",
|
||||
"rootDir": str(DEPLOY_DIR.relative_to(ROOT))
|
||||
}
|
||||
with open(ROOT / ".clasp.json", "w", encoding="utf-8") as f:
|
||||
json.dump(clasp_cfg, f, ensure_ascii=False, indent=2)
|
||||
print(f"Updated .clasp.json with rootDir={clasp_cfg['rootDir']}")
|
||||
|
||||
# 4. Run clasp push with shell=True for Windows compatibility
|
||||
print("Running npx @google/clasp push -f ...")
|
||||
env = dict(os.environ)
|
||||
def clasp_push() -> bool:
|
||||
print("[deploy_gas] clasp push -f ...")
|
||||
res = subprocess.run(
|
||||
["npx", "@google/clasp", "push", "-f"],
|
||||
cwd=str(ROOT),
|
||||
env=env,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace"
|
||||
errors="replace",
|
||||
)
|
||||
print(f"Return code: {res.returncode}")
|
||||
print("STDOUT:")
|
||||
print(res.stdout)
|
||||
print("STDERR:")
|
||||
print(res.stderr)
|
||||
|
||||
if res.stderr:
|
||||
print("STDERR: " + res.stderr[:500])
|
||||
if res.returncode == 0:
|
||||
print("GAS deploy completed successfully!")
|
||||
else:
|
||||
print("GAS deploy failed!")
|
||||
exit(1)
|
||||
print("[deploy_gas] clasp push OK")
|
||||
return True
|
||||
print("[deploy_gas] clasp push FAILED rc=" + str(res.returncode))
|
||||
return False
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="GAS auto-deploy")
|
||||
parser.add_argument("--dry-run", action="store_true", help="List files without writing")
|
||||
parser.add_argument("--skip-push", action="store_true", help="Bundle only, skip clasp push")
|
||||
args = parser.parse_args()
|
||||
|
||||
ok = build_deploy(dry_run=args.dry_run)
|
||||
if not ok:
|
||||
print("[deploy_gas] Some source files missing -- check warnings above")
|
||||
raise SystemExit(1)
|
||||
|
||||
if args.dry_run or args.skip_push:
|
||||
print("[deploy_gas] dry-run/skip-push -- push skipped")
|
||||
return
|
||||
|
||||
if not clasp_push():
|
||||
raise SystemExit(1)
|
||||
|
||||
print("[deploy_gas] Done. To run_all: python tools/automate_routine.py")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user