File size: 6,269 Bytes
da5cba1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53508e7
da5cba1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Deploy HF RL Explorer from this folder: the public explorer Space and its private admin Space.

    uv run python scripts/deploy_spaces.py                       # the plan, nothing changed
    uv run python scripts/deploy_spaces.py --apply               # create or update both Spaces
    uv run python scripts/deploy_spaces.py --apply --only admin  # one of them
    uv run python scripts/deploy_spaces.py --apply --listing-from-store   # once the indexer Job runs (DESIGN.md)

Both run this image (Dockerfile); the admin with RLX_APP=app.admin_app:app. They share only the bucket
FineEnvs/rl-explorer-data, mounted read-write at /data; the explorer also mounts the MiMo explorer's repository
snapshots read-only (one copy, the one MiMo's own runs use). Each Space has its own SESSION_SECRET: created once,
never printed, never replaced (replacing it signs everyone out). Each Space gets one commit with its own README,
so one build, and files no longer here are removed from it.
"""

from __future__ import annotations

import argparse
import fnmatch
import os
import secrets
import sys
from pathlib import Path

from huggingface_hub import CommitOperationAdd, CommitOperationDelete, HfApi, Volume

ROOT = Path(__file__).resolve().parents[1]
BUCKET = "FineEnvs/rl-explorer-data"
EXPLORER_URL = "https://fineenvs-rl-explorer.hf.space"
DATA = Volume(type="bucket", source=BUCKET, mount_path="/data")
SPACES = {
    "explorer": {
        "repo": "FineEnvs/RL-Explorer", "private": False, "readme": "SPACE_README.md", "hardware": "cpu-basic",
        "variables": {"MIMO_SNAPSHOT_DIR": "/mimo-snapshots"},
        "volumes": [DATA, Volume(type="bucket", source="FineEnvs/mimo-explorer-runs", mount_path="/mimo-snapshots",
                                 path="repo-snapshots", read_only=True)],
    },
    "admin": {
        "repo": "FineEnvs/RL-Explorer-admin", "private": True, "readme": "ADMIN_SPACE_README.md", "hardware": "cpu-basic",
        "variables": {"RLX_APP": "app.admin_app:app", "RLX_OAUTH_SCOPES": "openid profile read-memberships", "RLX_EXPLORER_URL": EXPLORER_URL},
        "volumes": [DATA],
    },
}
# never uploaded: local data and caches, the READMEs (each Space gets its own as README.md), editor and OS files
IGNORE = [".venv/*", ".local-data/*", "*/__pycache__/*", "__pycache__/*", "*.pyc", ".pytest_cache/*", ".ruff_cache/*",
          ".gradio/*", "node_modules/*", ".DS_Store", "*/.DS_Store", "README.md", "SPACE_README.md", "ADMIN_SPACE_README.md",
          ".env", "*.env", ".git/*"]


SKIP_DIRS = {".venv", ".local-data", "__pycache__", ".pytest_cache", ".ruff_cache", ".gradio", "node_modules", ".git"}


def files() -> list[str]:
    """What goes into the Space: walked without descending into local data, caches or the virtualenv."""
    out = []
    for root, dirs, names in os.walk(ROOT):
        dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
        for n in sorted(names):
            rel = (Path(root) / n).relative_to(ROOT).as_posix()
            if not any(fnmatch.fnmatch(rel, pat) for pat in IGNORE):
                out.append(rel)
    return out


def deploy(api: HfApi, name: str, spec: dict, listing_from_store: bool, apply: bool) -> None:
    repo = spec["repo"]
    variables = {**spec["variables"], **({"RLX_LISTING_FROM_STORE": "1"} if listing_from_store else {})}
    paths = files()
    print(f"\n{name}: {repo} ({'private' if spec['private'] else 'public'}, {spec['hardware']})")
    print(f"  volumes: " + "; ".join(f"{v.source}{'/' + v.path if v.path else ''} -> {v.mount_path}{' (read-only)' if v.read_only else ''}" for v in spec["volumes"]))
    print(f"  variables: {variables}")
    print(f"  files: {len(paths)} + README.md from {spec['readme']}")
    if not apply:
        return
    api.create_repo(repo, repo_type="space", space_sdk="docker", private=spec["private"], exist_ok=True,
                    space_hardware=spec["hardware"])
    info = api.space_info(repo)
    if bool(info.private) != spec["private"]:
        api.update_repo_settings(repo, repo_type="space", private=spec["private"])
    api.set_space_volumes(repo, spec["volumes"])
    for k, v in variables.items():
        api.add_space_variable(repo, k, v)
    if "SESSION_SECRET" not in {getattr(x, "key", x) for x in api.get_space_secrets(repo)}:   # names, or objects with .key
        api.add_space_secret(repo, "SESSION_SECRET", secrets.token_urlsafe(48), description="signs this Space's sessions")
        print("  SESSION_SECRET: created")
    runtime = api.get_space_runtime(repo)
    if (runtime.requested_hardware or runtime.hardware) != spec["hardware"]:
        api.request_space_hardware(repo, spec["hardware"])
    ops = [CommitOperationAdd(path_in_repo=rel, path_or_fileobj=str(ROOT / rel)) for rel in paths]
    ops.append(CommitOperationAdd(path_in_repo="README.md", path_or_fileobj=str(ROOT / spec["readme"])))
    keep = set(paths) | {"README.md", ".gitattributes"}
    ops += [CommitOperationDelete(path_in_repo=f) for f in api.list_repo_files(repo, repo_type="space") if f not in keep]
    commit = api.create_commit(repo, repo_type="space", operations=ops, commit_message="Deploy HF RL Explorer")
    print(f"  pushed {commit.oid[:10]}: https://huggingface.co/spaces/{repo}")


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--apply", action="store_true", help="create or update the Spaces (without it, print the plan)")
    ap.add_argument("--only", choices=sorted(SPACES), help="deploy one Space")
    ap.add_argument("--listing-from-store", action="store_true", help="read the indexer Job's listing (set once the Job runs)")
    args = ap.parse_args()
    api = HfApi()
    me = api.whoami()
    orgs = {o["name"]: o.get("roleInOrg") for o in me.get("orgs", [])}
    if orgs.get("FineEnvs") not in ("admin", "write", "contributor"):
        sys.exit(f"{me['name']} can't write to FineEnvs (role: {orgs.get('FineEnvs')})")
    for name, spec in SPACES.items():
        if not args.only or args.only == name:
            deploy(api, name, spec, args.listing_from_store, args.apply)
    if not args.apply:
        print("\nNothing changed: run with --apply.")


if __name__ == "__main__":
    main()