File size: 6,149 Bytes
4b23c8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Deploy this checkout to the Space: the files the image needs (workflows left out), one commit on the Space repo
naming the source commit; Hugging Face rebuilds the image on that push. With ``--base-tag`` the Space's Dockerfile
is ``Dockerfile.space`` on the prebuilt ``ghcr.io/dmrai-lab/disco-space:<tag>`` (the image workflow's output), so
the build copies files instead of installing the stack.

    HF_TOKEN=... python tools/deploy.py [--space rfick/disco] [--sha <git sha>] [--base-tag <sha|latest>] [--zero] [--config brain.toml]

``--config`` names the configuration the Space serves (a file in ``space/``; ``config.toml``, the DiSCo Space, by
default): the deploy sets the Space variable ``DISCO_CONFIG`` to it (and removes it for the default), and a ZeroGPU
deployment takes ``README-zero.md`` for the default and ``README-<stem>-zero.md`` for another (``brain.toml`` ->
``README-brain-zero.md``). ``rfick/brain-zero`` is this repository deployed with ``--zero --config brain.toml``.

The Spaces read their data from the Hub at the config's revision when their container starts (disco-space#4: the
bucket is retired), so the deploy also removes a ``DISCO_MOMENTS`` variable and any mounted volume it finds.
"""
import argparse
import os
import shutil
import subprocess
import tempfile

IGNORE = [".git/*", "__pycache__/*", "*.pyc", ".github/*", ".pytest_cache/*", ".gitignore", "gate.json"]   # tests and tools go too: the gate job runs on the Space's own files


def retire_bucket(api, space):
    """No ``DISCO_MOMENTS`` variable and no volume on ``space``: the app then downloads the layout from the Hub."""
    try:
        api.delete_space_variable(space, "DISCO_MOMENTS")
        print("removed the DISCO_MOMENTS variable")
    except Exception as e:                                  # absent already, or the API says so
        print("DISCO_MOMENTS variable:", repr(e)[:120])
    try:
        api.delete_space_volumes(space)
        print("volumes removed")
    except Exception as e:                                  # "has no attached volumes" is the state we want
        print("no volumes mounted" if "no attached" in str(e) else f"volumes: {e!r}"[:160])


DEFAULT_CONFIG = "config.toml"


def readme_for(config):
    """The ZeroGPU README of a configuration: ``README-zero.md`` for the default, ``README-<stem>-zero.md`` else."""
    return "README-zero.md" if config == DEFAULT_CONFIG else f"README-{os.path.splitext(config)[0]}-zero.md"


def set_config(api, space, config):
    """``DISCO_CONFIG`` on the Space: the configuration's name, absent for the default."""
    if config == DEFAULT_CONFIG:
        try:
            api.delete_space_variable(space, "DISCO_CONFIG")
            print("removed the DISCO_CONFIG variable (the default configuration)")
        except Exception as e:                              # absent already
            print("DISCO_CONFIG variable:", repr(e)[:120])
    else:
        api.add_space_variable(space, "DISCO_CONFIG", config)
        print(f"DISCO_CONFIG = {config}")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--space", default=os.environ.get("DISCO_SPACE", "rfick/disco"))
    ap.add_argument("--sha", default=None)
    ap.add_argument("--base-tag", default=None, help="deploy on ghcr.io/dmrai-lab/disco-space:<tag> through Dockerfile.space")
    ap.add_argument("--zero", action="store_true", help="the ZeroGPU (Gradio SDK) deployment: README-zero.md, requirements-zero.txt, app.py")
    ap.add_argument("--config", default=os.environ.get("DISCO_CONFIG", DEFAULT_CONFIG), help="the configuration in space/ the Space serves (DISCO_CONFIG)")
    a = ap.parse_args()
    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if not os.path.exists(os.path.join(root, "space", a.config)):
        raise SystemExit(f"no configuration space/{a.config}")
    sha = a.sha or subprocess.run(["git", "-C", root, "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip()
    from huggingface_hub import HfApi
    api = HfApi(token=os.environ.get("HF_TOKEN") or None)
    folder = root; staged = None
    if a.zero:
        staged = tempfile.mkdtemp(prefix="disco-zero-")
        for name in ("space", "data", "tests", "tools"):
            shutil.copytree(os.path.join(root, name), os.path.join(staged, name), ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
        for name in ("app.py", "pyproject.toml"):
            shutil.copy(os.path.join(root, name), staged)
        shutil.copy(os.path.join(root, readme_for(a.config)), os.path.join(staged, "README.md"))
        shutil.copy(os.path.join(root, "requirements-zero.txt"), os.path.join(staged, "requirements.txt"))
        folder = staged
    elif a.base_tag:
        staged = tempfile.mkdtemp(prefix="disco-space-")
        for name in ("space", "data", "tests", "tools"):
            shutil.copytree(os.path.join(root, name), os.path.join(staged, name), ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
        for name in ("README.md", "requirements.txt", "entrypoint.sh", "pyproject.toml"):
            shutil.copy(os.path.join(root, name), staged)
        with open(os.path.join(root, "Dockerfile.space")) as f, open(os.path.join(staged, "Dockerfile"), "w") as g:
            g.write(f.read().replace("BASE_TAG", a.base_tag))
        folder = staged
    info = api.upload_folder(folder_path=folder, repo_id=a.space, repo_type="space", ignore_patterns=IGNORE,
                             delete_patterns=["Dockerfile", "Dockerfile.base", "Dockerfile.space", "entrypoint.sh", "README-*zero.md", "requirements-zero.txt"] if a.zero
                             else (["Dockerfile.base", "Dockerfile.space"] if a.base_tag else None),
                             commit_message=f"disco-space {sha[:12]}" + (f" on base {a.base_tag[:12]}" if a.base_tag else "") + (f", {a.config}" if a.config != DEFAULT_CONFIG else ""))
    if staged:
        shutil.rmtree(staged)
    set_config(api, a.space, a.config)
    retire_bucket(api, a.space)
    print(info.commit_url)
    print("stage", api.get_space_runtime(a.space).stage)


if __name__ == "__main__":
    main()