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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.17
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any