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.
- {python_sendparcel-0.2.0.dist-info → python_sendparcel-0.3.1.dist-info}/METADATA +41 -24
- python_sendparcel-0.3.1.dist-info/RECORD +20 -0
- sendparcel/__init__.py +16 -12
- sendparcel/batch.py +77 -75
- sendparcel/concurrent.py +71 -0
- sendparcel/factory.py +33 -0
- sendparcel/flow.py +110 -51
- sendparcel/fsm.py +8 -8
- sendparcel/logging.py +5 -23
- sendparcel/provider.py +248 -30
- sendparcel/providers/dummy.py +7 -20
- sendparcel/registry.py +9 -0
- sendparcel/types.py +108 -4
- python_sendparcel-0.2.0.dist-info/RECORD +0 -19
- sendparcel/validators.py +0 -21
- {python_sendparcel-0.2.0.dist-info → python_sendparcel-0.3.1.dist-info}/WHEEL +0 -0
- {python_sendparcel-0.2.0.dist-info → python_sendparcel-0.3.1.dist-info}/licenses/LICENSE +0 -0
sendparcel/providers/dummy.py
CHANGED
|
@@ -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,
|
|
65
|
+
self, ctx: CallbackContext
|
|
79
66
|
) -> ShipmentUpdateResult:
|
|
80
67
|
await self._simulate_latency()
|
|
81
|
-
status_value =
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|