File size: 4,259 Bytes
d9c353a
efc7a8e
d9c353a
efc7a8e
 
6873536
 
d9c353a
efc7a8e
 
d9c353a
efc7a8e
 
d9c353a
efc7a8e
d9c353a
efc7a8e
 
 
 
 
 
 
d9c353a
efc7a8e
 
 
d9c353a
 
 
 
 
efc7a8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d9c353a
 
 
 
 
 
 
 
 
 
 
 
 
dfcff08
 
d9c353a
 
efc7a8e
d9c353a
efc7a8e
6873536
efc7a8e
d9c353a
 
efc7a8e
 
 
 
 
 
 
e7d09e5
efc7a8e
d9c353a
 
 
 
 
 
 
6873536
 
 
d9c353a
6873536
 
 
d9c353a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import os
from pathlib import Path
from typing import Literal, Optional, Set, Union

import gradio as gr
from fastapi import Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel


class GradioWebhookApp:
    """
    ```py
    from gradio_webhooks import GradioWebhookApp

    app = GradioWebhookApp()


    @app.add_webhook("/test_webhook")
    async def hello():
        return {"in_gradio": True}


    app.ready()
    ```
    """

    def __init__(
        self,
        landing_path: Union[str, Path] = "README.md",
        webhook_secret: Optional[str] = None,
    ) -> None:
        # Use README.md as landing page or provide any markdown file
        landing_path = Path(landing_path)
        landing_content = landing_path.read_text()
        if landing_path.name == "README.md":
            landing_content = landing_content.split("---")[-1].strip()

        # Simple gradio app with landing content
        block = gr.Blocks()
        with block:
            gr.Markdown(landing_content)

        # Launch gradio app:
        #   - as non-blocking so that webhooks can be added afterwards
        #   - as shared if launch locally (to receive webhooks)
        app, _, _ = block.launch(prevent_thread_lock=True, share=not block.is_space)
        self.gradio_app = block
        self.fastapi_app = app
        self.webhook_paths: Set[str] = set()

        # Add auth middleware to check the "X-Webhook-Secret" header
        self._webhook_secret = webhook_secret or os.getenv("WEBHOOK_SECRET")
        if self._webhook_secret is None:
            print(
                "\nWebhook secret is not defined. This means your webhook endpoints will be open to everyone."
            )
            print(
                "To add a secret, set `WEBHOOK_SECRET` as environment variable or pass it at initialization: "
                "\n\t`app = GradioWebhookApp(webhook_secret='my_secret', ...)`"
            )
            print(
                "For more details about Webhook secrets, please refer to https://huggingface.co/docs/hub/webhooks#webhook-secret."
            )
        else:
            print("\nWebhook secret is correctly defined.")
        app.middleware("http")(self._webhook_secret_middleware)

    def add_webhook(self, path: str):
        """Decorator to add a webhook to the server app."""
        self.webhook_paths.add(path)
        return self.fastapi_app.post(path)

    def ready(self) -> None:
        """Set the app as "ready" and block main thread to keep it running."""
        url = (
            self.gradio_app.share_url
            if self.gradio_app.share_url is not None
            else self.gradio_app.local_url
        ).strip("/")
        print("\nWebhooks are correctly setup and ready to use:")
        print("\n".join(f"  - POST {url}{webhook}" for webhook in self.webhook_paths))
        print("Go to https://huggingface.co/settings/webhooks to setup your webhooks.")
        self.gradio_app.block_thread()

    async def _webhook_secret_middleware(self, request: Request, call_next) -> None:
        """Middleware to check "X-Webhook-Secret" header on every webhook request."""
        if request.url.path in self.webhook_paths:
            if self._webhook_secret is not None:
                request_secret = request.headers.get("x-webhook-secret")
                if request_secret is None:
                    return JSONResponse(
                        {"error": "x-webhook-secret header not set."}, status_code=401
                    )
                if request_secret != self._webhook_secret:
                    return JSONResponse(
                        {"error": "Invalid webhook secret."}, status_code=403
                    )
        return await call_next(request)


class WebhookPayloadEvent(BaseModel):
    action: Literal["create", "update", "delete"]
    scope: str


class WebhookPayloadRepo(BaseModel):
    type: Literal["dataset", "model", "space"]
    name: str
    private: bool


class WebhookPayloadDiscussion(BaseModel):
    num: int
    isPullRequest: bool
    status: Literal["open", "closed", "merged"]


class WebhookPayload(BaseModel):
    event: WebhookPayloadEvent
    repo: WebhookPayloadRepo
    discussion: Optional[WebhookPayloadDiscussion]