promptlooper/backend/routers/webhooks.py
John Lightner 0f64dfbb02 MAESTRO: Implement webhook CRUD router, async dispatch with retry logic, and delivery logging
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.
2026-04-07 03:41:04 -05:00

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()