Skip to content

Commit c2e6129

Browse files
lhoestqWauplin
andauthored
Webhook jobs (#3379)
* webhook jobs * minor * very minor * create_webhook(job_id=job_id, ...) * fix tests * Apply suggestions from code review Co-authored-by: Lucain <lucain@huggingface.co> --------- Co-authored-by: Lucain <lucain@huggingface.co>
1 parent 88c5f09 commit c2e6129

File tree

3 files changed

+121
-11
lines changed

3 files changed

+121
-11
lines changed

docs/source/en/guides/jobs.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,24 @@ Manage scheduled jobs using [`list_scheduled_jobs`], [`inspect_scheduled_job`],
410410
>>> from huggingface_hub import delete_scheduled_job
411411
>>> delete_scheduled_job(scheduled_job_id)
412412
```
413+
414+
### Trigger Jobs with webhooks
415+
416+
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).
417+
418+
Use [`create_webhook`] to create a webhook that triggers a Job when a change happens in a Hugging Face repository:
419+
420+
```python
421+
from huggingface_hub import create_webhook
422+
423+
# Example: Creating a webhook that triggers a Job
424+
webhook = create_webhook(
425+
job_id=job_id,
426+
watched=[{"type": "user", "name": "your-username"}, {"type": "org", "name": "your-org-name"}],
427+
domains=["repo", "discussion"],
428+
secret="your-secret"
429+
)
430+
```
431+
432+
The webhook triggers the Job with the webhook payload in the environment variable `WEBHOOK_PAYLOAD`.
433+
You can find more information on webhooks in the [Webhooks documentation](./webhooks).

docs/source/en/guides/webhooks.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ webhook = create_webhook(
2828
)
2929
```
3030

31+
A webhook can also trigger a Job to run on Hugging face infrastructure instead of sending the payload to an URL.
32+
In this case you need to pass the ID of a source Job.
33+
34+
```python
35+
from huggingface_hub import create_webhook
36+
37+
# Example: Creating a webhook that triggers a Job
38+
webhook = create_webhook(
39+
job_id=job_id,
40+
watched=[{"type": "user", "name": "your-username"}, {"type": "org", "name": "your-org-name"}],
41+
domains=["repo", "discussion"],
42+
secret="your-secret"
43+
)
44+
```
45+
46+
The webhook triggers the Job with the webhook payload in the environment variable `WEBHOOK_PAYLOAD`.
47+
For more information on Hugging Face Jobs, available hardware (CPU, GPU) and UV scripts, see the [Jobs documentation](./jobs).
48+
3149
### Listing Webhooks
3250

3351
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.

src/huggingface_hub/hf_api.py

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
_warn_on_overwriting_operations,
6666
)
6767
from ._inference_endpoints import InferenceEndpoint, InferenceEndpointType
68-
from ._jobs_api import JobInfo, ScheduledJobInfo, _create_job_spec
68+
from ._jobs_api import JobInfo, JobSpec, ScheduledJobInfo, _create_job_spec
6969
from ._space_api import SpaceHardware, SpaceRuntime, SpaceStorage, SpaceVariable
7070
from ._upload_large_folder import upload_large_folder_internal
7171
from .community import (
@@ -503,11 +503,15 @@ class WebhookWatchedItem:
503503
class WebhookInfo:
504504
"""Data structure containing information about a webhook.
505505
506+
One of `url` or `job` is specified, but not both.
507+
506508
Attributes:
507509
id (`str`):
508510
ID of the webhook.
509-
url (`str`):
511+
url (`str`, *optional*):
510512
URL of the webhook.
513+
job (`JobSpec`, *optional*):
514+
Specifications of the Job to trigger.
511515
watched (`List[WebhookWatchedItem]`):
512516
List of items watched by the webhook, see [`WebhookWatchedItem`].
513517
domains (`List[WEBHOOK_DOMAIN_T]`):
@@ -519,7 +523,8 @@ class WebhookInfo:
519523
"""
520524

521525
id: str
522-
url: str
526+
url: Optional[str]
527+
job: Optional[JobSpec]
523528
watched: List[WebhookWatchedItem]
524529
domains: List[constants.WEBHOOK_DOMAIN_T]
525530
secret: Optional[str]
@@ -9035,6 +9040,7 @@ def get_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None)
90359040
>>> print(webhook)
90369041
WebhookInfo(
90379042
id="654bbbc16f2ec14d77f109cc",
9043+
job=None,
90389044
watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")],
90399045
url="https://webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548",
90409046
secret="my-secret",
@@ -9054,7 +9060,8 @@ def get_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = None)
90549060

