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.
@@ -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)