apple / scripts /_hf_upload.py
SZFSPC's picture
feat: deploy to hf space | model=mistral-21461b
2bfa191 verified
Raw History Blame Contribute Delete
11.2 kB
"""HF Upload helper - 计算上传大小 + 使用 upload_large_folder 上传大目录
用法:
python scripts/_hf_upload.py calc-size <dir> [exclude_patterns...]
python scripts/_hf_upload.py upload <repo_id> <dir> --token TOKEN [exclude_patterns...]
示例:
python scripts/_hf_upload.py calc-size . '.git/**' '.venv/**'
python scripts/_hf_upload.py upload user/space . --token hf_xxx '.git/**' '.venv/**'
"""
from __future__ import annotations
import argparse
import fnmatch
import os
import shutil
import subprocess
import sys
from pathlib import Path
_HF_EXTRA_PATH = "/root/.local/bin"
def _human_size(n: int) -> str:
for unit in ("B", "KB", "MB", "GB", "TB"):
if abs(n) < 1024:
return f"{n:.2f} {unit}"
n /= 1024
return f"{n:.2f} PB"
def _should_exclude(rel_path: str, patterns: list[str]) -> bool:
"""判断文件路径是否匹配任意排除模式。"""
for p in patterns:
if fnmatch.fnmatch(rel_path, p):
return True
parts = rel_path.split(os.sep)
for i in range(len(parts)):
prefix = os.sep.join(parts[:i + 1])
if fnmatch.fnmatch(prefix, p):
return True
if p.endswith("/**"):
dir_p = p[:-3]
if fnmatch.fnmatch(rel_path, dir_p) or rel_path.startswith(dir_p):
return True
return False
def calc_size(dir_path: str, exclude_patterns: list[str]) -> int:
"""计算目录中需要上传的文件总大小(字节),排除匹配的文件。"""
total = 0
dir_path = os.path.abspath(dir_path)
for root, dirs, files in os.walk(dir_path):
dirs[:] = [d for d in dirs if not _should_exclude(os.path.join(root, d), exclude_patterns)]
for fname in files:
full = os.path.join(root, fname)
rel = os.path.relpath(full, dir_path)
if _should_exclude(rel, exclude_patterns):
continue
try:
total += os.path.getsize(full)
except OSError:
pass
return total
def _hf_upload_with_progress(
repo_id: str,
local_dir: str,
token: str | None = None,
ignore_patterns: list[str] | None = None,
commit_message: str = "Upload",
repo_type: str = "space",
) -> bool:
"""使用 hf upload CLI 上传(有 tqdm 进度条)。"""
if ignore_patterns is None:
ignore_patterns = []
hf_path = shutil.which("hf") or shutil.which(os.path.join(_HF_EXTRA_PATH, "hf"))
if not hf_path:
print("[ERROR] 'hf' CLI not found in PATH", file=sys.stderr)
return False
extra_env = None
if token:
extra_env = os.environ.copy()
extra_env["HF_TOKEN"] = token
if _HF_EXTRA_PATH not in os.environ.get("PATH", ""):
extra_env = (extra_env or os.environ.copy())
extra_env["PATH"] = f"{_HF_EXTRA_PATH}:{extra_env['PATH']}"
cmd = ["hf", "upload", repo_id, local_dir, "--repo-type", repo_type, "--commit-message", commit_message]
for p in ignore_patterns:
cmd += ["--exclude", p]
print("[INFO] Falling back to hf upload CLI (tqdm progress will be shown)...")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=7200,
env=extra_env,
)
if result.returncode != 0:
print(f"[ERROR] hf upload failed: {result.stderr.strip()}", file=sys.stderr)
return False
return True
except subprocess.TimeoutExpired:
print("[ERROR] hf upload timed out (>2h)", file=sys.stderr)
return False
except FileNotFoundError:
print("[ERROR] 'hf' CLI not found", file=sys.stderr)
return False
def _ensure_repo_exists(
api,
repo_id: str,
repo_type: str,
private: bool = True,
) -> None:
"""确保目标仓库存在,对于 space 类型需要提供 space_sdk 参数。"""
import inspect
# 收集 create_repo 支持的参数(不同版本有差异)
try:
sig = inspect.signature(api.create_repo)
supported_params = set(sig.parameters.keys())
except (TypeError, ValueError):
supported_params = {"repo_id", "repo_type", "private", "exist_ok", "space_sdk"}
create_kwargs = {
"repo_id": repo_id,
"repo_type": repo_type,
"private": private,
"exist_ok": True,
}
# space 类型必须提供 space_sdk(docker / gradio / streamlit / static)
if repo_type == "space" and "space_sdk" in supported_params:
create_kwargs["space_sdk"] = "docker"
# 只传递支持的参数
create_kwargs = {k: v for k, v in create_kwargs.items() if k in supported_params}
try:
api.create_repo(**create_kwargs)
except Exception as e:
# 仓库已存在或其他非致命错误:静默继续,让后续 upload 去处理
err_lower = str(e).lower()
if "already" not in err_lower and "exists" not in err_lower:
print(
f"[DEBUG] create_repo({repo_id}, type={repo_type}) 可能失败但将继续: {e}",
file=sys.stderr,
)
def _get_upload_large_folder_kwargs(
api,
repo_id: str,
local_dir: str,
ignore_patterns: list[str],
commit_message: str,
repo_type: str = "space",
) -> dict:
"""根据当前 huggingface_hub 版本的 API 签名,构建 upload_large_folder 的 kwargs。
不同版本的 HfApi.upload_large_folder 支持的参数不同:
- 旧版可能只有 repo_id / repo_type / folder_path / ignore_patterns
- 新版新增了 commit_message / print_report / print_report_every
- 某些版本可能使用 allow_patterns / delete_patterns 而不是 ignore_patterns
使用 inspect.signature 动态检测,只传递支持的参数,避免 TypeError。
"""
import inspect
try:
sig = inspect.signature(api.upload_large_folder)
supported_params = set(sig.parameters.keys())
except (TypeError, ValueError):
# 某些旧版本签名无法 inspect,回退到最安全的参数子集
supported_params = {"repo_id", "repo_type", "folder_path", "ignore_patterns"}
all_candidates = {
"repo_id": repo_id,
"repo_type": repo_type,
"folder_path": local_dir,
"ignore_patterns": ignore_patterns,
"commit_message": commit_message,
"print_report": True,
"print_report_every": 20,
}
# 如果 upload_large_folder 内部会调用 create_repo,也需要 space_sdk
if repo_type == "space" and "space_sdk" in supported_params:
all_candidates["space_sdk"] = "docker"
kwargs = {k: v for k, v in all_candidates.items() if k in supported_params}
# 如果 ignore_patterns 不被支持,看看是否有 allow_patterns / delete_patterns
if "ignore_patterns" not in supported_params and ignore_patterns:
for alt in ("allow_patterns",):
if alt in supported_params:
# allow_patterns 语义不同(白名单 vs 黑名单),这里不做自动转换
break
unsupported = [k for k in all_candidates if k not in supported_params]
if unsupported:
# 只在首次调用时打印一次,且级别降为 DEBUG(非 stderr 主输出)
print(
f"[DEBUG] huggingface_hub upload_large_folder 不支持参数: "
f"{', '.join(unsupported)} — 将跳过(不影响功能)",
file=sys.stderr,
)
return kwargs
def upload_large_folder(
repo_id: str,
local_dir: str,
token: str | None = None,
ignore_patterns: list[str] | None = None,
commit_message: str = "Upload",
repo_type: str = "space",
) -> bool:
"""使用 HfApi.upload_large_folder 上传目录(有 print_report 进度)。
参数名使用 ignore_patterns(HfApi 的正式参数名)。
如果 huggingface_hub 不可用或上传失败,回退到 hf upload CLI(有 tqdm 进度条)。
为避免不同 huggingface_hub 版本的 API 签名差异(例如某些版本没有 commit_message),
使用 inspect.signature 动态检测并只传递支持的参数。
对于 repo_type="space",会确保先调用 create_repo 并提供 space_sdk="docker",
以避免 "No space_sdk provided" 错误。
"""
if ignore_patterns is None:
ignore_patterns = []
try:
from huggingface_hub import HfApi
except ModuleNotFoundError:
print("[WARN] huggingface_hub not found, falling back to hf upload CLI", file=sys.stderr)
return _hf_upload_with_progress(repo_id, local_dir, token, ignore_patterns, commit_message, repo_type)
api = HfApi(token=token)
# 先确保仓库存在(space 类型需要 space_sdk,必须显式提供)
_ensure_repo_exists(api, repo_id, repo_type=repo_type)
kwargs = _get_upload_large_folder_kwargs(
api, repo_id, local_dir, ignore_patterns, commit_message, repo_type=repo_type
)
try:
api.upload_large_folder(**kwargs)
return True
except TypeError as e:
# 如果动态检测仍未覆盖到,例如方法签名有 **kwargs 但内部再次调用时出错,
# 给出明确提示并回退到 CLI
print(f"[WARN] HfApi.upload_large_folder 参数签名不兼容: {e}", file=sys.stderr)
print("[INFO] 回退到 hf upload CLI ...", file=sys.stderr)
return _hf_upload_with_progress(repo_id, local_dir, token, ignore_patterns, commit_message, repo_type)
except Exception as e:
print(f"[WARN] HfApi.upload_large_folder failed: {e}", file=sys.stderr)
return _hf_upload_with_progress(repo_id, local_dir, token, ignore_patterns, commit_message, repo_type)
# ---- CLI ----
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="HF Upload helper")
sub = parser.add_subparsers(dest="cmd", required=True)
calc = sub.add_parser("calc-size", help="Calculate upload size")
calc.add_argument("dir", help="Directory to scan")
calc.add_argument("patterns", nargs="*", help="Exclude patterns")
up = sub.add_parser("upload", help="Upload folder using upload_large_folder")
up.add_argument("repo_id", help="Target repo ID")
up.add_argument("dir", help="Directory to upload")
up.add_argument("--token", help="HF token")
up.add_argument("--message", default="Upload", help="Commit message")
up.add_argument("--repo-type", default="space", choices=["space", "dataset", "model"],
help="Repository type (default: space)")
up.add_argument("patterns", nargs="*", help="Exclude patterns (passed as ignore_patterns)")
args = parser.parse_args()
if args.cmd == "calc-size":
size = calc_size(args.dir, args.patterns)
print(_human_size(size))
sys.exit(0)
if args.cmd == "upload":
ok = upload_large_folder(
repo_id=args.repo_id,
local_dir=args.dir,
token=args.token,
ignore_patterns=args.patterns,
commit_message=args.message,
repo_type=args.repo_type,
)
sys.exit(0 if ok else 1)