python-sendparcel 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.gitignore +4 -0
- python_sendparcel-0.1.1/PKG-INFO +212 -0
- python_sendparcel-0.1.1/README.md +150 -0
- python_sendparcel-0.1.1/docs/compatibility-matrix.md +25 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/conf.py +1 -1
- python_sendparcel-0.1.1/docs/getting-started.md +115 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/index.md +6 -6
- python_sendparcel-0.1.1/docs/installation-matrix.md +28 -0
- python_sendparcel-0.1.1/docs/plans/2026-02-17-order-optional.md +21 -0
- python_sendparcel-0.1.1/docs/provider-authoring.md +121 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/pyproject.toml +45 -29
- python_sendparcel-0.1.1/src/sendparcel/__init__.py +54 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/enums.py +10 -0
- python_sendparcel-0.1.1/src/sendparcel/exceptions.py +53 -0
- python_sendparcel-0.1.1/src/sendparcel/flow.py +193 -0
- python_sendparcel-0.1.1/src/sendparcel/fsm.py +99 -0
- python_sendparcel-0.1.1/src/sendparcel/protocols.py +26 -0
- python_sendparcel-0.1.1/src/sendparcel/provider.py +92 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/providers/dummy.py +44 -25
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/registry.py +19 -7
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/types.py +47 -19
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/validators.py +3 -2
- python_sendparcel-0.1.1/tests/conftest.py +89 -0
- python_sendparcel-0.1.1/tests/test_builtin_dummy_provider.py +166 -0
- python_sendparcel-0.1.1/tests/test_enums.py +26 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/tests/test_exceptions.py +9 -0
- python_sendparcel-0.1.1/tests/test_flow.py +380 -0
- python_sendparcel-0.1.1/tests/test_fsm.py +86 -0
- python_sendparcel-0.1.1/tests/test_integration.py +209 -0
- python_sendparcel-0.1.1/tests/test_protocols.py +41 -0
- python_sendparcel-0.1.1/tests/test_provider.py +96 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/tests/test_public_api.py +12 -1
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/tests/test_registry.py +69 -16
- python_sendparcel-0.1.1/tests/test_types.py +86 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/tests/test_validators.py +11 -8
- python_sendparcel-0.1.0/PKG-INFO +0 -449
- python_sendparcel-0.1.0/README.md +0 -397
- python_sendparcel-0.1.0/docs/compatibility-matrix.md +0 -13
- python_sendparcel-0.1.0/docs/getting-started.md +0 -218
- python_sendparcel-0.1.0/docs/installation-matrix.md +0 -24
- python_sendparcel-0.1.0/docs/provider-authoring.md +0 -40
- python_sendparcel-0.1.0/src/sendparcel/__init__.py +0 -29
- python_sendparcel-0.1.0/src/sendparcel/exceptions.py +0 -21
- python_sendparcel-0.1.0/src/sendparcel/flow.py +0 -164
- python_sendparcel-0.1.0/src/sendparcel/fsm.py +0 -131
- python_sendparcel-0.1.0/src/sendparcel/protocols.py +0 -41
- python_sendparcel-0.1.0/src/sendparcel/provider.py +0 -63
- python_sendparcel-0.1.0/tests/conftest.py +0 -131
- python_sendparcel-0.1.0/tests/test_builtin_dummy_provider.py +0 -185
- python_sendparcel-0.1.0/tests/test_enums.py +0 -23
- python_sendparcel-0.1.0/tests/test_flow.py +0 -515
- python_sendparcel-0.1.0/tests/test_fsm.py +0 -384
- python_sendparcel-0.1.0/tests/test_integration.py +0 -289
- python_sendparcel-0.1.0/tests/test_protocols.py +0 -34
- python_sendparcel-0.1.0/tests/test_provider.py +0 -47
- python_sendparcel-0.1.0/tests/test_types.py +0 -97
- python_sendparcel-0.1.0/uv.lock +0 -1195
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.github/workflows/ci.yml +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.github/workflows/ecosystem-smoke.yml +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.github/workflows/release.yml +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.pre-commit-config.yaml +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/CHANGELOG.md +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/CONTRIBUTING.md +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/LICENSE +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/Makefile +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/release-policy.md +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/requirements.txt +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/providers/__init__.py +0 -0
- {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/py.typed +0 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-sendparcel
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Framework-agnostic parcel shipping core for Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sendparcel/python-sendparcel
|
|
6
|
+
Project-URL: Documentation, https://python-sendparcel.readthedocs.io/
|
|
7
|
+
Project-URL: Repository, https://github.com/sendparcel/python-sendparcel
|
|
8
|
+
Project-URL: Changelog, https://github.com/sendparcel/python-sendparcel/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issue Tracker, https://github.com/sendparcel/python-sendparcel/issues
|
|
10
|
+
Author-email: Dominik Kozaczko <dominik@kozaczko.info>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: delivery,logistics,parcel,sendparcel,shipping
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: anyio>=4.0
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: django-sendparcel>=0.1.0; extra == 'all'
|
|
28
|
+
Requires-Dist: fastapi-sendparcel>=0.1.0; extra == 'all'
|
|
29
|
+
Requires-Dist: litestar-sendparcel>=0.1.0; extra == 'all'
|
|
30
|
+
Requires-Dist: python-sendparcel-cli>=0.1.0; extra == 'all'
|
|
31
|
+
Requires-Dist: python-sendparcel-dpdpl>=0.1.0; extra == 'all'
|
|
32
|
+
Requires-Dist: python-sendparcel-inpost>=0.1.0; extra == 'all'
|
|
33
|
+
Provides-Extra: cli
|
|
34
|
+
Requires-Dist: python-sendparcel-cli>=0.1.0; extra == 'cli'
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pre-commit-hooks>=6.0.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pre-commit>=4.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: ty>=0.0.16; extra == 'dev'
|
|
43
|
+
Provides-Extra: django
|
|
44
|
+
Requires-Dist: django-sendparcel>=0.1.0; extra == 'django'
|
|
45
|
+
Provides-Extra: dpdpl
|
|
46
|
+
Requires-Dist: python-sendparcel-dpdpl>=0.1.0; extra == 'dpdpl'
|
|
47
|
+
Provides-Extra: dummy
|
|
48
|
+
Provides-Extra: fastapi
|
|
49
|
+
Requires-Dist: fastapi-sendparcel>=0.1.0; extra == 'fastapi'
|
|
50
|
+
Provides-Extra: frameworks
|
|
51
|
+
Requires-Dist: django-sendparcel>=0.1.0; extra == 'frameworks'
|
|
52
|
+
Requires-Dist: fastapi-sendparcel>=0.1.0; extra == 'frameworks'
|
|
53
|
+
Requires-Dist: litestar-sendparcel>=0.1.0; extra == 'frameworks'
|
|
54
|
+
Provides-Extra: inpost
|
|
55
|
+
Requires-Dist: python-sendparcel-inpost>=0.1.0; extra == 'inpost'
|
|
56
|
+
Provides-Extra: litestar
|
|
57
|
+
Requires-Dist: litestar-sendparcel>=0.1.0; extra == 'litestar'
|
|
58
|
+
Provides-Extra: providers
|
|
59
|
+
Requires-Dist: python-sendparcel-dpdpl>=0.1.0; extra == 'providers'
|
|
60
|
+
Requires-Dist: python-sendparcel-inpost>=0.1.0; extra == 'providers'
|
|
61
|
+
Description-Content-Type: text/markdown
|
|
62
|
+
|
|
63
|
+
# python-sendparcel
|
|
64
|
+
|
|
65
|
+
Framework-agnostic parcel shipping core for Python.
|
|
66
|
+
|
|
67
|
+
> Alpha notice: `0.1.1` is still unstable. The API can change fast because the
|
|
68
|
+
> ecosystem is still being cleaned up.
|
|
69
|
+
|
|
70
|
+
## What it is
|
|
71
|
+
|
|
72
|
+
- Provider-agnostic shipment orchestration with a single core flow.
|
|
73
|
+
- Explicit shipment metadata persistence: `id`, `status`, `provider`, `external_id`, `tracking_number`.
|
|
74
|
+
- Label payloads are operation results, not persisted shipment fields.
|
|
75
|
+
- One normalized provider update contract for both callbacks and polling.
|
|
76
|
+
- Runtime-checkable `Shipment` and `ShipmentRepository` protocols so adapters can use their own models.
|
|
77
|
+
|
|
78
|
+
## Core contract
|
|
79
|
+
|
|
80
|
+
- `ShipmentFlow.create_shipment(...) -> CreateShipmentOutcome`
|
|
81
|
+
- `ShipmentFlow.create_label(...) -> CreateLabelOutcome`
|
|
82
|
+
- `ShipmentFlow.handle_callback(...) -> ShipmentUpdateOutcome`
|
|
83
|
+
- `ShipmentFlow.fetch_and_update_status(...) -> ShipmentUpdateOutcome`
|
|
84
|
+
- `ShipmentFlow.cancel_shipment(...) -> bool`
|
|
85
|
+
|
|
86
|
+
`CreateShipmentOutcome` and `CreateLabelOutcome` return label payloads when available.
|
|
87
|
+
The shipment object never stores label bytes or a persisted `label_url` in the core contract.
|
|
88
|
+
|
|
89
|
+
## Quick start
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from dataclasses import dataclass
|
|
93
|
+
from decimal import Decimal
|
|
94
|
+
|
|
95
|
+
import anyio
|
|
96
|
+
|
|
97
|
+
from sendparcel import ShipmentFlow
|
|
98
|
+
from sendparcel.types import AddressInfo, ParcelInfo
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class MyShipment:
|
|
103
|
+
id: str
|
|
104
|
+
status: str = "new"
|
|
105
|
+
provider: str = ""
|
|
106
|
+
external_id: str = ""
|
|
107
|
+
tracking_number: str = ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class InMemoryRepository:
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
self._store: dict[str, MyShipment] = {}
|
|
113
|
+
self._counter = 0
|
|
114
|
+
|
|
115
|
+
async def get_by_id(self, shipment_id: str) -> MyShipment:
|
|
116
|
+
return self._store[shipment_id]
|
|
117
|
+
|
|
118
|
+
async def create(self, **kwargs) -> MyShipment:
|
|
119
|
+
self._counter += 1
|
|
120
|
+
shipment = MyShipment(
|
|
121
|
+
id=str(self._counter),
|
|
122
|
+
status=str(kwargs.get("status", "new")),
|
|
123
|
+
provider=str(kwargs.get("provider", "")),
|
|
124
|
+
)
|
|
125
|
+
self._store[shipment.id] = shipment
|
|
126
|
+
return shipment
|
|
127
|
+
|
|
128
|
+
async def save(self, shipment: MyShipment) -> MyShipment:
|
|
129
|
+
self._store[shipment.id] = shipment
|
|
130
|
+
return shipment
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def main() -> None:
|
|
134
|
+
flow = ShipmentFlow(repository=InMemoryRepository())
|
|
135
|
+
|
|
136
|
+
created = await flow.create_shipment(
|
|
137
|
+
"dummy",
|
|
138
|
+
sender_address=AddressInfo(
|
|
139
|
+
name="Sender Co.",
|
|
140
|
+
line1="Marszalkowska 1",
|
|
141
|
+
city="Warsaw",
|
|
142
|
+
postal_code="00-001",
|
|
143
|
+
country_code="PL",
|
|
144
|
+
),
|
|
145
|
+
receiver_address=AddressInfo(
|
|
146
|
+
name="Jan Kowalski",
|
|
147
|
+
line1="Dluga 10",
|
|
148
|
+
city="Gdansk",
|
|
149
|
+
postal_code="80-001",
|
|
150
|
+
country_code="PL",
|
|
151
|
+
),
|
|
152
|
+
parcels=[ParcelInfo(weight_kg=Decimal("2.5"))],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
print(created.shipment.status)
|
|
156
|
+
print(created.shipment.external_id)
|
|
157
|
+
print(created.shipment.tracking_number)
|
|
158
|
+
|
|
159
|
+
labelled = await flow.create_label(created.shipment)
|
|
160
|
+
print(labelled.label.get("url"))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
anyio.run(main)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Provider model
|
|
167
|
+
|
|
168
|
+
- `BaseProvider.create_shipment(...)` returns `ShipmentCreateResult`.
|
|
169
|
+
- `BaseProvider.confirmation_method` defaults to `ConfirmationMethod.NONE`.
|
|
170
|
+
- `LabelProvider.create_label(...)` returns `LabelInfo`.
|
|
171
|
+
- `PushCallbackProvider.handle_callback(...)` returns `ShipmentUpdateResult`.
|
|
172
|
+
- `PullStatusProvider.fetch_shipment_status(...)` returns `ShipmentUpdateResult`.
|
|
173
|
+
- `CancellableProvider.cancel_shipment(...)` returns `bool`.
|
|
174
|
+
|
|
175
|
+
Use `ConfirmationMethod.PUSH` only with `PushCallbackProvider` and
|
|
176
|
+
`ConfirmationMethod.PULL` only with `PullStatusProvider`.
|
|
177
|
+
|
|
178
|
+
The core owns shipment state transitions. Providers translate carrier responses
|
|
179
|
+
into normalized results.
|
|
180
|
+
|
|
181
|
+
## Installation
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install python-sendparcel
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
With `uv`:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
uv add python-sendparcel
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Extras
|
|
194
|
+
|
|
195
|
+
- `django`
|
|
196
|
+
- `fastapi`
|
|
197
|
+
- `litestar`
|
|
198
|
+
- `inpost`
|
|
199
|
+
- `dpdpl`
|
|
200
|
+
- `cli`
|
|
201
|
+
- `frameworks`
|
|
202
|
+
- `providers`
|
|
203
|
+
- `all`
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
uv sync --extra dev
|
|
209
|
+
uv run pytest
|
|
210
|
+
uv run ruff check src tests
|
|
211
|
+
uv run mypy src tests
|
|
212
|
+
```
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# python-sendparcel
|
|
2
|
+
|
|
3
|
+
Framework-agnostic parcel shipping core for Python.
|
|
4
|
+
|
|
5
|
+
> Alpha notice: `0.1.1` is still unstable. The API can change fast because the
|
|
6
|
+
> ecosystem is still being cleaned up.
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
|
|
10
|
+
- Provider-agnostic shipment orchestration with a single core flow.
|
|
11
|
+
- Explicit shipment metadata persistence: `id`, `status`, `provider`, `external_id`, `tracking_number`.
|
|
12
|
+
- Label payloads are operation results, not persisted shipment fields.
|
|
13
|
+
- One normalized provider update contract for both callbacks and polling.
|
|
14
|
+
- Runtime-checkable `Shipment` and `ShipmentRepository` protocols so adapters can use their own models.
|
|
15
|
+
|
|
16
|
+
## Core contract
|
|
17
|
+
|
|
18
|
+
- `ShipmentFlow.create_shipment(...) -> CreateShipmentOutcome`
|
|
19
|
+
- `ShipmentFlow.create_label(...) -> CreateLabelOutcome`
|
|
20
|
+
- `ShipmentFlow.handle_callback(...) -> ShipmentUpdateOutcome`
|
|
21
|
+
- `ShipmentFlow.fetch_and_update_status(...) -> ShipmentUpdateOutcome`
|
|
22
|
+
- `ShipmentFlow.cancel_shipment(...) -> bool`
|
|
23
|
+
|
|
24
|
+
`CreateShipmentOutcome` and `CreateLabelOutcome` return label payloads when available.
|
|
25
|
+
The shipment object never stores label bytes or a persisted `label_url` in the core contract.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from decimal import Decimal
|
|
32
|
+
|
|
33
|
+
import anyio
|
|
34
|
+
|
|
35
|
+
from sendparcel import ShipmentFlow
|
|
36
|
+
from sendparcel.types import AddressInfo, ParcelInfo
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class MyShipment:
|
|
41
|
+
id: str
|
|
42
|
+
status: str = "new"
|
|
43
|
+
provider: str = ""
|
|
44
|
+
external_id: str = ""
|
|
45
|
+
tracking_number: str = ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class InMemoryRepository:
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._store: dict[str, MyShipment] = {}
|
|
51
|
+
self._counter = 0
|
|
52
|
+
|
|
53
|
+
async def get_by_id(self, shipment_id: str) -> MyShipment:
|
|
54
|
+
return self._store[shipment_id]
|
|
55
|
+
|
|
56
|
+
async def create(self, **kwargs) -> MyShipment:
|
|
57
|
+
self._counter += 1
|
|
58
|
+
shipment = MyShipment(
|
|
59
|
+
id=str(self._counter),
|
|
60
|
+
status=str(kwargs.get("status", "new")),
|
|
61
|
+
provider=str(kwargs.get("provider", "")),
|
|
62
|
+
)
|
|
63
|
+
self._store[shipment.id] = shipment
|
|
64
|
+
return shipment
|
|
65
|
+
|
|
66
|
+
async def save(self, shipment: MyShipment) -> MyShipment:
|
|
67
|
+
self._store[shipment.id] = shipment
|
|
68
|
+
return shipment
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def main() -> None:
|
|
72
|
+
flow = ShipmentFlow(repository=InMemoryRepository())
|
|
73
|
+
|
|
74
|
+
created = await flow.create_shipment(
|
|
75
|
+
"dummy",
|
|
76
|
+
sender_address=AddressInfo(
|
|
77
|
+
name="Sender Co.",
|
|
78
|
+
line1="Marszalkowska 1",
|
|
79
|
+
city="Warsaw",
|
|
80
|
+
postal_code="00-001",
|
|
81
|
+
country_code="PL",
|
|
82
|
+
),
|
|
83
|
+
receiver_address=AddressInfo(
|
|
84
|
+
name="Jan Kowalski",
|
|
85
|
+
line1="Dluga 10",
|
|
86
|
+
city="Gdansk",
|
|
87
|
+
postal_code="80-001",
|
|
88
|
+
country_code="PL",
|
|
89
|
+
),
|
|
90
|
+
parcels=[ParcelInfo(weight_kg=Decimal("2.5"))],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
print(created.shipment.status)
|
|
94
|
+
print(created.shipment.external_id)
|
|
95
|
+
print(created.shipment.tracking_number)
|
|
96
|
+
|
|
97
|
+
labelled = await flow.create_label(created.shipment)
|
|
98
|
+
print(labelled.label.get("url"))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
anyio.run(main)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Provider model
|
|
105
|
+
|
|
106
|
+
- `BaseProvider.create_shipment(...)` returns `ShipmentCreateResult`.
|
|
107
|
+
- `BaseProvider.confirmation_method` defaults to `ConfirmationMethod.NONE`.
|
|
108
|
+
- `LabelProvider.create_label(...)` returns `LabelInfo`.
|
|
109
|
+
- `PushCallbackProvider.handle_callback(...)` returns `ShipmentUpdateResult`.
|
|
110
|
+
- `PullStatusProvider.fetch_shipment_status(...)` returns `ShipmentUpdateResult`.
|
|
111
|
+
- `CancellableProvider.cancel_shipment(...)` returns `bool`.
|
|
112
|
+
|
|
113
|
+
Use `ConfirmationMethod.PUSH` only with `PushCallbackProvider` and
|
|
114
|
+
`ConfirmationMethod.PULL` only with `PullStatusProvider`.
|
|
115
|
+
|
|
116
|
+
The core owns shipment state transitions. Providers translate carrier responses
|
|
117
|
+
into normalized results.
|
|
118
|
+
|
|
119
|
+
## Installation
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install python-sendparcel
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
With `uv`:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uv add python-sendparcel
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Extras
|
|
132
|
+
|
|
133
|
+
- `django`
|
|
134
|
+
- `fastapi`
|
|
135
|
+
- `litestar`
|
|
136
|
+
- `inpost`
|
|
137
|
+
- `dpdpl`
|
|
138
|
+
- `cli`
|
|
139
|
+
- `frameworks`
|
|
140
|
+
- `providers`
|
|
141
|
+
- `all`
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
uv sync --extra dev
|
|
147
|
+
uv run pytest
|
|
148
|
+
uv run ruff check src tests
|
|
149
|
+
uv run mypy src tests
|
|
150
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Ecosystem compatibility matrix
|
|
2
|
+
|
|
3
|
+
| Package | Type | Depends on `python-sendparcel` |
|
|
4
|
+
|---|---|---|
|
|
5
|
+
| python-sendparcel-inpost | Provider | >=0.1.1 |
|
|
6
|
+
| python-sendparcel-dpdpl | Provider | >=0.1.1 |
|
|
7
|
+
| django-sendparcel | Framework adapter | >=0.1.1 |
|
|
8
|
+
| fastapi-sendparcel | Framework adapter | >=0.1.1 |
|
|
9
|
+
| litestar-sendparcel | Framework adapter | >=0.1.1 |
|
|
10
|
+
|
|
11
|
+
## Plugin discovery
|
|
12
|
+
|
|
13
|
+
The ecosystem entry point group is: `sendparcel.providers`.
|
|
14
|
+
|
|
15
|
+
The reference `dummy` provider is built into `python-sendparcel`.
|
|
16
|
+
|
|
17
|
+
### Available provider plugins
|
|
18
|
+
|
|
19
|
+
| Entry point | Provider class | Package |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `dummy` | `sendparcel.providers.dummy:DummyProvider` | `python-sendparcel` (built-in) |
|
|
22
|
+
| `inpost_locker` | `sendparcel_inpost.providers:InPostLockerProvider` | `python-sendparcel-inpost` |
|
|
23
|
+
| `inpost_courier` | `sendparcel_inpost.providers:InPostCourierProvider` | `python-sendparcel-inpost` |
|
|
24
|
+
| `dpd_standard` | `sendparcel_dpdpl.providers.standard:DPDStandardProvider` | `python-sendparcel-dpdpl` |
|
|
25
|
+
| `dpd_pickup` | `sendparcel_dpdpl.providers.pickup:DPDPickupProvider` | `python-sendparcel-dpdpl` |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
Requires Python 3.12+.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install python-sendparcel
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
With `uv`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv add python-sendparcel
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Core ideas
|
|
18
|
+
|
|
19
|
+
- `Shipment` and `ShipmentRepository` are protocols. Bring your own models.
|
|
20
|
+
- Providers translate carrier APIs into normalized core results.
|
|
21
|
+
- `ShipmentFlow` owns orchestration and status transitions.
|
|
22
|
+
- Labels are returned from operations and are not persisted by the core contract.
|
|
23
|
+
- Callback and polling paths share the same normalized update shape.
|
|
24
|
+
|
|
25
|
+
## Minimal example
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from decimal import Decimal
|
|
30
|
+
|
|
31
|
+
import anyio
|
|
32
|
+
|
|
33
|
+
from sendparcel import ShipmentFlow
|
|
34
|
+
from sendparcel.types import AddressInfo, ParcelInfo
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class MyShipment:
|
|
39
|
+
id: str
|
|
40
|
+
status: str = "new"
|
|
41
|
+
provider: str = ""
|
|
42
|
+
external_id: str = ""
|
|
43
|
+
tracking_number: str = ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InMemoryRepository:
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._store: dict[str, MyShipment] = {}
|
|
49
|
+
self._counter = 0
|
|
50
|
+
|
|
51
|
+
async def get_by_id(self, shipment_id: str) -> MyShipment:
|
|
52
|
+
return self._store[shipment_id]
|
|
53
|
+
|
|
54
|
+
async def create(self, **kwargs) -> MyShipment:
|
|
55
|
+
self._counter += 1
|
|
56
|
+
shipment = MyShipment(
|
|
57
|
+
id=str(self._counter),
|
|
58
|
+
status=str(kwargs.get("status", "new")),
|
|
59
|
+
provider=str(kwargs.get("provider", "")),
|
|
60
|
+
)
|
|
61
|
+
self._store[shipment.id] = shipment
|
|
62
|
+
return shipment
|
|
63
|
+
|
|
64
|
+
async def save(self, shipment: MyShipment) -> MyShipment:
|
|
65
|
+
self._store[shipment.id] = shipment
|
|
66
|
+
return shipment
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def main() -> None:
|
|
70
|
+
flow = ShipmentFlow(repository=InMemoryRepository())
|
|
71
|
+
created = await flow.create_shipment(
|
|
72
|
+
"dummy",
|
|
73
|
+
sender_address=AddressInfo(
|
|
74
|
+
name="Sender Co.",
|
|
75
|
+
line1="Marszalkowska 1",
|
|
76
|
+
city="Warsaw",
|
|
77
|
+
postal_code="00-001",
|
|
78
|
+
country_code="PL",
|
|
79
|
+
),
|
|
80
|
+
receiver_address=AddressInfo(
|
|
81
|
+
name="Jan Kowalski",
|
|
82
|
+
line1="Dluga 10",
|
|
83
|
+
city="Gdansk",
|
|
84
|
+
postal_code="80-001",
|
|
85
|
+
country_code="PL",
|
|
86
|
+
),
|
|
87
|
+
parcels=[ParcelInfo(weight_kg=Decimal("2.5"))],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
print(created.shipment.status)
|
|
91
|
+
print(created.shipment.external_id)
|
|
92
|
+
print(created.shipment.tracking_number)
|
|
93
|
+
|
|
94
|
+
label = await flow.create_label(created.shipment)
|
|
95
|
+
print(label.label.get("url"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
anyio.run(main)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Provider configuration
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
flow = ShipmentFlow(
|
|
105
|
+
repository=repo,
|
|
106
|
+
config={
|
|
107
|
+
"mycarrier": {
|
|
108
|
+
"api_key": "sk_live_...",
|
|
109
|
+
"sandbox": True,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Providers access settings with `self.get_setting("api_key")`.
|
|
@@ -9,7 +9,7 @@ communication, and plugin discovery.
|
|
|
9
9
|
|
|
10
10
|
```{admonition} Alpha notice
|
|
11
11
|
:class: warning
|
|
12
|
-
This project is at version **0.1.
|
|
12
|
+
This project is at version **0.1.1**. The public API may change between minor
|
|
13
13
|
releases until 1.0 is reached. Pin your dependency accordingly.
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -17,9 +17,9 @@ releases until 1.0 is reached. Pin your dependency accordingly.
|
|
|
17
17
|
|
|
18
18
|
- **Provider plugin system** — register carriers via entry points or manually; auto-discovered at first use.
|
|
19
19
|
- **Shipment domain types** — strict TypedDicts (`AddressInfo`, `ParcelInfo`, `LabelInfo`, and more).
|
|
20
|
-
- **
|
|
20
|
+
- **Explicit transition rules** — `NEW` through `DELIVERED`, plus `CANCELLED`, `FAILED`, and `RETURNED`, with core-owned status transitions.
|
|
21
21
|
- **ShipmentFlow orchestrator** — async workflow for creating shipments, fetching labels, handling callbacks, polling status, and cancelling.
|
|
22
|
-
- **Runtime protocols** — `
|
|
22
|
+
- **Runtime protocols** — `Shipment` and `ShipmentRepository` are `@runtime_checkable`; bring your own models and persistence.
|
|
23
23
|
- **Built-in DummyProvider** — deterministic reference provider for testing and local development.
|
|
24
24
|
- **Async-first** — the entire runtime is async, powered by [anyio](https://anyio.readthedocs.io/).
|
|
25
25
|
|
|
@@ -30,9 +30,9 @@ native models, views/routes, and repository implementations:
|
|
|
30
30
|
|
|
31
31
|
| Package | Framework |
|
|
32
32
|
|---|---|
|
|
33
|
-
| [django-sendparcel](https://github.com/sendparcel/django-sendparcel) | Django |
|
|
34
|
-
| [fastapi-sendparcel](https://github.com/sendparcel/fastapi-sendparcel) | FastAPI |
|
|
35
|
-
| [litestar-sendparcel](https://github.com/sendparcel/litestar-sendparcel) | Litestar |
|
|
33
|
+
| [django-sendparcel](https://github.com/python-sendparcel/django-sendparcel) | Django |
|
|
34
|
+
| [fastapi-sendparcel](https://github.com/python-sendparcel/fastapi-sendparcel) | FastAPI |
|
|
35
|
+
| [litestar-sendparcel](https://github.com/python-sendparcel/litestar-sendparcel) | Litestar |
|
|
36
36
|
|
|
37
37
|
Install adapters via extras: `pip install python-sendparcel[django]`,
|
|
38
38
|
`[fastapi]`, `[litestar]`, `[frameworks]`, or `[all]`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Installation matrix
|
|
2
|
+
|
|
3
|
+
## Compatibility policy
|
|
4
|
+
|
|
5
|
+
- Ecosystem packages follow semantic versioning.
|
|
6
|
+
- `python-sendparcel` is the compatibility anchor.
|
|
7
|
+
- Provider plugins and adapters should depend on `python-sendparcel>=0.1.1`.
|
|
8
|
+
- All ecosystem packages are async-first and use `anyio`.
|
|
9
|
+
|
|
10
|
+
## Current matrix
|
|
11
|
+
|
|
12
|
+
| Package | Version | Requires |
|
|
13
|
+
|---|---:|---|
|
|
14
|
+
| python-sendparcel | 0.1.1 | Python 3.12+, anyio>=4.0 |
|
|
15
|
+
| python-sendparcel-inpost | 0.1.1 | python-sendparcel>=0.1.1, httpx>=0.27.0, anyio>=4.0 |
|
|
16
|
+
| python-sendparcel-dpdpl | 0.1.1 | python-sendparcel>=0.1.1, httpx>=0.27.0, anyio>=4.0 |
|
|
17
|
+
| django-sendparcel | 0.1.0 | python-sendparcel>=0.1.1, Django>=5.2, anyio>=4.0 |
|
|
18
|
+
| fastapi-sendparcel | 0.1.0 | python-sendparcel>=0.1.1, FastAPI>=0.115, anyio>=4.0 |
|
|
19
|
+
| litestar-sendparcel | 0.1.0 | python-sendparcel>=0.1.1, Litestar>=2.0, anyio>=4.0 |
|
|
20
|
+
|
|
21
|
+
## Recommended bundles
|
|
22
|
+
|
|
23
|
+
- `python-sendparcel` already includes the built-in `dummy` provider.
|
|
24
|
+
- `python-sendparcel[inpost]` for the InPost ShipX provider.
|
|
25
|
+
- `python-sendparcel[dpdpl]` for the DPD Poland provider.
|
|
26
|
+
- `python-sendparcel[providers]` for bundled provider extras.
|
|
27
|
+
- `python-sendparcel[frameworks]` for all framework adapters.
|
|
28
|
+
- `python-sendparcel[all]` for core + all framework adapters.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Make Order Optional — Implementation Plan
|
|
2
|
+
|
|
3
|
+
**Goal:** Decouple `Order` from the `Shipment` protocol and flow, making it an optional convenience.
|
|
4
|
+
|
|
5
|
+
**Architecture:** Remove `order: Order` from `Shipment` protocol. `ShipmentFlow.create_shipment()` takes explicit address/parcel params. Add `create_shipment_from_order()` convenience. `BaseProvider.create_shipment()` receives address/parcel data as explicit params.
|
|
6
|
+
|
|
7
|
+
**Tech Stack:** Python 3.12+, asyncio, runtime_checkable protocols, TypedDict
|
|
8
|
+
|
|
9
|
+
## Tasks
|
|
10
|
+
|
|
11
|
+
1. Core protocols — Remove order from Shipment
|
|
12
|
+
2. Core flow — New create_shipment + create_shipment_from_order
|
|
13
|
+
3. Core provider — Explicit params on BaseProvider.create_shipment
|
|
14
|
+
4. Core tests — Update all fixtures and flow tests
|
|
15
|
+
5. DPD provider — Read from params not self.shipment.order
|
|
16
|
+
6. InPost provider — Read from params not self.shipment.order
|
|
17
|
+
7. CLI package — Remove order from CLIShipment
|
|
18
|
+
8. Django wrapper — Update adapter, keep OrderModelMixin
|
|
19
|
+
9. FastAPI wrapper — Support both order-based and direct creation
|
|
20
|
+
10. Litestar wrapper — Same as FastAPI changes
|
|
21
|
+
11. Documentation updates
|