Framework-agnostic parcel shipping core for Python.
Project description
python-sendparcel
Framework-agnostic parcel shipping core for Python.
Alpha notice — This project is at version 0.1.0. The public API may change between minor releases until 1.0 is reached. Pin your dependency accordingly.
Features
- Provider plugin system — register providers via entry points or manually; auto-discovery at first use.
- Shipment domain types —
AddressInfo,ParcelInfo,LabelInfo,ShipmentCreateResult,ShipmentStatusResponse, andTrackingEventas strict TypedDicts. - Finite state machine — 9-state
ShipmentStatusenum (NEW→CREATED→LABEL_READY→IN_TRANSIT→OUT_FOR_DELIVERY→DELIVERED, plusCANCELLED,FAILED,RETURNED) with guarded transitions powered by transitions. - ShipmentFlow orchestrator — framework-agnostic async workflow for creating shipments, fetching labels, handling callbacks, polling status, and cancelling.
- BaseProvider ABC — define your own provider by subclassing a single class with well-defined async methods.
- Built-in DummyProvider — deterministic reference provider for testing and local development.
- Pluggable validators — attach validator callables to
ShipmentFlowfor global or per-operation validation. - Runtime protocols —
Order,Shipment, andShipmentRepositoryare@runtime_checkableprotocols; bring your own models and persistence. - Async-first — the entire runtime is async, powered by anyio.
Installation
With pip
pip install python-sendparcel
With uv
uv add python-sendparcel
Framework adapters
Install the adapter for your web framework:
pip install python-sendparcel[django] # Django integration
pip install python-sendparcel[fastapi] # FastAPI integration
pip install python-sendparcel[litestar] # Litestar integration
pip install python-sendparcel[frameworks] # all framework adapters
pip install python-sendparcel[all] # everything
Extras reference
| Extra | Installs |
|---|---|
dummy |
Built-in dummy provider (no extra package) |
django |
django-sendparcel |
fastapi |
fastapi-sendparcel |
litestar |
litestar-sendparcel |
providers |
Built-in providers (currently dummy) |
frameworks |
All framework adapters |
all |
Framework adapters |
Quick Start
python-sendparcel is framework-agnostic. You provide implementations of three
protocols — Order, Shipment, and ShipmentRepository — and the library
handles orchestration, state transitions, and provider communication.
1. Implement the Order protocol
from decimal import Decimal
from dataclasses import dataclass, field
from sendparcel.types import AddressInfo, ParcelInfo
@dataclass
class MyOrder:
"""Satisfies the sendparcel Order protocol."""
sender: AddressInfo
receiver: AddressInfo
parcels: list[ParcelInfo] = field(default_factory=list)
def get_total_weight(self) -> Decimal:
return sum(p["weight_kg"] for p in self.parcels)
def get_parcels(self) -> list[ParcelInfo]:
return self.parcels
def get_sender_address(self) -> AddressInfo:
return self.sender
def get_receiver_address(self) -> AddressInfo:
return self.receiver
2. Implement the Shipment and ShipmentRepository protocols
from dataclasses import dataclass
@dataclass
class MyShipment:
"""Satisfies the sendparcel Shipment protocol."""
id: str
order: MyOrder
status: str = ""
provider: str = ""
external_id: str = ""
tracking_number: str = ""
label_url: str = ""
class InMemoryRepository:
"""Minimal in-memory ShipmentRepository for demonstration."""
def __init__(self):
self._store: dict[str, MyShipment] = {}
self._counter = 0
async def get_by_id(self, shipment_id: str) -> MyShipment:
return self._store[shipment_id]
async def create(self, **kwargs) -> MyShipment:
self._counter += 1
shipment_id = str(self._counter)
shipment = MyShipment(
id=shipment_id,
order=kwargs["order"],
status=kwargs.get("status", ""),
provider=kwargs.get("provider", ""),
)
self._store[shipment_id] = shipment
return shipment
async def save(self, shipment: MyShipment) -> MyShipment:
self._store[shipment.id] = shipment
return shipment
async def update_status(
self, shipment_id: str, status: str, **fields
) -> MyShipment:
shipment = self._store[shipment_id]
shipment.status = status
return shipment
3. Create a shipment with ShipmentFlow
import anyio
from sendparcel import ShipmentFlow, ShipmentStatus
from sendparcel.types import AddressInfo, ParcelInfo
async def main():
repo = InMemoryRepository()
flow = ShipmentFlow(repository=repo)
order = MyOrder(
sender=AddressInfo(
name="Sender Co.",
line1="ul. Marszalkowska 1",
city="Warszawa",
postal_code="00-001",
country_code="PL",
),
receiver=AddressInfo(
name="Jan Kowalski",
line1="ul. Dluga 10",
city="Gdansk",
postal_code="80-001",
country_code="PL",
),
parcels=[ParcelInfo(weight_kg=Decimal("2.5"))],
)
# Create shipment using the built-in dummy provider
shipment = await flow.create_shipment(order, provider_slug="dummy")
print(shipment.status) # "created" or "label_ready"
print(shipment.external_id) # "dummy-1"
print(shipment.tracking_number) # "DUMMY-1"
anyio.run(main)
Architecture
python-sendparcel is organized into focused modules:
sendparcel/
├── __init__.py # Public API surface
├── enums.py # ShipmentStatus, ConfirmationMethod
├── types.py # TypedDict definitions (AddressInfo, ParcelInfo, …)
├── protocols.py # Order, Shipment, ShipmentRepository protocols
├── provider.py # BaseProvider ABC
├── registry.py # PluginRegistry with entry-point discovery
├── flow.py # ShipmentFlow orchestrator
├── fsm.py # State machine transitions (pytransitions)
├── validators.py # Pluggable validation chain
├── exceptions.py # Exception hierarchy
└── providers/
├── __init__.py # Built-in provider list
└── dummy.py # DummyProvider reference implementation
Key components
| Component | Module | Description |
|---|---|---|
ShipmentFlow |
flow.py |
Async orchestrator — creates shipments, fetches labels, handles callbacks, polls status, cancels. |
BaseProvider |
provider.py |
Abstract base class that all shipping providers must subclass. |
PluginRegistry |
registry.py |
Discovers providers from sendparcel.providers entry points and built-ins. Global registry singleton. |
ShipmentStatus |
enums.py |
9-state StrEnum representing the shipment lifecycle. |
| Domain types | types.py |
AddressInfo, ParcelInfo, LabelInfo, ShipmentCreateResult, ShipmentStatusResponse, TrackingEvent. |
| Protocols | protocols.py |
Order, Shipment, ShipmentRepository — all @runtime_checkable. |
| FSM | fsm.py |
Transition definitions with guards (e.g. label_url required before confirm_label). |
| Validators | validators.py |
Chain of callables invoked before provider operations. |
Shipment state machine
mark_in_transit
┌────────────────────┐
│ ▼
NEW ──confirm_created──▸ CREATED ──confirm_label──▸ LABEL_READY IN_TRANSIT ──mark_out_for_delivery──▸ OUT_FOR_DELIVERY
│ │ │ │ │ │ │
│ │ │ │ ├── mark_delivered ─────────────────▸│ │
│ │ │ │ │ │ │
│ │ │ │ │ mark_delivered │ │
│ │ │ │ │ ┌────────────────────────────────-─┘ │
│ │ │ │ │ ▼ │
│ │ │ │ │ DELIVERED │
│ │ │ │ │ │ │
│ │ │ │ │ └── mark_returned ──▸ RETURNED ◂─────┤
│ │ │ │ │ ▴ │
│ │ │ │ └── mark_returned ─────────┘ │
│ │ │ │ │
└──────── cancel ─────────┴────── cancel ────────────┴──▸ CANCELLED │
│
Any of {NEW, CREATED, LABEL_READY, IN_TRANSIT, OUT_FOR_DELIVERY} ──fail──▸ FAILED │
Guards enforce data integrity:
confirm_labelrequireslabel_urlto be set on the shipment.mark_in_transitrequirestracking_numberto be set on the shipment.
Provider Authoring
Create a provider by subclassing BaseProvider and implementing create_shipment:
from typing import ClassVar
from sendparcel.provider import BaseProvider
from sendparcel.types import ShipmentCreateResult
class MyCarrierProvider(BaseProvider):
slug: ClassVar[str] = "mycarrier"
display_name: ClassVar[str] = "My Carrier"
supported_countries: ClassVar[list[str]] = ["PL", "DE"]
supported_services: ClassVar[list[str]] = ["standard"]
async def create_shipment(self, **kwargs) -> ShipmentCreateResult:
# Call your carrier's API here
api_key = self.get_setting("api_key")
sender = self.shipment.order.get_sender_address()
receiver = self.shipment.order.get_receiver_address()
# ... HTTP call to carrier API ...
return ShipmentCreateResult(
external_id="carrier-12345",
tracking_number="TRACK-12345",
)
Entry-point registration
Declare your provider in pyproject.toml so it is auto-discovered:
[project.entry-points."sendparcel.providers"]
mycarrier = "mycarrier_sendparcel.provider:MyCarrierProvider"
Manual registration
from sendparcel import registry
registry.register(MyCarrierProvider)
Provider configuration
Pass per-provider settings through ShipmentFlow:
flow = ShipmentFlow(
repository=repo,
config={
"mycarrier": {
"api_key": "sk_live_...",
"sandbox": True,
},
},
)
Settings are accessible inside the provider via self.get_setting("api_key").
Optional methods
Beyond the required create_shipment, providers can override:
| Method | Purpose |
|---|---|
create_label(**kwargs) |
Generate or fetch a shipping label. |
verify_callback(data, headers, **kwargs) |
Validate webhook authenticity. |
handle_callback(data, headers, **kwargs) |
Apply webhook status updates. |
fetch_shipment_status(**kwargs) |
Poll current shipment status. |
cancel_shipment(**kwargs) |
Cancel a shipment. |
Class-level attributes
| Attribute | Type | Description |
|---|---|---|
slug |
str |
Unique provider identifier. |
display_name |
str |
Human-readable name. |
supported_countries |
list[str] |
ISO country codes. |
supported_services |
list[str] |
Service level identifiers. |
confirmation_method |
ConfirmationMethod |
PUSH (webhook) or PULL (polling). Default: PUSH. |
user_selectable |
bool |
Whether this provider appears in registry.get_choices(). Default: True. |
Ecosystem
python-sendparcel is the core library. Framework-specific integrations are provided by separate packages:
| Package | Framework | Repository |
|---|---|---|
| django-sendparcel | Django | sendparcel/django-sendparcel |
| fastapi-sendparcel | FastAPI | sendparcel/fastapi-sendparcel |
| litestar-sendparcel | Litestar | sendparcel/litestar-sendparcel |
Each wrapper provides framework-native models, views/routes, and repository implementations so you don't have to write the boilerplate shown in the Quick Start above.
Supported Versions
| Python | Status |
|---|---|
| 3.12+ | Supported |
| 3.13 | Supported |
| < 3.12 | Not supported |
Core dependencies
| Package | Minimum version |
|---|---|
transitions |
0.9.0 |
httpx |
0.27.0 |
anyio |
4.0 |
Running Tests
The test suite uses pytest with pytest-asyncio.
# Install dev dependencies
uv sync --extra dev
# Run the full test suite
uv run pytest
# With coverage
uv run pytest --cov=sendparcel --cov-report=term-missing
Credits
- Author: Dominik Kozaczko (dominik@kozaczko.info)
- Inspired by the django-getpaid architecture and plugin model.
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 python_sendparcel-0.1.0.tar.gz.
File metadata
- Download URL: python_sendparcel-0.1.0.tar.gz
- Upload date:
- Size: 82.6 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 |
38821df1dc3b59dbcf026fffcbdf6ebfdaf3c2fba336cb138c28e2e419354383
|
|
| MD5 |
dcc498ad0feb9460fd75fbf2e6ce5f9e
|
|
| BLAKE2b-256 |
ddd4cb89381dbb5bcb2aa6635f0fd045b42b531cbcf838f8f9ec8812b0a38223
|
File details
Details for the file python_sendparcel-0.1.0-py3-none-any.whl.
File metadata
- Download URL: python_sendparcel-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.9 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 |
b212b23c57abb53be098e0f2de24da31e6614c1a54dd154eb082f22f572fa8f9
|
|
| MD5 |
a614e90fb113dea8fa17341c9e7dc98b
|
|
| BLAKE2b-256 |
b3ce753848c21e2e6a1a81ddea4fa507a18f48d5a6efe10f23f57313fb0b2e6e
|