Django adapter for python-sendparcel
Project description
django-sendparcel
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.
Features
- 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 - Order model mixin —
OrderModelMixindefines the contract your Order model must satisfy (weight, parcels, addresses) - Protocol adapters —
DjangoOrderAdapterandDjangoShipmentAdapterbridge Django models 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
Installation
Install with pip (or your preferred package manager):
pip install django-sendparcel
This will also install the required dependencies: python-sendparcel, Django, anyio, and swapper.
Quick Start
1. Add to INSTALLED_APPS
INSTALLED_APPS = [
# ...
"sendparcel_django",
# ...
]
2. Configure settings
# 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"
3. Create your Order model
Your Order model must extend OrderModelMixin and implement four methods:
from decimal import Decimal
from django.db import models
from sendparcel_django.models import OrderModelMixin
class Order(OrderModelMixin):
description = models.CharField(max_length=255)
recipient_name = models.CharField(max_length=128)
recipient_line1 = models.CharField(max_length=255)
recipient_city = models.CharField(max_length=128)
recipient_postal_code = models.CharField(max_length=16)
def get_total_weight(self) -> Decimal:
return Decimal("2.5")
def get_parcels(self) -> list[dict]:
return [{"weight_kg": self.get_total_weight()}]
def get_sender_address(self) -> dict:
return {
"name": "My Warehouse",
"line1": "1 Warehouse St",
"city": "Warsaw",
"postal_code": "00-001",
"country_code": "PL",
}
def get_receiver_address(self) -> dict:
return {
"name": self.recipient_name,
"line1": self.recipient_line1,
"city": self.recipient_city,
"postal_code": self.recipient_postal_code,
"country_code": "PL",
}
4. (Optional) Create a custom Shipment model
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):
order = models.ForeignKey(
"myapp.Order",
on_delete=models.CASCADE,
related_name="shipments",
)
class Meta:
verbose_name = "shipment"
Then in settings:
SENDPARCEL_DJANGO_SHIPMENT_MODEL = "myapp.Shipment"
5. Include URL configuration
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.
6. (Optional) Add the exception middleware
MIDDLEWARE = [
# ...
"sendparcel_django.middleware.SendParcelExceptionMiddleware",
]
This catches sendparcel exceptions and returns appropriate JSON error responses:
| Exception | HTTP Status |
|---|---|
CommunicationError |
502 |
InvalidCallbackError |
400 |
InvalidTransitionError |
409 |
SendParcelException |
400 |
7. Run migrations
python manage.py migrate
Usage
Creating a shipment
Use DjangoOrderAdapter to bridge your Django Order model to the core ShipmentFlow:
import anyio
from sendparcel.flow import ShipmentFlow
from sendparcel_django.protocols import DjangoOrderAdapter
from sendparcel_django.repository import DjangoShipmentRepository
async def create_shipment_for_order(order, provider_slug):
repository = DjangoShipmentRepository()
flow = ShipmentFlow(
repository=repository,
config=settings.SENDPARCEL_PROVIDER_SETTINGS,
)
adapted_order = DjangoOrderAdapter(wrapped=order)
shipment = await flow.create_shipment(adapted_order, provider_slug)
# Generate a label if the provider supports it
if not shipment.label_url:
shipment = await flow.create_label(shipment)
return shipment
Call from synchronous Django code using anyio.run():
shipment = anyio.run(create_shipment_for_order, order, "my-provider")
Provider choice form
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.
Admin
The ShipmentAdmin is auto-registered for the active Shipment model (default or swapped). It provides:
- List display: ID, order ID, status, provider, tracking number, label URL, creation date
- Filters: status, provider
- Search: tracking number, external ID, order ID
- Bulk actions: mark as in transit, mark as delivered, cancel — each action triggers FSM transitions with guard validation
Configuration Reference
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.
Shipment Model Fields
The ShipmentModelMixin provides these fields on every Shipment (default or custom):
| Field | Type | Description |
|---|---|---|
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 |
label_url |
URLField |
URL to the shipping label |
created_at |
DateTimeField |
Auto-set on creation |
updated_at |
DateTimeField |
Auto-set on save |
The default concrete Shipment model adds an order_id CharField. When creating a custom model, you can use a ForeignKey or any other relation to link shipments to orders.
Example Project
A full working example is included in the example/ directory. It demonstrates:
- An
Ordermodel withOrderModelMixin - A custom
Shipmentmodel with a ForeignKey to Order - 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
Supported Versions
| Dependency | Version |
|---|---|
| Python | >= 3.12 |
| Django | >= 5.2 |
| python-sendparcel | >= 0.1.0 |
| anyio | >= 4.0 |
| swapper | >= 1.4 |
Running Tests
The test suite uses pytest with pytest-django:
pip install -e ".[dev]"
pytest
Test configuration is in tests/settings.py. The test suite covers models, protocols, views, middleware, admin, forms, registry, repository, FSM integration, and callback retry logic.
Credits
- Author: Dominik Kozaczko (dominik@kozaczko.info)
- Built on top of python-sendparcel core library
- Model swapping powered by django-swapper
License
MIT License. See LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_sendparcel-0.1.0.tar.gz.
File metadata
- Download URL: django_sendparcel-0.1.0.tar.gz
- Upload date:
- Size: 72.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Manjaro Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf04b8a7c570a1a50b13ddc3f283558fa95470868028d4a9cea46804f3709b3e
|
|
| MD5 |
eeb30094dd0339d981e55dc2d02f19ae
|
|
| BLAKE2b-256 |
c9576c614e00b34dc5d2040c849f214dc41bda5ffcc820d36a4468e7d5efcd74
|
File details
Details for the file django_sendparcel-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_sendparcel-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Manjaro Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e92e02fe561a50a4d1d66a2fdf1d3d49f55c0afbac32741de91834a2a6f6cf67
|
|
| MD5 |
60f6da6f85aa165a1dbfb780202c278b
|
|
| BLAKE2b-256 |
e9cd7f4822e8e1c7d2e28fea8fb4ea3c34e0846a831959b0d06dea0b6c3316e3
|