Skip to main content

Django adapter for python-sendparcel

Project description

django-sendparcel

PyPI Python Version Django Version License

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 Shipment model with finite-state-machine transitions (new → created → label_ready → in_transit → delivered, etc.)
  • Swappable Shipment model — replace the default Shipment with your own via swapper, similar to Django's AUTH_USER_MODEL
  • Order model mixinOrderModelMixin defines the contract your Order model must satisfy (weight, parcels, addresses)
  • Protocol adaptersDjangoOrderAdapter and DjangoShipmentAdapter bridge Django models to the framework-agnostic core
  • Django ORM repositoryDjangoShipmentRepository provides async-compatible persistence via sync_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 integrationShipmentAdmin with list filters, search, and bulk actions (mark in transit, mark delivered, cancel)
  • Exception middlewareSendParcelExceptionMiddleware maps sendparcel exceptions to appropriate HTTP status codes
  • Provider choice formProviderChoiceForm dynamically populated from the plugin registry
  • Callback retry persistenceCallbackRetry model 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 Order model with OrderModelMixin
  • A custom Shipment model 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

License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_sendparcel-0.1.0.tar.gz (72.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_sendparcel-0.1.0-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

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

Hashes for django_sendparcel-0.1.0.tar.gz
Algorithm Hash digest
SHA256 bf04b8a7c570a1a50b13ddc3f283558fa95470868028d4a9cea46804f3709b3e
MD5 eeb30094dd0339d981e55dc2d02f19ae
BLAKE2b-256 c9576c614e00b34dc5d2040c849f214dc41bda5ffcc820d36a4468e7d5efcd74

See more details on using hashes here.

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

Hashes for django_sendparcel-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e92e02fe561a50a4d1d66a2fdf1d3d49f55c0afbac32741de91834a2a6f6cf67
MD5 60f6da6f85aa165a1dbfb780202c278b
BLAKE2b-256 e9cd7f4822e8e1c7d2e28fea8fb4ea3c34e0846a831959b0d06dea0b6c3316e3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page