File size: 5,018 Bytes
0206613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Create the private submissions dataset and the Space, set secrets, push the code.

Private inputs (HF_TOKEN in .env, teams.csv) are read from MAST_PRIVATE_DIR, by default the ``ops``
directory next to this checkout, so they never sit inside the repository.

    python ops/deploy.py --namespace mast-benchmark            # full deploy
    python ops/deploy.py --namespace sahel-sh --dry-run        # show what would happen
    python ops/deploy.py --sync-only                           # just refresh the vendored validator

Never prints the token.
"""
from __future__ import annotations

import argparse
import shutil
import sys
from pathlib import Path

import os

REPO_ROOT = Path(__file__).resolve().parents[1]                 # the submission-portal checkout
PRIVATE_DIR = Path(os.environ.get("MAST_PRIVATE_DIR") or REPO_ROOT.parent / "ops")   # .env, exports, roster: never inside a repository
SPACE_DIR = REPO_ROOT
VALIDATOR_SRC = Path(os.environ.get("MAST_VALIDATE_SRC") or REPO_ROOT.parent / "mast-validate" / "src" / "mast_validate")
ENV_FILE = PRIVATE_DIR / ".env"


def load_env() -> dict[str, str]:
    out: dict[str, str] = {}
    if ENV_FILE.is_file():
        for line in ENV_FILE.read_text().splitlines():
            line = line.strip()
            if line and not line.startswith("#") and "=" in line:
                k, v = line.split("=", 1)
                out[k.strip()] = v.strip().strip('"').strip("'")
    return out


def sync_validator() -> None:
    dst = SPACE_DIR / "mast_validate"
    if dst.exists():
        shutil.rmtree(dst)
    shutil.copytree(VALIDATOR_SRC, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
    print(f"vendored {VALIDATOR_SRC.name} -> {dst}")


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--namespace", default="mast-benchmark", help="owner of the private dataset")
    ap.add_argument("--space-namespace", default=None,
                    help="owner of the Space (default: --namespace). Orgs need a paid plan to host Gradio "
                         "Spaces; a personal account can host one free.")
    ap.add_argument("--space-name", default="submit")
    ap.add_argument("--dataset-name", default="mast-2026-submissions")
    ap.add_argument("--deadline", default=None, help="MAST_DEADLINE_UTC variable, ISO-8601")
    ap.add_argument("--sync-only", action="store_true")
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()

    sync_validator()
    if args.sync_only:
        return 0
    env = load_env()
    token = env.get("HF_TOKEN")
    if not token:
        print("ops/.env has no HF_TOKEN", file=sys.stderr)
        return 1
    from huggingface_hub import HfApi

    api = HfApi(token=token)
    who = api.whoami()
    orgs = {o["name"] for o in who.get("orgs", [])}
    space_ns = args.space_namespace or args.namespace
    for ns in {args.namespace, space_ns}:
        if ns != who["name"] and ns not in orgs:
            print(f"token user {who['name']!r} is not a member of {ns!r} (orgs: {sorted(orgs)})", file=sys.stderr)
            return 1
    dataset_id = f"{args.namespace}/{args.dataset_name}"
    space_id = f"{space_ns}/{args.space_name}"
    print(f"dataset (private): {dataset_id}\nspace (public):    {space_id}")
    if args.dry_run:
        return 0

    api.create_repo(dataset_id, repo_type="dataset", private=True, exist_ok=True)
    if not api.file_exists(dataset_id, "teams.csv", repo_type="dataset"):
        teams = PRIVATE_DIR / "teams.csv"
        content = teams.read_bytes() if teams.is_file() else b"team_name,contact_email,member_emails,tracks,aliases,registered_at\n"
        api.upload_file(path_or_fileobj=content, path_in_repo="teams.csv", repo_id=dataset_id,
                        repo_type="dataset", commit_message="initial roster")
        print("uploaded teams.csv" + ("" if teams.is_file() else " (empty header only; run import_roster.py --push)"))
    api.create_repo(space_id, repo_type="space", space_sdk="gradio", private=False, exist_ok=True)
    api.add_space_secret(space_id, "HF_TOKEN", token)
    api.add_space_variable(space_id, "SUBMISSIONS_REPO", dataset_id)
    api.add_space_variable(space_id, "STORAGE_BACKEND", "hf")
    if args.deadline:
        api.add_space_variable(space_id, "MAST_DEADLINE_UTC", args.deadline)
    for k in ("SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASSWORD", "SMTP_FROM"):
        if env.get(k):
            api.add_space_secret(space_id, k, env[k])
    api.upload_folder(folder_path=str(SPACE_DIR), repo_id=space_id, repo_type="space",
                      commit_message="deploy portal",
                      ignore_patterns=["local-store/*", "tests/*", "__pycache__/*", "*.pyc", ".pytest_cache/*", "*.log"],
                      delete_patterns=["*"])
    print(f"deployed: https://huggingface.co/spaces/{space_id}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())