python-sendparcel 0.1.0__py3-none-any.whl → 0.2.0__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,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-sendparcel
3
+ Version: 0.2.0
4
+ Summary: Framework-agnostic parcel shipping core for Python.
5
+ Project-URL: Homepage, https://github.com/python-sendparcel/python-sendparcel
6
+ Project-URL: Repository, https://github.com/python-sendparcel/python-sendparcel
7
+ Project-URL: Changelog, https://github.com/python-sendparcel/python-sendparcel/blob/main/CHANGELOG.md
8
+ Project-URL: Issue Tracker, https://github.com/python-sendparcel/python-sendparcel/issues
9
+ Author-email: Dominik Kozaczko <dominik@kozaczko.info>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: delivery,logistics,parcel,sendparcel,shipping
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: anyio>=4.0
24
+ Requires-Dist: httpx>=0.27.0
25
+ Provides-Extra: all
26
+ Requires-Dist: django-sendparcel>=0.1.0; extra == 'all'
27
+ Requires-Dist: python-sendparcel-cli>=0.1.0; extra == 'all'
28
+ Requires-Dist: python-sendparcel-dpdpl>=0.1.0; extra == 'all'
29
+ Requires-Dist: python-sendparcel-inpost>=0.1.0; extra == 'all'
30
+ Provides-Extra: cli
31
+ Requires-Dist: python-sendparcel-cli>=0.1.0; extra == 'cli'
32
+ Provides-Extra: dev
33
+ Requires-Dist: pre-commit-hooks>=6.0.0; extra == 'dev'
34
+ Requires-Dist: pre-commit>=4.0; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
36
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
37
+ Requires-Dist: pytest>=8.0; extra == 'dev'
38
+ Requires-Dist: ruff>=0.9.0; extra == 'dev'
39
+ Requires-Dist: ty>=0.0.16; extra == 'dev'
40
+ Provides-Extra: django
41
+ Requires-Dist: django-sendparcel>=0.1.0; extra == 'django'
42
+ Provides-Extra: dpdpl
43
+ Requires-Dist: python-sendparcel-dpdpl>=0.1.0; extra == 'dpdpl'
44
+ Provides-Extra: dummy
45
+ Provides-Extra: frameworks
46
+ Requires-Dist: django-sendparcel>=0.1.0; extra == 'frameworks'
47
+ Provides-Extra: inpost
48
+ Requires-Dist: python-sendparcel-inpost>=0.1.0; extra == 'inpost'
49
+ Provides-Extra: providers
50
+ Requires-Dist: python-sendparcel-dpdpl>=0.1.0; extra == 'providers'
51
+ Requires-Dist: python-sendparcel-inpost>=0.1.0; extra == 'providers'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # python-sendparcel
55
+
56
+ Framework-agnostic parcel shipping core for Python.
57
+
58
+ > Alpha notice: `0.1.1` is still unstable. The API can change fast because the
59
+ > ecosystem is still being cleaned up.
60
+
61
+ ## What it is
62
+
63
+ - Provider-agnostic shipment orchestration with a single core flow.
64
+ - Explicit shipment metadata persistence: `id`, `status`, `provider`, `external_id`, `tracking_number`.
65
+ - Label payloads are operation results, not persisted shipment fields.
66
+ - One normalized provider update contract for both callbacks and polling.
67
+ - Runtime-checkable `Shipment` and `ShipmentRepository` protocols so adapters can use their own models.
68
+
69
+ ## Core contract
70
+
71
+ - `ShipmentFlow.create_shipment(...) -> CreateShipmentOutcome`
72
+ - `ShipmentFlow.create_label(...) -> CreateLabelOutcome`
73
+ - `ShipmentFlow.handle_callback(...) -> ShipmentUpdateOutcome`
74
+ - `ShipmentFlow.fetch_and_update_status(...) -> ShipmentUpdateOutcome`
75
+ - `ShipmentFlow.cancel_shipment(...) -> bool`
76
+
77
+ `CreateShipmentOutcome` and `CreateLabelOutcome` return label payloads when available.
78
+ The shipment object never stores label bytes or a persisted `label_url` in the core contract.
79
+
80
+ ## Quick start
81
+
82
+ ```python
83
+ from dataclasses import dataclass
84
+ from decimal import Decimal
85
+
86
+ import anyio
87
+
88
+ from sendparcel import ShipmentFlow
89
+ from sendparcel.types import AddressInfo, ParcelInfo
90
+
91
+
92
+ @dataclass
93
+ class MyShipment:
94
+ id: str
95
+ status: str = "new"
96
+ provider: str = ""
97
+ external_id: str = ""
98
+ tracking_number: str = ""
99
+
100
+
101
+ class InMemoryRepository:
102
+ def __init__(self) -> None:
103
+ self._store: dict[str, MyShipment] = {}
104
+ self._counter = 0
105
+
106
+ async def get_by_id(self, shipment_id: str) -> MyShipment:
107
+ return self._store[shipment_id]
108
+
109
+ async def create(self, **kwargs) -> MyShipment:
110
+ self._counter += 1
111
+ shipment = MyShipment(
112
+ id=str(self._counter),
113
+ status=str(kwargs.get("status", "new")),
114
+ provider=str(kwargs.get("provider", "")),
115
+ )
116
+ self._store[shipment.id] = shipment
117
+ return shipment
118
+
119
+ async def save(self, shipment: MyShipment) -> MyShipment:
120
+ self._store[shipment.id] = shipment
121
+ return shipment
122
+
123
+
124
+ async def main() -> None:
125
+ flow = ShipmentFlow(repository=InMemoryRepository())
126
+
127
+ created = await flow.create_shipment(
128
+ "dummy",
129
+ sender_address=AddressInfo(
130
+ name="Sender Co.",
131
+ line1="Marszalkowska 1",
132
+ city="Warsaw",
133
+ postal_code="00-001",
134
+ country_code="PL",
135
+ ),
136
+ receiver_address=AddressInfo(
137
+ name="Jan Kowalski",
138
+ line1="Dluga 10",
139
+ city="Gdansk",
140
+ postal_code="80-001",
141
+ country_code="PL",
142
+ ),
143
+ parcels=[ParcelInfo(weight_kg=Decimal("2.5"))],
144
+ )
145
+
146
+ print(created.shipment.status)
147
+ print(created.shipment.external_id)
148
+ print(created.shipment.tracking_number)
149
+
150
+ labelled = await flow.create_label(created.shipment)
151
+ print(labelled.label.get("url"))
152
+
153
+
154
+ anyio.run(main)
155
+ ```
156
+
157
+ ## Provider model
158
+
159
+ - `BaseProvider.create_shipment(...)` returns `ShipmentCreateResult`.
160
+ - `BaseProvider.confirmation_method` defaults to `ConfirmationMethod.NONE`.
161
+ - `LabelProvider.create_label(...)` returns `LabelInfo`.
162
+ - `PushCallbackProvider.handle_callback(...)` returns `ShipmentUpdateResult`.
163
+ - `PullStatusProvider.fetch_shipment_status(...)` returns `ShipmentUpdateResult`.
164
+ - `CancellableProvider.cancel_shipment(...)` returns `bool`.
165
+
166
+ Use `ConfirmationMethod.PUSH` only with `PushCallbackProvider` and
167
+ `ConfirmationMethod.PULL` only with `PullStatusProvider`.
168
+
169
+ The core owns shipment state transitions. Providers translate carrier responses
170
+ into normalized results.
171
+
172
+ ## Installation
173
+
174
+ ```bash
175
+ pip install python-sendparcel
176
+ ```
177
+
178
+ With `uv`:
179
+
180
+ ```bash
181
+ uv add python-sendparcel
182
+ ```
183
+
184
+ ## Extras
185
+
186
+ - `django`
187
+ - `fastapi`
188
+ - `litestar`
189
+ - `inpost`
190
+ - `dpdpl`
191
+ - `cli`
192
+ - `frameworks`
193
+ - `providers`
194
+ - `all`
195
+
196
+ ## Development
197
+
198
+ ```bash
199
+ uv sync --extra dev
200
+ uv run pytest
201
+ uv run ruff check src tests
202
+ uv run mypy src tests
203
+ ```
@@ -0,0 +1,19 @@
1
+ sendparcel/__init__.py,sha256=aHh6hwMkQVrICkswatKnt3Aru-v3kQI0PotD0M3QocQ,1612
2
+ sendparcel/batch.py,sha256=wilO2jPbwlmLowYbhfhNZV17VPX6K_Lf9aHKxWUgbcQ,9400
3
+ sendparcel/enums.py,sha256=e1UM9ka5zWJg4naMCoVeUrIy_PVPPEYHOBh6XiXaW4Q,687
4
+ sendparcel/exceptions.py,sha256=zC5_o9oQfNuOBKaiw5csaHAJmhYsZc3WVbOaoq7s1W0,1437
5
+ sendparcel/flow.py,sha256=REP0i1W2YU09bLVBZWJDUf4mkQ4An4yTZOKMPsfGXL8,11258
6
+ sendparcel/fsm.py,sha256=7Oqccbn2U56htTJLQEidNhZn_0TVtR-js_6OHkTu0zE,3182
7
+ sendparcel/logging.py,sha256=SY7bTtXyPDM-tYCXHRV99YFwK8lHDXpfgB8IN8tOrH0,4843
8
+ sendparcel/protocols.py,sha256=7YX2KTOEG5Nje6bzibmIODFiTX7tgqb6I3WYgx9uVS4,2344
9
+ sendparcel/provider.py,sha256=6f6Z3hmLNrN2t-pulVAFEPrZnNttX1kmK6ATCnqdZD4,2690
10
+ sendparcel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ sendparcel/registry.py,sha256=xLDgux5kkp2n5DHfLwOD6mx7yBfMdOyD4QHKUcxJpnw,3575
12
+ sendparcel/types.py,sha256=afPuER1CslkvcTMMogY0EVYrTjUsA36gPxVQguYtIBg,2201
13
+ sendparcel/validators.py,sha256=i_y-edT1hgS_2HeRdARlFaQXiL9sYAzWVsjYPVBqRDc,589
14
+ sendparcel/providers/__init__.py,sha256=ff82dvrWuPS-UIGF5AoX_fgpcYZaGu2k6q2yrxMZacI,192
15
+ sendparcel/providers/dummy.py,sha256=sbXDtPel3DEH_FIN9_gX2xXBLEqZ7Pto3CP_JMcsaEM,3102
16
+ python_sendparcel-0.2.0.dist-info/METADATA,sha256=fozQ4Jz1PhvcQAEnl1zHHb2DTTIXNvhJdKDGjWBPnzE,6429
17
+ python_sendparcel-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
18
+ python_sendparcel-0.2.0.dist-info/licenses/LICENSE,sha256=IZXSBOjgGvChgayLmtTnU40iE7hsrrU3WVEYKx0sywY,1075
19
+ python_sendparcel-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
sendparcel/__init__.py CHANGED
@@ -1,29 +1,63 @@
1
1
  """sendparcel core package."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
4
4
 
5
- from sendparcel.enums import ConfirmationMethod, ShipmentStatus
5
+ from sendparcel.batch import BatchCreateResult, BatchResult, ShipmentBatch
6
+ from sendparcel.enums import ConfirmationMethod, LabelFormat, ShipmentStatus
6
7
  from sendparcel.exceptions import (
7
8
  CommunicationError,
8
9
  InvalidCallbackError,
9
10
  InvalidTransitionError,
11
+ ProviderCapabilityError,
12
+ ProviderNotFoundError,
10
13
  SendParcelException,
14
+ ShipmentNotFoundError,
11
15
  )
12
16
  from sendparcel.flow import ShipmentFlow
13
- from sendparcel.provider import BaseProvider
17
+ from sendparcel.logging import configure_logging, get_logger
18
+ from sendparcel.provider import (
19
+ BaseProvider,
20
+ CancellableProvider,
21
+ LabelProvider,
22
+ PullStatusProvider,
23
+ PushCallbackProvider,
24
+ )
14
25
  from sendparcel.providers.dummy import DummyProvider
15
26
  from sendparcel.registry import registry
27
+ from sendparcel.types import (
28
+ CreateLabelOutcome,
29
+ CreateShipmentOutcome,
30
+ ShipmentUpdateOutcome,
31
+ ShipmentUpdateResult,
32
+ )
16
33
 
17
34
  __all__ = [
18
35
  "BaseProvider",
36
+ "BatchCreateResult",
37
+ "BatchResult",
38
+ "CancellableProvider",
19
39
  "CommunicationError",
20
40
  "ConfirmationMethod",
41
+ "CreateLabelOutcome",
42
+ "CreateShipmentOutcome",
21
43
  "DummyProvider",
22
44
  "InvalidCallbackError",
23
45
  "InvalidTransitionError",
46
+ "LabelFormat",
47
+ "LabelProvider",
48
+ "ProviderCapabilityError",
49
+ "ProviderNotFoundError",
50
+ "PullStatusProvider",
51
+ "PushCallbackProvider",
24
52
  "SendParcelException",
53
+ "ShipmentBatch",
25
54
  "ShipmentFlow",
55
+ "ShipmentNotFoundError",
26
56
  "ShipmentStatus",
57
+ "ShipmentUpdateOutcome",
58
+ "ShipmentUpdateResult",
27
59
  "__version__",
60
+ "configure_logging",
61
+ "get_logger",
28
62
  "registry",
29
63
  ]
sendparcel/batch.py ADDED
@@ -0,0 +1,303 @@
1
+ """Batch shipment operations for sendparcel.
2
+
3
+ Provides batch creation, status fetching, and cancellation of shipments.
4
+ Designed for efficiency when processing multiple shipments at once.
5
+
6
+ Usage::
7
+
8
+ from sendparcel.batch import ShipmentBatch
9
+
10
+ batch = ShipmentBatch(repository=repo, config=config)
11
+ results = await batch.create_shipments(
12
+ [
13
+ {
14
+ "provider_slug": "inpost-courier",
15
+ "sender_address": {...},
16
+ "receiver_address": {...},
17
+ "parcels": [...],
18
+ },
19
+ # ... more shipments
20
+ ]
21
+ )
22
+ for result in results:
23
+ if result.success:
24
+ print(f"Created {result.shipment.tracking_number}")
25
+ else:
26
+ print(f"Failed: {result.error}")
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import asyncio
32
+ from dataclasses import dataclass, field
33
+ from typing import Any
34
+
35
+ from sendparcel.flow import ShipmentFlow
36
+ from sendparcel.logging import get_logger
37
+ from sendparcel.protocols import ShipmentRepository
38
+ from sendparcel.registry import PluginRegistry
39
+
40
+ logger = get_logger(__name__)
41
+
42
+
43
+ @dataclass
44
+ class BatchResult:
45
+ """Result of a single batch operation."""
46
+
47
+ index: int
48
+ success: bool
49
+ shipment: Any | None = None
50
+ error: str | None = None
51
+ outcome: Any | None = None
52
+
53
+
54
+ @dataclass
55
+ class BatchCreateResult:
56
+ """Result of a batch create operation."""
57
+
58
+ total: int
59
+ successful: int
60
+ failed: int
61
+ results: list[BatchResult] = field(default_factory=list)
62
+
63
+ @property
64
+ def success(self) -> bool:
65
+ """True if all shipments were created successfully."""
66
+ return self.failed == 0
67
+
68
+ @property
69
+ def summary(self) -> dict[str, Any]:
70
+ """Return a summary of the batch operation."""
71
+ return {
72
+ "total": self.total,
73
+ "successful": self.successful,
74
+ "failed": self.failed,
75
+ "success_rate": (
76
+ round(self.successful / self.total * 100, 2)
77
+ if self.total > 0
78
+ else 0.0
79
+ ),
80
+ }
81
+
82
+
83
+ class ShipmentBatch:
84
+ """Batch shipment operations.
85
+
86
+ Provides efficient batch creation, status fetching, and cancellation
87
+ of shipments. All operations are atomic within the batch — if one
88
+ shipment fails, the others are still processed.
89
+
90
+ Args:
91
+ repository: Shipment repository for persisting results.
92
+ config: Provider configuration.
93
+ registry: Provider registry (defaults to module-level singleton).
94
+ max_concurrent: Maximum number of concurrent provider calls
95
+ (default: 5).
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ repository: ShipmentRepository,
101
+ config: dict[str, Any] | None = None,
102
+ registry: PluginRegistry | None = None,
103
+ max_concurrent: int = 5,
104
+ ) -> None:
105
+ self.repository = repository
106
+ self.config = config or {}
107
+ self.registry = registry or PluginRegistry()
108
+ self.max_concurrent = max_concurrent
109
+
110
+ async def create_shipments(
111
+ self,
112
+ shipments: list[dict[str, Any]],
113
+ ) -> BatchCreateResult:
114
+ """Create multiple shipments in a batch.
115
+
116
+ Each shipment dict must contain:
117
+ - provider_slug: Provider identifier
118
+ - sender_address: Sender address info
119
+ - receiver_address: Receiver address info
120
+ - parcels: List of parcel definitions
121
+ - Optional: idempotency_key, reference_id, and other kwargs
122
+
123
+ Args:
124
+ shipments: List of shipment dicts to create.
125
+
126
+ Returns:
127
+ BatchCreateResult with per-shipment results.
128
+ """
129
+ semaphore = asyncio.Semaphore(self.max_concurrent)
130
+
131
+ async def _create_one(
132
+ index: int, shipment_data: dict[str, Any]
133
+ ) -> BatchResult:
134
+ async with semaphore:
135
+ return await self._create_single_shipment(index, shipment_data)
136
+
137
+ results = await asyncio.gather(
138
+ *(_create_one(i, d) for i, d in enumerate(shipments))
139
+ )
140
+
141
+ successful = sum(1 for r in results if r.success)
142
+ failed = sum(1 for r in results if not r.success)
143
+
144
+ # Sort by original index to preserve input order.
145
+ results.sort(key=lambda r: r.index)
146
+
147
+ return BatchCreateResult(
148
+ total=len(shipments),
149
+ successful=successful,
150
+ failed=failed,
151
+ results=results,
152
+ )
153
+
154
+ async def _create_single_shipment(
155
+ self, index: int, shipment_data: dict[str, Any]
156
+ ) -> BatchResult:
157
+ provider_slug = shipment_data.get("provider_slug")
158
+ if not provider_slug:
159
+ return BatchResult(
160
+ index=index,
161
+ success=False,
162
+ error="Missing provider_slug",
163
+ )
164
+
165
+ try:
166
+ self.registry.get_by_slug(provider_slug)
167
+
168
+ flow = ShipmentFlow(
169
+ repository=self.repository,
170
+ config=self.config,
171
+ registry=self.registry,
172
+ )
173
+
174
+ outcome = await flow.create_shipment(
175
+ provider_slug=provider_slug,
176
+ sender_address=shipment_data["sender_address"],
177
+ receiver_address=shipment_data["receiver_address"],
178
+ parcels=shipment_data["parcels"],
179
+ **{
180
+ k: v
181
+ for k, v in shipment_data.items()
182
+ if k
183
+ not in (
184
+ "provider_slug",
185
+ "sender_address",
186
+ "receiver_address",
187
+ "parcels",
188
+ )
189
+ },
190
+ )
191
+
192
+ logger.info(
193
+ "Batch create: shipment %d created successfully "
194
+ "(provider=%s, tracking=%s)",
195
+ index,
196
+ provider_slug,
197
+ outcome.shipment.tracking_number,
198
+ )
199
+
200
+ return BatchResult(
201
+ index=index,
202
+ success=True,
203
+ shipment=outcome.shipment,
204
+ outcome=outcome,
205
+ )
206
+
207
+ except Exception as exc:
208
+ logger.warning(
209
+ "Batch create: shipment %d failed: %s",
210
+ index,
211
+ exc,
212
+ )
213
+ return BatchResult(
214
+ index=index,
215
+ success=False,
216
+ error=str(exc),
217
+ )
218
+
219
+ async def fetch_statuses(
220
+ self,
221
+ shipment_ids: list[str],
222
+ ) -> list[BatchResult]:
223
+ """Fetch statuses for multiple shipments.
224
+
225
+ Args:
226
+ shipment_ids: List of shipment IDs to fetch.
227
+
228
+ Returns:
229
+ List of BatchResult with per-shipment status updates.
230
+ """
231
+ semaphore = asyncio.Semaphore(self.max_concurrent)
232
+
233
+ async def _fetch_one(index: int, sid: str) -> BatchResult:
234
+ async with semaphore:
235
+ try:
236
+ shipment = await self.repository.get_by_id(sid)
237
+ flow = ShipmentFlow(
238
+ repository=self.repository,
239
+ config=self.config,
240
+ registry=self.registry,
241
+ )
242
+ outcome = await flow.fetch_and_update_status(shipment)
243
+ return BatchResult(
244
+ index=index,
245
+ success=True,
246
+ shipment=outcome.shipment,
247
+ outcome=outcome,
248
+ )
249
+ except Exception as exc:
250
+ return BatchResult(
251
+ index=index,
252
+ success=False,
253
+ error=str(exc),
254
+ )
255
+
256
+ results = await asyncio.gather(
257
+ *(_fetch_one(i, sid) for i, sid in enumerate(shipment_ids))
258
+ )
259
+ results.sort(key=lambda r: r.index)
260
+ return results
261
+
262
+ async def cancel_shipments(
263
+ self,
264
+ shipment_ids: list[str],
265
+ ) -> list[BatchResult]:
266
+ """Cancel multiple shipments.
267
+
268
+ Args:
269
+ shipment_ids: List of shipment IDs to cancel.
270
+
271
+ Returns:
272
+ List of BatchResult with per-shipment cancellation results.
273
+ """
274
+ semaphore = asyncio.Semaphore(self.max_concurrent)
275
+
276
+ async def _cancel_one(index: int, sid: str) -> BatchResult:
277
+ async with semaphore:
278
+ try:
279
+ shipment = await self.repository.get_by_id(sid)
280
+ flow = ShipmentFlow(
281
+ repository=self.repository,
282
+ config=self.config,
283
+ registry=self.registry,
284
+ )
285
+ cancelled = await flow.cancel_shipment(shipment)
286
+ return BatchResult(
287
+ index=index,
288
+ success=True,
289
+ shipment=shipment,
290
+ outcome={"cancelled": cancelled},
291
+ )
292
+ except Exception as exc:
293
+ return BatchResult(
294
+ index=index,
295
+ success=False,
296
+ error=str(exc),
297
+ )
298
+
299
+ results = await asyncio.gather(
300
+ *(_cancel_one(i, sid) for i, sid in enumerate(shipment_ids))
301
+ )
302
+ results.sort(key=lambda r: r.index)
303
+ return results
sendparcel/enums.py CHANGED
@@ -15,10 +15,21 @@ class ShipmentStatus(StrEnum):
15
15
  CANCELLED = "cancelled"
16
16
  FAILED = "failed"
17
17
  RETURNED = "returned"
18
+ SUBMITTED = "submitted"
19
+
20
+
21
+ class LabelFormat(StrEnum):
22
+ """Shipping label format."""
23
+
24
+ PDF = "PDF"
25
+ ZPL = "ZPL"
26
+ PNG = "PNG"
27
+ EPL = "EPL"
18
28
 
19
29
 
20
30
  class ConfirmationMethod(StrEnum):
21
31
  """How the provider confirms shipment status updates."""
22
32
 
33
+ NONE = "NONE"
23
34
  PUSH = "PUSH"
24
35
  PULL = "PULL"
sendparcel/exceptions.py CHANGED
@@ -1,10 +1,16 @@
1
1
  """Exception hierarchy for sendparcel."""
2
2
 
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
3
7
 
4
8
  class SendParcelException(Exception):
5
9
  """Base exception for sendparcel."""
6
10
 
7
- def __init__(self, message: str = "", context: dict | None = None) -> None:
11
+ def __init__(
12
+ self, message: str = "", context: dict[str, Any] | None = None
13
+ ) -> None:
8
14
  super().__init__(message)
9
15
  self.context = context or {}
10
16
 
@@ -19,3 +25,29 @@ class InvalidCallbackError(SendParcelException):
19
25
 
20
26
  class InvalidTransitionError(SendParcelException):
21
27
  """Invalid shipment state transition requested."""
28
+
29
+
30
+ class ShipmentNotFoundError(SendParcelException):
31
+ """Shipment not found in repository."""
32
+
33
+ def __init__(
34
+ self, shipment_id: str, context: dict[str, Any] | None = None
35
+ ) -> None:
36
+ super().__init__(f"Shipment {shipment_id} not found", context)
37
+ self.shipment_id = shipment_id
38
+
39
+
40
+ class ProviderNotFoundError(SendParcelException):
41
+ """Provider slug not found in the registry."""
42
+
43
+ def __init__(
44
+ self, provider_slug: str, context: dict[str, Any] | None = None
45
+ ) -> None:
46
+ super().__init__(
47
+ f"Provider {provider_slug!r} is not registered", context
48
+ )
49
+ self.provider_slug = provider_slug
50
+
51
+
52
+ class ProviderCapabilityError(SendParcelException):
53
+ """Provider does not support the requested capability."""