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.
Files changed (69) hide show
  1. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.gitignore +4 -0
  2. python_sendparcel-0.1.1/PKG-INFO +212 -0
  3. python_sendparcel-0.1.1/README.md +150 -0
  4. python_sendparcel-0.1.1/docs/compatibility-matrix.md +25 -0
  5. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/conf.py +1 -1
  6. python_sendparcel-0.1.1/docs/getting-started.md +115 -0
  7. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/index.md +6 -6
  8. python_sendparcel-0.1.1/docs/installation-matrix.md +28 -0
  9. python_sendparcel-0.1.1/docs/plans/2026-02-17-order-optional.md +21 -0
  10. python_sendparcel-0.1.1/docs/provider-authoring.md +121 -0
  11. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/pyproject.toml +45 -29
  12. python_sendparcel-0.1.1/src/sendparcel/__init__.py +54 -0
  13. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/enums.py +10 -0
  14. python_sendparcel-0.1.1/src/sendparcel/exceptions.py +53 -0
  15. python_sendparcel-0.1.1/src/sendparcel/flow.py +193 -0
  16. python_sendparcel-0.1.1/src/sendparcel/fsm.py +99 -0
  17. python_sendparcel-0.1.1/src/sendparcel/protocols.py +26 -0
  18. python_sendparcel-0.1.1/src/sendparcel/provider.py +92 -0
  19. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/providers/dummy.py +44 -25
  20. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/registry.py +19 -7
  21. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/types.py +47 -19
  22. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/validators.py +3 -2
  23. python_sendparcel-0.1.1/tests/conftest.py +89 -0
  24. python_sendparcel-0.1.1/tests/test_builtin_dummy_provider.py +166 -0
  25. python_sendparcel-0.1.1/tests/test_enums.py +26 -0
  26. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/tests/test_exceptions.py +9 -0
  27. python_sendparcel-0.1.1/tests/test_flow.py +380 -0
  28. python_sendparcel-0.1.1/tests/test_fsm.py +86 -0
  29. python_sendparcel-0.1.1/tests/test_integration.py +209 -0
  30. python_sendparcel-0.1.1/tests/test_protocols.py +41 -0
  31. python_sendparcel-0.1.1/tests/test_provider.py +96 -0
  32. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/tests/test_public_api.py +12 -1
  33. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/tests/test_registry.py +69 -16
  34. python_sendparcel-0.1.1/tests/test_types.py +86 -0
  35. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/tests/test_validators.py +11 -8
  36. python_sendparcel-0.1.0/PKG-INFO +0 -449
  37. python_sendparcel-0.1.0/README.md +0 -397
  38. python_sendparcel-0.1.0/docs/compatibility-matrix.md +0 -13
  39. python_sendparcel-0.1.0/docs/getting-started.md +0 -218
  40. python_sendparcel-0.1.0/docs/installation-matrix.md +0 -24
  41. python_sendparcel-0.1.0/docs/provider-authoring.md +0 -40
  42. python_sendparcel-0.1.0/src/sendparcel/__init__.py +0 -29
  43. python_sendparcel-0.1.0/src/sendparcel/exceptions.py +0 -21
  44. python_sendparcel-0.1.0/src/sendparcel/flow.py +0 -164
  45. python_sendparcel-0.1.0/src/sendparcel/fsm.py +0 -131
  46. python_sendparcel-0.1.0/src/sendparcel/protocols.py +0 -41
  47. python_sendparcel-0.1.0/src/sendparcel/provider.py +0 -63
  48. python_sendparcel-0.1.0/tests/conftest.py +0 -131
  49. python_sendparcel-0.1.0/tests/test_builtin_dummy_provider.py +0 -185
  50. python_sendparcel-0.1.0/tests/test_enums.py +0 -23
  51. python_sendparcel-0.1.0/tests/test_flow.py +0 -515
  52. python_sendparcel-0.1.0/tests/test_fsm.py +0 -384
  53. python_sendparcel-0.1.0/tests/test_integration.py +0 -289
  54. python_sendparcel-0.1.0/tests/test_protocols.py +0 -34
  55. python_sendparcel-0.1.0/tests/test_provider.py +0 -47
  56. python_sendparcel-0.1.0/tests/test_types.py +0 -97
  57. python_sendparcel-0.1.0/uv.lock +0 -1195
  58. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.github/workflows/ci.yml +0 -0
  59. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.github/workflows/ecosystem-smoke.yml +0 -0
  60. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.github/workflows/release.yml +0 -0
  61. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/.pre-commit-config.yaml +0 -0
  62. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/CHANGELOG.md +0 -0
  63. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/CONTRIBUTING.md +0 -0
  64. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/LICENSE +0 -0
  65. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/Makefile +0 -0
  66. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/release-policy.md +0 -0
  67. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/docs/requirements.txt +0 -0
  68. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/providers/__init__.py +0 -0
  69. {python_sendparcel-0.1.0 → python_sendparcel-0.1.1}/src/sendparcel/py.typed +0 -0
@@ -25,6 +25,10 @@ dist/
25
25
  # IDE
26
26
  .vscode/
27
27
  .idea/
28
+ .sisyphus/
28
29
 
29
30
  # OS
30
31
  .DS_Store
32
+
33
+ # uv
34
+ uv.lock
@@ -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` |
@@ -9,7 +9,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
9
9
  project = "python-sendparcel"
10
10
  copyright = "2025, Dominik Kozaczko"
11
11
  author = "Dominik Kozaczko"
12
- release = "0.1.0"
12
+ release = "0.1.1"
13
13
 
14
14
  extensions = [
15
15
  "sphinx.ext.autodoc",
@@ -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.0**. The public API may change between minor
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
- - **9-state finite state machine** — `NEW` through `DELIVERED`, plus `CANCELLED`, `FAILED`, and `RETURNED`, with guarded transitions.
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** — `Order`, `Shipment`, and `ShipmentRepository` are `@runtime_checkable`; bring your own models and persistence.
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