python-sendparcel-inpost 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 (30) hide show
  1. python_sendparcel_inpost-0.1.1/.gitignore +34 -0
  2. python_sendparcel_inpost-0.1.1/CHANGELOG.md +23 -0
  3. python_sendparcel_inpost-0.1.1/CONTRIBUTING.md +73 -0
  4. python_sendparcel_inpost-0.1.1/PKG-INFO +371 -0
  5. python_sendparcel_inpost-0.1.1/README.md +343 -0
  6. python_sendparcel_inpost-0.1.1/docs/api.md +74 -0
  7. python_sendparcel_inpost-0.1.1/docs/configuration.md +235 -0
  8. python_sendparcel_inpost-0.1.1/docs/index.md +13 -0
  9. python_sendparcel_inpost-0.1.1/docs/quickstart.md +167 -0
  10. python_sendparcel_inpost-0.1.1/pyproject.toml +79 -0
  11. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/__init__.py +14 -0
  12. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/client.py +177 -0
  13. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/enums.py +18 -0
  14. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/exceptions.py +45 -0
  15. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/providers/__init__.py +6 -0
  16. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/providers/courier.py +305 -0
  17. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/providers/locker.py +320 -0
  18. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/status_mapping.py +46 -0
  19. python_sendparcel_inpost-0.1.1/src/sendparcel_inpost/types.py +72 -0
  20. python_sendparcel_inpost-0.1.1/tests/__init__.py +0 -0
  21. python_sendparcel_inpost-0.1.1/tests/conftest.py +17 -0
  22. python_sendparcel_inpost-0.1.1/tests/test_client.py +233 -0
  23. python_sendparcel_inpost-0.1.1/tests/test_config_schema.py +44 -0
  24. python_sendparcel_inpost-0.1.1/tests/test_courier_provider.py +212 -0
  25. python_sendparcel_inpost-0.1.1/tests/test_entry_points.py +33 -0
  26. python_sendparcel_inpost-0.1.1/tests/test_enums.py +25 -0
  27. python_sendparcel_inpost-0.1.1/tests/test_exceptions.py +59 -0
  28. python_sendparcel_inpost-0.1.1/tests/test_locker_provider.py +317 -0
  29. python_sendparcel_inpost-0.1.1/tests/test_status_mapping.py +57 -0
  30. python_sendparcel_inpost-0.1.1/tests/test_types.py +94 -0
