FlashCode-Lab commited on
Commit
3c763c9
·
verified ·
1 Parent(s): 2a319bf

Create scripts/deploy_to_hf_space.py

Browse files
Files changed (1) hide show
  1. scripts/deploy_to_hf_space.py +177 -0
scripts/deploy_to_hf_space.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """一键将当前前端发布到 Hugging Face Space(static/gradio/docker)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import urllib.error
14
+ import urllib.parse
15
+ import urllib.request
16
+ from pathlib import Path
17
+ from typing import Iterable
18
+
19
+ HF_ENDPOINT = "https://huggingface.co"
20
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
21
+ DEFAULT_PUBLISH_FILES = ["index.html", "styles.css", "app.js", "README.md"]
22
+
23
+
24
+ class DeployError(RuntimeError):
25
+ """部署失败错误。"""
26
+
27
+
28
+ def api_request(path: str, token: str, method: str = "GET", payload: dict | None = None) -> dict:
29
+ url = f"{HF_ENDPOINT}{path}"
30
+ data = None
31
+ headers = {"Authorization": f"Bearer {token}"}
32
+ if payload is not None:
33
+ headers["Content-Type"] = "application/json"
34
+ data = json.dumps(payload).encode("utf-8")
35
+
36
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
37
+ try:
38
+ with urllib.request.urlopen(req, timeout=30) as resp:
39
+ body = resp.read().decode("utf-8")
40
+ return json.loads(body) if body else {}
41
+ except urllib.error.HTTPError as exc:
42
+ body = exc.read().decode("utf-8", errors="ignore")
43
+ raise DeployError(f"{method} {path} 失败: {exc.code} {body}") from exc
44
+
45
+
46
+ def run(cmd: list[str], cwd: str | None = None) -> None:
47
+ subprocess.run(cmd, check=True, cwd=cwd)
48
+
49
+
50
+ def get_namespace(token: str) -> str:
51
+ whoami = api_request("/api/whoami-v2", token)
52
+ name = whoami.get("name")
53
+ if not name:
54
+ raise DeployError("无法从 /api/whoami-v2 获取用户名")
55
+ return name
56
+
57
+
58
+ def build_create_repo_payload(space_name: str, organization: str | None, private: bool, sdk: str) -> dict:
59
+ return {
60
+ "type": "space",
61
+ "name": space_name,
62
+ "organization": organization,
63
+ "private": private,
64
+ "sdk": sdk,
65
+ }
66
+
67
+
68
+ def resolve_repo_id(token: str, space_name: str, organization: str | None, private: bool, sdk: str) -> str:
69
+ namespace = organization or get_namespace(token)
70
+
71
+ payload = build_create_repo_payload(
72
+ space_name=space_name,
73
+ organization=organization,
74
+ private=private,
75
+ sdk=sdk,
76
+ )
77
+
78
+ try:
79
+ api_request("/api/repos/create", token, method="POST", payload=payload)
80
+ print(f"✅ 已创建 Space: {namespace}/{space_name}")
81
+ except DeployError as exc:
82
+ msg = str(exc).lower()
83
+ if "409" in msg or "already exists" in msg:
84
+ print(f"ℹ️ Space 已存在,继续发布: {namespace}/{space_name}")
85
+ else:
86
+ raise
87
+
88
+ return f"{namespace}/{space_name}"
89
+
90
+
91
+ def resolve_publish_files(extra_files: Iterable[str] | None = None) -> list[str]:
92
+ files = list(DEFAULT_PUBLISH_FILES)
93
+ if extra_files:
94
+ for item in extra_files:
95
+ if item and item not in files:
96
+ files.append(item)
97
+ return files
98
+
99
+
100
+ def publish(
101
+ repo_id: str,
102
+ token: str,
103
+ publish_files: list[str],
104
+ message: str,
105
+ force_push: bool,
106
+ branch: str,
107
+ ) -> None:
108
+ with tempfile.TemporaryDirectory(prefix="hf-space-") as tmp:
109
+ for rel in publish_files:
110
+ src = PROJECT_ROOT / rel
111
+ if not src.exists():
112
+ raise FileNotFoundError(f"缺少发布文件: {src}")
113
+ dst = Path(tmp) / rel
114
+ dst.parent.mkdir(parents=True, exist_ok=True)
115
+ shutil.copy2(src, dst)
116
+
117
+ run(["git", "init"], cwd=tmp)
118
+ run(["git", "checkout", "-b", branch], cwd=tmp)
119
+ run(["git", "config", "user.name", "my-ai-coder"], cwd=tmp)
120
+ run(["git", "config", "user.email", "bot@local"], cwd=tmp)
121
+ run(["git", "add", "."], cwd=tmp)
122
+ run(["git", "commit", "-m", message], cwd=tmp)
123
+
124
+ safe_token = urllib.parse.quote(token, safe="")
125
+ remote = f"https://user:{safe_token}@huggingface.co/spaces/{repo_id}"
126
+ run(["git", "remote", "add", "origin", remote], cwd=tmp)
127
+
128
+ push_cmd = ["git", "push", "-u", "origin", branch]
129
+ if force_push:
130
+ push_cmd.append("--force")
131
+ run(push_cmd, cwd=tmp)
132
+
133
+
134
+ def parse_args() -> argparse.Namespace:
135
+ parser = argparse.ArgumentParser(description="一键发布到 Hugging Face Space")
136
+ parser.add_argument("--space", required=True, help="Space 名称,例如 my-omni-ai-studio")
137
+ parser.add_argument("--token", default=None, help="HF Token,默认读取 HF_TOKEN 或 HUGGING_FACE_HUB_TOKEN")
138
+ parser.add_argument("--org", default=None, help="组织名(可选),不填则发布到个人账号")
139
+ parser.add_argument("--private", action="store_true", help="创建私有 Space")
140
+ parser.add_argument("--sdk", default="static", choices=["static", "gradio", "docker"], help="Space SDK 类型")
141
+ parser.add_argument("--branch", default="main", help="目标分支名,默认 main")
142
+ parser.add_argument("--message", default="Deploy My Omni AI Studio", help="部署提交信息")
143
+ parser.add_argument("--include", action="append", default=[], help="额外发布文件(可多次传入)")
144
+ parser.add_argument("--no-force", action="store_true", help="关闭默认 --force 推送")
145
+ return parser.parse_args()
146
+
147
+
148
+ def resolve_token(token_arg: str | None) -> str | None:
149
+ if token_arg:
150
+ return token_arg
151
+ return os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
152
+
153
+
154
+ def main() -> int:
155
+ args = parse_args()
156
+ token = resolve_token(args.token)
157
+ if not token:
158
+ print("❌ 缺少 Token,请通过 --token 或 HF_TOKEN/HUGGING_FACE_HUB_TOKEN 提供。", file=sys.stderr)
159
+ return 1
160
+
161
+ publish_files = resolve_publish_files(args.include)
162
+ repo_id = resolve_repo_id(token, args.space, args.org, args.private, args.sdk)
163
+ publish(
164
+ repo_id=repo_id,
165
+ token=token,
166
+ publish_files=publish_files,
167
+ message=args.message,
168
+ force_push=not args.no_force,
169
+ branch=args.branch,
170
+ )
171
+ print(f"🚀 发布完成: https://huggingface.co/spaces/{repo_id}")
172
+ print(f"📦 发布文件: {', '.join(publish_files)}")
173
+ return 0
174
+
175
+
176
+ if __name__ == "__main__":
177
+ raise SystemExit(main())