# 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 # 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 --url https://new-url.com/hook # enable / disable a webhook hf webhooks enable hf webhooks disable # delete a webhook hf webhooks delete """ import enum import json 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 ( FormatOpt, OutputFormat, QuietOpt, TokenOpt, api_object_to_dict, get_hf_api, print_list_output, typer_factory, ) # 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 -q", ], ) def webhooks_ls( format: FormatOpt = OutputFormat.table, quiet: QuietOpt = False, token: TokenOpt = None, ) -> None: """List all webhooks for the current user.""" api = get_hf_api(token=token) results = [api_object_to_dict(w) for w in api.list_webhooks()] print_list_output( results, format=format, quiet=quiet, headers=["id", "url", "disabled", "domains", "watched"], row_fn=lambda item: [ item.get("id", ""), item.get("url") or "(job)", str(item.get("disabled", False)), ", ".join(item.get("domains") or []), ", ".join( f"{w['type']}:{w['name']}" if isinstance(w, dict) else str(w) for w in (item.get("watched") or []) ), ], ) @webhooks_cli.command( "info", examples=[ "hf webhooks info abc123", ], ) def webhooks_info( webhook_id: Annotated[str, typer.Argument(help="The ID of the webhook.")], token: TokenOpt = None, ) -> None: """Show full details for a single webhook as JSON.""" api = get_hf_api(token=token) webhook = api.get_webhook(webhook_id) print(json.dumps(api_object_to_dict(webhook), indent=2)) @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, 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 print(f"Webhook created: {webhook.id}") print(json.dumps(api_object_to_dict(webhook), indent=2)) @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, 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 print(f"Webhook updated: {webhook.id}") print(json.dumps(api_object_to_dict(webhook), indent=2)) @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.")], token: TokenOpt = None, ) -> None: """Enable a disabled webhook.""" api = get_hf_api(token=token) webhook = api.enable_webhook(webhook_id) print(f"Webhook enabled: {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.")], token: TokenOpt = None, ) -> None: """Disable an active webhook.""" api = get_hf_api(token=token) webhook = api.disable_webhook(webhook_id) print(f"Webhook disabled: {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, token: TokenOpt = None, ) -> None: """Delete a webhook permanently.""" if not yes: confirm = typer.confirm(f"Are you sure you want to delete webhook '{webhook_id}'?") if not confirm: print("Aborted.") raise typer.Abort() api = get_hf_api(token=token) api.delete_webhook(webhook_id) print(f"Webhook deleted: {webhook_id}")