From ba9b7a311dcceaf88a5a28ae833d66b55194433e Mon Sep 17 00:00:00 2001 From: Quentin Lhoest Date: Mon, 22 Sep 2025 16:53:24 +0200 Subject: [PATCH 1/6] webhook jobs --- docs/source/en/guides/webhooks.md | 20 ++ src/huggingface_hub/__init__.py | 6 + src/huggingface_hub/hf_api.py | 426 +++++++++++++++++++++++++++--- 3 files changed, 413 insertions(+), 39 deletions(-) diff --git a/docs/source/en/guides/webhooks.md b/docs/source/en/guides/webhooks.md index 064a6cfa99..38cedf81d7 100644 --- a/docs/source/en/guides/webhooks.md +++ b/docs/source/en/guides/webhooks.md @@ -28,6 +28,26 @@ webhook = create_webhook( ) ``` +A webhook can also run a Job on Hugging face infrastructure instead of sending the payload to an URL. +In this case you need to pass the Docker image and command in [`create_webhook_job`], or a UV script in [`create_webhook_uv_job`]. + +```python +from huggingface_hub import create_webhook + +# Example: Run a fine-tuning +webhook = create_webhook_job( + image="huggingface/trl", + command=["trl", "sft", ...], + watched=[{"type": "user", "name": "your-username"}, {"type": "org", "name": "your-org-name"}], + domains=["repo", "discussion"], + env={"TRACKIO_PROJECT": "trl-jobs"}, + secrets={"HF_TOKEN": token}, + flavor="a100-large", +) +``` + +For more information on Hugging Face Jobs, available hardware (CPU, GPU) and UV scripts, see the [Jobs documentation](./jobs). + ### Listing Webhooks To see all the webhooks you have configured, you can list them with [`list_webhooks`]. This is useful to review their IDs, URLs, and statuses. diff --git a/src/huggingface_hub/__init__.py b/src/huggingface_hub/__init__.py index 0f4d0d1598..6a8665af5e 100644 --- a/src/huggingface_hub/__init__.py +++ b/src/huggingface_hub/__init__.py @@ -186,6 +186,8 @@ "create_scheduled_uv_job", "create_tag", "create_webhook", + "create_webhook_job", + "create_webhook_uv_job", "dataset_info", "delete_branch", "delete_collection", @@ -838,6 +840,8 @@ "create_scheduled_uv_job", "create_tag", "create_webhook", + "create_webhook_job", + "create_webhook_uv_job", "dataset_info", "delete_branch", "delete_collection", @@ -1206,6 +1210,8 @@ def __dir__(): create_scheduled_uv_job, # noqa: F401 create_tag, # noqa: F401 create_webhook, # noqa: F401 + create_webhook_job, # noqa: F401 + create_webhook_uv_job, # noqa: F401 dataset_info, # noqa: F401 delete_branch, # noqa: F401 delete_collection, # noqa: F401 diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index e2827a6f19..230762a1e3 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -67,7 +67,7 @@ _warn_on_overwriting_operations, ) from ._inference_endpoints import InferenceEndpoint, InferenceEndpointType -from ._jobs_api import JobInfo, ScheduledJobInfo, _create_job_spec +from ._jobs_api import JobInfo, JobSpec, ScheduledJobInfo, _create_job_spec from ._space_api import SpaceHardware, SpaceRuntime, SpaceStorage, SpaceVariable from ._upload_large_folder import upload_large_folder_internal from .community import ( @@ -533,6 +533,33 @@ class WebhookInfo: disabled: bool +@dataclass +class WebhookJobInfo: + """Data structure containing information about a webhook that triggers a Job. + + Attributes: + id (`str`): + ID of the webhook. + job (`JobSpec`): + Specifications of the Job to trigger with the webhook. + watched (`List[WebhookWatchedItem]`): + List of items watched by the webhook, see [`WebhookWatchedItem`]. + domains (`List[WEBHOOK_DOMAIN_T]`): + List of domains the webhook is watching. Can be one of `["repo", "discussions"]`. + secret (`str`, *optional*): + Secret of the webhook. + disabled (`bool`): + Whether the webhook is disabled or not. + """ + + id: str + job: JobSpec + watched: List[WebhookWatchedItem] + domains: List[constants.WEBHOOK_DOMAIN_T] + secret: Optional[str] + disabled: bool + + class RepoUrl(str): """Subclass of `str` describing a repo URL on the Hub. @@ -9171,7 +9198,9 @@ def grant_access( ################### @validate_hf_hub_args - def get_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) -> WebhookInfo: + def get_webhook( + self, webhook_id: str, *, token: Union[bool, str, None] = None + ) -> Union[WebhookInfo, WebhookJobInfo]: """Get a webhook by its id. Args: @@ -9183,7 +9212,7 @@ def get_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) To disable authentication, pass `False`. Returns: - [`WebhookInfo`]: + [`Union[WebhookInfo, WebhookJobInfo]`]: Info about the webhook. Example: @@ -9210,19 +9239,30 @@ def get_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], + webhook = ( + WebhookInfo( + id=webhook_data["id"], + url=webhook_data["url"], + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) + if "url" in webhook_data + else WebhookJobInfo( + id=webhook_data["id"], + job=JobSpec(**webhook_data["job"]), + watched=[WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]], + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) ) return webhook @validate_hf_hub_args - def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[WebhookInfo]: + def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Union[WebhookInfo, WebhookJobInfo]]: """List all configured webhooks. Args: @@ -9232,7 +9272,7 @@ def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Webhook To disable authentication, pass `False`. Returns: - `List[WebhookInfo]`: + `List[Union[WebhookInfo, WebhookJobInfo]]`: List of webhook info objects. Example: @@ -9268,6 +9308,15 @@ def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Webhook secret=webhook.get("secret"), disabled=webhook["disabled"], ) + if "url" in webhook + else WebhookJobInfo( + id=webhook["id"], + job=JobSpec(**webhook["job"]), + watched=[WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook["watched"]], + domains=webhook["domains"], + secret=webhook.get("secret"), + disabled=webhook["disabled"], + ) for webhook in webhooks_data ] @@ -9344,6 +9393,266 @@ def create_webhook( return webhook + def create_webhook_job( + self, + *, + image: str, + command: List[str], + watched: List[Union[Dict, WebhookWatchedItem]], + domains: Optional[List[constants.WEBHOOK_DOMAIN_T]] = None, + secret: Optional[str] = None, + env: Optional[Dict[str, Any]] = None, + secrets: Optional[Dict[str, Any]] = None, + flavor: Optional[SpaceHardware] = None, + timeout: Optional[Union[int, float, str]] = None, + namespace: Optional[str] = None, + token: Union[bool, str, None] = None, + ) -> WebhookJobInfo: + r"""Create a new webhook that triggers a Job running on Hugging Face infrastructure. + + Args: + image (`str`): + The Docker image to use. + Examples: `"ubuntu"`, `"python:3.12"`, `"pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel"`. + Example with an image from a Space: `"hf.co/spaces/lhoestq/duckdb"`. + + command (`List[str]`): + The command to run. Example: `["echo", "hello"]`. + + watched (`List[WebhookWatchedItem]`): + List of [`WebhookWatchedItem`] to be watched by the webhook. It can be users, orgs, models, datasets or spaces. + Watched items can also be provided as plain dictionaries. + + domains (`List[Literal["repo", "discussion"]]`, optional): + List of domains to watch. It can be "repo", "discussion" or both. + + secret (`str`, optional): + A secret to sign the payload with. + + env (`Dict[str, Any]`, *optional*): + Defines the environment variables for the Job. + + secrets (`Dict[str, Any]`, *optional*): + Defines the secret environment variables for the Job. + + flavor (`str`, *optional*): + Flavor for the hardware, as in Hugging Face Spaces. See [`SpaceHardware`] for possible values. + Defaults to `"cpu-basic"`. + + timeout (`Union[int, float, str]`, *optional*): + Max duration for the Job: int/float with s (seconds, default), m (minutes), h (hours) or d (days). + Example: `300` or `"5m"` for 5 minutes. + + namespace (`str`, *optional*): + The namespace where the Job will be created. Defaults to the current user's namespace. + + token (Union[bool, str, None], optional): + A valid user access token (string). Defaults to the locally saved token, which is the recommended + method for authentication (see https://huggingface.co/docs/huggingface_hub/quick-start#authentication). + To disable authentication, pass `False`. + + Returns: + [`WebhookJobInfo`]: + Info about the newly created webhook. + + Example: + ```python + >>> from huggingface_hub import create_webhook_job + >>> payload = create_webhook_job( + ... watched=[{"type": "user", "name": "julien-c"}, {"type": "org", "name": "HuggingFaceH4"}], + ... image="python:3.12", + ... command=["bash", "-c", r"echo An event occured in $WEBHOOK_REPO_ID: $WEBHOOK_PAYLOAD"], + ... domains=["repo", "discussion"], + ... secret="my-secret", + ... ) + >>> print(payload) + WebhookJobInfo( + id="654bbbc16f2ec14d77f109cc", + job=JobSpec( + docker_image='python:3.12', + space_id=None, + command=['bash', '-c', 'echo An event occured in $WEBHOOK_REPO_ID: $WEBHOOK_PAYLOAD'], + arguments=[], + environment={}, + secrets=[], + flavor='cpu-basic', + timeout=None, + tags=None, + arch=None + ), + watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")], + domains=["repo", "discussion"], + secret="my-secret", + disabled=False, + ) + ``` + """ + if namespace is None: + namespace = self.whoami(token=token)["name"] + + # prepare payload to send to HF Jobs API + job_spec = _create_job_spec( + image=image, + command=command, + env=env, + secrets=secrets, + flavor=flavor, + timeout=timeout, + ) + + watched_dicts = [asdict(item) if isinstance(item, WebhookWatchedItem) else item for item in watched] + + response = get_session().post( + f"{constants.ENDPOINT}/api/settings/webhooks", + json={"watched": watched_dicts, "job": job_spec, "domains": domains, "secret": secret}, + headers=self._build_hf_headers(token=token), + ) + hf_raise_for_status(response) + webhook_data = response.json()["webhook"] + watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] + + webhook = WebhookJobInfo( + id=webhook_data["id"], + job=JobSpec(**webhook_data["job"]), + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) + + return webhook + + def create_webhook_uv_job( + self, + script: str, + *, + script_args: Optional[List[str]] = None, + watched: List[Union[Dict, WebhookWatchedItem]], + domains: Optional[List[constants.WEBHOOK_DOMAIN_T]] = None, + secret: Optional[str] = None, + dependencies: Optional[List[str]] = None, + python: Optional[str] = None, + image: Optional[str] = None, + env: Optional[Dict[str, Any]] = None, + secrets: Optional[Dict[str, Any]] = None, + flavor: Optional[SpaceHardware] = None, + timeout: Optional[Union[int, float, str]] = None, + namespace: Optional[str] = None, + token: Union[bool, str, None] = None, + _repo: Optional[str] = None, + ) -> WebhookJobInfo: + r"""Create a new webhook that triggers a Job running on Hugging Face infrastructure. + + Args: + script (`str`): + Path or URL of the UV script, or a command. + + script_args (`List[str]`, *optional*) + Arguments to pass to the script, or a command. + + watched (`List[WebhookWatchedItem]`): + List of [`WebhookWatchedItem`] to be watched by the webhook. It can be users, orgs, models, datasets or spaces. + Watched items can also be provided as plain dictionaries. + + domains (`List[Literal["repo", "discussion"]]`, optional): + List of domains to watch. It can be "repo", "discussion" or both. + + secret (`str`, optional): + A secret to sign the payload with. + + dependencies (`List[str]`, *optional*) + Dependencies to use to run the UV script. + + python (`str`, *optional*) + Use a specific Python version. Default is 3.12. + + image (`str`, *optional*, defaults to "ghcr.io/astral-sh/uv:python3.12-bookworm"): + Use a custom Docker image with `uv` installed. + + env (`Dict[str, Any]`, *optional*): + Defines the environment variables for the Job. + + secrets (`Dict[str, Any]`, *optional*): + Defines the secret environment variables for the Job. + + flavor (`str`, *optional*): + Flavor for the hardware, as in Hugging Face Spaces. See [`SpaceHardware`] for possible values. + Defaults to `"cpu-basic"`. + + timeout (`Union[int, float, str]`, *optional*): + Max duration for the Job: int/float with s (seconds, default), m (minutes), h (hours) or d (days). + Example: `300` or `"5m"` for 5 minutes. + + namespace (`str`, *optional*): + The namespace where the Job will be created. Defaults to the current user's namespace. + + token (Union[bool, str, None], optional): + A valid user access token (string). Defaults to the locally saved token, which is the recommended + method for authentication (see https://huggingface.co/docs/huggingface_hub/quick-start#authentication). + To disable authentication, pass `False`. + + Returns: + [`WebhookJobInfo`]: + Info about the newly created webhook. + + Example: + ```python + >>> from huggingface_hub import create_webhook_job + >>> payload = create_webhook_job( + ... script="my_script.py" + ... watched=[{"type": "user", "name": "julien-c"}, {"type": "org", "name": "HuggingFaceH4"}], + ... domains=["repo", "discussion"], + ... secret="my-secret", + ... ) + >>> print(payload) + WebhookJobInfo( + id="654bbbc16f2ec14d77f109cc", + job=JobSpec( + docker_image='python:3.12', + space_id=None, + command=['bash', '-c', 'echo An event occured in $WEBHOOK_REPO_ID: $WEBHOOK_PAYLOAD'], + arguments=[], + environment={}, + secrets=[], + flavor='cpu-basic', + timeout=None, + tags=None, + arch=None + ), + watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")], + domains=["repo", "discussion"], + secret="my-secret", + disabled=False, + ) + ``` + """ + image = image or "ghcr.io/astral-sh/uv:python3.12-bookworm" + # Build command + command, env, secrets = self._create_uv_command_env_and_secrets( + script=script, + script_args=script_args, + dependencies=dependencies, + python=python, + env=env, + secrets=secrets, + namespace=namespace, + token=token, + _repo=_repo, + ) + return self.create_webhook_job( + image=image, + command=command, + watched=watched, + domains=domains, + secret=secret, + env=env, + secrets=secrets, + flavor=flavor, + timeout=timeout, + namespace=namespace, + token=token, + ) + @validate_hf_hub_args def update_webhook( self, @@ -9354,7 +9663,7 @@ def update_webhook( domains: Optional[List[constants.WEBHOOK_DOMAIN_T]] = None, secret: Optional[str] = None, token: Union[bool, str, None] = None, - ) -> WebhookInfo: + ) -> Union[WebhookInfo, WebhookJobInfo]: """Update an existing webhook. Args: @@ -9375,7 +9684,7 @@ def update_webhook( To disable authentication, pass `False`. Returns: - [`WebhookInfo`]: + [`Union[WebhookInfo, WebhookJobInfo]`]: Info about the updated webhook. Example: @@ -9412,19 +9721,32 @@ def update_webhook( watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], + webhook = ( + WebhookInfo( + id=webhook_data["id"], + url=webhook_data["url"], + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) + if "url" in webhook_data + else WebhookJobInfo( + id=webhook_data["id"], + job=JobSpec(**webhook_data["job"]), + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) ) return webhook @validate_hf_hub_args - def enable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) -> WebhookInfo: + def enable_webhook( + self, webhook_id: str, *, token: Union[bool, str, None] = None + ) -> Union[WebhookInfo, WebhookJobInfo]: """Enable a webhook (makes it "active"). Args: @@ -9436,7 +9758,7 @@ def enable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = Non To disable authentication, pass `False`. Returns: - [`WebhookInfo`]: + [`Union[WebhookInfo, WebhookJobInfo]`]: Info about the enabled webhook. Example: @@ -9463,19 +9785,32 @@ def enable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = Non watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], + webhook = ( + WebhookInfo( + id=webhook_data["id"], + url=webhook_data["url"], + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) + if "url" in webhook_data + else WebhookJobInfo( + id=webhook_data["id"], + job=JobSpec(**webhook_data["job"]), + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) ) return webhook @validate_hf_hub_args - def disable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) -> WebhookInfo: + def disable_webhook( + self, webhook_id: str, *, token: Union[bool, str, None] = None + ) -> Union[WebhookInfo, WebhookJobInfo]: """Disable a webhook (makes it "disabled"). Args: @@ -9487,7 +9822,7 @@ def disable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = No To disable authentication, pass `False`. Returns: - [`WebhookInfo`]: + [`Union[WebhookInfo, WebhookJobInfo]`]: Info about the disabled webhook. Example: @@ -9514,13 +9849,24 @@ def disable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = No watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], + webhook = ( + WebhookInfo( + id=webhook_data["id"], + url=webhook_data["url"], + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) + if "url" in webhook_data + else WebhookJobInfo( + id=webhook_data["id"], + job=JobSpec(**webhook_data["job"]), + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], + ) ) return webhook @@ -11039,6 +11385,8 @@ def _parse_revision_from_pr_url(pr_url: str) -> str: get_webhook = api.get_webhook list_webhooks = api.list_webhooks update_webhook = api.update_webhook +create_webhook_job = api.create_webhook_job +create_webhook_job = api.create_webhook_uv_job # User API From 63af2f0fc91ebe7cb965f58458959bc814756291 Mon Sep 17 00:00:00 2001 From: Quentin Lhoest Date: Mon, 22 Sep 2025 16:55:42 +0200 Subject: [PATCH 2/6] minor --- src/huggingface_hub/hf_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index 230762a1e3..b9eaac7d7f 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -9597,8 +9597,8 @@ def create_webhook_uv_job( Example: ```python - >>> from huggingface_hub import create_webhook_job - >>> payload = create_webhook_job( + >>> from huggingface_hub import create_webhook_uv_job + >>> payload = create_webhook_uv_job( ... script="my_script.py" ... watched=[{"type": "user", "name": "julien-c"}, {"type": "org", "name": "HuggingFaceH4"}], ... domains=["repo", "discussion"], @@ -9608,9 +9608,9 @@ def create_webhook_uv_job( WebhookJobInfo( id="654bbbc16f2ec14d77f109cc", job=JobSpec( - docker_image='python:3.12', + docker_image='ghcr.io/astral-sh/uv:python3.12-bookworm', space_id=None, - command=['bash', '-c', 'echo An event occured in $WEBHOOK_REPO_ID: $WEBHOOK_PAYLOAD'], + command=[...], arguments=[], environment={}, secrets=[], From 9b7d88b3a9082713c29119bdcf1ace911add1f3b Mon Sep 17 00:00:00 2001 From: Quentin Lhoest Date: Mon, 22 Sep 2025 17:06:18 +0200 Subject: [PATCH 3/6] very minor --- src/huggingface_hub/hf_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index b9eaac7d7f..e711b110c2 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -9599,7 +9599,7 @@ def create_webhook_uv_job( ```python >>> from huggingface_hub import create_webhook_uv_job >>> payload = create_webhook_uv_job( - ... script="my_script.py" + ... script="my_script.py", ... watched=[{"type": "user", "name": "julien-c"}, {"type": "org", "name": "HuggingFaceH4"}], ... domains=["repo", "discussion"], ... secret="my-secret", From fb441182fe285d57f910b4971c167a646dc34654 Mon Sep 17 00:00:00 2001 From: Quentin Lhoest Date: Tue, 23 Sep 2025 19:34:26 +0200 Subject: [PATCH 4/6] create_webhook(job_id=job_id, ...) --- docs/source/en/guides/jobs.md | 21 ++ docs/source/en/guides/webhooks.md | 16 +- src/huggingface_hub/__init__.py | 6 - src/huggingface_hub/hf_api.py | 463 ++++++------------------------ 4 files changed, 122 insertions(+), 384 deletions(-) diff --git a/docs/source/en/guides/jobs.md b/docs/source/en/guides/jobs.md index cc4a8f9515..ca00621fe6 100644 --- a/docs/source/en/guides/jobs.md +++ b/docs/source/en/guides/jobs.md @@ -427,3 +427,24 @@ Manage scheduled jobs using [`list_scheduled_jobs`], [`inspect_scheduled_job`], >>> from huggingface_hub import delete_scheduled_job >>> delete_scheduled_job(scheduled_job_id) ``` + +### Trigger Jobs with webhooks + +Webhooks allow you to listen for new changes on specific repos or to all repos belonging to particular set of users/organizations (not just your repos, but any repo). + +Use [`create_webhook`] to create a webhook that triggers a Job when a change happens in a Hugging Face repository: + +```python +from huggingface_hub import create_webhook + +# Example: Creating a webhook that triggers a Job +webhook = create_webhook( + job_id=job_id, + watched=[{"type": "user", "name": "your-username"}, {"type": "org", "name": "your-org-name"}], + domains=["repo", "discussion"], + secret="your-secret" +) +``` + +The webhook triggers the Job with the webhook payload in the environment variable WEBHOOK_PAYLOAD. +You can find more information on webhooks in the [Webhooks documentation](./webhooks). diff --git a/docs/source/en/guides/webhooks.md b/docs/source/en/guides/webhooks.md index 38cedf81d7..203676d8ce 100644 --- a/docs/source/en/guides/webhooks.md +++ b/docs/source/en/guides/webhooks.md @@ -28,24 +28,22 @@ webhook = create_webhook( ) ``` -A webhook can also run a Job on Hugging face infrastructure instead of sending the payload to an URL. -In this case you need to pass the Docker image and command in [`create_webhook_job`], or a UV script in [`create_webhook_uv_job`]. +A webhook can also trigger a Job to run on Hugging face infrastructure instead of sending the payload to an URL. +In this case you need to pass the ID of a source Job. ```python from huggingface_hub import create_webhook -# Example: Run a fine-tuning -webhook = create_webhook_job( - image="huggingface/trl", - command=["trl", "sft", ...], +# Example: Creating a webhook that triggers a Job +webhook = create_webhook( + job_id=job_id, watched=[{"type": "user", "name": "your-username"}, {"type": "org", "name": "your-org-name"}], domains=["repo", "discussion"], - env={"TRACKIO_PROJECT": "trl-jobs"}, - secrets={"HF_TOKEN": token}, - flavor="a100-large", + secret="your-secret" ) ``` +The webhook triggers the Job with the webhook payload in the environment variable WEBHOOK_PAYLOAD. For more information on Hugging Face Jobs, available hardware (CPU, GPU) and UV scripts, see the [Jobs documentation](./jobs). ### Listing Webhooks diff --git a/src/huggingface_hub/__init__.py b/src/huggingface_hub/__init__.py index 6a8665af5e..0f4d0d1598 100644 --- a/src/huggingface_hub/__init__.py +++ b/src/huggingface_hub/__init__.py @@ -186,8 +186,6 @@ "create_scheduled_uv_job", "create_tag", "create_webhook", - "create_webhook_job", - "create_webhook_uv_job", "dataset_info", "delete_branch", "delete_collection", @@ -840,8 +838,6 @@ "create_scheduled_uv_job", "create_tag", "create_webhook", - "create_webhook_job", - "create_webhook_uv_job", "dataset_info", "delete_branch", "delete_collection", @@ -1210,8 +1206,6 @@ def __dir__(): create_scheduled_uv_job, # noqa: F401 create_tag, # noqa: F401 create_webhook, # noqa: F401 - create_webhook_job, # noqa: F401 - create_webhook_uv_job, # noqa: F401 dataset_info, # noqa: F401 delete_branch, # noqa: F401 delete_collection, # noqa: F401 diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index e711b110c2..5b76068264 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -510,38 +510,15 @@ class WebhookWatchedItem: class WebhookInfo: """Data structure containing information about a webhook. - Attributes: - id (`str`): - ID of the webhook. - url (`str`): - URL of the webhook. - watched (`List[WebhookWatchedItem]`): - List of items watched by the webhook, see [`WebhookWatchedItem`]. - domains (`List[WEBHOOK_DOMAIN_T]`): - List of domains the webhook is watching. Can be one of `["repo", "discussions"]`. - secret (`str`, *optional*): - Secret of the webhook. - disabled (`bool`): - Whether the webhook is disabled or not. - """ - - id: str - url: str - watched: List[WebhookWatchedItem] - domains: List[constants.WEBHOOK_DOMAIN_T] - secret: Optional[str] - disabled: bool - - -@dataclass -class WebhookJobInfo: - """Data structure containing information about a webhook that triggers a Job. + One of `url` or `job` is specified, but not both. Attributes: id (`str`): ID of the webhook. - job (`JobSpec`): - Specifications of the Job to trigger with the webhook. + url (`str`, *optional*): + URL of the webhook. + job (`JobSpec`, *optional*): + Specifications of the Job to trigger. watched (`List[WebhookWatchedItem]`): List of items watched by the webhook, see [`WebhookWatchedItem`]. domains (`List[WEBHOOK_DOMAIN_T]`): @@ -553,7 +530,8 @@ class WebhookJobInfo: """ id: str - job: JobSpec + url: Optional[str] + job: Optional[JobSpec] watched: List[WebhookWatchedItem] domains: List[constants.WEBHOOK_DOMAIN_T] secret: Optional[str] @@ -9198,9 +9176,7 @@ def grant_access( ################### @validate_hf_hub_args - def get_webhook( - self, webhook_id: str, *, token: Union[bool, str, None] = None - ) -> Union[WebhookInfo, WebhookJobInfo]: + def get_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) -> WebhookInfo: """Get a webhook by its id. Args: @@ -9212,7 +9188,7 @@ def get_webhook( To disable authentication, pass `False`. Returns: - [`Union[WebhookInfo, WebhookJobInfo]`]: + [`WebhookInfo`]: Info about the webhook. Example: @@ -9222,6 +9198,7 @@ def get_webhook( >>> print(webhook) WebhookInfo( id="654bbbc16f2ec14d77f109cc", + job=None, watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")], url="https://webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548", secret="my-secret", @@ -9239,30 +9216,20 @@ def get_webhook( watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = ( - WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) - if "url" in webhook_data - else WebhookJobInfo( - id=webhook_data["id"], - job=JobSpec(**webhook_data["job"]), - watched=[WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]], - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) + webhook = WebhookInfo( + id=webhook_data["id"], + url=webhook_data["url"], + job=JobSpec(**webhook_data["job"]) if webhook_data["job"] else None, + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], ) return webhook @validate_hf_hub_args - def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Union[WebhookInfo, WebhookJobInfo]]: + def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[WebhookInfo]: """List all configured webhooks. Args: @@ -9272,7 +9239,7 @@ def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Union[W To disable authentication, pass `False`. Returns: - `List[Union[WebhookInfo, WebhookJobInfo]]`: + `List[WebhookInfo]`: List of webhook info objects. Example: @@ -9302,17 +9269,11 @@ def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Union[W return [ WebhookInfo( id=webhook["id"], - url=webhook["url"], - watched=[WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook["watched"]], - domains=webhook["domains"], - secret=webhook.get("secret"), - disabled=webhook["disabled"], - ) - if "url" in webhook - else WebhookJobInfo( - id=webhook["id"], - job=JobSpec(**webhook["job"]), - watched=[WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook["watched"]], + url=webhook.get("url"), + job=JobSpec(**webhook["job"]) if webhook.get("job") else None, + watched=[ + WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhooks_data["watched"] + ], domains=webhook["domains"], secret=webhook.get("secret"), disabled=webhook["disabled"], @@ -9324,7 +9285,8 @@ def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Union[W def create_webhook( self, *, - url: str, + url: Optional[str] = None, + job_id: Optional[str] = None, watched: List[Union[Dict, WebhookWatchedItem]], domains: Optional[List[constants.WEBHOOK_DOMAIN_T]] = None, secret: Optional[str] = None, @@ -9332,9 +9294,15 @@ def create_webhook( ) -> WebhookInfo: """Create a new webhook. + The webhook can either send a payload to a URL, or trigger a Job to run on Hugging Face infrastructure. + This function should be called with one of `url` or `job_id`, but not both. + Args: url (`str`): URL to send the payload to. + job_id (`str`): + ID of the source Job to trigger with the webhook payload in the environment variable WEBHOOK_PAYLOAD. + Additional environment variables are available for convenience: WEBHOOK_REPO_ID, WEBHOOK_REPO_TYPE and WEBHOOK_SECRET. watched (`List[WebhookWatchedItem]`): List of [`WebhookWatchedItem`] to be watched by the webhook. It can be users, orgs, models, datasets or spaces. Watched items can also be provided as plain dictionaries. @@ -9352,6 +9320,8 @@ def create_webhook( Info about the newly created webhook. Example: + + Create a webhook that sends a payload to a URL ```python >>> from huggingface_hub import create_webhook >>> payload = create_webhook( @@ -9364,112 +9334,33 @@ def create_webhook( WebhookInfo( id="654bbbc16f2ec14d77f109cc", url="https://webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548", + job=None, watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")], domains=["repo", "discussion"], secret="my-secret", disabled=False, ) ``` - """ - watched_dicts = [asdict(item) if isinstance(item, WebhookWatchedItem) else item for item in watched] - - response = get_session().post( - f"{constants.ENDPOINT}/api/settings/webhooks", - json={"watched": watched_dicts, "url": url, "domains": domains, "secret": secret}, - headers=self._build_hf_headers(token=token), - ) - hf_raise_for_status(response) - webhook_data = response.json()["webhook"] - watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - - webhook = WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) - - return webhook - - def create_webhook_job( - self, - *, - image: str, - command: List[str], - watched: List[Union[Dict, WebhookWatchedItem]], - domains: Optional[List[constants.WEBHOOK_DOMAIN_T]] = None, - secret: Optional[str] = None, - env: Optional[Dict[str, Any]] = None, - secrets: Optional[Dict[str, Any]] = None, - flavor: Optional[SpaceHardware] = None, - timeout: Optional[Union[int, float, str]] = None, - namespace: Optional[str] = None, - token: Union[bool, str, None] = None, - ) -> WebhookJobInfo: - r"""Create a new webhook that triggers a Job running on Hugging Face infrastructure. - - Args: - image (`str`): - The Docker image to use. - Examples: `"ubuntu"`, `"python:3.12"`, `"pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel"`. - Example with an image from a Space: `"hf.co/spaces/lhoestq/duckdb"`. - - command (`List[str]`): - The command to run. Example: `["echo", "hello"]`. - - watched (`List[WebhookWatchedItem]`): - List of [`WebhookWatchedItem`] to be watched by the webhook. It can be users, orgs, models, datasets or spaces. - Watched items can also be provided as plain dictionaries. - - domains (`List[Literal["repo", "discussion"]]`, optional): - List of domains to watch. It can be "repo", "discussion" or both. - - secret (`str`, optional): - A secret to sign the payload with. - - env (`Dict[str, Any]`, *optional*): - Defines the environment variables for the Job. - - secrets (`Dict[str, Any]`, *optional*): - Defines the secret environment variables for the Job. - - flavor (`str`, *optional*): - Flavor for the hardware, as in Hugging Face Spaces. See [`SpaceHardware`] for possible values. - Defaults to `"cpu-basic"`. - - timeout (`Union[int, float, str]`, *optional*): - Max duration for the Job: int/float with s (seconds, default), m (minutes), h (hours) or d (days). - Example: `300` or `"5m"` for 5 minutes. - - namespace (`str`, *optional*): - The namespace where the Job will be created. Defaults to the current user's namespace. - token (Union[bool, str, None], optional): - A valid user access token (string). Defaults to the locally saved token, which is the recommended - method for authentication (see https://huggingface.co/docs/huggingface_hub/quick-start#authentication). - To disable authentication, pass `False`. - - Returns: - [`WebhookJobInfo`]: - Info about the newly created webhook. - - Example: + Run a Job and then create a webhook that triggers this Job ```python - >>> from huggingface_hub import create_webhook_job - >>> payload = create_webhook_job( - ... watched=[{"type": "user", "name": "julien-c"}, {"type": "org", "name": "HuggingFaceH4"}], - ... image="python:3.12", + >>> from huggingface_hub import create_webhook, run_job + >>> job = run_job( + ... image="ubuntu", ... command=["bash", "-c", r"echo An event occured in $WEBHOOK_REPO_ID: $WEBHOOK_PAYLOAD"], + ... ) + >>> payload = create_webhook( + ... watched=[{"type": "user", "name": "julien-c"}, {"type": "org", "name": "HuggingFaceH4"}], + ... job_id=job.id, ... domains=["repo", "discussion"], ... secret="my-secret", ... ) >>> print(payload) - WebhookJobInfo( + WebhookInfo( id="654bbbc16f2ec14d77f109cc", + url=None, job=JobSpec( - docker_image='python:3.12', + docker_image='ubuntu', space_id=None, command=['bash', '-c', 'echo An event occured in $WEBHOOK_REPO_ID: $WEBHOOK_PAYLOAD'], arguments=[], @@ -9487,33 +9378,31 @@ def create_webhook_job( ) ``` """ - if namespace is None: - namespace = self.whoami(token=token)["name"] - - # prepare payload to send to HF Jobs API - job_spec = _create_job_spec( - image=image, - command=command, - env=env, - secrets=secrets, - flavor=flavor, - timeout=timeout, - ) - watched_dicts = [asdict(item) if isinstance(item, WebhookWatchedItem) else item for item in watched] + post_webhooks_json = {"watched": watched_dicts, "domains": domains, "secret": secret} + if url is not None and job_id is not None: + raise ValueError("Set `url` or `job_id` but not both.") + elif url is not None: + post_webhooks_json["url"] = url + elif job_id is not None: + post_webhooks_json["jobSourceId"] = job_id + else: + raise ValueError("Missing argument for webhook: `url` or `job_id`.") + response = get_session().post( f"{constants.ENDPOINT}/api/settings/webhooks", - json={"watched": watched_dicts, "job": job_spec, "domains": domains, "secret": secret}, + json=post_webhooks_json, headers=self._build_hf_headers(token=token), ) hf_raise_for_status(response) webhook_data = response.json()["webhook"] watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = WebhookJobInfo( + webhook = WebhookInfo( id=webhook_data["id"], - job=JobSpec(**webhook_data["job"]), + url=webhook_data.get("url"), + job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None, watched=watched_items, domains=webhook_data["domains"], secret=webhook_data.get("secret"), @@ -9522,137 +9411,6 @@ def create_webhook_job( return webhook - def create_webhook_uv_job( - self, - script: str, - *, - script_args: Optional[List[str]] = None, - watched: List[Union[Dict, WebhookWatchedItem]], - domains: Optional[List[constants.WEBHOOK_DOMAIN_T]] = None, - secret: Optional[str] = None, - dependencies: Optional[List[str]] = None, - python: Optional[str] = None, - image: Optional[str] = None, - env: Optional[Dict[str, Any]] = None, - secrets: Optional[Dict[str, Any]] = None, - flavor: Optional[SpaceHardware] = None, - timeout: Optional[Union[int, float, str]] = None, - namespace: Optional[str] = None, - token: Union[bool, str, None] = None, - _repo: Optional[str] = None, - ) -> WebhookJobInfo: - r"""Create a new webhook that triggers a Job running on Hugging Face infrastructure. - - Args: - script (`str`): - Path or URL of the UV script, or a command. - - script_args (`List[str]`, *optional*) - Arguments to pass to the script, or a command. - - watched (`List[WebhookWatchedItem]`): - List of [`WebhookWatchedItem`] to be watched by the webhook. It can be users, orgs, models, datasets or spaces. - Watched items can also be provided as plain dictionaries. - - domains (`List[Literal["repo", "discussion"]]`, optional): - List of domains to watch. It can be "repo", "discussion" or both. - - secret (`str`, optional): - A secret to sign the payload with. - - dependencies (`List[str]`, *optional*) - Dependencies to use to run the UV script. - - python (`str`, *optional*) - Use a specific Python version. Default is 3.12. - - image (`str`, *optional*, defaults to "ghcr.io/astral-sh/uv:python3.12-bookworm"): - Use a custom Docker image with `uv` installed. - - env (`Dict[str, Any]`, *optional*): - Defines the environment variables for the Job. - - secrets (`Dict[str, Any]`, *optional*): - Defines the secret environment variables for the Job. - - flavor (`str`, *optional*): - Flavor for the hardware, as in Hugging Face Spaces. See [`SpaceHardware`] for possible values. - Defaults to `"cpu-basic"`. - - timeout (`Union[int, float, str]`, *optional*): - Max duration for the Job: int/float with s (seconds, default), m (minutes), h (hours) or d (days). - Example: `300` or `"5m"` for 5 minutes. - - namespace (`str`, *optional*): - The namespace where the Job will be created. Defaults to the current user's namespace. - - token (Union[bool, str, None], optional): - A valid user access token (string). Defaults to the locally saved token, which is the recommended - method for authentication (see https://huggingface.co/docs/huggingface_hub/quick-start#authentication). - To disable authentication, pass `False`. - - Returns: - [`WebhookJobInfo`]: - Info about the newly created webhook. - - Example: - ```python - >>> from huggingface_hub import create_webhook_uv_job - >>> payload = create_webhook_uv_job( - ... script="my_script.py", - ... watched=[{"type": "user", "name": "julien-c"}, {"type": "org", "name": "HuggingFaceH4"}], - ... domains=["repo", "discussion"], - ... secret="my-secret", - ... ) - >>> print(payload) - WebhookJobInfo( - id="654bbbc16f2ec14d77f109cc", - job=JobSpec( - docker_image='ghcr.io/astral-sh/uv:python3.12-bookworm', - space_id=None, - command=[...], - arguments=[], - environment={}, - secrets=[], - flavor='cpu-basic', - timeout=None, - tags=None, - arch=None - ), - watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")], - domains=["repo", "discussion"], - secret="my-secret", - disabled=False, - ) - ``` - """ - image = image or "ghcr.io/astral-sh/uv:python3.12-bookworm" - # Build command - command, env, secrets = self._create_uv_command_env_and_secrets( - script=script, - script_args=script_args, - dependencies=dependencies, - python=python, - env=env, - secrets=secrets, - namespace=namespace, - token=token, - _repo=_repo, - ) - return self.create_webhook_job( - image=image, - command=command, - watched=watched, - domains=domains, - secret=secret, - env=env, - secrets=secrets, - flavor=flavor, - timeout=timeout, - namespace=namespace, - token=token, - ) - @validate_hf_hub_args def update_webhook( self, @@ -9663,7 +9421,7 @@ def update_webhook( domains: Optional[List[constants.WEBHOOK_DOMAIN_T]] = None, secret: Optional[str] = None, token: Union[bool, str, None] = None, - ) -> Union[WebhookInfo, WebhookJobInfo]: + ) -> WebhookInfo: """Update an existing webhook. Args: @@ -9684,7 +9442,7 @@ def update_webhook( To disable authentication, pass `False`. Returns: - [`Union[WebhookInfo, WebhookJobInfo]`]: + [`WebhookInfo`]: Info about the updated webhook. Example: @@ -9700,6 +9458,7 @@ def update_webhook( >>> print(updated_payload) WebhookInfo( id="654bbbc16f2ec14d77f109cc", + job=None, url="https://new.webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548", watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")], domains=["repo"], @@ -9721,32 +9480,20 @@ def update_webhook( watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = ( - WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) - if "url" in webhook_data - else WebhookJobInfo( - id=webhook_data["id"], - job=JobSpec(**webhook_data["job"]), - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) + webhook = WebhookInfo( + id=webhook_data["id"], + url=webhook_data.get("url"), + job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None, + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], ) return webhook @validate_hf_hub_args - def enable_webhook( - self, webhook_id: str, *, token: Union[bool, str, None] = None - ) -> Union[WebhookInfo, WebhookJobInfo]: + def enable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) -> WebhookInfo: """Enable a webhook (makes it "active"). Args: @@ -9758,7 +9505,7 @@ def enable_webhook( To disable authentication, pass `False`. Returns: - [`Union[WebhookInfo, WebhookJobInfo]`]: + [`WebhookInfo`]: Info about the enabled webhook. Example: @@ -9768,6 +9515,7 @@ def enable_webhook( >>> enabled_webhook WebhookInfo( id="654bbbc16f2ec14d77f109cc", + job=None, url="https://webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548", watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")], domains=["repo", "discussion"], @@ -9785,32 +9533,20 @@ def enable_webhook( watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = ( - WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) - if "url" in webhook_data - else WebhookJobInfo( - id=webhook_data["id"], - job=JobSpec(**webhook_data["job"]), - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) + webhook = WebhookInfo( + id=webhook_data["id"], + url=webhook_data.get("url"), + job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None, + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], ) return webhook @validate_hf_hub_args - def disable_webhook( - self, webhook_id: str, *, token: Union[bool, str, None] = None - ) -> Union[WebhookInfo, WebhookJobInfo]: + def disable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) -> WebhookInfo: """Disable a webhook (makes it "disabled"). Args: @@ -9822,7 +9558,7 @@ def disable_webhook( To disable authentication, pass `False`. Returns: - [`Union[WebhookInfo, WebhookJobInfo]`]: + [`WebhookInfo`]: Info about the disabled webhook. Example: @@ -9833,6 +9569,7 @@ def disable_webhook( WebhookInfo( id="654bbbc16f2ec14d77f109cc", url="https://webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548", + jon=None, watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")], domains=["repo", "discussion"], secret="my-secret", @@ -9849,24 +9586,14 @@ def disable_webhook( watched_items = [WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook_data["watched"]] - webhook = ( - WebhookInfo( - id=webhook_data["id"], - url=webhook_data["url"], - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) - if "url" in webhook_data - else WebhookJobInfo( - id=webhook_data["id"], - job=JobSpec(**webhook_data["job"]), - watched=watched_items, - domains=webhook_data["domains"], - secret=webhook_data.get("secret"), - disabled=webhook_data["disabled"], - ) + webhook = WebhookInfo( + id=webhook_data["id"], + url=webhook_data.get("url"), + job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None, + watched=watched_items, + domains=webhook_data["domains"], + secret=webhook_data.get("secret"), + disabled=webhook_data["disabled"], ) return webhook @@ -11385,8 +11112,6 @@ def _parse_revision_from_pr_url(pr_url: str) -> str: get_webhook = api.get_webhook list_webhooks = api.list_webhooks update_webhook = api.update_webhook -create_webhook_job = api.create_webhook_job -create_webhook_job = api.create_webhook_uv_job # User API From 827197991222752c54e303f528f7913c37beba3a Mon Sep 17 00:00:00 2001 From: Quentin Lhoest Date: Tue, 23 Sep 2025 19:47:45 +0200 Subject: [PATCH 5/6] fix tests --- src/huggingface_hub/hf_api.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index 5b76068264..364388c18e 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -9218,8 +9218,8 @@ def get_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None) webhook = WebhookInfo( id=webhook_data["id"], - url=webhook_data["url"], - job=JobSpec(**webhook_data["job"]) if webhook_data["job"] else None, + url=webhook_data.get("url"), + job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None, watched=watched_items, domains=webhook_data["domains"], secret=webhook_data.get("secret"), @@ -9271,9 +9271,7 @@ def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Webhook id=webhook["id"], url=webhook.get("url"), job=JobSpec(**webhook["job"]) if webhook.get("job") else None, - watched=[ - WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhooks_data["watched"] - ], + watched=[WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook["watched"]], domains=webhook["domains"], secret=webhook.get("secret"), disabled=webhook["disabled"], From a6dcaf1c6cde754ecd21532205c707b05e9da29c Mon Sep 17 00:00:00 2001 From: Quentin Lhoest <42851186+lhoestq@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:48:32 +0200 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: Lucain --- docs/source/en/guides/jobs.md | 2 +- docs/source/en/guides/webhooks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/en/guides/jobs.md b/docs/source/en/guides/jobs.md index ca00621fe6..466cdb0476 100644 --- a/docs/source/en/guides/jobs.md +++ b/docs/source/en/guides/jobs.md @@ -446,5 +446,5 @@ webhook = create_webhook( ) ``` -The webhook triggers the Job with the webhook payload in the environment variable WEBHOOK_PAYLOAD. +The webhook triggers the Job with the webhook payload in the environment variable `WEBHOOK_PAYLOAD`. You can find more information on webhooks in the [Webhooks documentation](./webhooks). diff --git a/docs/source/en/guides/webhooks.md b/docs/source/en/guides/webhooks.md index 203676d8ce..f6d53d7157 100644 --- a/docs/source/en/guides/webhooks.md +++ b/docs/source/en/guides/webhooks.md @@ -43,7 +43,7 @@ webhook = create_webhook( ) ``` -The webhook triggers the Job with the webhook payload in the environment variable WEBHOOK_PAYLOAD. +The webhook triggers the Job with the webhook payload in the environment variable `WEBHOOK_PAYLOAD`. For more information on Hugging Face Jobs, available hardware (CPU, GPU) and UV scripts, see the [Jobs documentation](./jobs). ### Listing Webhooks