python-sendparcel 0.2.0__py3-none-any.whl → 0.3.1__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.
@@ -8,15 +8,10 @@ import anyio
8
8
 
9
9
  from sendparcel.enums import ConfirmationMethod, LabelFormat
10
10
  from sendparcel.exceptions import InvalidCallbackError
11
- from sendparcel.provider import (
12
- BaseProvider,
13
- CancellableProvider,
14
- LabelProvider,
15
- PullStatusProvider,
16
- PushCallbackProvider,
17
- )
11
+ from sendparcel.provider import BaseProvider
18
12
  from sendparcel.types import (
19
13
  AddressInfo,
14
+ CallbackContext,
20
15
  LabelInfo,
21
16
  ParcelInfo,
22
17
  ShipmentCreateResult,
@@ -24,13 +19,7 @@ from sendparcel.types import (
24
19
  )
25
20
 
26
21
 
27
- class DummyProvider(
28
- BaseProvider,
29
- LabelProvider,
30
- PushCallbackProvider,
31
- PullStatusProvider,
32
- CancellableProvider,
33
- ):
22
+ class DummyProvider(BaseProvider):
34
23
  """Reference provider for local, development, and test usage."""
35
24
 
36
25
  slug: ClassVar[str] = "dummy"
@@ -66,19 +55,17 @@ class DummyProvider(
66
55
  await self._simulate_latency()
67
56
  return LabelInfo(format=LabelFormat.PDF, url=self._label_url())
68
57
 
69
- async def verify_callback(
70
- self, data: dict[str, Any], headers: dict[str, Any], **kwargs: Any
71
- ) -> None:
58
+ async def verify_callback(self, ctx: CallbackContext) -> None:
72
59
  expected = self.get_setting("callback_token", "dummy-token")
73
- provided = headers.get("x-dummy-token", "")
60
+ provided = ctx.headers.get("x-dummy-token", "")
74
61
  if provided != expected:
75
62
  raise InvalidCallbackError("BAD TOKEN")
76
63
 
77
64
  async def handle_callback(
78
- self, data: dict[str, Any], headers: dict[str, Any], **kwargs: Any
65
+ self, ctx: CallbackContext
79
66
  ) -> ShipmentUpdateResult:
80
67
  await self._simulate_latency()
81
- status_value = data.get("status")
68
+ status_value = ctx.payload.get("status")
82
69
  if not status_value:
83
70
  return ShipmentUpdateResult()
84
71
  return ShipmentUpdateResult(status=str(status_value))
sendparcel/registry.py CHANGED
@@ -62,6 +62,15 @@ class PluginRegistry:
62
62
  except KeyError as exc:
63
63
  raise ProviderNotFoundError(slug) from exc
64
64
 
65
+ def slugs(self) -> list[str]:
66
+ """Get all registered provider slugs.
67
+
68
+ Thread-safe.
69
+ """
70
+ self._ensure_discovered()
71
+ with self._lock:
72
+ return list(self._providers)
73
+
65
74
  def get_choices(self) -> list[tuple[str, str]]:
66
75
  """Get provider slug/display pairs for user-facing selection.
67
76
 
sendparcel/types.py CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import hashlib
6
+ import json
5
7
  from dataclasses import dataclass
8
+ from datetime import datetime
6
9
  from decimal import Decimal
7
- from typing import TypedDict
10
+ from enum import StrEnum
11
+ from typing import Any, TypedDict
8
12
 
9
13
  from sendparcel.enums import LabelFormat
10
14
  from sendparcel.protocols import Shipment
@@ -81,9 +85,6 @@ class ShipmentUpdateResult(TypedDict, total=False):
81
85
  tracking_events: list[TrackingEvent]
82
86
 
83
87
 
84
- ShipmentStatusResponse = ShipmentUpdateResult
85
-
86
-
87
88
  @dataclass(frozen=True, slots=True)
88
89
  class CreateShipmentOutcome:
89
90
  """Flow result for shipment creation."""
@@ -106,3 +107,106 @@ class ShipmentUpdateOutcome:
106
107
 
107
108
  shipment: Shipment
108
109
  update: ShipmentUpdateResult
110
+
111
+
112
+ class CancelReason(StrEnum):
113
+ """Reason for a cancel_shipment outcome."""
114
+
115
+ CANCELLED = "cancelled"
116
+ ALREADY_CANCELLED = "already_cancelled"
117
+ REFUSED_IN_TRANSIT = "refused_in_transit"
118
+ NOT_CANCELLABLE = "not_cancellable"
119
+ TRANSIENT_ERROR = "transient_error"
120
+ AUTH_ERROR = "auth_error"
121
+
122
+
123
+ class _CancelOutcomeRequired(TypedDict):
124
+ cancelled: bool
125
+ reason: CancelReason
126
+ retryable: bool
127
+
128
+
129
+ class CancelOutcome(_CancelOutcomeRequired, total=False):
130
+ """Structured result from cancel_shipment.
131
+
132
+ Replaces the bare bool return so callers can distinguish permanent
133
+ denies (REFUSED_IN_TRANSIT, NOT_CANCELLABLE) from retryable failures
134
+ (TRANSIENT_ERROR).
135
+ """
136
+
137
+ provider_status_code: int | None
138
+ detail: str | None
139
+
140
+
141
+ class GeoPoint(TypedDict):
142
+ """Geographic coordinate pair."""
143
+
144
+ lat: float
145
+ lng: float
146
+
147
+
148
+ class _PickupPointRequired(TypedDict):
149
+ code: str
150
+ name: str
151
+ provider_slug: str
152
+ address: str
153
+
154
+
155
+ class PickupPoint(_PickupPointRequired, total=False):
156
+ """Carrier pickup point (locker, parcel shop, etc.).
157
+
158
+ ``code`` is the machine id used as ``target_point`` in
159
+ ``create_shipment``, so a searched point is directly usable
160
+ without translation.
161
+ """
162
+
163
+ location: GeoPoint | None
164
+ opening_hours: str | None
165
+ point_type: str | None
166
+ raw: dict[str, Any] | None
167
+
168
+
169
+ class _QuoteRequired(TypedDict):
170
+ provider_slug: str
171
+ service: str
172
+ amount: Decimal
173
+ currency: str
174
+
175
+
176
+ class Quote(_QuoteRequired, total=False):
177
+ """Shipping rate quote from a carrier.
178
+
179
+ ``amount`` is the carrier cost in the provider's currency.
180
+ Use ``Decimal`` — never ``float`` — for monetary values.
181
+ """
182
+
183
+ valid_until: datetime | None
184
+ raw: dict[str, Any] | None
185
+
186
+
187
+ @dataclass(slots=True)
188
+ class CallbackContext:
189
+ """Everything needed to process a webhook callback.
190
+
191
+ Framework layers build this in one place. Core operates on it.
192
+ The retry store persists this as a single blob.
193
+
194
+ The ``dedup_hash`` is computed from the payload alone (not headers
195
+ or source_ip) so that legitimate re-submissions of the same callback
196
+ data are deduplicated regardless of transport metadata.
197
+ """
198
+
199
+ shipment_id: str
200
+ payload: dict[str, Any]
201
+ headers: dict[str, str]
202
+ source_ip: str
203
+ raw_body: bytes
204
+ provider_slug: str = ""
205
+
206
+ @property
207
+ def dedup_hash(self) -> str:
208
+ """Deterministic SHA-256 hash of the payload for dedup checks."""
209
+ raw = json.dumps(self.payload, sort_keys=True, default=str).encode(
210
+ "utf-8"
211
+ )
212
+ return hashlib.sha256(raw).hexdigest()
@@ -1,19 +0,0 @@
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,,
sendparcel/validators.py DELETED
@@ -1,21 +0,0 @@
1
- """Pluggable validation system.
2
-
3
- Validators are callables that receive a data dict, optionally
4
- modify it, and return it. Raise an exception to reject.
5
- """
6
-
7
- from collections.abc import Callable
8
- from typing import Any
9
-
10
-
11
- def run_validators(
12
- data: dict[str, Any], validators: list[Callable[..., Any]] | None = None
13
- ) -> dict[str, Any]:
14
- """Run a chain of validators on data.
15
-
16
- Each validator receives the data dict and must return it
17
- (possibly modified). Raise an exception to reject.
18
- """
19
- for validator in validators or []:
20
- data = validator(data)
21
- return data