Team Ai
Apppublic

openenv/echo_env

sourceHugging Faceupdated 1d agoView on Hugging Face
6likes
fork.py173 linesDownload Raw Back to commands
1# SPDX-License-Identifier: BSD-3-Clause2 3"""Fork (duplicate) a Hugging Face Space using the Hub API."""4 5from __future__ import annotations6 7from typing import Annotated8 9import typer10from huggingface_hub import HfApi, login, whoami11 12from .._cli_utils import _extract_hf_username, console13 14app = typer.Typer(15    help="Fork (duplicate) an OpenEnv environment on Hugging Face to your account"16)17 18 19def _parse_key_value(s: str) -> tuple[str, str]:20    """Parse KEY=VALUE string. Raises BadParameter if no '='."""21    if "=" not in s:22        raise typer.BadParameter(23            f"Expected KEY=VALUE format, got: {s!r}. "24            "Use --set-env KEY=VALUE or --set-secret KEY=VALUE"25        )26    key, _, value = s.partition("=")27    key = key.strip()28    if not key:29        raise typer.BadParameter(f"Empty key in: {s!r}")30    return key, value.strip()31 32 33def _ensure_hf_authenticated() -> str:34    """Ensure user is authenticated with Hugging Face. Returns username."""35    try:36        username = _extract_hf_username(whoami())37        if not username:38            raise ValueError("Could not extract username from whoami response")39        console.print(f"[bold green]✓[/bold green] Authenticated as: {username}")40        return username41    except Exception:42        console.print(43            "[bold yellow]Not authenticated with Hugging Face. Please login...[/bold yellow]"44        )45        try:46            login()47            username = _extract_hf_username(whoami())48            if not username:49                raise ValueError("Could not extract username from whoami response")50            console.print(f"[bold green]✓[/bold green] Authenticated as: {username}")51            return username52        except Exception as e:53            raise typer.BadParameter(54                f"Hugging Face authentication failed: {e}. Please run login manually."55            ) from e56 57 58@app.command()59def fork(60    source_space: Annotated[61        str,62        typer.Argument(63            help="Source Space ID in format 'owner/space-name' (e.g. org/my-openenv-space)"64        ),65    ],66    repo_id: Annotated[67        str | None,68        typer.Option(69            "--repo-id",70            "-r",71            help="Target repo ID for the fork (default: created under your account with same name)",72        ),73    ] = None,74    private: Annotated[75        bool,76        typer.Option("--private", help="Create the forked Space as private"),77    ] = False,78    set_env: Annotated[79        list[str],80        typer.Option(81            "--set-env",82            "-e",83            help="Set Space variable (public). Can be repeated. Format: KEY=VALUE",84        ),85    ] = [],86    set_secret: Annotated[87        list[str],88        typer.Option(89            "--set-secret",90            "--secret",91            "-s",92            help="Set Space secret. Can be repeated. Format: KEY=VALUE",93        ),94    ] = [],95    hardware: Annotated[96        str | None,97        typer.Option(98            "--hardware",99            "-H",100            help="Request hardware (e.g. t4-medium, cpu-basic). See Hub docs for options.",101        ),102    ] = None,103) -> None:104    """105    Fork (duplicate) a Hugging Face Space to your account using the Hub API.106 107    Uses the Hugging Face duplicate_space API. You can set environment variables108    and secrets, and request hardware/storage/sleep time at creation time.109 110    Examples:111 112        ```bash113        $ openenv fork owner/source-space114        $ openenv fork owner/source-space --private115        $ openenv fork owner/source-space --repo-id myuser/my-fork116        $ openenv fork owner/source-space --set-env MODEL_ID=user/model --set-secret HF_TOKEN=hf_xxx117        $ openenv fork owner/source-space --hardware t4-medium118        ```119    """120    if "/" not in source_space or source_space.count("/") != 1:121        raise typer.BadParameter(122            f"Invalid source Space ID: {source_space!r}. Expected format: 'owner/space-name'"123        )124 125    _ensure_hf_authenticated()126    api = HfApi()127 128    # Build kwargs for duplicate_space (only pass what we have)129    dup_kwargs: dict = {130        "from_id": source_space,131        "private": private,132    }133    if set_env:134        dup_kwargs["variables"] = [135            {"key": k, "value": v} for k, v in (_parse_key_value(x) for x in set_env)136        ]137    if set_secret:138        dup_kwargs["secrets"] = [139            {"key": k, "value": v} for k, v in (_parse_key_value(x) for x in set_secret)140        ]141    # HF API requires hardware when duplicating; default to free cpu-basic142    dup_kwargs["hardware"] = hardware if hardware is not None else "cpu-basic"143    if repo_id is not None:144        if "/" not in repo_id or repo_id.count("/") != 1:145            raise typer.BadParameter(146                f"Invalid --repo-id: {repo_id!r}. Expected format: 'username/repo-name'"147            )148        dup_kwargs["to_id"] = repo_id149 150    console.print(f"[bold cyan]Forking Space {source_space}...[/bold cyan]")151    try:152        result = api.duplicate_space(**dup_kwargs)153    except Exception as e:154        console.print(f"[bold red]✗[/bold red] Fork failed: {e}")155        raise typer.Exit(1) from e156 157    # result is RepoUrl (str-like) or similar; get repo_id for display158    if hasattr(result, "repo_id"):159        new_repo_id = result.repo_id160    elif isinstance(result, str):161        # URL like https://huggingface.co/spaces/owner/name -> owner/name162        if "/spaces/" in result:163            new_repo_id = result.split("/spaces/")[-1].rstrip("/")164        else:165            new_repo_id = result166    else:167        new_repo_id = getattr(result, "repo_id", str(result))168 169    console.print("[bold green]✓[/bold green] Space forked successfully")170    console.print(171        f"[bold]Space URL:[/bold] https://huggingface.co/spaces/{new_repo_id}"172    )173