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.
@@ -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,,
@@ -1,7 +1,7 @@
1
1
  """ShipX API async HTTP client."""
2
2
 
3
- from typing import Any
4
3
  from types import TracebackType
4
+ from typing import Any
5
5
 
6
6
  import httpx
7
7
 
@@ -19,12 +19,12 @@ from sendparcel.types import (
19
19
  LabelInfo,
20
20
  ParcelInfo,
21
21
  ShipmentCreateResult,
22
- ShipmentStatusResponse,
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 map_shipx_status
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
- ) -> None:
259
+ ) -> ShipmentUpdateResult:
260
260
  """Process InPost webhook payload."""
261
261
  payload = data.get("payload", {})
262
- shipx_status = payload.get("status", "")
263
- sendparcel_status = map_shipx_status(shipx_status)
264
- if sendparcel_status:
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
- sendparcel_status,
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
- ) -> ShipmentStatusResponse:
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
- shipx_status = response.get("status", "")
288
- sendparcel_status = map_shipx_status(shipx_status)
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
- ShipmentStatusResponse,
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 map_shipx_status
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
- ) -> None:
272
+ ) -> ShipmentUpdateResult:
273
273
  """Process InPost webhook payload.
274
274
 
275
- The actual FSM transition is handled by ShipmentFlow.
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
- sendparcel_status = map_shipx_status(shipx_status)
281
- if sendparcel_status:
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
- sendparcel_status,
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
- ) -> ShipmentStatusResponse:
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
- shipx_status = response.get("status", "")
303
- sendparcel_status = map_shipx_status(shipx_status)
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
- [![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)
@@ -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,,