Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

python-sendparcel/fastapi-sendparcel

Open more actions menu

Repository files navigation

fastapi-sendparcel

PyPI Python Version License Documentation

FastAPI adapter for the python-sendparcel shipping ecosystem.

Alpha notice — This package is at version 0.1.1 and its API is not yet stable. Breaking changes may occur in minor releases until 1.0.


Features

  • Router factory — single call to create_shipping_router() gives you a fully-configured APIRouter with shipment, label, status and callback endpoints.
  • Provider-agnostic — plug in any shipping provider that implements the python-sendparcel provider protocol.
  • Plugin registryFastAPIPluginRegistry discovers and manages provider plugins with optional per-provider routers.
  • Pydantic-native configurationSendparcelConfig reads from environment variables with the SENDPARCEL_ prefix.
  • Webhook callback handling — built-in endpoint for provider status callbacks with automatic retry queue support.
  • SQLAlchemy contrib — optional [sqlalchemy] extra provides SQLAlchemyShipmentRepository, SQLAlchemyRetryStore, and ready-made database models.
  • Exception mapping — core sendparcel exceptions are automatically converted to appropriate HTTP status codes (400, 404, 409, 502).
  • Async-first — fully asynchronous with async/await throughout.

Installation

Install the base package:

pip install fastapi-sendparcel

If you want SQLAlchemy-backed persistence (recommended):

pip install fastapi-sendparcel[sqlalchemy]

Note: The project uses uv for development. If you are contributing, run uv sync instead.

Quick Start

Below is a minimal but complete FastAPI application that wires up the shipping router with SQLAlchemy persistence.

from contextlib import asynccontextmanager
from collections.abc import AsyncGenerator

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import (
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)

from fastapi_sendparcel import (
    SendparcelConfig,
    FastAPIPluginRegistry,
    create_shipping_router,
)
from fastapi_sendparcel.contrib.sqlalchemy.models import Base
from fastapi_sendparcel.contrib.sqlalchemy.repository import (
    SQLAlchemyShipmentRepository,
)
from fastapi_sendparcel.contrib.sqlalchemy.retry_store import (
    SQLAlchemyRetryStore,
)

# --- Database ---
engine = create_async_engine("sqlite+aiosqlite:///./shipments.db")
async_session = async_sessionmaker(engine, class_=AsyncSession)

# --- Sendparcel setup ---
config = SendparcelConfig(
    default_provider="my-provider",
    providers={
        "my-provider": {
            "api_key": "...",
        },
    },
)

repository = SQLAlchemyShipmentRepository(async_session)
retry_store = SQLAlchemyRetryStore(async_session)
registry = FastAPIPluginRegistry()

shipping_router = create_shipping_router(
    config=config,
    repository=repository,
    registry=registry,
    retry_store=retry_store,
)

# --- App ---
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    await engine.dispose()


app = FastAPI(title="My Shipping App", lifespan=lifespan)
app.include_router(shipping_router, prefix="/api/shipping")

The create_shipping_router function accepts all its arguments as keyword-only parameters:

Parameter Type Required Description
config SendparcelConfig Yes Adapter configuration instance
repository ShipmentRepository Yes Persistence backend for shipments
registry FastAPIPluginRegistry No Plugin registry (auto-created if omitted)
retry_store CallbackRetryStore No Storage for webhook retry queue

Configuration

SendparcelConfig extends Pydantic's BaseSettings and reads environment variables with the SENDPARCEL_ prefix.

Setting Env variable Type Default Description
default_provider SENDPARCEL_DEFAULT_PROVIDER str (required) Slug of the default shipping provider
providers SENDPARCEL_PROVIDERS dict[str, dict] {} Per-provider configuration dicts
retry_max_attempts SENDPARCEL_RETRY_MAX_ATTEMPTS int 5 Max retry attempts for failed callbacks
retry_backoff_seconds SENDPARCEL_RETRY_BACKOFF_SECONDS int 60 Base backoff interval between retries
retry_enabled SENDPARCEL_RETRY_ENABLED bool True Enable/disable callback retry queue

You can instantiate the config directly or let it read from the environment:

# Explicit values
config = SendparcelConfig(
    default_provider="inpost",
    providers={"inpost": {"api_key": "secret"}},
)

# From environment variables
# (set SENDPARCEL_DEFAULT_PROVIDER=inpost, etc.)
config = SendparcelConfig()

