Wauplin HF staff commited on
Commit
d9c353a
1 Parent(s): efc7a8e

refacto + add example

Browse files
Files changed (3) hide show
  1. app.py +6 -44
  2. gradio_webhooks.py +66 -8
  3. simple.py +12 -0
app.py CHANGED
@@ -1,9 +1,8 @@
1
- # Taken from https://huggingface.co/spaces/huggingface-projects/auto-retrain
2
  import os
3
  from pathlib import Path
4
- from typing import Literal, Optional
5
 
6
- from fastapi import BackgroundTasks, Header, HTTPException
7
  from huggingface_hub import (
8
  CommitOperationAdd,
9
  CommitOperationDelete,
@@ -16,55 +15,18 @@ from huggingface_hub import (
16
  space_info,
17
  )
18
  from huggingface_hub.repocard import RepoCard
19
- from pydantic import BaseModel
20
  from requests import HTTPError
21
 
22
- from gradio_webhooks import WebhookGradioApp
23
 
24
- WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET")
25
  HF_TOKEN = os.getenv("HF_TOKEN")
26
 
27
 
28
- class WebhookPayloadEvent(BaseModel):
29
- action: Literal["create", "update", "delete"]
30
- scope: str
31
-
32
-
33
- class WebhookPayloadRepo(BaseModel):
34
- type: Literal["dataset", "model", "space"]
35
- name: str
36
- private: bool
37
-
38
-
39
- class WebhookPayloadDiscussion(BaseModel):
40
- num: int
41
- isPullRequest: bool
42
- status: Literal["open", "closed", "merged"]
43
-
44
-
45
- class WebhookPayload(BaseModel):
46
- event: WebhookPayloadEvent
47
- repo: WebhookPayloadRepo
48
- discussion: Optional[WebhookPayloadDiscussion]
49
-
50
-
51
- app = WebhookGradioApp()
52
 
53
 
54
  @app.add_webhook("/webhook")
55
- async def post_webhook(
56
- payload: WebhookPayload,
57
- task_queue: BackgroundTasks,
58
- x_webhook_secret: Optional[str] = Header(default=None),
59
- ):
60
- print("Received new hook!")
61
- if x_webhook_secret is None:
62
- print("HTTP 401: No webhook secret")
63
- raise HTTPException(401)
64
- if x_webhook_secret != WEBHOOK_SECRET:
65
- print("HTTP 403: wrong webhook secret")
66
- raise HTTPException(403)
67
-
68
  if payload.repo.type != "space":
69
  print("HTTP 400: not a space")
70
  raise HTTPException(400, f"Must be a Space, not {payload.repo.type}")
@@ -273,4 +235,4 @@ PR is now merged/closed. The temporary test Space has been deleted.
273
  """
274
 
275
 
276
- app.block_thread()
 
 
1
  import os
2
  from pathlib import Path
3
+ from typing import Literal
4
 
5
+ from fastapi import BackgroundTasks, HTTPException
6
  from huggingface_hub import (
7
  CommitOperationAdd,
8
  CommitOperationDelete,
 
15
  space_info,
16
  )
17
  from huggingface_hub.repocard import RepoCard
 
18
  from requests import HTTPError
19
 
20
+ from gradio_webhooks import GradioWebhookApp, WebhookPayload
21
 
 
22
  HF_TOKEN = os.getenv("HF_TOKEN")
23
 
24
 
25
+ app = GradioWebhookApp()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
 
28
  @app.add_webhook("/webhook")
29
+ async def post_webhook(payload: WebhookPayload, task_queue: BackgroundTasks):
 
 
 
 
 
 
 
 
 
 
 
 
30
  if payload.repo.type != "space":
31
  print("HTTP 400: not a space")
32
  raise HTTPException(400, f"Must be a Space, not {payload.repo.type}")
 
235
  """
236
 
237
 
238
+ app.ready()
gradio_webhooks.py CHANGED
@@ -1,15 +1,18 @@
 
1
  from pathlib import Path
2
- from typing import Set, Union
3
 
4
  import gradio as gr
 
 
5
 
6
 
7
- class WebhookGradioApp:
8
  """
9
  ```py
10
- from gradio_webhooks import WebhookGradioApp
11
 
12
- app = WebhookGradioApp()
13
 
14
 
15
  @app.add_webhook("/test_webhook")
@@ -17,11 +20,15 @@ class WebhookGradioApp:
17
  return {"in_gradio": True}
18
 
19
 
20
- app.block_thread()
21
  ```
22
  """
23
 
24
- def __init__(self, landing_path: Union[str, Path] = "README.md") -> None:
 
 
 
 
25
  # Use README.md as landing page or provide any markdown file
26
  landing_path = Path(landing_path)
27
  landing_content = landing_path.read_text()
@@ -41,11 +48,28 @@ class WebhookGradioApp:
41
  self.fastapi_app = app
42
  self.webhook_paths: Set[str] = set()
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def add_webhook(self, path: str):
 
45
  self.webhook_paths.add(path)
46
- return self.fastapi_app.post(path)
47
 
48
- def block_thread(self) -> None:
 
49
  url = (
50
  self.gradio_app.share_url
51
  if self.gradio_app.share_url is not None
@@ -55,3 +79,37 @@ class WebhookGradioApp:
55
  print("\n".join(f" - POST {url}{webhook}" for webhook in self.webhook_paths))
56
  print(f"Checkout {url}/docs for more details.")
57
  self.gradio_app.block_thread()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
  from pathlib import Path
3
+ from typing import Literal, Optional, Set, Union
4
 
5
  import gradio as gr
6
+ from fastapi import HTTPException, Request
7
+ from pydantic import BaseModel
8
 
9
 
10
+ class GradioWebhookApp:
11
  """
12
  ```py
13
+ from gradio_webhooks import GradioWebhookApp
14
 
15
+ app = GradioWebhookApp()
16
 
17
 
18
  @app.add_webhook("/test_webhook")
 
20
  return {"in_gradio": True}
21
 
22
 
23
+ app.ready()
24
  ```
25
  """
26
 
27
+ def __init__(
28
+ self,
29
+ landing_path: Union[str, Path] = "README.md",
30
+ webhook_secret: Optional[str] = None,
31
+ ) -> None:
32
  # Use README.md as landing page or provide any markdown file
33
  landing_path = Path(landing_path)
34
  landing_content = landing_path.read_text()
 
48
  self.fastapi_app = app
49
  self.webhook_paths: Set[str] = set()
50
 
51
+ # Add auth middleware to check the "X-Webhook-Secret" header
52
+ self._webhook_secret = webhook_secret or os.getenv("WEBHOOK_SECRET")
53
+ if self._webhook_secret is None:
54
+ print(
55
+ "\nWebhook secret is not defined. This means your webhook endpoints will be open to everyone."
56
+ )
57
+ print(
58
+ "To add a secret, set `WEBHOOK_SECRET` as environment variable or pass it at initialization: "
59
+ "\n\t`app = GradioWebhookApp(webhook_secret='my_secret', ...)`"
60
+ )
61
+ print(
62
+ "For more details about Webhook secrets, please refer to https://huggingface.co/docs/hub/webhooks#webhook-secret."
63
+ )
64
+ app.middleware("http")(self._webhook_secret_middleware)
65
+
66
  def add_webhook(self, path: str):
67
+ """Decorator to add a webhook to the server app."""
68
  self.webhook_paths.add(path)
69
+ return self.fastapi_app.get(path)
70
 
71
+ def ready(self) -> None:
72
+ """Set the app as "ready" and block main thread to keep it running."""
73
  url = (
74
  self.gradio_app.share_url
75
  if self.gradio_app.share_url is not None
 
79
  print("\n".join(f" - POST {url}{webhook}" for webhook in self.webhook_paths))
80
  print(f"Checkout {url}/docs for more details.")
81
  self.gradio_app.block_thread()
82
+
83
+ async def _webhook_secret_middleware(self, request: Request, call_next) -> None:
84
+ """Middleware to check "X-Webhook-Secret" header on every webhook request."""
85
+ if request.url.path in self.webhook_paths:
86
+ if self._webhook_secret is not None:
87
+ request_secret = request.headers.get("x-webhook-secret")
88
+ if request_secret is None:
89
+ raise HTTPException(401)
90
+ if request_secret != self._webhook_secret:
91
+ raise HTTPException(403)
92
+ return await call_next(request)
93
+
94
+
95
+ class WebhookPayloadEvent(BaseModel):
96
+ action: Literal["create", "update", "delete"]
97
+ scope: str
98
+
99
+
100
+ class WebhookPayloadRepo(BaseModel):
101
+ type: Literal["dataset", "model", "space"]
102
+ name: str
103
+ private: bool
104
+
105
+
106
+ class WebhookPayloadDiscussion(BaseModel):
107
+ num: int
108
+ isPullRequest: bool
109
+ status: Literal["open", "closed", "merged"]
110
+
111
+
112
+ class WebhookPayload(BaseModel):
113
+ event: WebhookPayloadEvent
114
+ repo: WebhookPayloadRepo
115
+ discussion: Optional[WebhookPayloadDiscussion]
simple.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from gradio_webhooks import GradioWebhookApp, WebhookPayload
2
+
3
+ app = GradioWebhookApp()
4
+
5
+
6
+ @app.add_webhook("/my_webhook")
7
+ async def hello(payload: WebhookPayload):
8
+ print(f"Received webhook for repo {payload.repo.name}")
9
+ return {"processed": True}
10
+
11
+
12
+ app.ready()