python-sendparcel-inpost 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl
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_inpost-0.1.2.dist-info/METADATA +100 -0
- python_sendparcel_inpost-0.1.2.dist-info/RECORD +13 -0
- sendparcel_inpost/client.py +1 -1
- sendparcel_inpost/providers/courier.py +16 -13
- sendparcel_inpost/providers/locker.py +17 -15
- sendparcel_inpost/status_mapping.py +17 -0
- python_sendparcel_inpost-0.1.1.dist-info/METADATA +0 -371
- python_sendparcel_inpost-0.1.1.dist-info/RECORD +0 -13
- {python_sendparcel_inpost-0.1.1.dist-info → python_sendparcel_inpost-0.1.2.dist-info}/WHEEL +0 -0
- {python_sendparcel_inpost-0.1.1.dist-info → python_sendparcel_inpost-0.1.2.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-sendparcel-inpost
|
|
3
|
+
Version: 0.1.2
|
|
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.1
|
|
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
|
+
InPost ShipX provider package for the `python-sendparcel` ecosystem.
|
|
32
|
+
|
|
33
|
+
> Alpha notice: this package tracks the still-changing `python-sendparcel` core.
|
|
34
|
+
|
|
35
|
+
## What it provides
|
|
36
|
+
|
|
37
|
+
- `InPostLockerProvider` for locker shipments
|
|
38
|
+
- `InPostCourierProvider` for courier shipments
|
|
39
|
+
- `ShipXClient` for direct async ShipX API access
|
|
40
|
+
- ShipX-to-sendparcel status normalization helpers
|
|
41
|
+
|
|
42
|
+
## Contract
|
|
43
|
+
|
|
44
|
+
This package follows the cleaned core contract:
|
|
45
|
+
|
|
46
|
+
- `create_shipment(...) -> ShipmentCreateResult`
|
|
47
|
+
- `create_label(...) -> LabelInfo`
|
|
48
|
+
- `handle_callback(...) -> ShipmentUpdateResult`
|
|
49
|
+
- `fetch_shipment_status(...) -> ShipmentUpdateResult`
|
|
50
|
+
- `cancel_shipment(...) -> bool`
|
|
51
|
+
|
|
52
|
+
Providers do not mutate shipment state directly. They translate ShipX responses into normalized results that the core flow applies.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv add python-sendparcel-inpost
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
or:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install python-sendparcel-inpost
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
| Key | Type | Description |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `token` | `str` | ShipX API bearer token |
|
|
71
|
+
| `organization_id` | `int` | ShipX organization ID |
|
|
72
|
+
| `sandbox` | `bool` | Use sandbox API |
|
|
73
|
+
| `base_url` | `str` | Optional API base override |
|
|
74
|
+
| `timeout` | `float` | Request timeout in seconds |
|
|
75
|
+
|
|
76
|
+
## Status normalization
|
|
77
|
+
|
|
78
|
+
ShipX statuses are normalized to sendparcel shipment statuses.
|
|
79
|
+
|
|
80
|
+
- recognized ShipX statuses produce `{"status": ...}`
|
|
81
|
+
- tracking numbers are included when available
|
|
82
|
+
- unknown ShipX statuses do not invent fake sendparcel statuses
|
|
83
|
+
|
|
84
|
+
That means callback and polling updates can safely return only tracking data when ShipX introduces a new status the mapper does not know yet.
|
|
85
|
+
|
|
86
|
+
## Labels
|
|
87
|
+
|
|
88
|
+
Labels are returned as payloads.
|
|
89
|
+
|
|
90
|
+
- PDF labels are returned as base64 content in `LabelInfo["content_base64"]`
|
|
91
|
+
- no label URL is persisted by the core contract
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv sync --extra dev
|
|
97
|
+
uv run pytest
|
|
98
|
+
uv run ruff check src tests
|
|
99
|
+
uv run mypy src tests
|
|
100
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
sendparcel_inpost/__init__.py,sha256=GO9AZGUt7-_2hKMcAI47hu6d8xC_0HoiRhgcCAsI4ZQ,372
|
|
2
|
+
sendparcel_inpost/client.py,sha256=vKkzsaFjHZIMMl8WcavR44rEOIwIAc6xu4OtuIfYzZg,5305
|
|
3
|
+
sendparcel_inpost/enums.py,sha256=8R0BPxTc_PQdPTjYXMIFlFjNju4X6hL8Xq3EEUe5J94,384
|
|
4
|
+
sendparcel_inpost/exceptions.py,sha256=ifYu5MDT-8MlIC4PilDDcNPc-n09CNR_GimtmhVbKz4,1229
|
|
5
|
+
sendparcel_inpost/status_mapping.py,sha256=XPmp-92CUOidLY1EPeL7d0PGDEsqgRAJxK3247y5zWk,2251
|
|
6
|
+
sendparcel_inpost/types.py,sha256=Pu-OJpC6te4_gY6PLv-Ag0o2IoulYytdr9t0FGf4GIA,1426
|
|
7
|
+
sendparcel_inpost/providers/__init__.py,sha256=P1vuLdzmw6Tx8h3pZf7T1AyHJODL9fqR6OrGgd992C4,235
|
|
8
|
+
sendparcel_inpost/providers/courier.py,sha256=DH4lkq89dwlN8VFMUkTLezW2AJX5VCTPqIxuCgrd5-Y,9805
|
|
9
|
+
sendparcel_inpost/providers/locker.py,sha256=LkbP3DkV9ycz7l6GB7wm4VINdvJC26VDoggRxccDSos,10202
|
|
10
|
+
python_sendparcel_inpost-0.1.2.dist-info/METADATA,sha256=hZGaHV1zw1JwvMGbD6iHEghP4l-DeZ31_QAD3ToOFXM,3002
|
|
11
|
+
python_sendparcel_inpost-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
python_sendparcel_inpost-0.1.2.dist-info/entry_points.txt,sha256=XuQdmE0LIuIc3TvGPWnYjoB14zXCEof6Q8h1tMp2arE,170
|
|
13
|
+
python_sendparcel_inpost-0.1.2.dist-info/RECORD,,
|
sendparcel_inpost/client.py
CHANGED
|
@@ -19,12 +19,12 @@ from sendparcel.types import (
|
|
|
19
19
|
LabelInfo,
|
|
20
20
|
ParcelInfo,
|
|
21
21
|
ShipmentCreateResult,
|
|
22
|
-
|
|
22
|
+
ShipmentUpdateResult,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
from sendparcel_inpost.client import ShipXClient
|
|
26
26
|
from sendparcel_inpost.exceptions import ShipXAPIError
|
|
27
|
-
from sendparcel_inpost.status_mapping import
|
|
27
|
+
from sendparcel_inpost.status_mapping import build_shipment_update
|
|
28
28
|
from sendparcel_inpost.types import ShipXAddress, ShipXPeer
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
@@ -256,23 +256,28 @@ class InPostCourierProvider(
|
|
|
256
256
|
data: dict[str, Any],
|
|
257
257
|
headers: dict[str, Any],
|
|
258
258
|
**kwargs: Any,
|
|
259
|
-
) ->
|
|
259
|
+
) -> ShipmentUpdateResult:
|
|
260
260
|
"""Process InPost webhook payload."""
|
|
261
261
|
payload = data.get("payload", {})
|
|
262
|
-
shipx_status = payload.get("status", "")
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
shipx_status = str(payload.get("status", ""))
|
|
263
|
+
tracking_number = str(payload.get("tracking_number", ""))
|
|
264
|
+
update = build_shipment_update(
|
|
265
|
+
shipx_status,
|
|
266
|
+
tracking_number=tracking_number,
|
|
267
|
+
)
|
|
268
|
+
if update:
|
|
265
269
|
logger.info(
|
|
266
270
|
"InPost webhook: %s -> %s (shipment %s)",
|
|
267
271
|
shipx_status,
|
|
268
|
-
|
|
272
|
+
update,
|
|
269
273
|
payload.get("shipment_id"),
|
|
270
274
|
)
|
|
275
|
+
return update
|
|
271
276
|
|
|
272
277
|
async def fetch_shipment_status(
|
|
273
278
|
self,
|
|
274
279
|
**kwargs: Any,
|
|
275
|
-
) ->
|
|
280
|
+
) -> ShipmentUpdateResult:
|
|
276
281
|
"""Fetch current status from ShipX API."""
|
|
277
282
|
shipment_id = int(self.shipment.external_id)
|
|
278
283
|
|
|
@@ -284,11 +289,9 @@ class InPostCourierProvider(
|
|
|
284
289
|
finally:
|
|
285
290
|
await client.close()
|
|
286
291
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return ShipmentStatusResponse(
|
|
291
|
-
status=sendparcel_status.value if sendparcel_status else None,
|
|
292
|
+
return build_shipment_update(
|
|
293
|
+
str(response.get("status", "")),
|
|
294
|
+
tracking_number=str(response.get("tracking_number", "")),
|
|
292
295
|
)
|
|
293
296
|
|
|
294
297
|
async def cancel_shipment(self, **kwargs: Any) -> bool:
|
|
@@ -19,12 +19,12 @@ from sendparcel.types import (
|
|
|
19
19
|
LabelInfo,
|
|
20
20
|
ParcelInfo,
|
|
21
21
|
ShipmentCreateResult,
|
|
22
|
-
|
|
22
|
+
ShipmentUpdateResult,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
from sendparcel_inpost.client import ShipXClient
|
|
26
26
|
from sendparcel_inpost.exceptions import ShipXAPIError
|
|
27
|
-
from sendparcel_inpost.status_mapping import
|
|
27
|
+
from sendparcel_inpost.status_mapping import build_shipment_update
|
|
28
28
|
from sendparcel_inpost.types import ShipXAddress, ShipXPeer
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
@@ -269,27 +269,31 @@ class InPostLockerProvider(
|
|
|
269
269
|
data: dict[str, Any],
|
|
270
270
|
headers: dict[str, Any],
|
|
271
271
|
**kwargs: Any,
|
|
272
|
-
) ->
|
|
272
|
+
) -> ShipmentUpdateResult:
|
|
273
273
|
"""Process InPost webhook payload.
|
|
274
274
|
|
|
275
|
-
The
|
|
276
|
-
This method extracts and normalizes the status.
|
|
275
|
+
The core flow applies transitions. Providers only normalize payloads.
|
|
277
276
|
"""
|
|
278
277
|
payload = data.get("payload", {})
|
|
279
|
-
shipx_status = payload.get("status", "")
|
|
280
|
-
|
|
281
|
-
|
|
278
|
+
shipx_status = str(payload.get("status", ""))
|
|
279
|
+
tracking_number = str(payload.get("tracking_number", ""))
|
|
280
|
+
update = build_shipment_update(
|
|
281
|
+
shipx_status,
|
|
282
|
+
tracking_number=tracking_number,
|
|
283
|
+
)
|
|
284
|
+
if update:
|
|
282
285
|
logger.info(
|
|
283
286
|
"InPost webhook: %s -> %s (shipment %s)",
|
|
284
287
|
shipx_status,
|
|
285
|
-
|
|
288
|
+
update,
|
|
286
289
|
payload.get("shipment_id"),
|
|
287
290
|
)
|
|
291
|
+
return update
|
|
288
292
|
|
|
289
293
|
async def fetch_shipment_status(
|
|
290
294
|
self,
|
|
291
295
|
**kwargs: Any,
|
|
292
|
-
) ->
|
|
296
|
+
) -> ShipmentUpdateResult:
|
|
293
297
|
"""Fetch current status from ShipX API."""
|
|
294
298
|
shipment_id = int(self.shipment.external_id)
|
|
295
299
|
|
|
@@ -299,11 +303,9 @@ class InPostLockerProvider(
|
|
|
299
303
|
finally:
|
|
300
304
|
await client.close()
|
|
301
305
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return ShipmentStatusResponse(
|
|
306
|
-
status=sendparcel_status.value if sendparcel_status else None,
|
|
306
|
+
return build_shipment_update(
|
|
307
|
+
str(response.get("status", "")),
|
|
308
|
+
tracking_number=str(response.get("tracking_number", "")),
|
|
307
309
|
)
|
|
308
310
|
|
|
309
311
|
async def cancel_shipment(self, **kwargs: Any) -> bool:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""ShipX status to sendparcel status mapping."""
|
|
2
2
|
|
|
3
3
|
from sendparcel.enums import ShipmentStatus
|
|
4
|
+
from sendparcel.types import ShipmentUpdateResult
|
|
4
5
|
|
|
5
6
|
SHIPX_TO_SENDPARCEL_STATUS: dict[str, ShipmentStatus] = {
|
|
6
7
|
# CREATED
|
|
@@ -44,3 +45,19 @@ def map_shipx_status(shipx_status: str) -> ShipmentStatus | None:
|
|
|
44
45
|
Returns None if the status is not recognized.
|
|
45
46
|
"""
|
|
46
47
|
return SHIPX_TO_SENDPARCEL_STATUS.get(shipx_status)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_shipment_update(
|
|
51
|
+
shipx_status: str,
|
|
52
|
+
*,
|
|
53
|
+
tracking_number: str = "",
|
|
54
|
+
) -> ShipmentUpdateResult:
|
|
55
|
+
"""Build a normalized sendparcel update from ShipX data."""
|
|
56
|
+
|
|
57
|
+
update: ShipmentUpdateResult = {}
|
|
58
|
+
mapped_status = map_shipx_status(shipx_status)
|
|
59
|
+
if mapped_status is not None:
|
|
60
|
+
update["status"] = mapped_status.value
|
|
61
|
+
if tracking_number:
|
|
62
|
+
update["tracking_number"] = tracking_number
|
|
63
|
+
return update
|
|
@@ -1,371 +0,0 @@
|
|
|
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
|
-
[](https://pypi.org/project/python-sendparcel-inpost/)
|
|
32
|
-
[](https://pypi.org/project/python-sendparcel-inpost/)
|
|
33
|
-
[](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)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
sendparcel_inpost/__init__.py,sha256=GO9AZGUt7-_2hKMcAI47hu6d8xC_0HoiRhgcCAsI4ZQ,372
|
|
2
|
-
sendparcel_inpost/client.py,sha256=bWAHEJCGMiC92JmFZCDvh_Cmth6ccWXwftwmwgNokEU,5305
|
|
3
|
-
sendparcel_inpost/enums.py,sha256=8R0BPxTc_PQdPTjYXMIFlFjNju4X6hL8Xq3EEUe5J94,384
|
|
4
|
-
sendparcel_inpost/exceptions.py,sha256=ifYu5MDT-8MlIC4PilDDcNPc-n09CNR_GimtmhVbKz4,1229
|
|
5
|
-
sendparcel_inpost/status_mapping.py,sha256=3NFdZcpgx8wGf1Q-YjapFAEumNICklS55Qph8fy0AAo,1755
|
|
6
|
-
sendparcel_inpost/types.py,sha256=Pu-OJpC6te4_gY6PLv-Ag0o2IoulYytdr9t0FGf4GIA,1426
|
|
7
|
-
sendparcel_inpost/providers/__init__.py,sha256=P1vuLdzmw6Tx8h3pZf7T1AyHJODL9fqR6OrGgd992C4,235
|
|
8
|
-
sendparcel_inpost/providers/courier.py,sha256=2bZo2WGjMAtafVkEDJ6Ry5WQ_NZFZxjQXMONlq4QUBg,9726
|
|
9
|
-
sendparcel_inpost/providers/locker.py,sha256=naEgv11nfEyb6nP6elL0dA5MQE9s1xUi-EEgCs3z1kE,10163
|
|
10
|
-
python_sendparcel_inpost-0.1.1.dist-info/METADATA,sha256=hS2bjEoI2IvgURDkmy35Qar3QqXI4FDxOQLhZWSNenM,12098
|
|
11
|
-
python_sendparcel_inpost-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
-
python_sendparcel_inpost-0.1.1.dist-info/entry_points.txt,sha256=XuQdmE0LIuIc3TvGPWnYjoB14zXCEof6Q8h1tMp2arE,170
|
|
13
|
-
python_sendparcel_inpost-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|