burtenshaw's picture
burtenshaw HF Staff
Upload folder using huggingface_hub
b66db95 verified
"""Providers for launching Hugging Face Spaces via ``uv run``."""
from __future__ import annotations
import os
import socket
import subprocess
import time
from dataclasses import dataclass, field
from typing import Dict, Optional
import requests
from .providers import ContainerProvider
def _poll_health(health_url: str, timeout_s: float) -> None:
"""Poll a health endpoint until it returns HTTP 200 or times out."""
deadline = time.time() + timeout_s
while time.time() < deadline:
try:
response = requests.get(health_url, timeout=2.0)
if response.status_code == 200:
return
except requests.RequestException:
pass
time.sleep(0.5)
raise TimeoutError(
f"Server did not become ready within {timeout_s:.1f} seconds"
)
def _create_uv_command(
space_id: str,
host: str,
port: int,
reload: bool,
project_url: Optional[str] = None,
) -> list[str]:
command = [
"uv",
"run",
"--project",
project_url or f"git+https://huggingface.co/spaces/{space_id}",
"--",
"server",
"--host",
host,
"--port",
str(port),
]
if reload:
command.append("--reload")
return command
@dataclass
class UVProvider(ContainerProvider):
"""ContainerProvider implementation backed by ``uv run``."""
space_id: str
host: str = "0.0.0.0"
port: Optional[int] = None
reload: bool = False
project_url: Optional[str] = None
connect_host: Optional[str] = None
extra_env: Optional[Dict[str, str]] = None
context_timeout_s: float = 60.0
_process: subprocess.Popen | None = field(init=False, default=None)
_base_url: str | None = field(init=False, default=None)
def start_container(
self,
image: str,
port: Optional[int] = None,
env_vars: Optional[Dict[str, str]] = None,
**_: Dict[str, str],
) -> str:
if self._process is not None and self._process.poll() is None:
raise RuntimeError("UVProvider is already running")
self.space_id = image or self.space_id
bind_port = port or self.port or self._find_free_port()
command = _create_uv_command(
self.space_id,
self.host,
bind_port,
self.reload,
project_url=self.project_url,
)
env = os.environ.copy()
if self.extra_env:
env.update(self.extra_env)
if env_vars:
env.update(env_vars)
try:
self._process = subprocess.Popen(command, env=env)
except FileNotFoundError as exc:
raise RuntimeError(
"`uv` executable not found. Install uv from "
"https://github.com/astral-sh/uv and ensure it is on PATH."
) from exc
except OSError as exc:
raise RuntimeError(f"Failed to launch `uv run`: {exc}") from exc
client_host = self.connect_host or (
"127.0.0.1" if self.host in {"0.0.0.0", "::"} else self.host
)
self._base_url = f"http://{client_host}:{bind_port}"
self.port = bind_port
return self._base_url
def wait_for_ready(self, base_url: str, timeout_s: float = 60.0) -> None:
if self._process and self._process.poll() is not None:
code = self._process.returncode
raise RuntimeError(
f"uv process exited prematurely with code {code}"
)
_poll_health(f"{base_url}/health", timeout_s)
def stop_container(self) -> None:
if self._process is None:
return
if self._process.poll() is None:
self._process.terminate()
try:
self._process.wait(timeout=10.0)
except subprocess.TimeoutExpired:
self._process.kill()
self._process.wait(timeout=5.0)
self._process = None
self._base_url = None
def start(self) -> str:
return self.start_container(self.space_id, port=self.port)
def stop(self) -> None:
self.stop_container()
def wait_for_ready_default(self, timeout_s: float | None = None) -> None:
if self._base_url is None:
raise RuntimeError("UVProvider has not been started")
self.wait_for_ready(
self._base_url,
timeout_s or self.context_timeout_s,
)
def close(self) -> None:
self.stop_container()
def __enter__(self) -> "UVProvider":
if self._base_url is None:
base_url = self.start_container(self.space_id, port=self.port)
self.wait_for_ready(base_url, timeout_s=self.context_timeout_s)
return self
def __exit__(self, exc_type, exc, tb) -> None:
self.stop_container()
def _find_free_port(self) -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("", 0))
sock.listen(1)
return sock.getsockname()[1]
@property
def base_url(self) -> str:
if self._base_url is None:
raise RuntimeError("UVProvider has not been started")
return self._base_url