#!/usr/bin/env python3 """Build the downloadable skills pack. 1. Copies `skills/_shared/hfmd.py` into every `skills/hfmd-*/scripts/hfmd.py` (self-contained skills). 2. Zips the five skill folders (+ a top-level INSTALL.md) into `hfmarketdata/web/public/downloads/hfmarketdata-skills.zip`, deterministically (fixed timestamps, sorted entries) so re-running without changes yields an identical file. Usage: python3 skills/scripts/build_skills.py [--check] [--out PATH] """ from __future__ import annotations import argparse import shutil import sys import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[2] SKILLS = ROOT / "skills" SHARED = SKILLS / "_shared" / "hfmd.py" OUT = ROOT / "hfmarketdata" / "web" / "public" / "downloads" / "hfmarketdata-skills.zip" FIXED_TIME = (2026, 1, 1, 0, 0, 0) INSTALL = """# HF Market Data skills — install Unzip into `~/.claude/skills/` (all projects) or `.claude/skills/` (this project only): unzip -o hfmarketdata-skills.zip -d ~/.claude/skills/ Requirements: Python 3.10+, `pip install requests pandas` (+ `matplotlib` for charts). Optional: `export HFMD_API_KEY=hfmd_live_…` (free API key → 120 requests/min; keyless mode has low hourly limits, 30/h). Everything is free; higher limits are granted on request by e-mail to contact@spboucher.ai (also free). See https://www.hfmarketdata.io/limits Skills: hfmd-data-analysis · hfmd-quick-backtest · hfmd-continuous-futures · hfmd-fundamentals-screen · hfmd-term-structure Docs: https://www.hfmarketdata.io/integrations/skills """ def skill_dirs() -> list[Path]: return sorted(p for p in SKILLS.iterdir() if p.is_dir() and p.name.startswith("hfmd-") and (p / "SKILL.md").exists()) def sync_shared(check: bool) -> int: drift = 0 src = SHARED.read_bytes() for d in skill_dirs(): dst = d / "scripts" / "hfmd.py" if dst.exists() and dst.read_bytes() == src: continue if check: print(f"DRIFT: {dst.relative_to(ROOT)} differs from _shared/hfmd.py", file=sys.stderr) drift += 1 else: dst.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(SHARED, dst) print(f"synced {dst.relative_to(ROOT)}") return drift def build_zip(out: Path) -> None: out.parent.mkdir(parents=True, exist_ok=True) entries: list[tuple[str, bytes]] = [("INSTALL.md", INSTALL.encode())] for d in skill_dirs(): for f in sorted(d.rglob("*")): if f.is_dir() or "__pycache__" in f.parts or f.suffix in {".pyc", ".png", ".csv"}: continue entries.append((str(f.relative_to(SKILLS)), f.read_bytes())) with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: for name, data in entries: info = zipfile.ZipInfo(name, date_time=FIXED_TIME) info.compress_type = zipfile.ZIP_DEFLATED info.external_attr = (0o755 if name.endswith(".py") else 0o644) << 16 z.writestr(info, data) print(f"wrote {out.relative_to(ROOT)} ({out.stat().st_size:,} bytes, {len(entries)} files, {len(skill_dirs())} skills)") def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--check", action="store_true", help="only verify the copies of hfmd.py are in sync") ap.add_argument("--out", type=Path, default=OUT) a = ap.parse_args() drift = sync_shared(a.check) if a.check: print("in sync" if not drift else f"{drift} file(s) out of sync") return 1 if drift else 0 build_zip(a.out) return 0 if __name__ == "__main__": sys.exit(main())