API Endpoints

The router created by create_shipping_router() exposes the following endpoints. All paths are relative to the prefix you mount the router at (e.g. /api/shipping).

Method Path Description
GET /shipments/health Healthcheck — returns {"status": "ok"}
POST /shipments Create a new shipment
POST /shipments/{shipment_id}/label Generate a shipping label
GET /shipments/{shipment_id}/status Fetch and update shipment status from the provider
POST /callbacks/{provider_slug}/{shipment_id} Handle a provider webhook callback

Request and Response Schemas

POST /shipments — request body:

{
  "reference_id": "my-ref-123",
  "provider": "my-provider",
  "sender_address": {
    "name": "John Smith",
    "line1": "1 Example St",
    "city": "Warsaw",
    "postal_code": "00-001",
    "country_code": "PL"
  },
  "receiver_address": {
    "name": "Jane Doe",
    "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 the config is used. The reference_id field is optional and can be used for external reference tracking.

ShipmentOperationResponse — returned by shipment, label and status endpoints:

{
  "id": "abc-def",
  "status": "label_ready",
  "provider": "my-provider",
  "external_id": "EXT123",
  "tracking_number": "TRACK456",
  "label": {
    "format": "PDF",
    "url": "https://...",
    "content_base64": null
  },
  "update": null
}

Label payloads are returned as operation results. They are not persisted on the shipment model.

CallbackResponse — returned by the callback endpoint:

{
  "provider": "my-provider",
  "status": "accepted",
  "shipment": {
    "id": "abc-def",
    "status": "in_transit",
    "provider": "my-provider",
    "external_id": "EXT123",
    "tracking_number": "TRACK456"
  },
  "update": {
    "status": "in_transit",
    "tracking_number": "TRACK456",
    "tracking_events": []
  }
}

Exception Handling

The router automatically registers exception handlers that map core sendparcel exceptions to HTTP responses:

Exception HTTP Status 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
SendParcelException 400 shipment_error

Protocols

CallbackRetryStore

Stores failed webhook callbacks for retry processing. The SQLAlchemy contrib provides a ready-made implementation (SQLAlchemyRetryStore), but you can implement this protocol with any backend (Redis, DynamoDB, etc.).

from fastapi_sendparcel import CallbackRetryStore


class MyRetryStore:
    async def store_failed_callback(
        self, shipment_id: 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: ...

SQLAlchemy Contrib

The optional [sqlalchemy] extra provides production-ready persistence components:

  • ShipmentModel — SQLAlchemy model mapped to the sendparcel_shipments table.
  • CallbackRetryModel — SQLAlchemy model mapped to the sendparcel_callback_retries table.
  • SQLAlchemyShipmentRepository — async repository implementing the ShipmentRepository protocol.
  • SQLAlchemyRetryStore — async retry store implementing the CallbackRetryStore protocol.

Both require an async_sessionmaker[AsyncSession] at construction time:

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from fastapi_sendparcel.contrib.sqlalchemy.repository import (
    SQLAlchemyShipmentRepository,
)
from fastapi_sendparcel.contrib.sqlalchemy.retry_store import (
    SQLAlchemyRetryStore,
)

session_factory = async_sessionmaker(engine, class_=AsyncSession)

repository = SQLAlchemyShipmentRepository(session_factory)
retry_store = SQLAlchemyRetryStore(session_factory, backoff_seconds=60)

Example Project

The example/ directory contains a full demo application with:

  • Tabler-based UI with Jinja2 templates and HTMX
  • Shipment creation with sender/receiver address forms
  • Label generation and PDF download
  • A simulated delivery provider (delivery_sim.py)

Running the example

cd example
uv sync
uv run uvicorn app:app --reload

Then open http://localhost:8000 in your browser.

Supported Versions

Dependency Version
Python >= 3.12
FastAPI >= 0.115.0
Pydantic Settings >= 2.0.0
python-sendparcel >= 0.1.1
SQLAlchemy (optional) >= 2.0.0

Running Tests

uv sync --all-extras
uv run pytest

The test suite uses pytest with pytest-asyncio in auto mode. Configuration is in pyproject.toml.

Credits

Created and maintained by Dominik Kozaczko.

This project is the FastAPI adapter for the python-sendparcel ecosystem.

License

MIT

About

FastAPI adapter for python-sendparcel

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Morty Proxy This is a proxified and sanitized view of the page, visit original site.