Litestar adapter for python-sendparcel
Project description
litestar-sendparcel
Litestar framework adapter for python-sendparcel — a pluggable shipping and parcel delivery library for Python.
Alpha (0.1.1) — The API is functional but may change before 1.0. Use in production at your own discretion.
Features
- Router factory — single
create_shipping_router()call wires up all shipping endpoints - Shipment lifecycle — create shipments, generate labels, fetch status updates
- Provider webhooks — callback endpoint with automatic retry and exponential backoff
- Plugin registry — auto-discovers
sendparcelprovider plugins at startup - SQLAlchemy contrib — optional async SQLAlchemy 2.0 models and repository (install the
[sqlalchemy]extra) - Protocol-driven —
CallbackRetryStoreprotocol lets you plug in your own retry logic - Structured error handling — maps core
sendparcelexceptions to proper HTTP status codes (400, 404, 409, 502) - Pydantic configuration —
SendparcelConfigreads from environment variables withSENDPARCEL_prefix
Installation
pip install litestar-sendparcel
With the optional SQLAlchemy async persistence layer:
pip install litestar-sendparcel[sqlalchemy]
Quick Start
A minimal Litestar application with litestar-sendparcel:
from litestar import Litestar
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from litestar_sendparcel import SendparcelConfig, create_shipping_router
from litestar_sendparcel.contrib.sqlalchemy.repository import (
SQLAlchemyShipmentRepository,
)
# 1. Configure
config = SendparcelConfig(default_provider="inpost")
# 2. Set up persistence
engine = create_async_engine("sqlite+aiosqlite:///shipments.db")
session_factory = async_sessionmaker(engine, expire_on_commit=False)
repository = SQLAlchemyShipmentRepository(session_factory)
# 3. Create the shipping router
shipping_router = create_shipping_router(
config=config,
repository=repository,
)
# 4. Mount in your Litestar app
app = Litestar(route_handlers=[shipping_router])
This gives you a fully working set of shipment and callback endpoints.
With custom components
You can plug in your own CallbackRetryStore:
from litestar_sendparcel import (
CallbackRetryStore,
create_shipping_router,
)
class MyRetryStore:
async def store_failed_callback(
self, shipment_id: str, provider_slug: str,
payload: dict, headers: dict,
) -> str:
...
async def get_due_retries(self, limit: int = 10) -> list[dict]:
...
async def mark_succeeded(self, retry_id: str) -> None:
...
async def mark_failed(self, retry_id: str, error: str) -> None:
...
async def mark_exhausted(self, retry_id: str) -> None:
...
shipping_router = create_shipping_router(
config=config,
repository=repository,
retry_store=MyRetryStore(),
)
Configuration
SendparcelConfig is a Pydantic Settings
model. Values can be set via constructor arguments or environment variables
with the SENDPARCEL_ prefix.
| Setting | Type | Default | Env Variable | Description |
|---|---|---|---|---|
default_provider |
str |
(required) | SENDPARCEL_DEFAULT_PROVIDER |
Default shipping provider slug |
providers |
dict[str, dict] |
{} |
SENDPARCEL_PROVIDERS |
Per-provider configuration dicts |
retry_enabled |
bool |
True |
SENDPARCEL_RETRY_ENABLED |
Enable webhook callback retries |
retry_max_attempts |
int |
5 |
SENDPARCEL_RETRY_MAX_ATTEMPTS |
Max retry attempts before dead-lettering |
retry_backoff_seconds |
int |
60 |
SENDPARCEL_RETRY_BACKOFF_SECONDS |
Base backoff delay (exponential: base * 2^(attempt-1)) |
API Endpoints
All endpoints are mounted under the router's path (default /).
| Method | Path | Description | Response |
|---|---|---|---|
GET |
/shipments/health |
Healthcheck | {"status": "ok"} |
POST |
/shipments/ |
Create a shipment | ShipmentOperationResponse |
POST |
/shipments/{shipment_id}/label |
Generate shipping label | ShipmentOperationResponse |
GET |
/shipments/{shipment_id}/status |
Fetch and update shipment status | ShipmentOperationResponse |
POST |
/callbacks/{provider_slug}/{shipment_id} |
Handle provider webhook callback | CallbackResponse |
Request/Response Schemas
CreateShipmentRequest (POST /shipments/):
{
"reference_id": "SHP-0042",
"provider": "inpost",
"sender_address": {
"name": "Sender Name",
"line1": "10 Origin St",
"city": "Warsaw",
"postal_code": "00-001",
"country_code": "PL"
},
"receiver_address": {
"name": "Receiver Name",
"line1": "5 Destination St",
"city": "Krakow",
"postal_code": "30-001",
"country_code": "PL"
},
"parcels": [
{"weight_kg": 2.5}
]
}
The provider field is optional. When omitted, default_provider from config is used.
The reference_id field is optional and can be used for external reference tracking.
ShipmentOperationResponse:
{
"id": "uuid-string",
"status": "label_ready",
"provider": "inpost",
"external_id": "PROVIDER-123",
"tracking_number": "TRACK-456",
"label": {
"format": "PDF",
"url": "https://...",
"content_base64": null
},
"update": null
}
Label payloads are returned from create-label operations. They are not persisted on the shipment model.
CallbackResponse:
{
"provider": "inpost",
"status": "accepted",
"shipment": {
"id": "uuid-string",
"status": "in_transit",
"provider": "inpost",
"external_id": "PROVIDER-123",
"tracking_number": "TRACK-456"
},
"update": {
"status": "in_transit",
"tracking_number": "TRACK-456",
"tracking_events": []
}
}
Error Responses
The router registers exception handlers that map sendparcel exceptions to HTTP status codes:
| Exception | Status Code | Code |
|---|---|---|
ShipmentNotFoundError |
404 | shipment_not_found |
ProviderNotFoundError |
404 | provider_not_found |
ProviderCapabilityError |
409 | provider_capability_error |
InvalidCallbackError |
400 | invalid_callback |
InvalidTransitionError |
409 | invalid_transition |
CommunicationError |
502 | communication_error |
ConfigurationError |
500 | configuration_error |
SendParcelException |
400 | sendparcel_error |
All error responses have the shape {"detail": "...", "code": "..."}.
SQLAlchemy Contrib
The optional litestar_sendparcel.contrib.sqlalchemy module provides async
SQLAlchemy 2.0 models and a repository implementation:
ShipmentModel— maps tosendparcel_shipmentstableCallbackRetryModel— maps tosendparcel_callback_retriestableSQLAlchemyShipmentRepository— implements theShipmentRepositoryprotocol
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from litestar_sendparcel.contrib.sqlalchemy.models import Base, ShipmentModel
from litestar_sendparcel.contrib.sqlalchemy.repository import (
SQLAlchemyShipmentRepository,
)
engine = create_async_engine("sqlite+aiosqlite:///shipments.db")
# Create tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
session_factory = async_sessionmaker(engine, expire_on_commit=False)
repository = SQLAlchemyShipmentRepository(session_factory)
The repository provides these async methods:
| Method | Description |
|---|---|
get_by_id(shipment_id) |
Fetch a shipment by ID (raises ShipmentNotFoundError if not found) |
create(**kwargs) |
Create a new shipment record |
save(shipment) |
Merge and commit an existing shipment |
update_status(shipment_id, status, **fields) |
Update status and optional extra fields |
list_by_reference(reference_id) |
List all shipments for a given reference |
Webhook Retry Mechanism
When a CallbackRetryStore is provided, failed webhook callbacks are
automatically queued for retry with exponential backoff.
Use process_due_retries() from a background task or scheduled job:
from litestar_sendparcel.retry import process_due_retries
processed = await process_due_retries(
retry_store=my_retry_store,
repository=repository,
config=config,
limit=10,
)
Retries use exponential backoff: backoff_seconds * 2^(attempt - 1).
After retry_max_attempts failures, the retry is marked as exhausted (dead-lettered).
Example Project
A full working example is included in the example/ directory. It demonstrates:
- Shipment creation with sender/receiver address and parcel data
- Delivery simulation provider with configurable status progression
- Label generation (PDF)
- HTMX-powered status updates
- Tabler UI framework
Running the example
cd litestar-sendparcel/example
uv sync
uv run litestar --app app:app run --reload
Open http://localhost:8000 in your browser.
Supported Versions
| Dependency | Version |
|---|---|
| Python | >= 3.12 |
| Litestar | >= 2.0.0 |
| python-sendparcel | >= 0.1.1 |
| pydantic-settings | >= 2.0.0 |
| SQLAlchemy (optional) | >= 2.0.0 |
Running Tests
# Install dev dependencies
pip install litestar-sendparcel[dev]
# Run tests
pytest
Or with uv:
uv sync --extra dev
uv run pytest
Credits
Created and maintained by Dominik Kozaczko.
Built on top of:
- python-sendparcel — core shipping abstraction
- Litestar — high-performance async Python web framework
- SQLAlchemy — Python SQL toolkit (optional)
License
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 litestar_sendparcel-0.1.1.tar.gz.
File metadata
- Download URL: litestar_sendparcel-0.1.1.tar.gz
- Upload date:
- Size: 36.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","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 |
818114b4e6a8ec6d94e20ae0861b7ee1d19b9a1ecc5c47ba67b2b7129c79dd79
|
|
| MD5 |
42d952d624dad53d06908a6ddbe27677
|
|
| BLAKE2b-256 |
b927991b2db1a6ef107c2d7775359e4c711f05bb1642d7d69f18e790e0faae7a
|
File details
Details for the file litestar_sendparcel-0.1.1-py3-none-any.whl.
File metadata
- Download URL: litestar_sendparcel-0.1.1-py3-none-any.whl
- Upload date:
- Size: 19.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","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 |
ba18b206e95c004ed1c536ee4b666d8c2b0c182b6cfddc7b38c221a1f48b659d
|
|
| MD5 |
89e0a0e476f6d342d35affb0c0e63bf3
|
|
| BLAKE2b-256 |
2ec058962a7fab56ae515db422a24a7ea19c72a2a3a14d6c25045bb29d810670
|