whassan's picture
Deploy Sensorflow Studio
729628a verified
Raw History Blame Contribute Delete
4.58 kB
#!/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())