90559061
webhook = WebhookInfo(
90569062
id=webhook_data["id"],
9057-
url=webhook_data["url"],
9063+
url=webhook_data.get("url"),
9064+
job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None,
90589065
watched=watched_items,
90599066
domains=webhook_data["domains"],
90609067
secret=webhook_data.get("secret"),
@@ -9104,7 +9111,8 @@ def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Webhook
91049111
return [
91059112
WebhookInfo(
91069113
id=webhook["id"],
9107-
url=webhook["url"],
9114+
url=webhook.get("url"),
9115+
job=JobSpec(**webhook["job"]) if webhook.get("job") else None,
91089116
watched=[WebhookWatchedItem(type=item["type"], name=item["name"]) for item in webhook["watched"]],
91099117
domains=webhook["domains"],
91109118
secret=webhook.get("secret"),
@@ -9117,17 +9125,24 @@ def list_webhooks(self, *, token: Union[bool, str, None] = None) -> List[Webhook
91179125
def create_webhook(
91189126
self,
91199127
*,
9120-
url: str,
9128+
url: Optional[str] = None,
9129+
job_id: Optional[str] = None,
91219130
watched: List[Union[Dict, WebhookWatchedItem]],
91229131
domains: Optional[List[constants.WEBHOOK_DOMAIN_T]] = None,
91239132
secret: Optional[str] = None,
91249133
token: Union[bool, str, None] = None,
91259134
) -> WebhookInfo:
91269135
"""Create a new webhook.
91279136
9137+
The webhook can either send a payload to a URL, or trigger a Job to run on Hugging Face infrastructure.
9138+
This function should be called with one of `url` or `job_id`, but not both.
9139+
91289140
Args:
91299141
url (`str`):
91309142
URL to send the payload to.
9143+
job_id (`str`):
9144+
ID of the source Job to trigger with the webhook payload in the environment variable WEBHOOK_PAYLOAD.
9145+
Additional environment variables are available for convenience: WEBHOOK_REPO_ID, WEBHOOK_REPO_TYPE and WEBHOOK_SECRET.
91319146
watched (`List[WebhookWatchedItem]`):
91329147
List of [`WebhookWatchedItem`] to be watched by the webhook. It can be users, orgs, models, datasets or spaces.
91339148
Watched items can also be provided as plain dictionaries.
@@ -9145,6 +9160,8 @@ def create_webhook(
91459160
Info about the newly created webhook.
91469161
91479162
Example:
9163+
9164+
Create a webhook that sends a payload to a URL
91489165
```python
91499166
>>> from huggingface_hub import create_webhook
91509167
>>> payload = create_webhook(
@@ -9157,6 +9174,43 @@ def create_webhook(
91579174
WebhookInfo(
91589175
id="654bbbc16f2ec14d77f109cc",
91599176
url="https://webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548",
9177+
job=None,
9178+
watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")],
9179+
domains=["repo", "discussion"],
9180+
secret="my-secret",
9181+
disabled=False,
9182+
)
9183+
```
9184+
9185+
Run a Job and then create a webhook that triggers this Job
9186+
```python
9187+
>>> from huggingface_hub import create_webhook, run_job
9188+
>>> job = run_job(
9189+
... image="ubuntu",
9190+
... command=["bash", "-c", r"echo An event occured in $WEBHOOK_REPO_ID: $WEBHOOK_PAYLOAD"],
9191+
... )
9192+
>>> payload = create_webhook(
9193+
... watched=[{"type": "user", "name": "julien-c"}, {"type": "org", "name": "HuggingFaceH4"}],
9194+
... job_id=job.id,
9195+
... domains=["repo", "discussion"],
9196+
... secret="my-secret",
9197+
... )
9198+
>>> print(payload)
9199+
WebhookInfo(
9200+
id="654bbbc16f2ec14d77f109cc",
9201+
url=None,
9202+
job=JobSpec(
9203+
docker_image='ubuntu',
9204+
space_id=None,
9205+
command=['bash', '-c', 'echo An event occured in $WEBHOOK_REPO_ID: $WEBHOOK_PAYLOAD'],
9206+
arguments=[],
9207+
environment={},
9208+
secrets=[],
9209+
flavor='cpu-basic',
9210+
timeout=None,
9211+
tags=None,
9212+
arch=None
9213+
),
91609214
watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")],
91619215
domains=["repo", "discussion"],
91629216
secret="my-secret",
@@ -9166,9 +9220,19 @@ def create_webhook(
91669220
"""
91679221
watched_dicts = [asdict(item) if isinstance(item, WebhookWatchedItem) else item for item in watched]
91689222

9223+
post_webhooks_json = {"watched": watched_dicts, "domains": domains, "secret": secret}
9224+
if url is not None and job_id is not None:
9225+
raise ValueError("Set `url` or `job_id` but not both.")
9226+
elif url is not None:
9227+
post_webhooks_json["url"] = url
9228+
elif job_id is not None:
9229+
post_webhooks_json["jobSourceId"] = job_id
9230+
else:
9231+
raise ValueError("Missing argument for webhook: `url` or `job_id`.")
9232+
91699233
response = get_session().post(
91709234
f"{constants.ENDPOINT}/api/settings/webhooks",
9171-
json={"watched": watched_dicts, "url": url, "domains": domains, "secret": secret},
9235+
json=post_webhooks_json,
91729236
headers=self._build_hf_headers(token=token),
91739237
)
91749238
hf_raise_for_status(response)
@@ -9177,7 +9241,8 @@ def create_webhook(
91779241

91789242
webhook = WebhookInfo(
91799243
id=webhook_data["id"],
9180-
url=webhook_data["url"],
9244+
url=webhook_data.get("url"),
9245+
job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None,
91819246
watched=watched_items,
91829247
domains=webhook_data["domains"],
91839248
secret=webhook_data.get("secret"),
@@ -9233,6 +9298,7 @@ def update_webhook(
92339298
>>> print(updated_payload)
92349299
WebhookInfo(
92359300
id="654bbbc16f2ec14d77f109cc",
9301+
job=None,
92369302
url="https://new.webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548",
92379303
watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")],
92389304
domains=["repo"],
@@ -9256,7 +9322,8 @@ def update_webhook(
92569322

92579323
webhook = WebhookInfo(
92589324
id=webhook_data["id"],
9259-
url=webhook_data["url"],
9325+
url=webhook_data.get("url"),
9326+
job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None,
92609327
watched=watched_items,
92619328
domains=webhook_data["domains"],
92629329
secret=webhook_data.get("secret"),
@@ -9288,6 +9355,7 @@ def enable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = Non
92889355
>>> enabled_webhook
92899356
WebhookInfo(
92909357
id="654bbbc16f2ec14d77f109cc",
9358+
job=None,
92919359
url="https://webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548",
92929360
watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")],
92939361
domains=["repo", "discussion"],
@@ -9307,7 +9375,8 @@ def enable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = Non
93079375

93089376
webhook = WebhookInfo(
93099377
id=webhook_data["id"],
9310-
url=webhook_data["url"],
9378+
url=webhook_data.get("url"),
9379+
job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None,
93119380
watched=watched_items,
93129381
domains=webhook_data["domains"],
93139382
secret=webhook_data.get("secret"),
@@ -9340,6 +9409,7 @@ def disable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = No
93409409
WebhookInfo(
93419410
id="654bbbc16f2ec14d77f109cc",
93429411
url="https://webhook.site/a2176e82-5720-43ee-9e06-f91cb4c91548",
9412+
jon=None,
93439413
watched=[WebhookWatchedItem(type="user", name="julien-c"), WebhookWatchedItem(type="org", name="HuggingFaceH4")],
93449414
domains=["repo", "discussion"],
93459415
secret="my-secret",
@@ -9358,7 +9428,8 @@ def disable_webhook(self, webhook_id: str, *, token: Union[bool, str, None] = No
93589428

93599429
webhook = WebhookInfo(
93609430
id=webhook_data["id"],
9361-
url=webhook_data["url"],
9431+
url=webhook_data.get("url"),
9432+
job=JobSpec(**webhook_data["job"]) if webhook_data.get("job") else None,
93629433
watched=watched_items,
93639434
domains=webhook_data["domains"],
93649435
secret=webhook_data.get("secret"),

0 commit comments

Comments
 (0)