Octohook parses GitHub webhook payloads into typed Python classes and provides a decorator-based system for routing webhooks to handlers.
pip install octohookDefine a handler for pull request events:
# handlers.py
from octohook import hook, WebhookEvent, WebhookEventAction
from octohook.events import PullRequestEvent
@hook(WebhookEvent.PULL_REQUEST, [WebhookEventAction.OPENED])
def on_pr_opened(event: PullRequestEvent):
print(f"PR opened: {event.pull_request.title}")Wire it up in your web framework (example using Flask):
# app.py
from flask import Flask, request, Response
import octohook
app = Flask(__name__)
octohook.setup(modules=["handlers"])
@app.route('/webhook', methods=['POST'])
def webhook():
github_event = request.headers.get('X-GitHub-Event')
octohook.handle_webhook(event_name=github_event, payload=request.json)
return Response("OK", status=200)Use octohook.parse() when you want direct control over webhook handling:
from flask import Flask, request, Response
import octohook
from octohook.events import PullRequestEvent
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
github_event = request.headers.get('X-GitHub-Event')
event = octohook.parse(github_event, request.json)
if isinstance(event, PullRequestEvent):
return Response(event.pull_request.title, status=200)
return Response("OK", status=200)Use @hook when you have multiple handlers or want cleaner routing. The decorator takes four parameters:
event: AWebhookEventenum value (required)actions: List ofWebhookEventActionvalues (optional - omit to match any action)repositories: List of repository full names to filter on (optional)debug: WhenTrue, only debug hooks fire for that event (default:False)
from octohook import hook, WebhookEvent, WebhookEventAction
from octohook.events import PullRequestEvent
@hook(WebhookEvent.PULL_REQUEST, [WebhookEventAction.OPENED, WebhookEventAction.EDITED])
def on_pr_change(event: PullRequestEvent):
print(event.pull_request.title)on_pr_change() is called with the parsed PullRequestEvent whenever a pull_request webhook arrives with an opened or edited action.
If you omit the actions parameter, the handler fires for any action. For events like push that have no action field, always omit actions.
Use setup() to load handlers from your modules:
import octohook
# Recursively imports handlers from the specified modules
octohook.setup(modules=["hooks", "webhooks.github"])Filter hooks to specific repositories using their full name (e.g., "owner/repo"):
from octohook import hook, WebhookEvent
from octohook.events import PushEvent
@hook(WebhookEvent.PUSH, repositories=["myorg/backend", "myorg/frontend"])
def on_push(event: PushEvent):
print(f"Push to {event.repository.full_name}")Set debug=True on any hook to make only debug hooks fire for that event type:
from octohook import hook, WebhookEvent
from octohook.events import PullRequestEvent
@hook(WebhookEvent.PULL_REQUEST, debug=True)
def debug_pr(event: PullRequestEvent):
print(event) # Only this runs for PR events when debug=True# hooks/github.py
from octohook import hook, WebhookEvent, WebhookEventAction
from octohook.events import LabelEvent, PullRequestEvent
@hook(WebhookEvent.LABEL, [WebhookEventAction.CREATED])
def on_label_created(event: LabelEvent):
print(f"Label created: {event.label.name}")
@hook(WebhookEvent.PULL_REQUEST)
def on_any_pr_event(event: PullRequestEvent):
print(f"PR #{event.number}: {event.action}")# app.py
from flask import Flask, request, Response
import octohook
app = Flask(__name__)
octohook.setup(modules=["hooks"])
@app.route('/webhook', methods=['POST'])
def webhook():
github_event = request.headers.get('X-GitHub-Event')
octohook.handle_webhook(event_name=github_event, payload=request.json)
return Response("OK", status=200)handle_webhookruns handlers sequentially and blocks until complete.- Exceptions are logged to
logging.getLogger('octohook')but don't stop execution.
GitHub webhook payloads include URL templates with placeholders:
{
"repository": {
"archive_url": "https://api.github.com/repos/owner/repo/{archive_format}{/ref}"
}
}Octohook models provide methods to interpolate these templates:
# event.repository is a Repository model with helper methods
>>> event.repository.archive_url("tarball")
'https://api.github.com/repos/owner/repo/tarball'
>>> event.repository.archive_url("tarball", "main")
'https://api.github.com/repos/owner/repo/tarball/main'GitHub sends different payload structures depending on the event type and action. Not all fields are present in all payloads, so octohook uses Optional types extensively. Fields are only marked as required if they appear in all known payloads for that model.
For example, the changes key only appears with edited actions. For other actions, event.changes is None.
Current test coverage is documented in tests/TestCases.md. PRs adding missing event payloads are welcome.