"""One-shot Hugging Face Space deployment for asos-tools-py. Usage: set HF_TOKEN=hf_xxx (Windows) export HF_TOKEN=hf_xxx (macOS/Linux) python deploy/hf_deploy.py [--space-name NAME] [--private] Reads the token from the HF_TOKEN env var only. Never logs or commits it. Release #2 architecture: the HF Space runs a multi-process Docker container (nginx + streamlit + uvicorn under supervisord), so this script uploads: * asos_tools/ Python package (includes api.py + icons.py) * assets/ 4K transparent image assets * .streamlit/ Streamlit theme config * deploy/nginx.conf supervisord + nginx configs referenced by Dockerfile * deploy/supervisord.conf * deploy/entrypoint.sh * app.py, owl_logo.png, requirements.txt, Dockerfile * deploy/huggingface_README.md -> uploaded as README.md (Space frontmatter) Intentionally excluded from the Space: * deploy/hf_deploy.py (this script itself) * deploy/build_*.py (one-time dev tooling) * .github/ (GitHub Actions, not needed inside the container) * tests/, images/, CLAUDE.md, CHANGELOG, etc. """ from __future__ import annotations import argparse import os import sys from pathlib import Path from huggingface_hub import HfApi # Files inside deploy/ that MUST be on the HF Space because the Dockerfile # references them. deploy/hf_deploy.py and deploy/build_*.py stay local. _DEPLOY_FILES_TO_SHIP = [ "nginx.conf", "supervisord.conf", "entrypoint.sh", ] def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--space-name", default="asos-tools", help="Space repo name (default: asos-tools)") parser.add_argument("--owner", default=None, help="HF user or org; default = whoami()") parser.add_argument("--private", action="store_true") args = parser.parse_args() token = os.environ.get("HF_TOKEN") if not token: print("ERROR: HF_TOKEN not set", file=sys.stderr) return 1 api = HfApi(token=token) try: who = api.whoami() except Exception as e: print(f"ERROR: token validation failed: {e}", file=sys.stderr) return 1 owner = args.owner or who.get("name") or who.get("username") print(f"Authenticated as: {owner!r}") repo_id = f"{owner}/{args.space_name}" print(f"Creating (or reusing) Space: {repo_id}") api.create_repo( repo_id=repo_id, repo_type="space", space_sdk="docker", private=args.private, exist_ok=True, token=token, ) root = Path(__file__).resolve().parent.parent print(f"Repo root: {root}") # ---- asos_tools/ package ------------------------------------------------ print("Uploading asos_tools/ ...") api.upload_folder( folder_path=str(root / "asos_tools"), path_in_repo="asos_tools", repo_id=repo_id, repo_type="space", commit_message="Upload asos_tools package", token=token, ignore_patterns=["__pycache__", "*.pyc"], ) # ---- assets/ (4K images) ------------------------------------------------ if (root / "assets").exists(): print("Uploading assets/ ...") api.upload_folder( folder_path=str(root / "assets"), path_in_repo="assets", repo_id=repo_id, repo_type="space", commit_message="Upload 4K transparent assets", token=token, ) # ---- .streamlit/ -------------------------------------------------------- if (root / ".streamlit").exists(): print("Uploading .streamlit/ ...") api.upload_folder( folder_path=str(root / ".streamlit"), path_in_repo=".streamlit", repo_id=repo_id, repo_type="space", commit_message="Upload Streamlit theme config", token=token, ) # ---- deploy/ runtime configs (nginx, supervisord, entrypoint) ---------- for fname in _DEPLOY_FILES_TO_SHIP: src = root / "deploy" / fname if not src.exists(): print(f"WARN: deploy/{fname} missing; skipping", file=sys.stderr) continue print(f"Uploading deploy/{fname}") api.upload_file( path_or_fileobj=str(src), path_in_repo=f"deploy/{fname}", repo_id=repo_id, repo_type="space", commit_message=f"Upload deploy/{fname}", token=token, ) # ---- Top-level single files -------------------------------------------- for local, remote, msg in [ ("app.py", "app.py", "Upload Streamlit app"), ("owl_logo.png", "owl_logo.png", "Upload O.W.L. logo"), ("requirements.txt", "requirements.txt", "Upload requirements"), ("Dockerfile", "Dockerfile", "Upload Dockerfile"), ("deploy/huggingface_README.md", "README.md", "Upload README with Spaces frontmatter"), ]: print(f"Uploading {local} -> {remote}") api.upload_file( path_or_fileobj=str(root / local), path_in_repo=remote, repo_id=repo_id, repo_type="space", commit_message=msg, token=token, ) print() print(f"Done. Live URL: https://huggingface.co/spaces/{repo_id}") print("First multi-process build takes ~3-4 min. Watch the build log at:") print(f" https://huggingface.co/spaces/{repo_id}/logs/build") print("Then curl /api/health to confirm the FastAPI side-car is live:") print(f" curl https://{owner}-{args.space_name}.hf.space/api/health") return 0 if __name__ == "__main__": raise SystemExit(main())