Full webhook system: CRUD endpoints (list/filter/get/create/update/delete), WebhookDelivery model for delivery audit trail, dispatch engine with 3-attempt retry and exponential backoff, Celery task integration with sync fallback, and webhook firing hooks in runner.py and sweep.py event paths.
124 lines
3.8 KiB
Python
124 lines
3.8 KiB
Python
"""Webhooks router — CRUD for webhook configurations and async dispatch.
|
|
|
|
Webhooks fire when events occur in runner.py and sweep.py. Delivery is
|
|
dispatched asynchronously via Celery (or synchronous fallback). Each delivery
|
|
attempt retries up to 3 times with exponential backoff. Delivery status is
|
|
logged to the WebhookDelivery table.
|
|
"""
|
|
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from auth import get_current_user
|
|
from main import get_db
|
|
from models import User, WebhookConfig
|
|
from schemas import (
|
|
WebhookCreate,
|
|
WebhookListResponse,
|
|
WebhookResponse,
|
|
WebhookUpdate,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _get_webhook_or_404(db: Session, webhook_id: uuid.UUID) -> WebhookConfig:
|
|
webhook = db.query(WebhookConfig).filter(WebhookConfig.id == webhook_id).first()
|
|
if webhook is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Webhook not found")
|
|
return webhook
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CRUD
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.get("/", response_model=WebhookListResponse)
|
|
def list_webhooks(
|
|
event_type: str | None = None,
|
|
db: Session = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> WebhookListResponse:
|
|
"""List all webhook configurations, optionally filtered by event_type."""
|
|
query = db.query(WebhookConfig)
|
|
if event_type:
|
|
query = query.filter(WebhookConfig.event_type == event_type)
|
|
webhooks = query.order_by(WebhookConfig.event_type).all()
|
|
return WebhookListResponse(
|
|
items=[WebhookResponse.model_validate(wh) for wh in webhooks],
|
|
total=len(webhooks),
|
|
)
|
|
|
|
|
|
@router.post("/", response_model=WebhookResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_webhook(
|
|
body: WebhookCreate,
|
|
db: Session = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> WebhookResponse:
|
|
"""Create a new webhook configuration."""
|
|
webhook = WebhookConfig(
|
|
event_type=body.event_type,
|
|
url=body.url,
|
|
headers=body.headers,
|
|
is_active=body.is_active,
|
|
)
|
|
db.add(webhook)
|
|
db.commit()
|
|
db.refresh(webhook)
|
|
return WebhookResponse.model_validate(webhook)
|
|
|
|
|
|
@router.get("/{webhook_id}", response_model=WebhookResponse)
|
|
def get_webhook(
|
|
webhook_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> WebhookResponse:
|
|
"""Get a single webhook configuration."""
|
|
webhook = _get_webhook_or_404(db, webhook_id)
|
|
return WebhookResponse.model_validate(webhook)
|
|
|
|
|
|
@router.put("/{webhook_id}", response_model=WebhookResponse)
|
|
def update_webhook(
|
|
webhook_id: uuid.UUID,
|
|
body: WebhookUpdate,
|
|
db: Session = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> WebhookResponse:
|
|
"""Update a webhook configuration."""
|
|
webhook = _get_webhook_or_404(db, webhook_id)
|
|
|
|
if body.event_type is not None:
|
|
webhook.event_type = body.event_type
|
|
if body.url is not None:
|
|
webhook.url = body.url
|
|
if body.headers is not None:
|
|
webhook.headers = body.headers
|
|
if body.is_active is not None:
|
|
webhook.is_active = body.is_active
|
|
|
|
db.commit()
|
|
db.refresh(webhook)
|
|
return WebhookResponse.model_validate(webhook)
|
|
|
|
|
|
@router.delete("/{webhook_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_webhook(
|
|
webhook_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> None:
|
|
"""Delete a webhook configuration."""
|
|
webhook = _get_webhook_or_404(db, webhook_id)
|
|
db.delete(webhook)
|
|
db.commit()
|