@@ -0,0 +1,34 @@
1
+ # Python bytecode
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Virtual environments
7
+ .venv/
8
+ venv/
9
+ env/
10
+
11
+ # Tool caches
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .mypy_cache/
15
+ .ty/
16
+ .coverage
17
+ coverage.xml
18
+ htmlcov/
19
+
20
+ # Build artifacts
21
+ build/
22
+ dist/
23
+ *.egg-info/
24
+
25
+ # IDE
26
+ .vscode/
27
+ .idea/
28
+ .sisyphus/
29
+
30
+ # OS
31
+ .DS_Store
32
+
33
+ # uv
34
+ uv.lock
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.1.0] - 2026-02-16
9
+
10
+ ### Added
11
+
12
+ - InPost ShipX provider for python-sendparcel
13
+ - `InPostLockerProvider` for Paczkomat locker deliveries
14
+ - `InPostCourierProvider` for door-to-door courier deliveries
15
+ - `ShipXClient` standalone async HTTP client for the ShipX API
16
+ - ShipX exception hierarchy (`ShipXAPIError`, `ShipXAuthenticationError`, `ShipXValidationError`)
17
+ - ShipX-specific enums (`ShipXService`, `ShipXParcelTemplate`)
18
+ - ShipX-specific TypedDicts (`ShipXAddress`, `ShipXPeer`, `ShipXParcel`, `ShipXShipmentPayload`)
19
+ - Status mapping from 24 ShipX statuses to 8 sendparcel statuses
20
+ - Webhook verification by InPost source IP range (`91.216.25.0/24`)
21
+ - Address conversion with legacy name-splitting fallback
22
+ - Entry-point registration for auto-discovery
23
+ - Full test suite (93 tests)
@@ -0,0 +1,73 @@
1
+ # Contributing to python-sendparcel-inpost
2
+
3
+ Thank you for considering a contribution to `python-sendparcel-inpost` — the
4
+ InPost ShipX provider for the sendparcel ecosystem.
5
+
6
+ ## Prerequisites
7
+
8
+ - Python 3.12 or later
9
+ - [uv](https://docs.astral.sh/uv/) package manager
10
+
11
+ ## Development setup
12
+
13
+ 1. Clone the repository and navigate to the `python-sendparcel-inpost` directory.
14
+ 2. Install the project with dev dependencies:
15
+
16
+ ```bash
17
+ uv sync --extra dev
18
+ ```
19
+
20
+ ## Running tests
21
+
22
+ Tests use **pytest** with **pytest-asyncio** (`asyncio_mode = "auto"`) and
23
+ **respx** for HTTP mocking.
24
+
25
+ ```bash
26
+ uv run pytest tests/ -q
27
+ ```
28
+
29
+ Always run tests through `uv run` so the correct virtualenv is used.
30
+
31
+ ## Linting and formatting
32
+
33
+ The project uses **ruff** for both linting and formatting:
34
+
35
+ ```bash
36
+ uv run ruff check src tests
37
+ uv run ruff format --check src tests
38
+ ```
39
+
40
+ ## Code style
41
+
42
+ - All code, comments, docstrings, and messages **must be in English**.
43
+ - Keep APIs **async-first**.
44
+ - Use `anyio` for async primitives and async/sync bridging points.
45
+ - Imports belong at the **top of the file** (PEP 8). Inline imports are
46
+ only acceptable to break a verified circular import, with a comment
47
+ explaining the reason.
48
+ - Follow the ruff rule set configured in `pyproject.toml`
49
+ (`E`, `W`, `F`, `I`, `N`, `UP`, `B`, `A`, `SIM`, `RUF`).
50
+
51
+ ## Pull request process
52
+
53
+ 1. Fork the repository and create a feature branch from `main`.
54
+ 2. Make your changes in small, focused commits.
55
+ 3. Ensure all quality checks pass (tests, linting, formatting).
56
+ 4. Open a pull request against `main` with a clear description of your
57
+ changes.
58
+
59
+ ## Commit messages
60
+
61
+ - Use the **imperative mood** ("Add feature", not "Added feature").
62
+ - Keep the subject line concise (72 characters or fewer).
63
+ - Reference related issues when applicable (e.g., `Fix #42`).
64
+
65
+ ## Ecosystem rules
66
+
67
+ - Keep APIs async-first.
68
+ - Use `anyio` for async primitives and async/sync bridging points.
69
+ - Preserve plugin compatibility with `python-sendparcel` core contracts —
70
+ this provider must conform to the interfaces defined in the core package.
71
+ - Test against the editable local core (`python-sendparcel`) to catch
72
+ breaking changes early.
73
+ - Use `httpx` for HTTP communication, `respx` for HTTP mocking in tests.
@@ -0,0 +1,371 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-sendparcel-inpost
3
+ Version: 0.1.1
4
+ Summary: InPost ShipX provider for python-sendparcel.
5
+ Author-email: Dominik Kozaczko <dominik@kozaczko.info>
6
+ License: MIT
7
+ Keywords: inpost,parcel,sendparcel,shipping,shipx
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Natural Language :: English
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: anyio>=4.0
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: python-sendparcel>=0.1.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: respx>=0.22.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.9.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # python-sendparcel-inpost
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/python-sendparcel-inpost.svg)](https://pypi.org/project/python-sendparcel-inpost/)
32
+ [![Python Version](https://img.shields.io/pypi/pyversions/python-sendparcel-inpost.svg)](https://pypi.org/project/python-sendparcel-inpost/)
33
+ [![License](https://img.shields.io/pypi/l/python-sendparcel-inpost.svg)](https://github.com/python-sendparcel/python-sendparcel-inpost/blob/main/LICENSE)
34
+
35
+ InPost ShipX API provider for the [python-sendparcel](https://github.com/python-sendparcel/python-sendparcel) shipping ecosystem.
36
+
37
+ > **Alpha (0.1.0)** — API may change between minor releases. Pin your dependency if you use it in production.
38
+
39
+ ## Features
40
+
41
+ - **Two providers** — `InPostLockerProvider` (Paczkomat locker) and `InPostCourierProvider` (door-to-door courier) as separate `BaseProvider` subclasses.
42
+ - **Standalone ShipX client** — `ShipXClient` async HTTP wrapper usable independently of the sendparcel framework.
43
+ - **Auto-discovery** — both providers register via the `sendparcel.providers` entry-point group; no manual registration needed.
44
+ - **Status mapping** — 24 ShipX statuses mapped to 8 sendparcel lifecycle states.
45
+ - **Webhook support** — callback verification by InPost source IP range (`91.216.25.0/24`).
46
+ - **Address conversion** — automatic conversion between sendparcel `AddressInfo` and ShipX peer format, with legacy name-splitting fallback.
47
+ - **Structured error handling** — `ShipXAPIError` hierarchy inheriting from core `CommunicationError` with status codes and validation details.
48
+ - **Async-first** — fully asynchronous with `httpx` and `anyio`.
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ uv add python-sendparcel-inpost
54
+ ```
55
+
56
+ Or with pip:
57
+
58
+ ```bash
59
+ pip install python-sendparcel-inpost
60
+ ```
61
+
62
+ Both providers are auto-discovered via the `sendparcel.providers` entry-point group — no manual registration needed.
63
+
64
+ ## Quick Start
65
+
66
+ ### Using providers through sendparcel
67
+
68
+ The providers integrate with the `sendparcel` flow automatically:
69
+
70
+ ```python
71
+ from sendparcel.registry import PluginRegistry
72
+
73
+ # Providers are discovered via entry points
74
+ registry = PluginRegistry()
75
+ choices = registry.get_choices()
76
+ # [('inpost_locker', 'InPost Paczkomat'), ('inpost_courier', 'InPost Kurier'), ...]
77
+ ```
78
+
79
+ ### Creating a locker shipment
80
+
81
+ ```python
82
+ provider = InPostLockerProvider(shipment=shipment, config={
83
+ "token": "your-shipx-api-token",
84
+ "organization_id": 12345,
85
+ "sandbox": True, # use sandbox for testing
86
+ })
87
+
88
+ result = await provider.create_shipment(
89
+ target_point="KRA010", # required: locker machine ID
90
+ parcel_template="small", # optional: "small", "medium", "large"
91
+ sending_method="dispatch_order", # optional
92
+ )
93
+ # result["external_id"] = "123456789"
94
+ # result["tracking_number"] = "6100..."
95
+ ```
96
+
97
+ ### Creating a courier shipment
98
+
99
+ ```python
100
+ provider = InPostCourierProvider(shipment=shipment, config={
101
+ "token": "your-shipx-api-token",
102
+ "organization_id": 12345,
103
+ "sandbox": True,
104
+ })
105
+
106
+ result = await provider.create_shipment()
107
+ # Parcels are passed as explicit parameters to create_shipment()
108
+ # Dimensions are converted from cm to mm automatically
109
+ ```
110
+
111
+ ### Using ShipXClient standalone
112
+
113
+ The HTTP client can be used independently of the sendparcel framework:
114
+
115
+ ```python
116
+ from sendparcel_inpost import ShipXClient
117
+
118
+ async with ShipXClient(
119
+ token="your-token",
120
+ organization_id=12345,
121
+ sandbox=True,
122
+ ) as client:
123
+ # Create shipment
124
+ result = await client.create_shipment(payload={
125
+ "receiver": {
126
+ "first_name": "Jan",
127
+ "last_name": "Kowalski",
128
+ "phone": "500100200",
129
+ "email": "jan@example.com",
130
+ },
131
+ "parcels": [{"template": "small"}],
132
+ "service": "inpost_locker_standard",
133
+ "custom_attributes": {
134
+ "target_point": "KRA010",
135
+ "sending_method": "dispatch_order",
136
+ },
137
+ })
138
+
139
+ # Get shipment details
140
+ shipment = await client.get_shipment(result["id"])
141
+
142
+ # Download label
143
+ label_pdf = await client.get_label(result["id"])
144
+
145
+ # Track (public, no auth required)
146
+ tracking = await client.get_tracking("6100123456789")
147
+
148
+ # Cancel (only for created/offers_prepared/offer_selected statuses)
149
+ await client.cancel_shipment(result["id"])
150
+ ```
151
+
152
+ ## Configuration
153
+
154
+ Provider configuration is passed as a dict either through the `config` constructor parameter or via your framework adapter's settings:
155
+
156
+ | Key | Type | Default | Description |
157
+ |---|---|---|---|
158
+ | `token` | `str` | *(required)* | ShipX API bearer token |
159
+ | `organization_id` | `int` | *(required)* | ShipX organization ID |
160
+ | `sandbox` | `bool` | `False` | Use sandbox API endpoint |
161
+ | `base_url` | `str` | `None` | Override API base URL (takes precedence over `sandbox`) |
162
+ | `timeout` | `float` | `30.0` | HTTP request timeout in seconds |
163
+
164
+ ### API endpoints
165
+
166
+ | Environment | Base URL |
167
+ |---|---|
168
+ | Production | `https://api-shipx-pl.easypack24.net` |
169
+ | Sandbox | `https://sandbox-api-shipx-pl.easypack24.net` |
170
+
171
+ ### Integration with framework adapters
172
+
173
+ Pass InPost configuration through your adapter's provider settings:
174
+
175
+ ```python
176
+ # Django settings.py
177
+ SENDPARCEL_PROVIDER_SETTINGS = {
178
+ "inpost_locker": {
179
+ "token": "your-shipx-token",
180
+ "organization_id": 12345,
181
+ "sandbox": True,
182
+ },
183
+ "inpost_courier": {
184
+ "token": "your-shipx-token",
185
+ "organization_id": 12345,
186
+ "sandbox": True,
187
+ },
188
+ }
189
+
190
+ # FastAPI / Litestar
191
+ config = SendparcelConfig(
192
+ default_provider="inpost_locker",
193
+ providers={
194
+ "inpost_locker": {
195
+ "token": "your-shipx-token",
196
+ "organization_id": 12345,
197
+ "sandbox": True,
198
+ },
199
+ },
200
+ )
201
+ ```
202
+
203
+ ## Providers
204
+
205
+ ### InPostLockerProvider
206
+
207
+ Paczkomat locker delivery. The receiver picks up the parcel from a self-service locker machine.
208
+
209
+ - **Slug**: `inpost_locker`
210
+ - **Service**: `inpost_locker_standard`
211
+ - **Confirmation method**: PUSH (webhook-based)
212
+ - **Supported countries**: PL
213
+
214
+ **`create_shipment` parameters:**
215
+
216
+ | Parameter | Required | Description |
217
+ |---|---|---|
218
+ | `target_point` | yes | Locker machine ID (e.g. `"KRA010"`) |
219
+ | `parcel_template` | no | Size: `"small"`, `"medium"`, or `"large"`. Auto-detected from parcel dimensions if omitted. |
220
+ | `sending_method` | no | Default: `"dispatch_order"` |
221
+
222
+ Parcel template auto-detection logic (based on height):
223
+ - height > 19 cm: `large`
224
+ - height > 8 cm: `medium`
225
+ - otherwise: `small`
226
+
227
+ ### InPostCourierProvider
228
+
229
+ Door-to-door courier delivery.
230
+
231
+ - **Slug**: `inpost_courier`
232
+ - **Service**: `inpost_courier_standard`
233
+ - **Confirmation method**: PUSH (webhook-based)
234
+ - **Supported countries**: PL
235
+
236
+ Parcel dimensions are received as explicit `parcels` parameter and converted from cm to mm for the ShipX API. If no parcels are provided, a default 1 kg parcel is used.
237
+
238
+ ### Common provider methods
239
+
240
+ Both providers implement the full `BaseProvider` interface:
241
+
242
+ | Method | Purpose |
243
+ |---|---|
244
+ | `create_shipment(**kwargs)` | Create a shipment in ShipX |
245
+ | `create_label(**kwargs)` | Download shipping label (PDF by default) |
246
+ | `fetch_shipment_status(**kwargs)` | Poll ShipX API for current status |
247
+ | `cancel_shipment(**kwargs)` | Cancel the shipment (returns `True`/`False`) |
248
+ | `verify_callback(data, headers, **kwargs)` | Verify webhook source IP is in InPost's `91.216.25.0/24` range |
249
+ | `handle_callback(data, headers, **kwargs)` | Process webhook payload, map ShipX status to sendparcel status |
250
+
251
+ ## Address Handling
252
+
253
+ The providers accept `sendparcel.types.AddressInfo` and convert it to the ShipX peer format. Two addressing styles are supported:
254
+
255
+ **InPost-style** (preferred):
256
+ ```python
257
+ address: AddressInfo = {
258
+ "first_name": "Jan",
259
+ "last_name": "Kowalski",
260
+ "street": "Krakowska",
261
+ "building_number": "10",
262
+ "flat_number": "5",
263
+ "city": "Krakow",
264
+ "postal_code": "30-001",
265
+ "country_code": "PL",
266
+ "phone": "500100200",
267
+ "email": "jan@example.com",
268
+ }
269
+ ```
270
+
271
+ **Legacy style** (auto-split):
272
+ ```python
273
+ address: AddressInfo = {
274
+ "name": "Jan Kowalski", # split on first space -> first_name + last_name
275
+ "line1": "Krakowska 10/5", # used as street fallback
276
+ "city": "Krakow",
277
+ "postal_code": "30-001",
278
+ "phone": "500100200",
279
+ }
280
+ ```
281
+
282
+ ## Status Mapping
283
+
284
+ ShipX uses 24 internal statuses. These are mapped to 8 sendparcel statuses:
285
+
286
+ | sendparcel status | ShipX statuses |
287
+ |---|---|
288
+ | `CREATED` | `created`, `offers_prepared`, `offer_selected` |
289
+ | `LABEL_READY` | `confirmed` |
290
+ | `IN_TRANSIT` | `dispatched_by_sender`, `collected_from_sender`, `taken_by_courier`, `adopted_at_source_branch`, `sent_from_source_branch`, `adopted_at_sorting_center` |
291
+ | `OUT_FOR_DELIVERY` | `out_for_delivery`, `ready_to_pickup`, `pickup_reminder_sent`, `avizo`, `stack_in_box_machine`, `stack_in_customer_service_point` |
292
+ | `DELIVERED` | `delivered` |
293
+ | `CANCELLED` | `canceled` |
294
+ | `RETURNED` | `returned_to_sender` |
295
+ | `FAILED` | `rejected_by_receiver`, `undelivered`, `oversized`, `missing`, `claim_created` |
296
+
297
+ ## Error Handling
298
+
299
+ All ShipX API errors inherit from `sendparcel.exceptions.CommunicationError`:
300
+
301
+ ```python
302
+ from sendparcel_inpost.exceptions import (
303
+ ShipXAPIError, # base: any non-2xx response
304
+ ShipXAuthenticationError, # 401 Unauthorized
305
+ ShipXValidationError, # 422 Unprocessable Entity
306
+ )
307
+
308
+ try:
309
+ result = await client.create_shipment(payload=payload)
310
+ except ShipXAuthenticationError:
311
+ # Invalid or expired token
312
+ ...
313
+ except ShipXValidationError as exc:
314
+ # Payload validation failed
315
+ print(exc.detail) # human-readable message
316
+ print(exc.errors) # list of field-level error dicts from ShipX
317
+ except ShipXAPIError as exc:
318
+ # Other API error
319
+ print(exc.status_code, exc.detail)
320
+ ```
321
+
322
+ ## Webhooks
323
+
324
+ Both providers support InPost webhook callbacks for real-time status updates.
325
+
326
+ **Verification**: Webhook source IP must be in the `91.216.25.0/24` range. The IP is read from the `X-Forwarded-For` header (first entry). Invalid or missing IPs raise `sendparcel.exceptions.InvalidCallbackError`.
327
+
328
+ **Payload format** (expected from InPost):
329
+ ```json
330
+ {
331
+ "payload": {
332
+ "shipment_id": 123456,
333
+ "status": "delivered"
334
+ }
335
+ }
336
+ ```
337
+
338
+ ## Supported Versions
339
+
340
+ | Dependency | Version |
341
+ |---|---|
342
+ | Python | >= 3.12 |
343
+ | python-sendparcel | >= 0.1.0 |
344
+ | httpx | >= 0.27.0 |
345
+ | anyio | >= 4.0 |
346
+
347
+ ## Running Tests
348
+
349
+ The test suite uses **pytest** with **pytest-asyncio** (`asyncio_mode = "auto"`)
350
+ and **respx** for HTTP mocking.
351
+
352
+ ```bash
353
+ # Install dev dependencies
354
+ uv sync --extra dev
355
+
356
+ # Run the full test suite
357
+ uv run pytest
358
+
359
+ # With coverage
360
+ uv run pytest --cov=sendparcel_inpost --cov-report=term-missing
361
+ ```
362
+
363
+ ## Credits
364
+
365
+ - **Author**: Dominik Kozaczko ([dominik@kozaczko.info](mailto:dominik@kozaczko.info))
366
+ - Built on top of [python-sendparcel](https://github.com/python-sendparcel/python-sendparcel) core library
367
+ - Integrates with the [InPost ShipX API](https://docs.inpost24.com/)
368
+
369
+ ## License
370
+
371
+ [MIT](https://github.com/python-sendparcel/python-sendparcel-inpost/blob/main/LICENSE)