Spaces:
Running
Running
File size: 9,948 Bytes
5936e76 e317359 5936e76 e317359 5936e76 e317359 5936e76 e317359 5936e76 e317359 5936e76 e317359 5936e76 e317359 5936e76 e317359 5936e76 e317359 a9c5d6e e317359 5936e76 e317359 5936e76 e317359 5936e76 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | #!/usr/bin/env python3
"""Publish public metrics and bounded visualization snapshots atomically to HF."""
from __future__ import annotations
import argparse
import json
import os
import shutil
import sys
import tempfile
import time
from datetime import datetime, timezone
from pathlib import Path
from dotenv import load_dotenv
REPO_ROOT = Path(__file__).resolve().parents[1]
load_dotenv(REPO_ROOT / ".env")
DEFAULT_RESULTS = REPO_ROOT / "space" / "results"
DEFAULT_REPO = "ThinkcatLab/LiveHouse-TS"
API_RETRIES = 5
UPLOAD_IGNORE_PATTERNS = [
# Raw inference/task archives stay local. public_snapshots/<model>/<dataset>.json
# contains the bounded, explicit visualization export and MUST be published.
"prequential/**",
"saved_evals/**",
"*/forecasts/**",
"**/forecasts/**",
"evaluation_metrics.jsonl",
"evaluation_metrics_canonical.jsonl",
"aggregates/**/evaluation_metrics_canonical.jsonl",
"aggregates/canonical_all_history/**",
"aggregates/*.zip",
".DS_Store",
]
REMOTE_PRUNE_PATHS = [
"results/evaluation_metrics.jsonl",
"results/evaluation_metrics_canonical.jsonl",
"results/aggregates/results.zip",
"results/aggregates/aggregates.zip",
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", default=os.getenv("HF_SPACE_REPO", DEFAULT_REPO))
parser.add_argument("--results", type=Path, default=DEFAULT_RESULTS)
parser.add_argument("--results-repo", default=os.getenv("HF_RESULTS_REPO"))
parser.add_argument(
"--commit-message",
default="Update leaderboard results from local eval",
)
parser.add_argument(
"--restart-space",
action="store_true",
help=(
"Explicitly restart the Space after pushing results. Disabled by default "
"to avoid periodic 504s during live leaderboard refreshes."
),
)
return parser.parse_args()
def load_token() -> str:
load_dotenv(REPO_ROOT / ".env")
token = (os.getenv("HF_TOKEN") or "").strip()
if not token:
print("HF_TOKEN is required (add to .env)", file=sys.stderr)
raise SystemExit(1)
return token
def get_hf_api(token: str):
from huggingface_hub import HfApi
endpoint = (os.getenv("HF_ENDPOINT") or "").strip() or None
return HfApi(token=token, endpoint=endpoint)
def ensure_space_repo(token: str, repo_id: str) -> None:
try:
from huggingface_hub.utils import RepositoryNotFoundError
except ImportError:
from huggingface_hub.errors import RepositoryNotFoundError
api = get_hf_api(token)
try:
api.repo_info(repo_id, repo_type="space")
except RepositoryNotFoundError:
print(f"Creating Space {repo_id}…", file=sys.stderr)
api.create_repo(
repo_id=repo_id,
repo_type="space",
space_sdk="gradio",
exist_ok=True,
)
print(f"Created Space {repo_id}", file=sys.stderr)
def prune_remote_non_leaderboard_artifacts(token: str, repo_id: str) -> bool:
"""Delete remote-only artifacts that are not read by the Space UI.
The evaluation metric JSONL files are local aggregation caches. Once they grow beyond
the Hub git limit, keeping it in the Space repository blocks every refresh.
"""
api = get_hf_api(token)
try:
remote_files = set(api.list_repo_files(repo_id=repo_id, repo_type="space"))
except Exception as exc:
print(f"Could not verify remote files before upload: {exc}", file=sys.stderr)
return False
stale_paths = sorted(path for path in remote_files if (
path in REMOTE_PRUNE_PATHS or path.startswith("results/prequential/")
or path.startswith("results/saved_evals/") or "/forecasts/" in path
))
if not stale_paths:
return True
try:
from huggingface_hub import CommitOperationDelete
api.create_commit(
repo_id=repo_id,
repo_type="space",
operations=[CommitOperationDelete(path_in_repo=path) for path in stale_paths],
commit_message="Prune non-leaderboard result artifacts",
)
print(f"Pruned {len(stale_paths)} non-leaderboard artifacts from current remote revision")
return True
except Exception as exc:
print(f"Could not prune remote non-leaderboard artifacts: {exc}", file=sys.stderr)
return False
def update_push_status(results: Path, *, ok: bool, detail: str = "") -> None:
status_path = results / "online_status.json"
if not status_path.exists():
return
try:
payload = json.loads(status_path.read_text())
except (json.JSONDecodeError, OSError):
return
payload["push_status"] = "ok" if ok else "failed"
payload["pushed_at"] = datetime.now(timezone.utc).isoformat()
if detail:
payload["push_detail"] = detail
status_path.write_text(json.dumps(payload, indent=4) + "\n")
# Evaluation rewrites online_status while rapid cycles wait for publication.
# Keep the last actual publish attempt independently for the watchdog.
receipt = results / "publication_status.json"
temporary = receipt.with_suffix(".tmp")
temporary.write_text(json.dumps({key: payload[key] for key in ("push_status", "pushed_at")}))
temporary.replace(receipt)
def restart_space(token: str, repo_id: str) -> None:
api = get_hf_api(token)
try:
api.restart_space(repo_id)
print(f"Restarted Space {repo_id} to load new results", file=sys.stderr)
except Exception as exc:
print(f"Space restart failed (non-fatal): {exc}", file=sys.stderr)
def maybe_restart_space(args: argparse.Namespace, token: str) -> None:
"""Restart only when explicitly requested by a full deployment workflow."""
if args.restart_space:
restart_space(token, args.repo)
def stage_public_results(results: Path, destination: Path) -> None:
from huggingface_hub.utils import filter_repo_objects
candidates = [p.relative_to(results).as_posix() for p in results.rglob("*") if p.is_file()]
for rel in filter_repo_objects(candidates, ignore_patterns=UPLOAD_IGNORE_PATTERNS):
if Path(rel).suffix not in {".csv", ".json", ".tex"}:
continue
output = destination / rel
output.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(results / rel, output)
def push_via_api(args: argparse.Namespace, token: str) -> int:
try:
from huggingface_hub import HfApi
except ImportError:
print("huggingface_hub not installed; run setup_portable.sh", file=sys.stderr)
return 1
api = get_hf_api(token)
results_repo = getattr(args, "results_repo", None) or args.repo + "-results"
api.create_repo(results_repo, repo_type="dataset", exist_ok=True)
last_error = ""
for attempt in range(1, API_RETRIES + 1):
try:
# Refuse an old clone replacing a newer operator's cumulative history.
from huggingface_hub import hf_hub_download
from huggingface_hub.utils import EntryNotFoundError
import pandas as pd
revision = api.dataset_info(results_repo).sha
metrics = pd.read_csv(args.results / "aggregates/live_release_metrics.csv")
history_rows = len(metrics)
try:
previous_path = hf_hub_download(results_repo, "results/online_status.json",
repo_type="dataset", revision=revision, token=token)
previous = json.loads(Path(previous_path).read_text())
except EntryNotFoundError:
previous = {}
if int(previous.get("canonical_history_rows", 0)) > history_rows:
print("Remote history is newer/larger. Restore the latest operator backup before publishing.", file=sys.stderr)
return 1
with tempfile.TemporaryDirectory(prefix="livehouse-results-") as temp:
stage = Path(temp)
stage_public_results(args.results, stage)
# The public status and tables belong to the same Hub commit.
update_push_status(stage, ok=True, detail="HF Hub API")
status_path = stage / "online_status.json"
status = json.loads(status_path.read_text())
status["canonical_history_rows"] = history_rows
status["latest_scored_release_time"] = str(metrics["release_time"].max()) if history_rows else None
status_path.write_text(json.dumps(status, indent=2) + "\n")
commit = api.upload_folder(
folder_path=str(stage), path_in_repo="results", repo_id=results_repo,
repo_type="dataset", revision="main", parent_commit=revision, commit_message=args.commit_message,
)
print(f"Pushed results → https://huggingface.co/datasets/{results_repo}/commit/{commit.oid}")
update_push_status(args.results, ok=True, detail="HF Hub API")
maybe_restart_space(args, token)
return 0
except Exception as exc:
last_error = str(exc)
print(
f"HF Hub API upload retry {attempt}/{API_RETRIES}: {exc}",
file=sys.stderr,
)
time.sleep(3 * attempt)
print(f"HF Hub API upload failed after {API_RETRIES} retries: {last_error}", file=sys.stderr)
return 1
def main() -> int:
args = parse_args()
if not args.results.is_dir():
print(f"Results directory not found: {args.results}", file=sys.stderr)
return 1
token = load_token()
code = push_via_api(args, token)
if code != 0:
update_push_status(args.results, ok=False, detail="HF Hub API upload failed")
return code
if __name__ == "__main__":
raise SystemExit(main())
|