| """Cell 24 — Hugging Face Hub + Spaces deployment. |
| |
| Implements ``docs/modules/deploy_env_space.md`` §8.2 and DESIGN.md §11.3, §11.4 |
| deliverables. Four push helpers, all using the **new** ``hf upload`` CLI per |
| deploy_env_space.md §8.2 (deprecated ``huggingface-cli`` is forbidden). |
| |
| Public surface: |
| * ``push_lora_to_hub(checkpoint_path, repo_id, token)`` — LoRA-only adapter |
| push with ``safe_serialization=True``. Never the naive 4-bit → 16-bit merge |
| path (DESIGN.md §10.5, CLAUDE.md §13). |
| * ``push_env_space(repo_id, token)`` — Docker-based env Space (CPU basic, |
| deploy_env_space.md §6.3). |
| * ``push_demo_space(repo_id, token)`` — Demo Space targeting ZeroGPU with |
| A10G fallback (deploy_demo_space.md §3.1, §3.7). |
| * ``push_dataset(brief_path, repo_id, token)`` — ``driftcall-indic-briefs`` |
| dataset (DESIGN.md §11.4). |
| |
| All four return a frozen :class:`DeploymentResult` so a caller can audit the |
| exact ``hf`` invocation. Heavy deps (``huggingface_hub``, ``subprocess`` for |
| ``hf``) are loaded lazily; tests monkeypatch the loaders to assert the |
| command construction without making network calls. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import logging |
| import subprocess |
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import TYPE_CHECKING, Any, Literal |
|
|
| if TYPE_CHECKING: |
| from collections.abc import Callable, Mapping |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
| |
| |
|
|
|
|
| DEFAULT_LORA_REPO_ID: str = "DGXAI/gemma-3n-e2b-driftcall-lora" |
| DEFAULT_DATASET_REPO_ID: str = "driftcall/driftcall-indic-briefs" |
| DEFAULT_ENV_SPACE_ID: str = "driftcall/driftcall-env" |
| DEFAULT_DEMO_SPACE_ID: str = "driftcall/driftcall-demo" |
|
|
| RepoType = Literal["model", "dataset", "space"] |
|
|
| DEPRECATED_CLI_NAMES: tuple[str, ...] = ("huggingface-cli",) |
|
|
|
|
| |
| |
| |
|
|
|
|
| class DeploymentError(Exception): |
| """Root for every typed deploy-cell error.""" |
|
|
|
|
| class HFTokenMissingError(DeploymentError): |
| """Raised when the ``token`` argument is None or empty.""" |
|
|
|
|
| class CheckpointPathMissingError(DeploymentError): |
| """Raised when the LoRA checkpoint path does not exist.""" |
|
|
|
|
| class NaiveMergeForbiddenError(DeploymentError): |
| """Raised when the caller requests a 4-bit → 16-bit merge path |
| (CLAUDE.md §13, DESIGN.md §10.5).""" |
|
|
|
|
| class DeploymentCommandError(DeploymentError): |
| """Raised when the ``hf upload`` invocation exits non-zero.""" |
|
|
|
|
| class DeprecatedCliError(DeploymentError): |
| """Raised when a caller would invoke ``huggingface-cli`` instead of ``hf``.""" |
|
|
|
|
| |
| |
| |
|
|
|
|
| @dataclass(frozen=True) |
| class DeploymentResult: |
| """Audit record for one deployment call.""" |
|
|
| repo_id: str |
| repo_type: RepoType |
| command: tuple[str, ...] |
| return_code: int |
| stdout: str |
| stderr: str |
| success: bool |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _load_hf_api() -> Any: |
| """Return the ``huggingface_hub.HfApi`` class. Patched in tests.""" |
|
|
| from huggingface_hub import HfApi |
|
|
| return HfApi |
|
|
|
|
| def _load_subprocess_run() -> Callable[..., Any]: |
| """Return ``subprocess.run``. Patched in tests.""" |
|
|
| return subprocess.run |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _validate_token(token: str | None) -> str: |
| if token is None or token.strip() == "": |
| raise HFTokenMissingError("token argument is required and must be non-empty") |
| return token |
|
|
|
|
| def _validate_repo_id(repo_id: str) -> str: |
| if not isinstance(repo_id, str) or "/" not in repo_id: |
| raise DeploymentError(f"repo_id must be 'org/name'; got {repo_id!r}") |
| org, name = repo_id.split("/", 1) |
| if not org or not name: |
| raise DeploymentError(f"repo_id must be 'org/name'; got {repo_id!r}") |
| return repo_id |
|
|
|
|
| def _validate_path_exists(path: Path, *, label: str) -> Path: |
| if not isinstance(path, Path): |
| raise DeploymentError(f"{label} must be pathlib.Path; got {type(path).__name__}") |
| if not path.exists(): |
| raise CheckpointPathMissingError(f"{label} not found: {path}") |
| return path |
|
|
|
|
| def _ensure_not_deprecated(executable: str) -> str: |
| if executable in DEPRECATED_CLI_NAMES: |
| raise DeprecatedCliError( |
| f"{executable!r} is deprecated; use 'hf upload' (deploy_env_space.md §8.2)", |
| ) |
| return executable |
|
|
|
|
| |
| |
| |
|
|
|
|
| def build_hf_upload_command( |
| *, |
| repo_id: str, |
| local_path: Path, |
| repo_type: RepoType, |
| revision: str | None = None, |
| extra_args: tuple[str, ...] = (), |
| ) -> tuple[str, ...]: |
| """Construct an argv tuple for ``hf upload``. |
| |
| Shape per the new ``hf`` CLI (deploy_env_space.md §8.2): |
| ``hf upload <repo_id> <local_path> --repo-type=<type> [--revision=<r>]`` |
| """ |
|
|
| _validate_repo_id(repo_id) |
| if repo_type not in ("model", "dataset", "space"): |
| raise DeploymentError(f"repo_type must be model|dataset|space; got {repo_type!r}") |
| executable = _ensure_not_deprecated("hf") |
| cmd: list[str] = [ |
| executable, |
| "upload", |
| repo_id, |
| str(local_path), |
| f"--repo-type={repo_type}", |
| ] |
| if revision is not None: |
| cmd.append(f"--revision={revision}") |
| cmd.extend(extra_args) |
| return tuple(cmd) |
|
|
|
|
| def _run_command( |
| cmd: tuple[str, ...], |
| *, |
| token: str, |
| env_extra: Mapping[str, str] | None = None, |
| ) -> tuple[int, str, str]: |
| """Invoke ``cmd`` via subprocess; return ``(rc, stdout, stderr)``. |
| |
| The token is passed via environment, never via argv (avoids shell |
| history leak). ``env_extra`` lets callers add per-deploy env vars. |
| """ |
|
|
| import os |
|
|
| run = _load_subprocess_run() |
| env = dict(os.environ) |
| env["HF_TOKEN"] = token |
| env["HUGGINGFACE_HUB_TOKEN"] = token |
| if env_extra is not None: |
| env.update(env_extra) |
| try: |
| completed = run( |
| list(cmd), |
| check=False, |
| capture_output=True, |
| text=True, |
| env=env, |
| ) |
| except FileNotFoundError as exc: |
| raise DeploymentCommandError(f"hf CLI not found on PATH: {exc}") from exc |
| rc = int(getattr(completed, "returncode", 1)) |
| stdout = str(getattr(completed, "stdout", "") or "") |
| stderr = str(getattr(completed, "stderr", "") or "") |
| return rc, stdout, stderr |
|
|
|
|
| |
| |
| |
|
|
|
|
| def push_lora_to_hub( |
| checkpoint_path: Path, |
| repo_id: str = DEFAULT_LORA_REPO_ID, |
| token: str | None = None, |
| *, |
| merge_4bit_to_16bit: bool = False, |
| revision: str | None = None, |
| ) -> DeploymentResult: |
| """Push the LoRA adapter directory to the HF Hub. |
| |
| Pushes adapter-only artifacts (``adapter_config.json``, |
| ``adapter_model.safetensors``, ``tokenizer.json``, ``README.md``). |
| Never the merged-fp16 weights — see DESIGN.md §10.5 + CLAUDE.md §13: |
| naive 4-bit → 16-bit merging is the catastrophic-quality path. |
| """ |
|
|
| if merge_4bit_to_16bit: |
| raise NaiveMergeForbiddenError( |
| "merge_4bit_to_16bit=True is forbidden: 4-bit → 16-bit merge " |
| "produces silently broken weights (DESIGN.md §10.5, CLAUDE.md §13). " |
| "Push the LoRA adapter only.", |
| ) |
| resolved_token = _validate_token(token) |
| _validate_path_exists(checkpoint_path, label="checkpoint_path") |
| cmd = build_hf_upload_command( |
| repo_id=repo_id, |
| local_path=checkpoint_path, |
| repo_type="model", |
| revision=revision, |
| ) |
| rc, stdout, stderr = _run_command(cmd, token=resolved_token) |
| success = rc == 0 |
| if not success: |
| logger.warning("push_lora_to_hub failed (rc=%d): %s", rc, stderr) |
| return DeploymentResult( |
| repo_id=repo_id, |
| repo_type="model", |
| command=cmd, |
| return_code=rc, |
| stdout=stdout, |
| stderr=stderr, |
| success=success, |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def push_env_space( |
| repo_id: str = DEFAULT_ENV_SPACE_ID, |
| token: str | None = None, |
| *, |
| space_dir: Path | None = None, |
| revision: str | None = None, |
| ) -> DeploymentResult: |
| """Push the env Space (Docker SDK, CPU basic). deploy_env_space.md §4.4.""" |
|
|
| resolved_token = _validate_token(token) |
| if space_dir is None: |
| space_dir = Path(".") |
| _validate_path_exists(space_dir, label="space_dir") |
| cmd = build_hf_upload_command( |
| repo_id=repo_id, |
| local_path=space_dir, |
| repo_type="space", |
| revision=revision, |
| ) |
| rc, stdout, stderr = _run_command(cmd, token=resolved_token) |
| success = rc == 0 |
| return DeploymentResult( |
| repo_id=repo_id, |
| repo_type="space", |
| command=cmd, |
| return_code=rc, |
| stdout=stdout, |
| stderr=stderr, |
| success=success, |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def push_demo_space( |
| repo_id: str = DEFAULT_DEMO_SPACE_ID, |
| token: str | None = None, |
| *, |
| space_dir: Path | None = None, |
| hardware: Literal["zero-gpu", "a10g-small"] = "zero-gpu", |
| revision: str | None = None, |
| ) -> DeploymentResult: |
| """Push the demo Space. Default hardware ``zero-gpu`` per |
| deploy_demo_space.md §3.1; pass ``a10g-small`` to redeploy on the |
| fallback hardware (§3.1 step 2).""" |
|
|
| resolved_token = _validate_token(token) |
| if hardware not in ("zero-gpu", "a10g-small"): |
| raise DeploymentError( |
| f"hardware must be zero-gpu|a10g-small; got {hardware!r}", |
| ) |
| if space_dir is None: |
| space_dir = Path(".") |
| _validate_path_exists(space_dir, label="space_dir") |
| cmd = build_hf_upload_command( |
| repo_id=repo_id, |
| local_path=space_dir, |
| repo_type="space", |
| revision=revision, |
| ) |
| env_extra = {"DRIFTCALL_HARDWARE": hardware} |
| rc, stdout, stderr = _run_command(cmd, token=resolved_token, env_extra=env_extra) |
| success = rc == 0 |
| return DeploymentResult( |
| repo_id=repo_id, |
| repo_type="space", |
| command=cmd, |
| return_code=rc, |
| stdout=stdout, |
| stderr=stderr, |
| success=success, |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def push_dataset( |
| brief_path: Path, |
| repo_id: str = DEFAULT_DATASET_REPO_ID, |
| token: str | None = None, |
| *, |
| revision: str | None = None, |
| ) -> DeploymentResult: |
| """Push the ``driftcall-indic-briefs`` dataset (DESIGN.md §11.4).""" |
|
|
| resolved_token = _validate_token(token) |
| _validate_path_exists(brief_path, label="brief_path") |
| cmd = build_hf_upload_command( |
| repo_id=repo_id, |
| local_path=brief_path, |
| repo_type="dataset", |
| revision=revision, |
| ) |
| rc, stdout, stderr = _run_command(cmd, token=resolved_token) |
| success = rc == 0 |
| return DeploymentResult( |
| repo_id=repo_id, |
| repo_type="dataset", |
| command=cmd, |
| return_code=rc, |
| stdout=stdout, |
| stderr=stderr, |
| success=success, |
| ) |
|
|
|
|
| __all__ = [ |
| "DEFAULT_DATASET_REPO_ID", |
| "DEFAULT_DEMO_SPACE_ID", |
| "DEFAULT_ENV_SPACE_ID", |
| "DEFAULT_LORA_REPO_ID", |
| "DEPRECATED_CLI_NAMES", |
| "CheckpointPathMissingError", |
| "DeploymentCommandError", |
| "DeploymentError", |
| "DeploymentResult", |
| "DeprecatedCliError", |
| "HFTokenMissingError", |
| "NaiveMergeForbiddenError", |
| "RepoType", |
| "build_hf_upload_command", |
| "push_dataset", |
| "push_demo_space", |
| "push_env_space", |
| "push_lora_to_hub", |
| ] |
|
|