Python SDK
Django Integration
Complete Django integration with AppConfig initialization, forms, class-based views, middleware, signals, and Celery support.
This example follows Django 5.0 best practices including proper AppConfig, class-based views, and async-ready patterns.
Project Structure
myproject/
├── myproject/
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── sent_integration/
│ ├── __init__.py
│ ├── apps.py # AppConfig with initialization
│ ├── admin.py # Admin integration
│ ├── models.py # Custom model fields
│ ├── forms.py # Forms with validation
│ ├── views.py # Class-based views
│ ├── urls.py # URL patterns
│ ├── middleware.py # Request logging middleware
│ ├── signals.py # Message event signals
│ ├── handlers.py # Signal handlers
│ ├── tasks.py # Celery tasks
│ ├── serializers.py # DRF serializers
│ ├── api_views.py # DRF viewsets
│ └── tests/ # Test files
└── manage.pyConfiguration
settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# Sent DM Configuration
SENT_DM_API_KEY = os.environ.get("SENT_DM_API_KEY")
SENT_DM_WEBHOOK_SECRET = os.environ.get("SENT_DM_WEBHOOK_SECRET")
SENT_DM_WEBHOOK_PATH = os.environ.get("SENT_DM_WEBHOOK_PATH", "/webhooks/sent/")
SENT_DM_MAX_RETRIES = int(os.environ.get("SENT_DM_MAX_RETRIES", "3"))
SENT_DM_RETRY_DELAY = int(os.environ.get("SENT_DM_RETRY_DELAY", "60"))
if not SENT_DM_API_KEY:
raise ValueError("SENT_DM_API_KEY environment variable is required.")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"celery",
"sent_integration.apps.SentIntegrationConfig",
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/0")Environment Variables
SENT_DM_API_KEY=your_api_key_here
SENT_DM_WEBHOOK_SECRET=your_webhook_secret_here
SENT_DM_WEBHOOK_PATH=/webhooks/sent/
SENT_DM_MAX_RETRIES=3
SENT_DM_RETRY_DELAY=60
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/0Environment Variables Reference
| Variable | Required | Default | Description |
|---|---|---|---|
SENT_DM_API_KEY | Yes | - | Your Sent DM API key |
SENT_DM_WEBHOOK_SECRET | No | - | Secret for webhook signature verification |
SENT_DM_WEBHOOK_PATH | No | /webhooks/sent/ | URL path for webhook endpoint |
SENT_DM_MAX_RETRIES | No | 3 | Maximum retries for failed API calls |
SENT_DM_RETRY_DELAY | No | 60 | Delay in seconds between retries |
CELERY_BROKER_URL | No | redis://localhost:6379/0 | Celery broker URL |
CELERY_RESULT_BACKEND | No | redis://localhost:6379/0 | Celery result backend URL |
AppConfig with Initialization
apps.py
from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
class SentIntegrationConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "sent_integration"
verbose_name = "Sent DM Integration"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._sent_client = None
@property
def sent_client(self):
if self._sent_client is None:
self._sent_client = self._create_client()
return self._sent_client
def _create_client(self):
from sent_dm import SentDm
api_key = getattr(settings, "SENT_DM_API_KEY", None)
if not api_key:
raise ImproperlyConfigured("SENT_DM_API_KEY must be set in settings")
return SentDm(api_key)
def ready(self):
import sent_integration.handlers # noqa: F401
self._validate_configuration()
def _validate_configuration(self):
webhook_secret = getattr(settings, "SENT_DM_WEBHOOK_SECRET", None)
if not webhook_secret:
import warnings
warnings.warn(
"SENT_DM_WEBHOOK_SECRET is not set. Webhook verification will fail.",
RuntimeWarning,
stacklevel=2,
)
def get_sent_client():
from django.apps import apps
config = apps.get_app_config("sent_integration")
return config.sent_clientModels
models.py
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
import re
class PhoneNumberField(models.CharField):
E164_REGEX = re.compile(r"^\+[1-9]\d{1,14}$")
def __init__(self, *args, **kwargs):
kwargs.setdefault("max_length", 20)
kwargs.setdefault("help_text", _("Phone number in E.164 format (e.g., +1234567890)"))
super().__init__(*args, **kwargs)
def validate(self, value, model_instance):
super().validate(value, model_instance)
if value and not self.E164_REGEX.match(value):
raise ValidationError(
_("Enter a valid phone number in E.164 format (e.g., +1234567890)."),
code="invalid_phone_number"
)
class SentMessage(models.Model):
id = models.BigAutoField(primary_key=True)
sent_id = models.CharField(max_length=100, unique=True, db_index=True, verbose_name=_("Sent Message ID"))
phone_number = PhoneNumberField(verbose_name=_("Phone Number"))
template_id = models.CharField(max_length=100, verbose_name=_("Template ID"))
template_name = models.CharField(max_length=100, verbose_name=_("Template Name"))
variables = models.JSONField(default=dict, blank=True, verbose_name=_("Template Variables"))
status = models.CharField(max_length=20, default="pending", verbose_name=_("Status"))
channels = models.JSONField(default=list, blank=True, verbose_name=_("Channels"))
error_message = models.TextField(blank=True, verbose_name=_("Error Message"))
sent_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Sent At"))
delivered_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Delivered At"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
class Meta:
verbose_name = _("Sent Message")
verbose_name_plural = _("Sent Messages")
ordering = ["-created_at"]
indexes = [
models.Index(fields=["status", "created_at"]),
models.Index(fields=["phone_number", "created_at"]),
]
def __str__(self):
return f"{self.template_name} to {self.phone_number} ({self.status})"
def mark_as_sent(self, sent_id: str):
from django.utils import timezone
self.sent_id = sent_id
self.status = "sent"
self.sent_at = timezone.now()
self.save(update_fields=["sent_id", "status", "sent_at"])
def mark_as_delivered(self):
from django.utils import timezone
self.status = "delivered"
self.delivered_at = timezone.now()
self.save(update_fields=["status", "delivered_at"])
def mark_as_failed(self, error_message: str):
self.status = "failed"
self.error_message = error_message
self.save(update_fields=["status", "error_message"])
class MessageTemplate(models.Model):
template_id = models.CharField(max_length=100, unique=True, db_index=True)
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
content = models.TextField()
variables = models.JSONField(default=list)
channels = models.JSONField(default=list)
is_active = models.BooleanField(default=True)
last_synced = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.nameForms (Condensed)
forms.py
import re
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
class SendMessageForm(forms.Form):
phone_number = forms.CharField(max_length=20, label=_("Phone Number"))
template_id = forms.CharField(max_length=100, label=_("Template ID"))
template_name = forms.CharField(max_length=100, label=_("Template Name"))
variables = forms.JSONField(required=False, initial=dict, label=_("Template Variables"))
channels = forms.CharField(required=False, label=_("Channels"))
def clean_phone_number(self):
phone = self.cleaned_data.get("phone_number", "").strip()
phone = re.sub(r"\s+", "", phone)
e164_pattern = re.compile(r"^\+[1-9]\d{1,14}$")
if not e164_pattern.match(phone):
raise ValidationError(_("Enter a valid phone number in E.164 format."))
return phone
def clean_channels(self):
channels_str = self.cleaned_data.get("channels", "").strip()
if not channels_str:
return ["whatsapp"]
channels = [c.strip().lower() for c in channels_str.split(",")]
valid_channels = {"whatsapp", "sms", "email", "push"}
invalid = set(channels) - valid_channels
if invalid:
raise ValidationError(_("Invalid channels: %(channels)s"), params={"channels": ", ".join(invalid)})
return channels
class WelcomeMessageForm(forms.Form):
phone_number = forms.CharField(max_length=20, label=_("Phone Number"))
name = forms.CharField(max_length=100, required=False, label=_("Customer Name"))
def clean_phone_number(self):
phone = self.cleaned_data.get("phone_number", "").strip()
phone = re.sub(r"\s+", "", phone)
if not re.compile(r"^\+[1-9]\d{1,14}$").match(phone):
raise ValidationError(_("Enter a valid phone number in E.164 format."))
return phone
# Additional forms: OrderConfirmationForm, BulkMessageForm...
# See full source for complete implementationsSignals
signals.py
from django.dispatch import Signal
message_queued = Signal() # Sent when a message is queued
message_sent = Signal() # Sent when a message is successfully sent
message_delivered = Signal() # Sent when a message is delivered
message_failed = Signal() # Sent when a message fails
webhook_received = Signal() # Sent when a webhook is receivedhandlers.py
import logging
from django.dispatch import receiver
from .signals import message_sent, message_delivered, message_failed
logger = logging.getLogger(__name__)
@receiver(message_sent)
def log_message_sent(sender, instance, response, **kwargs):
logger.info("Message sent: %s (Sent ID: %s)", instance.id, instance.sent_id)
@receiver(message_delivered)
def log_message_delivered(sender, instance, event_data, **kwargs):
logger.info("Message delivered: %s (Sent ID: %s)", instance.id, instance.sent_id)
@receiver(message_failed)
def handle_message_failed(sender, instance, error, **kwargs):
logger.error("Message failed: %s - Error: %s", instance.id, error)
# Add retry logic or alert notifications here
# Additional handlers for message_queued, message_read, webhook_received...
# See full source for complete implementationsClass-Based Views
views.py
import json
import logging
from django.http import JsonResponse
from django.views import View
from django.views.generic import FormView, TemplateView
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy
from django.contrib import messages
from django.conf import settings
from .forms import SendMessageForm, WelcomeMessageForm
from .models import SentMessage
from .apps import get_sent_client
from .signals import message_queued, message_sent, message_delivered, message_failed
logger = logging.getLogger(__name__)
class SendMessageView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
template_name = "sent_integration/send_message.html"
form_class = SendMessageForm
permission_required = "sent_integration.send_message"
success_url = reverse_lazy("sent_integration:message_list")
def form_valid(self, form):
client = get_sent_client()
message_record = SentMessage.objects.create(
phone_number=form.cleaned_data["phone_number"],
template_id=form.cleaned_data["template_id"],
template_name=form.cleaned_data["template_name"],
variables=form.cleaned_data["variables"],
channels=form.cleaned_data["channels"],
status="pending",
)
message_queued.send(sender=SentMessage, instance=message_record)
try:
result = client.messages.send(
phone_number=form.cleaned_data["phone_number"],
template_id=form.cleaned_data["template_id"],
template_name=form.cleaned_data["template_name"],
variables=form.cleaned_data["variables"],
channels=form.cleaned_data["channels"],
)
if result.success:
message_record.mark_as_sent(result.data.id)
message_sent.send(sender=SentMessage, instance=message_record, response=result.data)
messages.success(self.request, f"Message sent! ID: {result.data.id}")
else:
message_record.mark_as_failed(result.error.message)
message_failed.send(sender=SentMessage, instance=message_record, error=result.error.message)
messages.error(self.request, f"Failed: {result.error.message}")
except Exception as e:
logger.exception("Error sending message")
message_record.mark_as_failed(str(e))
message_failed.send(sender=SentMessage, instance=message_record, error=str(e))
messages.error(self.request, f"Error: {str(e)}")
return super().form_valid(form)
class MessageListView(LoginRequiredMixin, TemplateView):
template_name = "sent_integration/message_list.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["messages"] = SentMessage.objects.all()[:100]
return context
@method_decorator(csrf_exempt, name="dispatch")
class WebhookView(View):
http_method_names = ["post", "head", "options"]
def post(self, request, *args, **kwargs):
client = get_sent_client()
signature = request.headers.get("X-Webhook-Signature")
if not signature:
return JsonResponse({"error": "Missing signature"}, status=401)
payload = request.body.decode("utf-8")
webhook_secret = getattr(settings, "SENT_DM_WEBHOOK_SECRET", None)
if webhook_secret:
is_valid = client.webhooks.verify_signature(
payload=payload, signature=signature, secret=webhook_secret
)
if not is_valid:
return JsonResponse({"error": "Invalid signature"}, status=401)
try:
event = json.loads(payload)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
event_type = event.get("type")
event_data = event.get("data", {})
return self.handle_event(event_type, event_data)
def handle_event(self, event_type: str, event_data: dict) -> JsonResponse:
handlers = {
"message.status.updated": self.handle_status_update,
"message.delivered": self.handle_delivered,
"message.failed": self.handle_failed,
}
handler = handlers.get(event_type)
if handler:
return handler(event_data)
return JsonResponse({"received": True, "handled": False})
def handle_delivered(self, data: dict) -> JsonResponse:
message_id = data.get("id")
try:
message = SentMessage.objects.get(sent_id=message_id)
message.mark_as_delivered()
message_delivered.send(sender=SentMessage, instance=message, event_data=data)
except SentMessage.DoesNotExist:
logger.warning(f"Message not found: {message_id}")
return JsonResponse({"received": True})
def handle_failed(self, data: dict) -> JsonResponse:
message_id = data.get("id")
error_message = data.get("error", {}).get("message", "Unknown error")
try:
message = SentMessage.objects.get(sent_id=message_id)
message.mark_as_failed(error_message)
message_failed.send(sender=SentMessage, instance=message, error=error_message)
except SentMessage.DoesNotExist:
logger.warning(f"Message not found: {message_id}")
return JsonResponse({"received": True})
def handle_status_update(self, data: dict) -> JsonResponse:
# Handle status updates - implementation similar to above
return JsonResponse({"received": True})
# Additional views: SendWelcomeMessageView, MessageDetailView, APIStatusView...
# See full source for complete implementationsMiddleware
middleware.py
import time
import logging
import uuid
from typing import Callable
from django.http import HttpRequest, HttpResponse
from django.conf import settings
logger = logging.getLogger("sent_dm")
class SentRequestLoggingMiddleware:
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def __call__(self, request: HttpRequest) -> HttpResponse:
if not self._is_sent_request(request):
return self.get_response(request)
correlation_id = str(uuid.uuid4())[:8]
request.sent_correlation_id = correlation_id
start_time = time.time()
try:
response = self.get_response(request)
duration = (time.time() - start_time) * 1000
logger.info("[Sent %s] Response: %d - Duration: %.2fms",
correlation_id, response.status_code, duration)
response["X-Correlation-ID"] = correlation_id
return response
except Exception as e:
duration = (time.time() - start_time) * 1000
logger.error("[Sent %s] Request failed after %.2fms: %s",
correlation_id, duration, str(e), exc_info=True)
raise
def _is_sent_request(self, request: HttpRequest) -> bool:
webhook_path = getattr(settings, "SENT_DM_WEBHOOK_PATH", "/webhooks/sent/")
if request.path.startswith(webhook_path):
return True
resolver = getattr(request, "resolver_match", None)
if resolver and resolver.namespace == "sent_integration":
return True
return False
# Additional middleware: SentWebhookSecurityMiddleware...
# See full source for complete implementationsAdmin Configuration
admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import SentMessage, MessageTemplate
@admin.register(SentMessage)
class SentMessageAdmin(admin.ModelAdmin):
list_display = ["id", "phone_number", "template_name", "status_badge", "sent_at", "created_at"]
list_filter = ["status", "template_name", "channels", "created_at"]
search_fields = ["phone_number", "sent_id", "template_id", "template_name"]
readonly_fields = ["sent_id", "sent_at", "delivered_at", "created_at", "updated_at"]
date_hierarchy = "created_at"
ordering = ["-created_at"]
actions = ["mark_as_failed", "retry_message"]
def status_badge(self, obj: SentMessage) -> str:
colors = {
"pending": "orange", "queued": "blue", "sent": "purple",
"delivered": "green", "read": "teal", "failed": "red", "cancelled": "gray",
}
color = colors.get(obj.status, "gray")
return format_html('<span style="background-color: {}; color: white; padding: 2px 8px; '
'border-radius: 4px; font-size: 0.8em;">{}</span>', color, obj.status.upper())
status_badge.short_description = "Status"
@admin.action(description="Mark selected messages as failed")
def mark_as_failed(self, request, queryset):
updated = queryset.update(status="failed")
self.message_user(request, f"{updated} messages marked as failed.")
@admin.action(description="Retry selected failed messages")
def retry_message(self, request, queryset):
from .tasks import send_message_task
count = 0
for message in queryset.filter(status="failed"):
send_message_task.delay(
phone_number=message.phone_number,
template_id=message.template_id,
template_name=message.template_name,
variables=message.variables,
channels=message.channels,
)
count += 1
self.message_user(request, f"Queued {count} messages for retry.")
@admin.register(MessageTemplate)
class MessageTemplateAdmin(admin.ModelAdmin):
list_display = ["name", "template_id", "is_active", "last_synced"]
list_filter = ["is_active", "channels"]
search_fields = ["name", "template_id", "description"]
readonly_fields = ["last_synced"]Celery Tasks
tasks.py
import logging
from celery import shared_task
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from sent_dm.exceptions import RateLimitError, APIError
from .apps import get_sent_client
from .models import SentMessage
from .signals import message_sent, message_failed
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=3, default_retry_delay=60, autoretry_for=(RateLimitError,))
def send_message_task(self, phone_number: str, template_id: str, template_name: str = None,
variables: dict = None, channels: list = None, message_record_id: int = None):
client = get_sent_client()
message_record = None
if message_record_id:
try:
message_record = SentMessage.objects.get(id=message_record_id)
except SentMessage.DoesNotExist:
logger.warning(f"Message record {message_record_id} not found")
try:
result = client.messages.send(
phone_number=phone_number,
template_id=template_id,
template_name=template_name or template_id,
variables=variables or {},
channels=channels or ["whatsapp"],
)
if result.success:
if message_record:
message_record.mark_as_sent(result.data.id)
message_sent.send(sender=SentMessage, instance=message_record, response=result.data)
return {"success": True, "message_id": result.data.id}
else:
error_msg = result.error.message if result.error else "Unknown error"
if message_record:
message_record.mark_as_failed(error_msg)
message_failed.send(sender=SentMessage, instance=message_record, error=error_msg)
raise APIError(error_msg)
except RateLimitError as exc:
retry_delay = 60 * (2 ** self.request.retries)
raise self.retry(exc=exc, countdown=retry_delay)
@shared_task
def send_bulk_messages_task(phone_numbers: list[str], template_id: str, template_name: str = None,
variables: dict = None, channels: list = None):
from .tasks import send_message_task
task_ids = []
for phone_number in phone_numbers:
message_record = SentMessage.objects.create(
phone_number=phone_number,
template_id=template_id,
template_name=template_name or template_id,
variables=variables or {},
channels=channels or ["whatsapp"],
status="pending",
)
task = send_message_task.delay(
phone_number=phone_number, template_id=template_id, template_name=template_name,
variables=variables, channels=channels, message_record_id=message_record.id
)
task_ids.append(task.id)
return {"queued": len(task_ids), "task_ids": task_ids}
# Additional tasks: process_webhook_event_task, retry_failed_messages_task...
# See full source for complete implementationsDjango REST Framework Integration
serializers.py
import re
from rest_framework import serializers
from .models import SentMessage, MessageTemplate
class SendMessageSerializer(serializers.Serializer):
phone_number = serializers.CharField(max_length=20)
template_id = serializers.CharField(max_length=100)
template_name = serializers.CharField(max_length=100, required=False)
variables = serializers.DictField(required=False, default=dict)
channels = serializers.ListField(child=serializers.CharField(), required=False, default=list)
async_send = serializers.BooleanField(required=False, default=False)
def validate_phone_number(self, value: str) -> str:
phone = re.sub(r"\s+", "", value.strip())
if not re.compile(r"^\+[1-9]\d{1,14}$").match(phone):
raise serializers.ValidationError("Enter a valid phone number in E.164 format.")
return phone
class SentMessageSerializer(serializers.ModelSerializer):
class Meta:
model = SentMessage
fields = ["id", "sent_id", "phone_number", "template_id", "template_name",
"variables", "status", "channels", "error_message", "sent_at",
"delivered_at", "created_at", "updated_at"]
read_only_fields = ["id", "sent_id", "status", "error_message", "sent_at",
"delivered_at", "created_at", "updated_at"]
# Additional serializers: MessageTemplateSerializer, BulkMessageSerializer...
# See full source for complete implementationsapi_views.py (Key Parts)
from rest_framework import viewsets, status
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from .models import SentMessage, MessageTemplate
from .apps import get_sent_client
from .tasks import send_message_task, send_bulk_messages_task
from .serializers import SendMessageSerializer, SentMessageSerializer, MessageTemplateSerializer
class MessageViewSet(viewsets.ModelViewSet):
queryset = SentMessage.objects.all()
serializer_class = SentMessageSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
queryset = SentMessage.objects.all()
status_filter = self.request.query_params.get("status")
if status_filter:
queryset = queryset.filter(status=status_filter)
return queryset.order_by("-created_at")
@action(detail=False, methods=["post"], url_path="send")
def send_message(self, request):
serializer = SendMessageSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
message_record = SentMessage.objects.create(
phone_number=data["phone_number"],
template_id=data["template_id"],
template_name=data.get("template_name", data["template_id"]),
variables=data.get("variables", {}),
channels=data.get("channels", ["whatsapp"]),
status="pending",
)
if data.get("async_send", False):
task = send_message_task.delay(
phone_number=data["phone_number"], template_id=data["template_id"],
template_name=data.get("template_name"), variables=data.get("variables"),
channels=data.get("channels"), message_record_id=message_record.id
)
return Response({"success": True, "task_id": task.id, "status": "queued"},
status=status.HTTP_202_ACCEPTED)
# Synchronous sending
client = get_sent_client()
result = client.messages.send(
phone_number=data["phone_number"], template_id=data["template_id"],
template_name=data.get("template_name", data["template_id"]),
variables=data.get("variables", {}), channels=data.get("channels", ["whatsapp"])
)
if result.success:
message_record.mark_as_sent(result.data.id)
return Response({"success": True, "message_id": result.data.id})
else:
error_msg = result.error.message if result.error else "Unknown error"
message_record.mark_as_failed(error_msg)
return Response({"success": False, "error": error_msg}, status=status.HTTP_400_BAD_REQUEST)
# Additional viewsets: TemplateViewSet, webhook_api, api_status...
# See full source for complete implementationsTesting (Condensed)
Test Configuration
# myproject/settings_test.py
from .settings import *
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}
SENT_DM_API_KEY = "your_api_key_here"
SENT_DM_WEBHOOK_SECRET = "your_webhook_secret_here"
CELERY_TASK_ALWAYS_EAGER = True
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]Example Tests
# sent_integration/tests/test_forms.py
from django.test import TestCase
from sent_integration.forms import SendMessageForm
class SendMessageFormTest(TestCase):
def test_valid_form(self):
data = {
"phone_number": "+1234567890",
"template_id": "welcome-template",
"template_name": "welcome",
"variables": {"name": "John"},
"channels": "whatsapp, sms",
}
form = SendMessageForm(data)
self.assertTrue(form.is_valid())
def test_invalid_phone_number(self):
data = {"phone_number": "1234567890", "template_id": "welcome", "template_name": "welcome"}
form = SendMessageForm(data)
self.assertFalse(form.is_valid())
self.assertIn("phone_number", form.errors)
# Additional test cases...
# See full source for complete implementations# sent_integration/tests/test_models.py
from django.test import TestCase
from sent_integration.models import SentMessage
class SentMessageModelTest(TestCase):
def test_create_message(self):
message = SentMessage.objects.create(
sent_id="msg_123", phone_number="+1234567890",
template_id="welcome", template_name="welcome", status="pending"
)
self.assertEqual(str(message), "welcome to +1234567890 (pending)")
def test_mark_as_sent(self):
message = SentMessage.objects.create(
phone_number="+1234567890", template_id="welcome", template_name="welcome"
)
message.mark_as_sent("sent_msg_123")
self.assertEqual(message.sent_id, "sent_msg_123")
self.assertEqual(message.status, "sent")
# Additional test cases: test_mark_as_delivered, test_mark_as_failed...
# See full source for complete implementations# sent_integration/tests/test_views.py
import json
from unittest.mock import Mock, patch
from django.test import TestCase, RequestFactory
from sent_integration.models import SentMessage
from sent_integration.views import WebhookView
class WebhookViewTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.view = WebhookView()
@patch("sent_integration.views.get_sent_client")
def test_webhook_signature_verification(self, mock_get_client):
mock_client = Mock()
mock_client.webhooks.verify_signature.return_value = True
mock_get_client.return_value = mock_client
payload = json.dumps({"type": "message.delivered", "data": {"id": "msg_123"}})
request = self.factory.post("/webhooks/sent/", data=payload, content_type="application/json")
request.headers = {"X-Webhook-Signature": "valid_signature"}
with self.settings(SENT_DM_WEBHOOK_SECRET="secret"):
response = self.view.post(request)
self.assertEqual(response.status_code, 200)
# Additional test cases: test_views, test_forms...
# See full source for complete implementationsURL Patterns
sent_integration/urls.py
from django.urls import path
from . import views
app_name = "sent_integration"
urlpatterns = [
path("messages/", views.MessageListView.as_view(), name="message_list"),
path("messages/<int:pk>/", views.MessageDetailView.as_view(), name="message_detail"),
path("messages/send/", views.SendMessageView.as_view(), name="send_message"),
path("messages/send/welcome/", views.SendWelcomeMessageView.as_view(), name="send_welcome"),
path("webhook/", views.WebhookView.as_view(), name="webhook"),
path("api/status/", views.APIStatusView.as_view(), name="api_status"),
]Project urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("sent/", include("sent_integration.urls", namespace="sent_integration")),
path("webhooks/sent/", include("sent_integration.urls")),
]Running Tests
# Run all tests
python manage.py test sent_integration
# Run specific test file
python manage.py test sent_integration.tests.test_forms
# Run with coverage
pip install coverage
coverage run --source=sent_integration manage.py test sent_integration
coverage reportManagement Commands
# Sync all templates from Sent
python manage.py sync_templates
# Sync specific template
python manage.py sync_templates --template-id=template_123
# Dry run
python manage.py sync_templates --dry-run
# Send test message
python manage.py send_test_message +1234567890 --template-id=welcome --var=name=JohnNext Steps
- Learn about best practices for production deployments
- Set up webhooks for delivery status updates
- Explore the Python SDK reference for advanced features
- See Celery Integration for background tasks