Django adapter for the python-sendparcel multi-carrier shipping library.
Alpha (0.1.0) — API may change between minor releases. Pin your dependency if you use it in production.
- Shipment model with FSM — built-in
Shipmentmodel with finite-state-machine transitions (new → created → label_ready → in_transit → delivered, etc.) - Swappable Shipment model — replace the default
Shipmentwith your own viaswapper, similar to Django'sAUTH_USER_MODEL - Protocol adapter —
DjangoShipmentAdapterbridges the Django Shipment model to the framework-agnostic core - Django ORM repository —
DjangoShipmentRepositoryprovides async-compatible persistence viasync_to_async - Provider plugin registry — auto-discovers shipping provider plugins at app startup
- Callback endpoint — receives provider status webhooks and routes them through
ShipmentFlow - Admin integration —
ShipmentAdminwith list filters, search, and bulk actions (mark in transit, mark delivered, cancel) - Exception middleware —
SendParcelExceptionMiddlewaremaps sendparcel exceptions to appropriate HTTP status codes - Provider choice form —
ProviderChoiceFormdynamically populated from the plugin registry - Callback retry persistence —
CallbackRetrymodel stores failed callback attempts for later reprocessing
Install with pip (or your preferred package manager):
pip install django-sendparcelThis will also install the required dependencies: python-sendparcel, Django, anyio, and swapper.
INSTALLED_APPS = [
# ...
"sendparcel_django",
# ...
]# Provider-specific configuration, keyed by provider slug
SENDPARCEL_PROVIDER_SETTINGS = {
"my-provider": {
"api_url": "https://api.example.com/",
"api_key": "your-api-key",
},
}
# Default provider slug (optional)
SENDPARCEL_DEFAULT_PROVIDER = "my-provider"
# Custom shipment model (optional, default: "sendparcel_django.Shipment")
# Uses django-swapper convention: <APP_LABEL>_<MODEL_NAME>
SENDPARCEL_DJANGO_SHIPMENT_MODEL = "myapp.Shipment"If you need additional fields on the Shipment, extend ShipmentModelMixin and point the setting to your model:
from django.db import models
from sendparcel_django.models import ShipmentModelMixin
class Shipment(ShipmentModelMixin):
# Add custom fields as needed
notes = models.TextField(blank=True, default="")
class Meta:
verbose_name = "shipment"Then in settings:
SENDPARCEL_DJANGO_SHIPMENT_MODEL = "myapp.Shipment"from django.urls import include, path
urlpatterns = [
# ...
path("sendparcel/", include("sendparcel_django.urls")),
]This exposes the callback endpoint at sendparcel/callback/<shipment_id>/ for receiving provider webhooks.
Successful callback responses include provider, status, shipment, and
update. The adapter no longer persists label URLs on shipment models.
MIDDLEWARE = [
# ...
"sendparcel_django.middleware.SendParcelExceptionMiddleware",
]This catches sendparcel exceptions and returns appropriate JSON error responses:
| Exception | HTTP Status |
|---|---|
CommunicationError |
502 |
ProviderNotFoundError |
404 |
ProviderCapabilityError |
409 |
InvalidCallbackError |
400 |
InvalidTransitionError |
409 |
SendParcelException |
400 |
python manage.py migrateUse ShipmentFlow to create shipments with explicit address and parcel data:
import anyio
from sendparcel.flow import ShipmentFlow
from sendparcel_django.repository import DjangoShipmentRepository
async def create_shipment(provider_slug):
repository = DjangoShipmentRepository()
flow = ShipmentFlow(
repository=repository,
config=settings.SENDPARCEL_PROVIDER_SETTINGS,
)
outcome = await flow.create_shipment(
provider_slug,
sender_address={
"name": "My Warehouse",
"line1": "1 Warehouse St",
"city": "Warsaw",
"postal_code": "00-001",
"country_code": "PL",
},
receiver_address={
"name": "Customer Name",
"line1": "10 Customer Ave",
"city": "Krakow",
"postal_code": "30-001",
"country_code": "PL",
},
parcels=[{"weight_kg": 2.5}],
reference_id="my-order-123", # optional reference for your system
)
shipment = outcome.shipment
if outcome.label is None:
label_outcome = await flow.create_label(shipment)
return label_outcome.shipment
return shipmentCall from synchronous Django code using anyio.run():
shipment = anyio.run(create_shipment, "my-provider")Use ProviderChoiceForm to let users select a shipping provider:
from sendparcel_django.forms import ProviderChoiceForm
form = ProviderChoiceForm(request.POST)
if form.is_valid():
provider_slug = form.cleaned_data["provider"]The form choices are dynamically populated from the plugin registry.
The ShipmentAdmin is auto-registered for the active Shipment model (default or swapped). It provides:
- List display: ID, reference ID, status, provider, tracking number, creation date
- Filters: status, provider
- Search: tracking number, external ID, reference ID
- Bulk actions: mark as in transit, mark as delivered, cancel — each action triggers FSM transitions with guard validation
All settings are read from your Django settings module.
| Setting | Type | Default | Description |
|---|---|---|---|
SENDPARCEL_PROVIDER_SETTINGS |
dict |
{} |
Provider-specific configuration, keyed by provider slug |
SENDPARCEL_DEFAULT_PROVIDER |
str |
"" |
Default provider slug |
SENDPARCEL_DJANGO_SHIPMENT_MODEL |
str |
"sendparcel_django.Shipment" |
Dotted path to the Shipment model (swappable via django-swapper) |
Settings are resolved at call time via sendparcel_django.conf.get_settings(), so @override_settings works correctly in tests.
The ShipmentModelMixin provides these fields on every Shipment (default or custom):
| Field | Type | Description |
|---|---|---|
reference_id |
CharField |
Your system's reference (e.g. order ID) |
provider |
CharField |
Provider slug |
status |
CharField |
Current FSM state (default: "new") |
external_id |
CharField |
Provider-assigned shipment ID |
tracking_number |
CharField |
Tracking number from provider |
created_at |
DateTimeField |
Auto-set on creation |
updated_at |
DateTimeField |
Auto-set on save |
The default concrete Shipment model uses these fields directly. When creating a custom model, you can add any additional fields you need.
A full working example is included in the example/ directory. It demonstrates:
- A custom
Shipmentmodel with inline address fields - Shipment creation through the
ShipmentFlow - A delivery simulation provider for local testing
- HTMX-powered shipment tracking UI
To run the example:
cd example
pip install -e ..
pip install -e ../../python-sendparcel
python manage.py migrate
python manage.py runserver| Dependency | Version |
|---|---|
| Python | >= 3.12 |
| Django | >= 5.2 |
| python-sendparcel | >= 0.1.1 |
| anyio | >= 4.0 |
| swapper | >= 1.4 |
The test suite uses pytest with pytest-django:
pip install -e ".[dev]"
pytestTest configuration is in tests/settings.py. The test suite covers models, protocols, views, middleware, admin, forms, registry, repository, FSM integration, and callback retry logic.
- Author: Dominik Kozaczko (dominik@kozaczko.info)
- Built on top of python-sendparcel core library
- Model swapping powered by django-swapper
MIT License. See LICENSE for details.