Skip to main content

Framework-agnostic parcel shipping core for Python.

Project description

python-sendparcel

PyPI Python Version License

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 typesAddressInfo, ParcelInfo, LabelInfo, ShipmentCreateResult, ShipmentStatusResponse, and TrackingEvent as strict TypedDicts.
  • Finite state machine — 9-state ShipmentStatus enum (NEWCREATEDLABEL_READYIN_TRANSITOUT_FOR_DELIVERYDELIVERED, plus CANCELLED, 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 ShipmentFlow for global or per-operation validation.
  • Runtime protocolsOrder, Shipment, and ShipmentRepository are @runtime_checkable protocols; 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_label requires label_url to be set on the shipment.
  • mark_in_transit requires tracking_number to 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

License

MIT

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

python_sendparcel-0.1.0.tar.gz (82.6 kB view details)

Uploaded Source

Built Distribution

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

python_sendparcel-0.1.0-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

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

Hashes for python_sendparcel-0.1.0.tar.gz
Algorithm Hash digest
SHA256 38821df1dc3b59dbcf026fffcbdf6ebfdaf3c2fba336cb138c28e2e419354383
MD5 dcc498ad0feb9460fd75fbf2e6ce5f9e
BLAKE2b-256 ddd4cb89381dbb5bcb2aa6635f0fd045b42b531cbcf838f8f9ec8812b0a38223

See more details on using hashes here.

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

Hashes for python_sendparcel-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b212b23c57abb53be098e0f2de24da31e6614c1a54dd154eb082f22f572fa8f9
MD5 a614e90fb113dea8fa17341c9e7dc98b
BLAKE2b-256 b3ce753848c21e2e6a1a81ddea4fa507a18f48d5a6efe10f23f57313fb0b2e6e

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