File size: 4,582 Bytes
729628a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python
"""Deploy Sensorflow Studio to a Hugging Face Space (Docker SDK).

Idempotent / re-runnable: creates the Space if missing, uploads the current
repo state (frontend is built inside the Docker image on HF's side), waits for
the build, and smoke-tests the deployed endpoints.

Usage:
    .venv/bin/python deploy/huggingface/deploy.py [--space-name NAME] [--no-wait]

Requires a Hugging Face token with write access (`hf auth login` or HF_TOKEN).
"""
import argparse
import json
import sys
import time
import urllib.request
from pathlib import Path

from huggingface_hub import HfApi
from huggingface_hub.errors import LocalTokenNotFoundError

REPO_ROOT = Path(__file__).resolve().parents[2]

# Mirrors .dockerignore, plus files that must not reach the Space repo as-is.
IGNORE_PATTERNS = [
    "node_modules/**",
    ".venv/**",
    ".git/**",
    "runs/**",
    "dist/**",
    "models/**",
    "*.pt",
    "**/*.pt",
    "**/__pycache__/**",
    ".pytest_cache/**",
    ".claude/**",
    ".DS_Store",
    "**/.DS_Store",
    "README.md",  # root project README; the Space README is uploaded separately
]

TERMINAL_ERROR_STAGES = {"BUILD_ERROR", "RUNTIME_ERROR", "CONFIG_ERROR", "DELETING"}


def http_check(url: str, method: str = "GET", body: dict | None = None) -> str:
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(
        url, data=data, method=method,
        headers={"Content-Type": "application/json"} if body is not None else {},
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            snippet = resp.read(200).decode(errors="replace").replace("\n", " ")
            return f"{resp.status} {snippet[:120]}"
    except Exception as exc:  # noqa: BLE001 - smoke test, report anything
        return f"FAILED: {exc}"


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--space-name", default="sensorflow-studio")
    parser.add_argument("--no-wait", action="store_true",
                        help="upload only; don't wait for the build")
    parser.add_argument("--timeout-minutes", type=float, default=15)
    args = parser.parse_args()

    api = HfApi()
    try:
        user = api.whoami()["name"]
    except LocalTokenNotFoundError:
        print("ERROR: no Hugging Face token found. Run `hf auth login` "
              "(or set HF_TOKEN) with a WRITE token, then re-run this script.")
        return 1

    repo_id = f"{user}/{args.space_name}"
    print(f"Deploying to Space: {repo_id}")

    api.create_repo(repo_id, repo_type="space", space_sdk="docker", exist_ok=True)

    print("Uploading repository files...")
    api.upload_folder(
        folder_path=str(REPO_ROOT),
        repo_id=repo_id,
        repo_type="space",
        ignore_patterns=IGNORE_PATTERNS,
        commit_message="Deploy Sensorflow Studio",
    )
    # The Space README (with docker frontmatter) replaces the project README.
    api.upload_file(
        path_or_fileobj=str(REPO_ROOT / "deploy" / "huggingface" / "README.md"),
        path_in_repo="README.md",
        repo_id=repo_id,
        repo_type="space",
        commit_message="Update Space README",
    )

    space_url = f"https://{user}-{args.space_name.replace('_', '-')}.hf.space"
    print(f"Space page: https://huggingface.co/spaces/{repo_id}")
    print(f"App URL:    {space_url}/")

    if args.no_wait:
        return 0

    print("Waiting for the Space build (this can take several minutes)...")
    deadline = time.time() + args.timeout_minutes * 60
    stage = None
    while time.time() < deadline:
        runtime = api.get_space_runtime(repo_id)
        if runtime.stage != stage:
            stage = runtime.stage
            print(f"  stage: {stage}")
        if stage == "RUNNING":
            break
        if stage in TERMINAL_ERROR_STAGES:
            print(f"Build/runtime failed (stage={stage}). Check logs at "
                  f"https://huggingface.co/spaces/{repo_id}?logs=build")
            return 2
        time.sleep(15)
    else:
        print(f"Timed out after {args.timeout_minutes} min (last stage: {stage}).")
        return 3

    print("Space is RUNNING. Verifying endpoints...")
    print(f"  GET  /            -> {http_check(space_url + '/')}")
    print(f"  GET  /dashboard/  -> {http_check(space_url + '/dashboard/')}")
    print(f"  POST /api/ssam/statewide -> "
          f"{http_check(space_url + '/api/ssam/statewide', 'POST', {'page': 1, 'page_size': 1})}")
    return 0


if __name__ == "__main__":
    sys.exit(main())