python-sendparcel
Framework-agnostic parcel shipping core for Python.
Alpha notice:
0.3.0is still unstable. The API can change fast because the ecosystem is still being cleaned up.
What it is
- Provider-agnostic shipment orchestration with a single core flow.
- Explicit shipment metadata persistence:
id,status,provider,external_id,tracking_number. - Label payloads are operation results, not persisted shipment fields.
- One normalized provider update contract for both callbacks and polling.
- Runtime-checkable
ShipmentandShipmentRepositoryprotocols so adapters can use their own models.
Core contract
ShipmentFlow.create_shipment(...) -> CreateShipmentOutcomeShipmentFlow.create_label(...) -> CreateLabelOutcomeShipmentFlow.handle_callback(...) -> ShipmentUpdateOutcomeShipmentFlow.fetch_and_update_status(...) -> ShipmentUpdateOutcomeShipmentFlow.cancel_shipment(...) -> bool
CreateShipmentOutcome and CreateLabelOutcome return label payloads when available.
The shipment object never stores label bytes or a persisted label_url in the core contract.
Quick start
from dataclasses import dataclass
from decimal import Decimal
import anyio
from sendparcel import ShipmentFlow
from sendparcel.types import AddressInfo, ParcelInfo
@dataclass
class MyShipment:
id: str
status: str = "new"
provider: str = ""
external_id: str = ""
tracking_number: str = ""
class InMemoryRepository:
def __init__(self) -> None:
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 = MyShipment(
id=str(self._counter),
status=str(kwargs.get("status", "new")),
provider=str(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_fields(self, shipment_id: str, **fields) -> MyShipment:
shipment = self._store[shipment_id]
for key, value in fields.items():
setattr(shipment, key, value)
return shipment
async def main() -> None:
flow = ShipmentFlow(repository=InMemoryRepository())
created = await flow.create_shipment(
"dummy",
sender_address=AddressInfo(
name="Sender Co.",
line1="Marszalkowska 1",
city="Warsaw",
postal_code="00-001",
country_code="PL",
),
receiver_address=AddressInfo(
name="Jan Kowalski",
line1="Dluga 10",
city="Gdansk",
postal_code="80-001",
country_code="PL",
),
parcels=[ParcelInfo(weight_kg=Decimal("2.5"))],
)
print(created.shipment.status)
print(created.shipment.external_id)
print(created.shipment.tracking_number)
labelled = await flow.create_label(created.shipment)
print(labelled.label.get("url"))
anyio.run(main)
Provider model
All providers subclass BaseProvider; capability methods raise
ProviderCapabilityError unless overridden:
BaseProvider.create_shipment(...)returnsShipmentCreateResult(required).BaseProvider.confirmation_methoddefaults toConfirmationMethod.NONE.BaseProvider.create_label(...)returnsLabelInfo.BaseProvider.verify_callback(ctx)/handle_callback(ctx)returnNone/ShipmentUpdateResult.BaseProvider.fetch_shipment_status(...)returnsShipmentUpdateResult.BaseProvider.cancel_shipment(...)returnsbool.
Use ConfirmationMethod.PUSH only when the callback methods are overridden
and ConfirmationMethod.PULL only when fetch_shipment_status is. See
docs/provider-authoring.md for the full guide.
The core owns shipment state transitions. Providers translate carrier responses into normalized results.
Installation
pip install python-sendparcel
With uv:
uv add python-sendparcel
Extras
djangoinpostdpdplorlenpaczkacliframeworksprovidersall
Development
uv sync --extra dev
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run ty check
uv run mypy src
Metadata
Release files for python-sendparcel 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| python_sendparcel-0.3.2.tar.gz | 42.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| python_sendparcel-0.3.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 67.6 kB
Release files / python_sendparcel-0.3.2.tar.gz
| Download URL | python_sendparcel-0.3.2.tar.gz |
|---|---|
| Size | 42.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ccf1140e0b1840bb7465c0f279a5e3a6d9f834a3c99c68f2be3b65aac8efdab1
|
|
BLAKE2b-256 checksum How to use checksums |
5dbd5adf1faf0b76910dbc6300f9ec4a2c34e025b469eefe290069aba4ba34ad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.
Transparency logRelease files / python_sendparcel-0.3.2-py3-none-any.whl
| Download URL | python_sendparcel-0.3.2-py3-none-any.whl |
|---|---|
| Size | 25.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e87cb6aa58ae1343c0542610b61b9a6bd727f25007294e97bc868367448fe45e
|
|
BLAKE2b-256 checksum How to use checksums |
14a1f930c77ed43f5a56c75951d801a26e3dbd10eff831aeac2b1b5a7c6e7af2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.
Transparency log