"""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__) # --------------------------------------------------------------------------- # Constants — repo defaults (DESIGN.md §11.3, §11.4, deploy_*_space.md §3.7) # --------------------------------------------------------------------------- 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",) # --------------------------------------------------------------------------- # Errors # --------------------------------------------------------------------------- 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``.""" # --------------------------------------------------------------------------- # DeploymentResult # --------------------------------------------------------------------------- @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 # --------------------------------------------------------------------------- # Lazy dep loaders — patched by tests # --------------------------------------------------------------------------- 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 # --------------------------------------------------------------------------- # Argument validation helpers # --------------------------------------------------------------------------- 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 # --------------------------------------------------------------------------- # Command construction # --------------------------------------------------------------------------- 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-type= [--revision=]`` """ _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 # --------------------------------------------------------------------------- # push_lora_to_hub (DESIGN.md §11.3) # --------------------------------------------------------------------------- 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, ) # --------------------------------------------------------------------------- # push_env_space (deploy_env_space.md §4.4, §6.3) # --------------------------------------------------------------------------- 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, ) # --------------------------------------------------------------------------- # push_demo_space (deploy_demo_space.md §3.1, §3.7) # --------------------------------------------------------------------------- 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, ) # --------------------------------------------------------------------------- # push_dataset (DESIGN.md §11.4) # --------------------------------------------------------------------------- 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", ]