| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- # Copyright 2026 The HuggingFace Team. All rights reserved.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """Contains commands to manage webhooks on the Hugging Face Hub.
- Usage:
- # list all webhooks
- hf webhooks ls
- # show details of a single webhook
- hf webhooks info <webhook_id>
- # create a new webhook
- hf webhooks create --url https://example.com/hook --watch model:bert-base-uncased
- # create a webhook watching multiple items and domains
- hf webhooks create --url https://example.com/hook --watch org:HuggingFace --watch model:gpt2 --domain repo
- # update a webhook
- hf webhooks update <webhook_id> --url https://new-url.com/hook
- # enable / disable a webhook
- hf webhooks enable <webhook_id>
- hf webhooks disable <webhook_id>
- # delete a webhook
- hf webhooks delete <webhook_id>
- """
- import enum
- from typing import Annotated, get_args, get_type_hints
- import typer
- from huggingface_hub.constants import WEBHOOK_DOMAIN_T
- from huggingface_hub.hf_api import WebhookWatchedItem
- from ._cli_utils import (
- FormatWithAutoOpt,
- TokenOpt,
- get_hf_api,
- typer_factory,
- )
- from ._output import OutputFormatWithAuto, out
- # Build enums dynamically from Literal types to avoid duplication
- _WATCHED_TYPES = get_args(get_type_hints(WebhookWatchedItem)["type"])
- WatchedItemType = enum.Enum("WatchedItemType", {t: t for t in _WATCHED_TYPES}, type=str) # type: ignore[misc]
- _DOMAIN_TYPES = get_args(WEBHOOK_DOMAIN_T)
- WebhookDomain = enum.Enum("WebhookDomain", {d: d for d in _DOMAIN_TYPES}, type=str) # type: ignore[misc]
- def _parse_watch(values: list[str]) -> list[WebhookWatchedItem]:
- """Parse 'type:name' strings into WebhookWatchedItem objects.
- Args:
- values: List of strings in the format 'type:name'
- (e.g., 'model:bert-base-uncased', 'org:HuggingFace').
- Returns:
- List of WebhookWatchedItem objects.
- Raises:
- typer.BadParameter: If any value doesn't match the expected format.
- """
- items = []
- valid_types = tuple(_WATCHED_TYPES)
- for v in values:
- if ":" not in v:
- raise typer.BadParameter(
- f"Expected format 'type:name' (e.g. 'model:bert-base-uncased'), got '{v}'."
- f" Valid types: {', '.join(valid_types)}."
- )
- kind, name = v.split(":", 1)
- if kind not in valid_types:
- raise typer.BadParameter(f"Invalid type '{kind}'. Valid types: {', '.join(valid_types)}.")
- items.append(WebhookWatchedItem(type=kind, name=name)) # type: ignore
- return items
- webhooks_cli = typer_factory(help="Manage webhooks on the Hub.")
- @webhooks_cli.command(
- "list | ls",
- examples=[
- "hf webhooks ls",
- "hf webhooks ls --format json",
- "hf webhooks ls --format quiet",
- ],
- )
- def webhooks_ls(
- format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
- token: TokenOpt = None,
- ) -> None:
- """List all webhooks for the current user."""
- api = get_hf_api(token=token)
- results = [
- {
- "id": w.id,
- "url": w.url or "(job)",
- "disabled": w.disabled,
- "domains": w.domains or [],
- "watched": [f"{wi.type}:{wi.name}" for wi in (w.watched or [])],
- }
- for w in api.list_webhooks()
- ]
- out.table(results)
- @webhooks_cli.command(
- "info",
- examples=[
- "hf webhooks info abc123",
- ],
- )
- def webhooks_info(
- webhook_id: Annotated[str, typer.Argument(help="The ID of the webhook.")],
- format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
- token: TokenOpt = None,
- ) -> None:
- """Show full details for a single webhook."""
- api = get_hf_api(token=token)
- webhook = api.get_webhook(webhook_id)
- out.dict(webhook)
- @webhooks_cli.command(
- "create",
- examples=[
- "hf webhooks create --url https://example.com/hook --watch model:bert-base-uncased",
- "hf webhooks create --url https://example.com/hook --watch org:HuggingFace --watch model:gpt2 --domain repo",
- "hf webhooks create --job-id 687f911eaea852de79c4a50a --watch user:julien-c",
- ],
- )
- def webhooks_create(
- watch: Annotated[
- list[str],
- typer.Option(
- "--watch",
- help="Item to watch, in 'type:name' format (e.g. 'model:bert-base-uncased'). Repeatable.",
- ),
- ],
- url: Annotated[
- str | None,
- typer.Option(help="URL to send webhook payloads to. Mutually exclusive with --job-id."),
- ] = None,
- job_id: Annotated[
- str | None,
- typer.Option(
- "--job-id",
- help="ID of a Job to trigger (from job.id) instead of pinging a URL. Mutually exclusive with --url.",
- ),
- ] = None,
- domain: Annotated[
- list[WebhookDomain] | None,
- typer.Option(
- "--domain",
- help="Domain to watch: 'repo' or 'discussions'. Repeatable. Defaults to all domains.",
- ),
- ] = None,
- secret: Annotated[
- str | None,
- typer.Option(help="Optional secret used to sign webhook payloads."),
- ] = None,
- format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
- token: TokenOpt = None,
- ) -> None:
- """Create a new webhook.
- Provide either --url (to ping a remote server) or --job-id (to trigger a Job), but not both.
- """
- if url is not None and job_id is not None:
- raise typer.BadParameter("Provide either --url or --job-id, not both.")
- if url is None and job_id is None:
- raise typer.BadParameter("Provide either --url or --job-id.")
- api = get_hf_api(token=token)
- watched_items = _parse_watch(watch)
- domains = [d.value for d in domain] if domain else None
- webhook = api.create_webhook(url=url, job_id=job_id, watched=watched_items, domains=domains, secret=secret) # type: ignore
- out.result("Webhook created", id=webhook.id)
- @webhooks_cli.command(
- "update",
- examples=[
- "hf webhooks update abc123 --url https://new-url.com/hook",
- "hf webhooks update abc123 --watch model:gpt2 --domain repo",
- "hf webhooks update abc123 --secret newsecret",
- ],
- )
- def webhooks_update(
- webhook_id: Annotated[str, typer.Argument(help="The ID of the webhook to update.")],
- url: Annotated[
- str | None,
- typer.Option(help="New URL to send webhook payloads to."),
- ] = None,
- watch: Annotated[
- list[str] | None,
- typer.Option(
- "--watch",
- help=(
- "New list of items to watch, in 'type:name' format. "
- "Repeatable. Replaces the entire existing watched list."
- ),
- ),
- ] = None,
- domain: Annotated[
- list[WebhookDomain] | None,
- typer.Option(
- "--domain",
- help="New list of domains to watch: 'repo' or 'discussions'. Repeatable.",
- ),
- ] = None,
- secret: Annotated[
- str | None,
- typer.Option(help="New secret used to sign webhook payloads."),
- ] = None,
- format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
- token: TokenOpt = None,
- ) -> None:
- """Update an existing webhook. Only provided options are changed."""
- api = get_hf_api(token=token)
- watched_items = _parse_watch(watch) if watch else None
- domains = [d.value for d in domain] if domain else None
- webhook = api.update_webhook(webhook_id, url=url, watched=watched_items, domains=domains, secret=secret) # type: ignore
- out.result("Webhook updated", id=webhook.id)
- @webhooks_cli.command(
- "enable",
- examples=[
- "hf webhooks enable abc123",
- ],
- )
- def webhooks_enable(
- webhook_id: Annotated[str, typer.Argument(help="The ID of the webhook to enable.")],
- format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
- token: TokenOpt = None,
- ) -> None:
- """Enable a disabled webhook."""
- api = get_hf_api(token=token)
- webhook = api.enable_webhook(webhook_id)
- out.result("Webhook enabled", id=webhook.id)
- @webhooks_cli.command(
- "disable",
- examples=[
- "hf webhooks disable abc123",
- ],
- )
- def webhooks_disable(
- webhook_id: Annotated[str, typer.Argument(help="The ID of the webhook to disable.")],
- format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
- token: TokenOpt = None,
- ) -> None:
- """Disable an active webhook."""
- api = get_hf_api(token=token)
- webhook = api.disable_webhook(webhook_id)
- out.result("Webhook disabled", id=webhook.id)
- @webhooks_cli.command(
- "delete",
- examples=[
- "hf webhooks delete abc123",
- "hf webhooks delete abc123 --yes",
- ],
- )
- def webhooks_delete(
- webhook_id: Annotated[str, typer.Argument(help="The ID of the webhook to delete.")],
- yes: Annotated[
- bool,
- typer.Option(
- "--yes",
- "-y",
- help="Skip confirmation prompt.",
- ),
- ] = False,
- format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
- token: TokenOpt = None,
- ) -> None:
- """Delete a webhook permanently."""
- out.confirm(f"Are you sure you want to delete webhook '{webhook_id}'?", yes=yes)
- api = get_hf_api(token=token)
- api.delete_webhook(webhook_id)
- out.result("Webhook deleted", id=webhook_id)
|