ai / scripts /deploy_to_hf_space.py
FlashCode-Lab's picture
Create scripts/deploy_to_hf_space.py
3c763c9 verified
#!/usr/bin/env python3
"""一键将当前前端发布到 Hugging Face Space(static/gradio/docker)."""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Iterable
HF_ENDPOINT = "https://huggingface.co"
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_PUBLISH_FILES = ["index.html", "styles.css", "app.js", "README.md"]
class DeployError(RuntimeError):
"""部署失败错误。"""
def api_request(path: str, token: str, method: str = "GET", payload: dict | None = None) -> dict:
url = f"{HF_ENDPOINT}{path}"
data = None
headers = {"Authorization": f"Bearer {token}"}
if payload is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
body = resp.read().decode("utf-8")
return json.loads(body) if body else {}
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="ignore")
raise DeployError(f"{method} {path} 失败: {exc.code} {body}") from exc
def run(cmd: list[str], cwd: str | None = None) -> None:
subprocess.run(cmd, check=True, cwd=cwd)
def get_namespace(token: str) -> str:
whoami = api_request("/api/whoami-v2", token)
name = whoami.get("name")
if not name:
raise DeployError("无法从 /api/whoami-v2 获取用户名")
return name
def build_create_repo_payload(space_name: str, organization: str | None, private: bool, sdk: str) -> dict:
return {
"type": "space",
"name": space_name,
"organization": organization,
"private": private,
"sdk": sdk,
}
def resolve_repo_id(token: str, space_name: str, organization: str | None, private: bool, sdk: str) -> str:
namespace = organization or get_namespace(token)
payload = build_create_repo_payload(
space_name=space_name,
organization=organization,
private=private,
sdk=sdk,
)
try:
api_request("/api/repos/create", token, method="POST", payload=payload)
print(f"✅ 已创建 Space: {namespace}/{space_name}")
except DeployError as exc:
msg = str(exc).lower()
if "409" in msg or "already exists" in msg:
print(f"ℹ️ Space 已存在,继续发布: {namespace}/{space_name}")
else:
raise
return f"{namespace}/{space_name}"
def resolve_publish_files(extra_files: Iterable[str] | None = None) -> list[str]:
files = list(DEFAULT_PUBLISH_FILES)
if extra_files:
for item in extra_files:
if item and item not in files:
files.append(item)
return files
def publish(
repo_id: str,
token: str,
publish_files: list[str],
message: str,
force_push: bool,
branch: str,
) -> None:
with tempfile.TemporaryDirectory(prefix="hf-space-") as tmp:
for rel in publish_files:
src = PROJECT_ROOT / rel
if not src.exists():
raise FileNotFoundError(f"缺少发布文件: {src}")
dst = Path(tmp) / rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
run(["git", "init"], cwd=tmp)
run(["git", "checkout", "-b", branch], cwd=tmp)
run(["git", "config", "user.name", "my-ai-coder"], cwd=tmp)
run(["git", "config", "user.email", "bot@local"], cwd=tmp)
run(["git", "add", "."], cwd=tmp)
run(["git", "commit", "-m", message], cwd=tmp)
safe_token = urllib.parse.quote(token, safe="")
remote = f"https://user:{safe_token}@huggingface.co/spaces/{repo_id}"
run(["git", "remote", "add", "origin", remote], cwd=tmp)
push_cmd = ["git", "push", "-u", "origin", branch]
if force_push:
push_cmd.append("--force")
run(push_cmd, cwd=tmp)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="一键发布到 Hugging Face Space")
parser.add_argument("--space", required=True, help="Space 名称,例如 my-omni-ai-studio")
parser.add_argument("--token", default=None, help="HF Token,默认读取 HF_TOKEN 或 HUGGING_FACE_HUB_TOKEN")
parser.add_argument("--org", default=None, help="组织名(可选),不填则发布到个人账号")
parser.add_argument("--private", action="store_true", help="创建私有 Space")
parser.add_argument("--sdk", default="static", choices=["static", "gradio", "docker"], help="Space SDK 类型")
parser.add_argument("--branch", default="main", help="目标分支名,默认 main")
parser.add_argument("--message", default="Deploy My Omni AI Studio", help="部署提交信息")
parser.add_argument("--include", action="append", default=[], help="额外发布文件(可多次传入)")
parser.add_argument("--no-force", action="store_true", help="关闭默认 --force 推送")
return parser.parse_args()
def resolve_token(token_arg: str | None) -> str | None:
if token_arg:
return token_arg
return os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
def main() -> int:
args = parse_args()
token = resolve_token(args.token)
if not token:
print("❌ 缺少 Token,请通过 --token 或 HF_TOKEN/HUGGING_FACE_HUB_TOKEN 提供。", file=sys.stderr)
return 1
publish_files = resolve_publish_files(args.include)
repo_id = resolve_repo_id(token, args.space, args.org, args.private, args.sdk)
publish(
repo_id=repo_id,
token=token,
publish_files=publish_files,
message=args.message,
force_push=not args.no_force,
branch=args.branch,
)
print(f"🚀 发布完成: https://huggingface.co/spaces/{repo_id}")
print(f"📦 发布文件: {', '.join(publish_files)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())