09bdb8aaad
* feat(python-sdk): add webhook verification and event handling Add webhook support to the Python SDK matching the JS SDK implementation: - Add Webhooks class with verify() and construct_event() methods - Implement HMAC-SHA256 signature verification with timing-safe comparison - Add timestamp validation with configurable tolerance (default 5 minutes) - Add comprehensive webhook event types (18 events: email, contact, domain, test) - Add WebhookVerificationError with typed error codes - Export webhook constants (headers) and types * fix(python-sdk): harden webhook parsing and typing Normalize invalid UTF-8 webhook payloads to INVALID_BODY errors so verify() safely returns false, and narrow base email webhook event types to avoid discriminated-union overlap. Add regression tests for both paths. * chore(python-sdk): bump package version to 0.2.9 * feat(python-sdk): add local webhook test example project Add a runnable Flask receiver and signed webhook sender under packages/python-sdk/example, and link it from the Python SDK README for local verification. --------- Co-authored-by: Claude <noreply@anthropic.com>
38 lines
1007 B
Python
38 lines
1007 B
Python
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from flask import Flask, jsonify, request
|
|
from flask.typing import ResponseReturnValue
|
|
|
|
from usesend import UseSend, WebhookVerificationError # type: ignore[import-not-found]
|
|
|
|
|
|
WEBHOOK_SECRET = os.getenv("USESEND_WEBHOOK_SECRET", "whsec_test")
|
|
|
|
app = Flask(__name__)
|
|
usesend = UseSend("us_test")
|
|
webhooks = usesend.webhooks(WEBHOOK_SECRET)
|
|
|
|
|
|
@app.post("/webhook")
|
|
def webhook() -> ResponseReturnValue:
|
|
raw_body = request.get_data()
|
|
|
|
try:
|
|
event = webhooks.construct_event(raw_body, headers=request.headers)
|
|
except WebhookVerificationError as exc:
|
|
return jsonify({"ok": False, "code": exc.code, "message": str(exc)}), 400
|
|
|
|
print(f"Received event: {event['type']}")
|
|
|
|
if event["type"] == "email.bounced":
|
|
bounce = event["data"].get("bounce", {})
|
|
print("Bounce details:", bounce)
|
|
|
|
return jsonify({"ok": True, "type": event["type"]}), 200
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="127.0.0.1", port=8000)
|