defmodule HuggingfaceClient.Hub.Repos do
@moduledoc """
Repository management on the HuggingFace Hub.
Covers creating, updating, and deleting repos, branch/tag operations,
and individual file downloads.
## Usage
# Create a new model repository
{:ok, repo} = HuggingfaceClient.Hub.Repos.create("my-model",
repo_type: :model,
private: false,
access_token: token
)
# Download a single file
{:ok, content} = HuggingfaceClient.Hub.Repos.download_file(
"gpt2", "config.json",
access_token: token
)
"""
alias HuggingfaceClient.Error.InputError
alias HuggingfaceClient.Error.HubApiError
alias HuggingfaceClient.Hub.Client
# ── Create / Delete ───────────────────────────────────────────────────────────
@doc """
Creates a new repository on the Hub.
## Required arguments
- `repo_id` — `"namespace/name"` or just `"name"` (owner inferred from token)
## Optional options
- `:repo_type` — `:model` (default), `:dataset`, `:space`
- `:private` — `false` (default)
- `:exist_ok` — if `true`, return `:ok` on 409 Conflict (default `false`)
- `:access_token`
"""
@spec create(String.t(), keyword()) :: {:ok, map()} | {:error, Exception.t()}
def create(repo_id, opts \\ []) do
type = opts[:repo_type] || :model
private = opts[:private] || false
exist_ok = opts[:exist_ok] || false
payload = %{
"name" => repo_id,
"type" => type_string(type),
"private" => private
}
case Client.post("/api/repos/create", payload, opts) do
{:error, %{http_response: %{status: 409}}} when exist_ok ->
{:ok, %{"id" => repo_id, "type" => type_string(type)}}
other ->
other
end
end
@doc """
Deletes a repository. Irreversible.
## Options
- `:repo_type`, `:access_token`
"""
@spec delete(String.t(), keyword()) :: :ok | {:error, Exception.t()}
def delete(repo_id, opts \\ []) do
type = opts[:repo_type] || :model
payload = %{"name" => repo_id, "type" => type_string(type)}
Client.post("/api/repos/delete", payload, Keyword.put(opts, :method, :delete))
end
@doc """
Updates repository metadata (visibility, description, etc.).
"""
@spec update(String.t(), keyword()) :: {:ok, map()} | {:error, Exception.t()}
def update(repo_id, opts \\ []) do
type = opts[:repo_type] || :model
prefix = type_prefix(type)
payload =
%{}
|> maybe_put("private", opts[:private])
|> maybe_put("description", opts[:description])
|> maybe_put("gated", opts[:gated])
Client.put("/api/#{prefix}/#{repo_id}/settings", payload, opts)
end
# ── Branch management ─────────────────────────────────────────────────────────
@doc """
Creates a new branch in a repository.
## Required arguments
- `repo_id` — full repo id (e.g. `"my-org/my-model"`)
- `branch_name` — name for the new branch
## Options
- `:starting_point` — commit SHA or existing branch (default: `"main"`)
- `:repo_type`, `:access_token`
"""
@spec create_branch(String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, Exception.t()}
def create_branch(repo_id, branch_name, opts \\ [])
def create_branch(repo_id, branch_name, opts) when is_binary(repo_id) and is_binary(branch_name) do
type = opts[:repo_type] || :model
prefix = type_prefix(type)
payload = %{
"branch" => branch_name,
"startingPoint" => opts[:starting_point] || "main"
}
Client.post("/api/#{prefix}/#{repo_id}/branch", payload, opts)
end
def create_branch(_repo_id, _branch_name, _opts) do
{:error, InputError.exception("create_branch/3 requires binary repo_id and branch_name")}
end
@doc """
Deletes a branch from a repository.
## Required arguments
- `repo_id` — full repo id
- `branch_name` — branch to delete
"""
@spec delete_branch(String.t(), String.t(), keyword()) :: :ok | {:error, Exception.t()}
def delete_branch(repo_id, branch_name, opts \\ [])
def delete_branch(repo_id, branch_name, opts) when is_binary(repo_id) and is_binary(branch_name) do
type = opts[:repo_type] || :model
prefix = type_prefix(type)
Client.delete("/api/#{prefix}/#{repo_id}/branch/#{branch_name}", opts)
end
def delete_branch(_repo_id, _branch_name, _opts) do
{:error, InputError.exception("delete_branch/3 requires binary repo_id and branch_name")}
end
@doc """
Creates a new tag pointing to a commit or branch.
"""
@spec create_tag(String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, Exception.t()}
def create_tag(repo_id, tag, opts \\ []) do
type = opts[:repo_type] || :model
prefix = type_prefix(type)
payload = %{
"tag" => tag,
"startingPoint" => opts[:starting_point] || "main"
}
Client.post("/api/#{prefix}/#{repo_id}/tag", payload, opts)
end
@doc "Deletes a tag from a repository."
@spec delete_tag(String.t(), String.t(), keyword()) :: :ok | {:error, Exception.t()}
def delete_tag(repo_id, tag, opts \\ []) do
type = opts[:repo_type] || :model
prefix = type_prefix(type)
Client.delete("/api/#{prefix}/#{repo_id}/tag/#{tag}", opts)
end
# ── File download ─────────────────────────────────────────────────────────────
@doc """
Downloads a single file from a Hub repository.
Returns `{:ok, binary}` on success.
## Required arguments
- `repo_id` — e.g. `"gpt2"` or `"meta-llama/Llama-3.1-8B-Instruct"`
- `file_path` — path inside the repo, e.g. `"config.json"`
## Options
- `:repo_type` — `:model` (default), `:dataset`, `:space`
- `:revision` — branch/tag/commit SHA (default: `"main"`)
- `:access_token`
"""
@spec download_file(String.t(), String.t(), keyword()) :: {:ok, binary()} | {:error, Exception.t()}
def download_file(repo_id, file_path, opts \\ [])
def download_file(repo_id, file_path, opts) when is_binary(repo_id) and is_binary(file_path) do
type = opts[:repo_type] || :model
revision = opts[:revision] || "main"
prefix = type_prefix(type)
url =
"#{HuggingfaceClient.Inference.Config.hub_url()}/#{prefix}/#{repo_id}/resolve/#{revision}/#{file_path}"
headers = Client.build_headers(nil, nil, opts)
req_opts = opts[:req_opts] || []
case Req.get(
url,
[finch: HuggingfaceClient.Finch, headers: headers, receive_timeout: 60_000] ++ req_opts
) do
{:ok, %{status: 200, body: body}} ->
{:ok, body}
{:ok, %{status: status, body: body}} ->
{:error,
%HubApiError{
message: "download_file failed with HTTP #{status}",
http_request: %{url: url, method: "GET", headers: headers, body: nil},
http_response: %{request_id: nil, status: status, body: body}
}}
{:error, reason} ->
{:error,
%HubApiError{
message: "download_file network error: #{inspect(reason)}",
http_request: %{url: url, method: "GET", headers: headers, body: nil},
http_response: %{request_id: nil, status: nil, body: nil}
}}
end
end
def download_file(_repo_id, _file_path, _opts) do
{:error, InputError.exception("download_file/3 requires binary repo_id and file_path")}
end
@doc """
Lists all files in a repository at a given revision.
Returns `{:ok, [file_map]}` where each map has `"path"`, `"size"`, `"type"`, `"oid"`.
"""
@spec list_files(String.t(), keyword()) :: {:ok, list()} | {:error, Exception.t()}
def list_files(repo_id, opts \\ []) do
type = opts[:repo_type] || :model
revision = opts[:revision] || "main"
prefix = type_prefix(type)
Client.get("/api/#{prefix}/#{repo_id}/tree/#{revision}", opts)
end
# ── Commit listing ────────────────────────────────────────────────────────────
@doc """
Returns a lazy stream of commits for a repository.
"""
@spec list_commits(String.t(), keyword()) :: Enumerable.t()
def list_commits(repo_id, opts \\ []) do
type = opts[:repo_type] || :model
prefix = type_prefix(type)
Client.paginated_stream("/api/#{prefix}/#{repo_id}/commits/main", opts)
|> Stream.map(&normalise_commit/1)
end
# ── Private helpers ───────────────────────────────────────────────────────────
defp normalise_commit(c) do
%{
"oid" => c["id"],
"title" => c["title"],
"message" => c["message"],
"date" => c["date"],
"authors" =>
Enum.map(c["authors"] || [], fn a ->
%{"username" => a["user"], "avatar_url" => a["avatar"]}
end)
}
end
defp type_string(:model), do: "model"
defp type_string(:dataset), do: "dataset"
defp type_string(:space), do: "space"
defp type_string(s) when is_binary(s), do: s
defp type_prefix(:model), do: "models"
defp type_prefix(:dataset), do: "datasets"
defp type_prefix(:space), do: "spaces"
defp type_prefix(nil), do: "models"
defp type_prefix(s) when is_binary(s), do: s
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end