hfdol.deploy¶
Deploy webapps to HuggingFace Spaces.
This module wraps huggingface_hub with opinionated, idempotent helpers for
publishing a Python package’s webapp to a Docker-SDK Space. The primary entry
point is deploy_webapp(), which composes the smaller helpers into a
full create-or-update + restart cycle.
Functions are designed to be composable: each does one thing, fails loudly, and prints what it’s about to do. The module assumes you already have a package on PyPI (or a Dockerfile that knows how to install it).
Quick reference:
>>> from hfdol.deploy import deploy_webapp
>>> deploy_webapp(
... repo_id="thorwhalen/typola",
... source_dir="/path/to/staging",
... )
For ad-hoc operations:
>>> from hfdol.deploy import factory_reboot, wait_for_build
>>> factory_reboot("thorwhalen/typola")
>>> wait_for_build("thorwhalen/typola")
Token resolution: Operations need a write-scoped HF token. By default,
ensure_write_token() reads HF_WRITE_TOKEN from the environment, then
falls back to sourcing ~/.keys (a shell file that exports it). If you keep
your write token elsewhere, pass token=... to any function explicitly.
Functions
|
Create an HF Space if it doesn't exist; otherwise return its info. |
|
End-to-end: create-if-missing → upload → factory reboot → wait for build. |
|
Return a write-scoped HF token; source |
|
Trigger a from-scratch rebuild of the Space (busts Docker layer cache). |
|
Render a Dockerfile for the canonical "PyPI package + FastAPI + React" pattern. |
|
Render an HF Space README with the YAML frontmatter HF requires. |
|
Lay out the contents of a Space repo in a local staging directory. |
|
Upload a local folder to an HF Space, replacing the live files. |
|
Poll Space status until it lands in a terminal state or times out. |
Classes
|
Outcome of waiting for a Space build. |
- class hfdol.deploy.BuildResult(final_stage, elapsed_seconds, timed_out=False, stages_seen=<factory>)[source]¶
Bases:
objectOutcome of waiting for a Space build.
- hfdol.deploy.create_or_update_space(repo_id, *, sdk='docker', private=False, token=None, exist_ok=True)[source]¶
Create an HF Space if it doesn’t exist; otherwise return its info.
Idempotent. Safe to call before every deploy.
- Parameters:
repo_id (
str) –owner/space-name.sdk (
str) – Space SDK —docker(recommended for custom apps),gradio,streamlit,static.private (
bool) – If True, create as private (only matters on first creation).token (
Optional[str]) – Write token; defaults toensure_write_token().exist_ok (
bool) – If True (default), an already-existing repo is fine.
- Returns:
SpaceInfofor the (now-existing) Space.
- hfdol.deploy.deploy_webapp(repo_id, source_dir, *, sdk='docker', private=False, token=None, message='Deploy webapp', ignore_patterns=('node_modules/**', '__pycache__/**', '*.pyc', '.DS_Store', '.git/**'), delete_patterns=None, rebuild=True, wait=True, timeout=900)[source]¶
End-to-end: create-if-missing → upload → factory reboot → wait for build.
The most common deploy entry point.
source_dirshould contain everything that goes on the Space repo:Dockerfile,README.md(with HF YAML frontmatter), and your app source (e.g.,webapp/api/,webapp/ui/dist/).- Parameters:
repo_id (
str) –owner/space-name.sdk (
str) – Space SDK (only matters on first creation).private (
bool) – Create as private (only matters on first creation).token (
Optional[str]) – Write token; defaults toensure_write_token().message (
str) – Commit message.ignore_patterns (
Iterable[str]) – Glob patterns to skip during upload.delete_patterns (
Optional[Iterable[str]]) – Glob patterns to delete from the Space before uploading (e.g., to clear stale build artifacts).rebuild (
bool) – If True (default), trigger a factory reboot after upload. Set False to skip — useful when only README/metadata changed.wait (
bool) – If True (default), poll until the build settles.timeout (
int) – Max seconds to wait for the build.
- Return type:
- Returns:
BuildResultifwait=True, elseNone.
- hfdol.deploy.ensure_write_token(*, env_var='HF_WRITE_TOKEN', keys_file=PosixPath('/home/runner/.keys'))[source]¶
Return a write-scoped HF token; source
~/.keysif needed.HF Spaces operations (create, upload, restart) need a write token. The default
HF_TOKENis often read-only. Convention: keep the write token in~/.keysasexport HF_WRITE_TOKEN=hf_....- Parameters:
- Return type:
- Returns:
The token string.
- Raises:
SystemExit – If no token can be resolved.
- hfdol.deploy.factory_reboot(repo_id, *, token=None)[source]¶
Trigger a from-scratch rebuild of the Space (busts Docker layer cache).
A normal restart reuses cached Docker layers. A factory reboot blows the cache away — required when
pip installshould re-fetch a new package version, or when system deps in the Dockerfile changed.- Parameters:
repo_id (
str) –owner/space-name.token (
Optional[str]) – Write token; defaults toensure_write_token().
- Return type:
- hfdol.deploy.render_pypi_webapp_dockerfile(package, *, extras='web', version_spec='', python_version='3.12', api_module='webapp.api.main:app', port=7860, api_dir='webapp/api', ui_dist_dir='webapp/ui/dist', extra_run_lines=())[source]¶
Render a Dockerfile for the canonical “PyPI package + FastAPI + React” pattern.
Assumes:
Your Python package is on PyPI (with optional
[extras]).Your FastAPI app is in
api_dir/(will be COPYed into the image).Your React UI is pre-built in
ui_dist_dir/(will be COPYed in).
Why pre-build the UI locally instead of in the Dockerfile? Because
package-lock.jsonmay havefile:deps to a local monorepo that doesn’t exist on the build machine. Pre-building sidesteps the issue.- Parameters:
package (
str) – PyPI package name.extras (
str) – Extras spec (without brackets), e.g."web"→package[web]. Pass empty string to omit.version_spec (
str) – PEP 440 version specifier, e.g."~=0.1.1". Empty string means “latest”.python_version (
str) – Python base image tag ("3.12"→python:3.12-slim).api_module (
str) –module.path:app_attrfor uvicorn.port (
int) – Port to expose (HF Spaces uses 7860 for Docker SDK).api_dir (
str) – Local path of the FastAPI source, relative to the Space repo root.ui_dist_dir (
str) – Local path of the pre-built React dist, relative to the Space repo root.extra_run_lines (
Iterable[str]) – AdditionalRUNlines to insert (e.g., to pre-cache datasets so cold start is sub-second).
- Return type:
- Returns:
The Dockerfile contents as a string.
- hfdol.deploy.render_space_readme(title, *, color_from='blue', color_to='green', sdk='docker', port=7860, pinned=False, body='')[source]¶
Render an HF Space README with the YAML frontmatter HF requires.
The frontmatter sets the Space’s metadata (sdk, port, etc.) — required even for Docker SDK Spaces. Body is appended after.
- Parameters:
title (
str) – Display title for the Space.color_from (
str) – Gradient start color (HF picks from a small palette).color_to (
str) – Gradient end color.sdk (
str) –"docker","gradio","streamlit","static".port (
int) –app_port(Docker SDK uses this; ignored otherwise).pinned (
bool) – Whether to pin to the user’s HF profile.body (
str) – Markdown content to append after the frontmatter.
- Return type:
- hfdol.deploy.stage_webapp(*, staging_dir, api_src, ui_dist_src, api_dir_in_space='webapp/api', ui_dist_dir_in_space='webapp/ui/dist', dockerfile_text=None, readme_text=None, extra_files=None)[source]¶
Lay out the contents of a Space repo in a local staging directory.
Idempotent: re-running replaces the API and UI dirs but reuses what’s already there for things you didn’t pass (e.g., an existing Dockerfile).
- Parameters:
staging_dir (
str|Path) – Where to assemble the Space contents.api_src (
str|Path) – Local source directory for the FastAPI app.ui_dist_src (
str|Path) – Local source directory for the pre-built React dist.api_dir_in_space (
str) – Path inside the Space repo for the API.ui_dist_dir_in_space (
str) – Path inside the Space repo for the UI dist.dockerfile_text (
Optional[str]) – Dockerfile contents (userender_pypi_webapp_dockerfile()). If None, only writes one if the staging dir doesn’t already have it.readme_text (
Optional[str]) – README contents (userender_space_readme()). Same conditional behavior asdockerfile_text.extra_files (
Optional[dict[str,str]]) –{relative_path: contents}extras (e.g.,.gitignore).
- Return type:
- Returns:
The staging directory path.
- hfdol.deploy.upload_app_dir(repo_id, folder_path, *, message='Update app contents', token=None, ignore_patterns=('node_modules/**', '__pycache__/**', '*.pyc', '.DS_Store', '.git/**'), delete_patterns=None)[source]¶
Upload a local folder to an HF Space, replacing the live files.
Wraps
huggingface_hub.HfApi.upload_folder()with sensible ignore patterns and a printed plan. Existing files on the Space that are not in the local folder are NOT deleted unless you passdelete_patterns.- Parameters:
repo_id (
str) –owner/space-name.message (
str) – Commit message on the Space repo.token (
Optional[str]) – Write token; defaults toensure_write_token().delete_patterns (
Optional[Iterable[str]]) – Optional glob patterns of remote files to delete before upload (e.g.,["webapp/ui/dist/**"]to ensure stale build artifacts are removed).
- Return type:
- hfdol.deploy.wait_for_build(repo_id, *, token=None, timeout=900, poll_interval=15, on_stage_change=None)[source]¶
Poll Space status until it lands in a terminal state or times out.
Terminal states:
RUNNING,RUNNING_BUILDING(success);BUILD_ERROR,RUNTIME_ERROR(failure). Times out aftertimeoutseconds.- Parameters:
repo_id (
str) –owner/space-name.token (
Optional[str]) – Write token (read access also fine); defaults to env.timeout (
int) – Max seconds to wait.poll_interval (
int) – Seconds between polls.on_stage_change (
Optional[Callable[[str,int],None]]) – Callback(stage, elapsed_seconds) -> Noneinvoked each time the stage transitions. Default prints to stdout.
- Return type:
- Returns: