EmailMaestro / scripts /deploy_hf_space.py
Flickinshots's picture
Deploy Project Epsilon Space bundle
93e8468 verified
from __future__ import annotations
import argparse
import os
import shutil
import sys
import tempfile
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from src.executive_assistant.deployment import (
DEFAULT_SPACE_TITLE,
HFSpaceDeployConfig,
parse_hf_usernames,
stage_space_bundle,
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Create or update a Hugging Face Space from this repository in one command."
)
parser.add_argument(
"--repo-id",
default=os.environ.get("HF_SPACE_REPO", "").strip(),
help="Target Space repo in owner/name form. Defaults to HF_SPACE_REPO.",
)
parser.add_argument(
"--token",
default=os.environ.get("HF_TOKEN", "").strip(),
help="Hugging Face token. Defaults to HF_TOKEN.",
)
parser.add_argument(
"--title",
default=os.environ.get("HF_SPACE_TITLE", DEFAULT_SPACE_TITLE),
help="Space title used in the generated HF README.",
)
parser.add_argument(
"--team-name",
default=os.environ.get("HF_SPACE_TEAM_NAME", "Team Epsilon"),
help="Team name shown in the generated HF README.",
)
parser.add_argument(
"--hf-usernames",
default=os.environ.get(
"HF_SPACE_TEAM_USERNAMES",
"flickinshots,ShreyaKhatik,itsayushdey",
),
help="Comma-separated HF usernames for the HF README placeholders.",
)
parser.add_argument(
"--checkpoint-name",
default=os.environ.get("HF_SPACE_CHECKPOINT_NAME", "q_policy_notebook.json"),
help="Checkpoint filename staged into artifacts/checkpoints/ for RL replay.",
)
parser.add_argument(
"--openrouter-api-key",
default=os.environ.get("OPENROUTER_API_KEY", "").strip(),
help="Optional secret to set on the Space during deployment.",
)
parser.add_argument(
"--api-base-url",
default=os.environ.get("API_BASE_URL", "https://openrouter.ai/api/v1").strip(),
help="OpenAI-compatible API base URL for inference.py. Defaults to OpenRouter.",
)
parser.add_argument(
"--model-name",
default=os.environ.get("MODEL_NAME", "google/gemma-4-31b-it").strip(),
help="OpenRouter model id for inference.py. Defaults to Gemma 4.",
)
parser.add_argument(
"--private",
action="store_true",
default=os.environ.get("HF_SPACE_PRIVATE", "").strip().lower() == "true",
help="Create or keep the Space private.",
)
parser.add_argument(
"--skip-checkpoint",
action="store_true",
help="Skip bundling the RL checkpoint.",
)
parser.add_argument(
"--keep-stage-dir",
default="",
help="Optional local folder where the prepared Space bundle should be copied after upload.",
)
return parser
def require_huggingface_hub():
try:
from huggingface_hub import HfApi # type: ignore
except ImportError as exc:
raise SystemExit(
"huggingface_hub is required for deployment. Install the training environment "
"or run `python -m pip install huggingface_hub` first."
) from exc
return HfApi
def maybe_set_space_secret(api, repo_id: str, key: str, value: str) -> str:
if not value.strip():
return f"Skipped secret {key} because no value was provided."
add_secret = getattr(api, "add_space_secret", None)
if add_secret is None:
return f"Upload succeeded, but this huggingface_hub version cannot set {key} automatically."
add_secret(repo_id=repo_id, key=key, value=value)
return f"Set Space secret {key}."
def maybe_set_space_variable(api, repo_id: str, key: str, value: str) -> str:
add_variable = getattr(api, "add_space_variable", None)
if add_variable is None:
return f"Upload succeeded, but this huggingface_hub version cannot set variable {key} automatically."
add_variable(repo_id=repo_id, key=key, value=value)
return f"Set Space variable {key}={value}."
def main() -> int:
parser = build_parser()
args = parser.parse_args()
if not args.repo_id:
parser.error("A Space repo id is required. Pass --repo-id or set HF_SPACE_REPO.")
if "/" not in args.repo_id:
parser.error("Space repo id must be in owner/name form.")
if not args.token:
parser.error("A Hugging Face token is required. Pass --token or set HF_TOKEN.")
config = HFSpaceDeployConfig(
repo_id=args.repo_id,
title=args.title,
team_name=args.team_name,
hf_usernames=parse_hf_usernames(args.hf_usernames),
checkpoint_name=args.checkpoint_name,
private=args.private,
include_checkpoint=not args.skip_checkpoint,
)
HfApi = require_huggingface_hub()
api = HfApi(token=args.token)
with tempfile.TemporaryDirectory(prefix="hf-space-stage-") as tmp_dir:
stage_dir = Path(tmp_dir)
checkpoint_path = stage_space_bundle(config, stage_dir)
api.create_repo(
repo_id=config.repo_id,
repo_type="space",
space_sdk="docker",
private=config.private,
exist_ok=True,
)
api.upload_folder(
folder_path=str(stage_dir),
repo_id=config.repo_id,
repo_type="space",
commit_message="Deploy Project Epsilon Space bundle",
delete_patterns=["*", "**/*"],
)
messages = [
f"Uploaded Space bundle to {config.space_url}",
f"App URL: {config.app_url}",
]
if checkpoint_path is not None:
messages.append(f"Bundled RL checkpoint: {checkpoint_path.relative_to(stage_dir)}")
messages.append(maybe_set_space_secret(api, config.repo_id, "OPENROUTER_API_KEY", args.openrouter_api_key))
messages.append(maybe_set_space_secret(api, config.repo_id, "OPENAI_API_KEY", args.openrouter_api_key))
messages.append(maybe_set_space_variable(api, config.repo_id, "API_BASE_URL", args.api_base_url))
messages.append(maybe_set_space_variable(api, config.repo_id, "MODEL_NAME", args.model_name))
messages.append(maybe_set_space_variable(api, config.repo_id, "OPENROUTER_APP_NAME", config.title))
messages.append(maybe_set_space_variable(api, config.repo_id, "OPENROUTER_SITE_URL", config.app_url))
if args.keep_stage_dir:
target_dir = Path(args.keep_stage_dir).resolve()
if target_dir.exists():
shutil.rmtree(target_dir)
shutil.copytree(stage_dir, target_dir)
messages.append(f"Saved staged bundle to {target_dir}")
for message in messages:
print(message)
return 0
if __name__ == "__main__":
sys.exit(main())