github-events 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- github_events/__init__.py +42 -0
- github_events/_models.py +7033 -0
- github_events/_registry.py +485 -0
- github_events/dispatch.py +138 -0
- github_events/dispatch.pyi +878 -0
- github_events/fastapi.py +56 -0
- github_events/py.typed +0 -0
- github_events/validation.py +70 -0
- github_events-0.1.0.dist-info/METADATA +37 -0
- github_events-0.1.0.dist-info/RECORD +11 -0
- github_events-0.1.0.dist-info/WHEEL +4 -0
github_events/fastapi.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""FastAPI integration: mount one endpoint that verifies and dispatches deliveries.
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
from github_events import WebhookDispatcher
|
|
5
|
+
|
|
6
|
+
hook = WebhookDispatcher(secret="...")
|
|
7
|
+
app = FastAPI()
|
|
8
|
+
app.include_router(hook.as_fastapi_router(path="/webhooks/github"))
|
|
9
|
+
|
|
10
|
+
Responses: 204 on success (handled or not — unknown events are acknowledged so
|
|
11
|
+
GitHub doesn't retry), 400 for a missing event header or malformed JSON, 401
|
|
12
|
+
for a bad signature, 422 when a present field has the wrong type.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from fastapi import APIRouter, HTTPException, Request, Response
|
|
21
|
+
except ImportError as e: # pragma: no cover
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"github_events.fastapi requires fastapi — install github-events[fastapi]"
|
|
24
|
+
) from e
|
|
25
|
+
|
|
26
|
+
from pydantic import ValidationError # fastapi always brings pydantic
|
|
27
|
+
|
|
28
|
+
from github_events.dispatch import SignatureVerificationError, WebhookDispatcher
|
|
29
|
+
|
|
30
|
+
__all__ = ["create_router"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_router(dispatcher: WebhookDispatcher, *, path: str = "/") -> APIRouter:
|
|
34
|
+
"""An APIRouter whose single POST endpoint feeds deliveries to `dispatcher`."""
|
|
35
|
+
router = APIRouter()
|
|
36
|
+
|
|
37
|
+
@router.post(path, status_code=204)
|
|
38
|
+
async def receive_github_event(request: Request) -> Response:
|
|
39
|
+
event = request.headers.get("x-github-event")
|
|
40
|
+
if event is None:
|
|
41
|
+
raise HTTPException(400, "missing X-GitHub-Event header")
|
|
42
|
+
try:
|
|
43
|
+
await dispatcher.dispatch(
|
|
44
|
+
event,
|
|
45
|
+
await request.body(),
|
|
46
|
+
signature_256=request.headers.get("x-hub-signature-256"),
|
|
47
|
+
)
|
|
48
|
+
except SignatureVerificationError as e:
|
|
49
|
+
raise HTTPException(401, str(e)) from e
|
|
50
|
+
except json.JSONDecodeError as e:
|
|
51
|
+
raise HTTPException(400, f"invalid JSON body: {e}") from e
|
|
52
|
+
except ValidationError as e:
|
|
53
|
+
raise HTTPException(422, e.errors()) from e
|
|
54
|
+
return Response(status_code=204)
|
|
55
|
+
|
|
56
|
+
return router
|
github_events/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Optional pydantic validation of webhook payloads (github-events[pydantic]).
|
|
2
|
+
|
|
3
|
+
Validation is deliberately lenient — GitHub evolves payloads constantly, and
|
|
4
|
+
GitHub Enterprise Server deliveries lag the api.github.com schema:
|
|
5
|
+
|
|
6
|
+
- Fields the schema doesn't know are ignored, and the payload returned here is
|
|
7
|
+
the exact dict GitHub sent — validation never mutates or drops keys.
|
|
8
|
+
- Missing fields are tolerated: the schema's `required` lists reflect current
|
|
9
|
+
api.github.com, not every delivery (recorded examples and GHES omit fields).
|
|
10
|
+
- `null` for a non-nullable field is tolerated (schema nullability drift).
|
|
11
|
+
- A field present with a non-null value of the wrong type fails: ValidationError.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from functools import cache
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from pydantic import TypeAdapter, ValidationError
|
|
21
|
+
except ImportError as e: # pragma: no cover
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"github_events.validation requires pydantic — install github-events[pydantic]"
|
|
24
|
+
) from e
|
|
25
|
+
|
|
26
|
+
from github_events._registry import ACTION_EVENTS, ACTION_TYPES, EVENT_TYPES
|
|
27
|
+
|
|
28
|
+
__all__ = ["ValidationError", "get_adapter", "payload_type_for", "validate_payload"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cache
|
|
32
|
+
def get_adapter(payload_type: Any) -> TypeAdapter[Any]:
|
|
33
|
+
"""One lazily-built TypeAdapter per payload type (they're expensive to build)."""
|
|
34
|
+
return TypeAdapter(payload_type)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def payload_type_for(event: str, action: str | None) -> Any | None:
|
|
38
|
+
"""The payload type to validate against, or None to pass the payload through.
|
|
39
|
+
|
|
40
|
+
Unknown events — and known events with an action GitHub added after these
|
|
41
|
+
types were generated — get no validation rather than a rejection.
|
|
42
|
+
"""
|
|
43
|
+
if action is not None:
|
|
44
|
+
if (t := ACTION_TYPES.get((event, action))) is not None:
|
|
45
|
+
return t
|
|
46
|
+
if event in ACTION_EVENTS:
|
|
47
|
+
return None
|
|
48
|
+
return EVENT_TYPES.get(event)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_payload(event: str, action: str | None, data: Any) -> Any:
|
|
52
|
+
"""Check `data` against the payload type for (event, action); return `data`.
|
|
53
|
+
|
|
54
|
+
Raises pydantic.ValidationError only when a present, non-null field has the
|
|
55
|
+
wrong type. Missing fields, unknown fields and unexpected nulls never fail
|
|
56
|
+
validation (see module docstring).
|
|
57
|
+
"""
|
|
58
|
+
if (t := payload_type_for(event, action)) is None:
|
|
59
|
+
return data
|
|
60
|
+
try:
|
|
61
|
+
get_adapter(t).validate_python(data)
|
|
62
|
+
except ValidationError as e:
|
|
63
|
+
hard = [
|
|
64
|
+
err
|
|
65
|
+
for err in e.errors()
|
|
66
|
+
if err["type"] != "missing" and err.get("input") is not None
|
|
67
|
+
]
|
|
68
|
+
if hard:
|
|
69
|
+
raise
|
|
70
|
+
return data
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: github-events
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TypedDicts for every GitHub webhook event, with optional pydantic validation and FastAPI dispatch
|
|
5
|
+
Requires-Dist: typing-extensions>=4.12
|
|
6
|
+
Requires-Dist: fastapi>=0.110 ; extra == 'fastapi'
|
|
7
|
+
Requires-Dist: pydantic>=2.0 ; extra == 'fastapi'
|
|
8
|
+
Requires-Dist: pydantic>=2.0 ; extra == 'pydantic'
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Provides-Extra: fastapi
|
|
11
|
+
Provides-Extra: pydantic
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# github-events
|
|
15
|
+
|
|
16
|
+
Typed GitHub webhook payloads, HMAC signature verification, and event dispatch.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install github-events
|
|
20
|
+
# Optional payload validation and FastAPI integration:
|
|
21
|
+
pip install 'github-events[fastapi]'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import os
|
|
26
|
+
from github_events import WebhookDispatcher
|
|
27
|
+
|
|
28
|
+
hooks = WebhookDispatcher(secret=os.environ["GITHUB_WEBHOOK_SECRET"])
|
|
29
|
+
|
|
30
|
+
@hooks.on("push")
|
|
31
|
+
async def on_push(payload):
|
|
32
|
+
print(payload["ref"])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Pass the raw request body and GitHub webhook headers to the dispatcher, or mount
|
|
36
|
+
its `as_fastapi_router()` in your FastAPI application. The base package depends only on `typing-extensions`. The `pydantic` extra adds validation; the `fastapi` extra
|
|
37
|
+
adds the HTTP integration and validation dependencies.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
github_events/__init__.py,sha256=6NJYlFk0b6SuY5VyRILO6DLVCzfAVQ_oNqbP4N42mgU,1314
|
|
2
|
+
github_events/_models.py,sha256=CuMOZ2PcamNBynzbGSyNsW5NkUx72O0KcWPbI96cG3E,217679
|
|
3
|
+
github_events/_registry.py,sha256=D_3C7KnmeriBUGKMjCzGZ3Ao86PIIhKPN29NT3mOkuQ,22542
|
|
4
|
+
github_events/dispatch.py,sha256=PdYx7YGixy8soWuE7HTxFNfxRFGp6rt0JeJvLvgKWDE,5594
|
|
5
|
+
github_events/dispatch.pyi,sha256=Y0qvBNdvQcuBgGEG_9AXYmHtXOEGYyFuqa6exdRZg24,49625
|
|
6
|
+
github_events/fastapi.py,sha256=1Q61IwXk9a-KYr4spYEZmrUnEWBNwlTpkHcbRG8WBDA,2019
|
|
7
|
+
github_events/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
github_events/validation.py,sha256=3A8-wIOTFVvjNyFQjV7CzrmtP3ITnPRR9fA4boTp1mg,2611
|
|
9
|
+
github_events-0.1.0.dist-info/WHEEL,sha256=Q9FtwzuR2QE37l-JIkuyklGnJJiCBHKnsPVQ9vzCMzQ,81
|
|
10
|
+
github_events-0.1.0.dist-info/METADATA,sha256=WejitsdMWbXR0oJsY9drmtEGGqbqNlnZov2lNzVHZRo,1212
|
|
11
|
+
github_events-0.1.0.dist-info/RECORD,,
|