karrio-easypost 2025.5rc1__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.
- karrio/mappers/easypost/__init__.py +3 -0
- karrio/mappers/easypost/mapper.py +67 -0
- karrio/mappers/easypost/proxy.py +100 -0
- karrio/mappers/easypost/settings.py +18 -0
- karrio/plugins/easypost/__init__.py +19 -0
- karrio/providers/easypost/__init__.py +8 -0
- karrio/providers/easypost/error.py +31 -0
- karrio/providers/easypost/rate.py +194 -0
- karrio/providers/easypost/shipment/__init__.py +8 -0
- karrio/providers/easypost/shipment/cancel.py +31 -0
- karrio/providers/easypost/shipment/create.py +214 -0
- karrio/providers/easypost/tracking.py +118 -0
- karrio/providers/easypost/units.py +1038 -0
- karrio/providers/easypost/utils.py +33 -0
- karrio/schemas/easypost/__init__.py +0 -0
- karrio/schemas/easypost/error_response.py +15 -0
- karrio/schemas/easypost/shipment_purchase.py +14 -0
- karrio/schemas/easypost/shipment_request.py +79 -0
- karrio/schemas/easypost/shipments_response.py +248 -0
- karrio/schemas/easypost/trackers_response.py +79 -0
- karrio_easypost-2025.5rc1.dist-info/METADATA +46 -0
- karrio_easypost-2025.5rc1.dist-info/RECORD +26 -0
- karrio_easypost-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_easypost-2025.5rc1.dist-info/entry_points.txt +2 -0
- karrio_easypost-2025.5rc1.dist-info/licenses/LICENSE +165 -0
- karrio_easypost-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,214 @@
|
|
1
|
+
import karrio.schemas.easypost.shipment_request as easypost
|
2
|
+
import karrio.schemas.easypost.shipments_response as shipping
|
3
|
+
|
4
|
+
import typing
|
5
|
+
import karrio.lib as lib
|
6
|
+
import karrio.core.units as units
|
7
|
+
import karrio.core.models as models
|
8
|
+
import karrio.providers.easypost.error as provider_error
|
9
|
+
import karrio.providers.easypost.units as provider_units
|
10
|
+
import karrio.providers.easypost.utils as provider_utils
|
11
|
+
|
12
|
+
|
13
|
+
def parse_shipment_response(
|
14
|
+
_response: lib.Deserializable[dict],
|
15
|
+
settings: provider_utils.Settings,
|
16
|
+
) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
|
17
|
+
response = _response.deserialize()
|
18
|
+
errors = provider_error.parse_error_response(response, settings)
|
19
|
+
shipment = _extract_details(response, settings) if "error" not in response else None
|
20
|
+
|
21
|
+
return shipment, errors
|
22
|
+
|
23
|
+
|
24
|
+
def _extract_details(
|
25
|
+
response: dict, settings: provider_utils.Settings
|
26
|
+
) -> models.ShipmentDetails:
|
27
|
+
shipment = lib.to_object(shipping.Shipment, response)
|
28
|
+
label_type = shipment.postage_label.label_file_type.split("/")[-1]
|
29
|
+
label = provider_utils.download_label(shipment.postage_label.label_url)
|
30
|
+
|
31
|
+
return models.ShipmentDetails(
|
32
|
+
carrier_id=settings.carrier_id,
|
33
|
+
carrier_name=settings.carrier_name,
|
34
|
+
shipment_identifier=shipment.id,
|
35
|
+
tracking_number=shipment.tracking_code,
|
36
|
+
label_type=label_type.upper(),
|
37
|
+
docs=models.Documents(label=label),
|
38
|
+
meta=dict(
|
39
|
+
carrier_tracking_link=getattr(shipment.tracker, "public_url", None),
|
40
|
+
rate_provider=shipment.selected_rate.carrier,
|
41
|
+
service_name=shipment.selected_rate.service,
|
42
|
+
label_url=shipment.postage_label.label_url,
|
43
|
+
fees=lib.to_dict(shipment.fees or []),
|
44
|
+
),
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
def shipment_request(payload: models.ShipmentRequest, _) -> lib.Serializable:
|
49
|
+
shipper = lib.to_address(payload.shipper)
|
50
|
+
recipient = lib.to_address(payload.recipient)
|
51
|
+
return_address = lib.to_address(payload.return_address)
|
52
|
+
billing_address = lib.to_address(payload.billing_address)
|
53
|
+
service = provider_units.Service.map(payload.service).value_or_key
|
54
|
+
package = lib.to_packages(
|
55
|
+
payload.parcels,
|
56
|
+
package_option_type=provider_units.ShippingOption,
|
57
|
+
).single
|
58
|
+
|
59
|
+
payment = payload.payment or models.Payment()
|
60
|
+
payor = payload.billing_address or (
|
61
|
+
payload.shipper if payment.paid_by == "sender" else payload.recipient
|
62
|
+
)
|
63
|
+
options = lib.to_shipping_options(
|
64
|
+
payload,
|
65
|
+
payor=payor,
|
66
|
+
package_options=package.options,
|
67
|
+
initializer=provider_units.shipping_options_initializer,
|
68
|
+
)
|
69
|
+
is_intl = shipper.country_code != recipient.country_code
|
70
|
+
customs = lib.to_customs_info(
|
71
|
+
payload.customs,
|
72
|
+
weight_unit=package.weight_unit,
|
73
|
+
default_to=(
|
74
|
+
models.Customs(
|
75
|
+
commodities=(
|
76
|
+
package.parcel.items
|
77
|
+
if any(package.parcel.items)
|
78
|
+
else [
|
79
|
+
models.Commodity(
|
80
|
+
sku="0000",
|
81
|
+
quantity=1,
|
82
|
+
weight=package.weight.value,
|
83
|
+
weight_unit=package.weight_unit.value,
|
84
|
+
description=package.parcel.content,
|
85
|
+
)
|
86
|
+
]
|
87
|
+
)
|
88
|
+
)
|
89
|
+
if is_intl
|
90
|
+
else None
|
91
|
+
),
|
92
|
+
)
|
93
|
+
|
94
|
+
requests = dict(
|
95
|
+
service=service,
|
96
|
+
insurance=options.insurance.state,
|
97
|
+
data=easypost.ShipmentRequest(
|
98
|
+
shipment=easypost.Shipment(
|
99
|
+
reference=payload.reference,
|
100
|
+
to_address=easypost.Address(
|
101
|
+
company=recipient.company_name,
|
102
|
+
street1=recipient.street,
|
103
|
+
street2=recipient.address_line2,
|
104
|
+
city=recipient.city,
|
105
|
+
state=recipient.state_code,
|
106
|
+
zip=recipient.postal_code,
|
107
|
+
country=recipient.country_code,
|
108
|
+
residential=recipient.residential,
|
109
|
+
name=recipient.person_name,
|
110
|
+
phone=recipient.phone_number,
|
111
|
+
email=recipient.email,
|
112
|
+
federal_tax_id=recipient.federal_tax_id,
|
113
|
+
state_tax_id=recipient.state_tax_id,
|
114
|
+
),
|
115
|
+
from_address=easypost.Address(
|
116
|
+
company=shipper.company_name,
|
117
|
+
street1=shipper.street,
|
118
|
+
street2=shipper.address_line2,
|
119
|
+
city=shipper.city,
|
120
|
+
state=shipper.state_code,
|
121
|
+
zip=shipper.postal_code,
|
122
|
+
country=shipper.country_code,
|
123
|
+
residential=shipper.residential,
|
124
|
+
name=shipper.person_name,
|
125
|
+
phone=shipper.phone_number,
|
126
|
+
email=shipper.email,
|
127
|
+
federal_tax_id=shipper.federal_tax_id,
|
128
|
+
state_tax_id=shipper.state_tax_id,
|
129
|
+
),
|
130
|
+
return_address=lib.identity(
|
131
|
+
easypost.Address(
|
132
|
+
company=return_address.company_name,
|
133
|
+
street1=return_address.street,
|
134
|
+
street2=return_address.address_line2,
|
135
|
+
city=return_address.city,
|
136
|
+
state=return_address.state_code,
|
137
|
+
zip=return_address.postal_code,
|
138
|
+
country=return_address.country_code,
|
139
|
+
residential=return_address.residential,
|
140
|
+
name=return_address.person_name,
|
141
|
+
phone=return_address.phone_number,
|
142
|
+
email=return_address.email,
|
143
|
+
federal_tax_id=return_address.federal_tax_id,
|
144
|
+
state_tax_id=return_address.state_tax_id,
|
145
|
+
)
|
146
|
+
if payload.return_address
|
147
|
+
else None
|
148
|
+
),
|
149
|
+
buyer_address=lib.identity(
|
150
|
+
easypost.Address(
|
151
|
+
company=billing_address.company_name,
|
152
|
+
street1=billing_address.street,
|
153
|
+
street2=billing_address.address_line2,
|
154
|
+
city=billing_address.city,
|
155
|
+
state=billing_address.state_code,
|
156
|
+
zip=billing_address.postal_code,
|
157
|
+
country=billing_address.country_code,
|
158
|
+
residential=billing_address.residential,
|
159
|
+
name=billing_address.person_name,
|
160
|
+
phone=billing_address.phone_number,
|
161
|
+
email=billing_address.email,
|
162
|
+
federal_tax_id=billing_address.federal_tax_id,
|
163
|
+
state_tax_id=billing_address.state_tax_id,
|
164
|
+
)
|
165
|
+
if payload.billing_address
|
166
|
+
else None
|
167
|
+
),
|
168
|
+
parcel=easypost.Parcel(
|
169
|
+
length=package.length.IN,
|
170
|
+
width=package.width.IN,
|
171
|
+
height=package.height.IN,
|
172
|
+
weight=package.weight.OZ,
|
173
|
+
predefined_package=provider_units.PackagingType.map(
|
174
|
+
package.packaging_type
|
175
|
+
).value,
|
176
|
+
),
|
177
|
+
options={option.code: option.state for _, option in options.items()},
|
178
|
+
customs_info=(
|
179
|
+
easypost.CustomsInfo(
|
180
|
+
contents_explanation=customs.content_description,
|
181
|
+
contents_type=customs.content_type,
|
182
|
+
customs_certify=customs.certify,
|
183
|
+
customs_signer=(customs.signer or shipper.person_name),
|
184
|
+
eel_pfc=customs.options.eel_pfc.state,
|
185
|
+
non_delivery_option=customs.options.non_delivery_option.state,
|
186
|
+
restriction_type=customs.options.restriction_type.state,
|
187
|
+
declaration=customs.options.declaration.state,
|
188
|
+
customs_items=[
|
189
|
+
easypost.CustomsItem(
|
190
|
+
description=lib.text(
|
191
|
+
item.description or item.title or "N/A"
|
192
|
+
),
|
193
|
+
origin_country=item.origin_country,
|
194
|
+
quantity=item.quantity,
|
195
|
+
value=item.value_amount,
|
196
|
+
weight=units.Weight(item.weight, item.weight_unit).OZ,
|
197
|
+
code=item.sku,
|
198
|
+
manufacturer=None,
|
199
|
+
currency=item.value_currency,
|
200
|
+
eccn=(item.metadata or {}).get("eccn"),
|
201
|
+
printed_commodity_identifier=(item.sku or item.id),
|
202
|
+
hs_tariff_number=item.hs_code,
|
203
|
+
)
|
204
|
+
for item in customs.commodities
|
205
|
+
],
|
206
|
+
)
|
207
|
+
if payload.customs
|
208
|
+
else None
|
209
|
+
),
|
210
|
+
)
|
211
|
+
),
|
212
|
+
)
|
213
|
+
|
214
|
+
return lib.Serializable(requests, lib.to_dict)
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import karrio.schemas.easypost.trackers_response as easypost
|
2
|
+
import typing
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core.models as models
|
5
|
+
import karrio.providers.easypost.error as error
|
6
|
+
import karrio.providers.easypost.utils as provider_utils
|
7
|
+
import karrio.providers.easypost.units as provider_units
|
8
|
+
|
9
|
+
|
10
|
+
def parse_tracking_response(
|
11
|
+
_responses: lib.Deserializable[typing.List[typing.Tuple[str, dict]]],
|
12
|
+
settings: provider_utils.Settings,
|
13
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
14
|
+
responses = _responses.deserialize()
|
15
|
+
errors: typing.List[models.Message] = sum(
|
16
|
+
[
|
17
|
+
error.parse_error_response(response, settings, tracking_number=code)
|
18
|
+
for code, response in responses
|
19
|
+
if "error" in response
|
20
|
+
],
|
21
|
+
start=[],
|
22
|
+
)
|
23
|
+
trackers = [
|
24
|
+
_extract_details(response, settings)
|
25
|
+
for _, response in responses
|
26
|
+
if "error" not in response
|
27
|
+
]
|
28
|
+
|
29
|
+
return trackers, errors
|
30
|
+
|
31
|
+
|
32
|
+
def _extract_details(
|
33
|
+
data: dict,
|
34
|
+
settings: provider_utils.Settings,
|
35
|
+
) -> models.TrackingDetails:
|
36
|
+
tracker = lib.to_object(easypost.Tracker, data)
|
37
|
+
expected_delivery = lib.fdate(tracker.est_delivery_date, "%Y-%m-%dT%H:%M:%SZ")
|
38
|
+
events: typing.List[dict] = data.get("tracking_details", [])
|
39
|
+
|
40
|
+
return models.TrackingDetails(
|
41
|
+
carrier_id=settings.carrier_id,
|
42
|
+
carrier_name=settings.carrier_name,
|
43
|
+
tracking_number=tracker.tracking_code,
|
44
|
+
events=[
|
45
|
+
models.TrackingEvent(
|
46
|
+
date=lib.fdate(event.get("datetime"), "%Y-%m-%dT%H:%M:%SZ"),
|
47
|
+
description=event.get("message") or "",
|
48
|
+
code=event.get("status"),
|
49
|
+
time=lib.flocaltime(event.get("datetime"), "%Y-%m-%dT%H:%M:%SZ"),
|
50
|
+
location=lib.join(
|
51
|
+
event.get("tracking_location", {}).get("city"),
|
52
|
+
event.get("tracking_location", {}).get("state"),
|
53
|
+
event.get("tracking_location", {}).get("zip"),
|
54
|
+
event.get("tracking_location", {}).get("country"),
|
55
|
+
join=True,
|
56
|
+
separator=", ",
|
57
|
+
),
|
58
|
+
)
|
59
|
+
for event in events
|
60
|
+
if event.get("datetime") is not None
|
61
|
+
],
|
62
|
+
delivered=(tracker.status == "delivered"),
|
63
|
+
estimated_delivery=expected_delivery,
|
64
|
+
info=models.TrackingInfo(
|
65
|
+
carrier_tracking_link=tracker.public_url,
|
66
|
+
package_weight=tracker.weight,
|
67
|
+
signed_by=tracker.signed_by,
|
68
|
+
shipment_destination_postal_code=getattr(
|
69
|
+
tracker.carrier_detail, "zip", None
|
70
|
+
),
|
71
|
+
shipment_origin_country=getattr(tracker.carrier_detail, "country", None),
|
72
|
+
shipment_service=getattr(tracker.carrier_detail, "service", None),
|
73
|
+
),
|
74
|
+
meta=dict(
|
75
|
+
carrier=provider_units.CarrierId.map(tracker.carrier).name_or_key,
|
76
|
+
shipment_id=tracker.shipment_id,
|
77
|
+
tracker_id=tracker.id,
|
78
|
+
fees=lib.to_dict(tracker.fees),
|
79
|
+
),
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
def tracking_request(payload: models.TrackingRequest, _) -> lib.Serializable:
|
84
|
+
"""Send one or multiple tracking request(s) to EasyPost.
|
85
|
+
the payload must match the following schema:
|
86
|
+
{
|
87
|
+
"tracking_numbers": ["123456789"],
|
88
|
+
"options": {
|
89
|
+
"123456789": {
|
90
|
+
"carrier": "usps",
|
91
|
+
"tracker_id": "trk_xxxxxxxx", # optional
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
"""
|
96
|
+
requests = []
|
97
|
+
|
98
|
+
for tracking_code in payload.tracking_numbers:
|
99
|
+
options = payload.options.get(tracking_code)
|
100
|
+
|
101
|
+
if options is None:
|
102
|
+
raise ValueError(f"No options found for {tracking_code}")
|
103
|
+
|
104
|
+
if "carrier" not in options:
|
105
|
+
raise ValueError(
|
106
|
+
"invalid options['tracking_number'].carriers."
|
107
|
+
"Please provide a 'carrier_name' for each tracking_number"
|
108
|
+
)
|
109
|
+
|
110
|
+
requests.append(
|
111
|
+
dict(
|
112
|
+
tracking_code=tracking_code,
|
113
|
+
carrier=provider_units.CarrierId.map(options["carrier"]).value_or_key,
|
114
|
+
tracker_id=options.get("tracker_id"),
|
115
|
+
)
|
116
|
+
)
|
117
|
+
|
118
|
+
return lib.Serializable(requests, lib.to_dict)
|