FastAPI adapter for python-sendparcel
Project description
fastapi-sendparcel
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-configuredAPIRouterwith shipment, label, status and callback endpoints. - Provider-agnostic — plug in any shipping provider that implements the
python-sendparcelprovider protocol. - Plugin registry —
FastAPIPluginRegistrydiscovers and manages provider plugins with optional per-provider routers. - Pydantic-native configuration —
SendparcelConfigreads from environment variables with theSENDPARCEL_prefix. - Webhook callback handling — built-in endpoint for provider status callbacks with automatic retry queue support.
- SQLAlchemy contrib — optional
[sqlalchemy]extra providesSQLAlchemyShipmentRepository,SQLAlchemyRetryStore, and ready-made database models. - Exception mapping — core
sendparcelexceptions are automatically converted to appropriate HTTP status codes (400, 404, 409, 502). - Async-first — fully asynchronous with
async/awaitthroughout.
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 syncinstead.
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 thesendparcel_shipmentstable.CallbackRetryModel— SQLAlchemy model mapped to thesendparcel_callback_retriestable.SQLAlchemyShipmentRepository— async repository implementing theShipmentRepositoryprotocol.SQLAlchemyRetryStore— async retry store implementing theCallbackRetryStoreprotocol.
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
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 fastapi_sendparcel-0.1.1.tar.gz.
File metadata
- Download URL: fastapi_sendparcel-0.1.1.tar.gz
- Upload date:
- Size: 39.1 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 |
bbfa5bcdf247cebfd24ad4829bfa2dc7085e3126cc253d38d749bcbe842dd649
|
|
| MD5 |
fa4501d5d2208c1c52bda0b55fa88517
|
|
| BLAKE2b-256 |
2f95c53dfecb6052ea746541a4f4ade797bb5bdf7f9e22945d3d38f401f3fa04
|
File details
Details for the file fastapi_sendparcel-0.1.1-py3-none-any.whl.
File metadata
- Download URL: fastapi_sendparcel-0.1.1-py3-none-any.whl
- Upload date:
- Size: 19.2 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 |
b6bb6e22f28aa8db10f34749cf3c8cc37006c9c827afb9ec8e89150690f1230e
|
|
| MD5 |
b92470873c1805d03b6aea95d6a4db2e
|
|
| BLAKE2b-256 |
94245f070295b6e6b4f0e594a2c0c0fc76cc9225fe0c0e6fa4344f35f088b0e8
|