karrio-easyship 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/easyship/__init__.py +3 -0
- karrio/mappers/easyship/mapper.py +94 -0
- karrio/mappers/easyship/proxy.py +155 -0
- karrio/mappers/easyship/settings.py +20 -0
- karrio/plugins/easyship/__init__.py +20 -0
- karrio/providers/easyship/__init__.py +25 -0
- karrio/providers/easyship/error.py +38 -0
- karrio/providers/easyship/manifest.py +72 -0
- karrio/providers/easyship/metadata.json +6985 -0
- karrio/providers/easyship/pickup/__init__.py +4 -0
- karrio/providers/easyship/pickup/cancel.py +42 -0
- karrio/providers/easyship/pickup/create.py +86 -0
- karrio/providers/easyship/pickup/update.py +88 -0
- karrio/providers/easyship/rate.py +215 -0
- karrio/providers/easyship/shipment/__init__.py +9 -0
- karrio/providers/easyship/shipment/cancel.py +39 -0
- karrio/providers/easyship/shipment/create.py +306 -0
- karrio/providers/easyship/tracking.py +110 -0
- karrio/providers/easyship/units.py +162 -0
- karrio/providers/easyship/utils.py +98 -0
- karrio/schemas/easyship/__init__.py +0 -0
- karrio/schemas/easyship/error_response.py +17 -0
- karrio/schemas/easyship/manifest_request.py +9 -0
- karrio/schemas/easyship/manifest_response.py +31 -0
- karrio/schemas/easyship/pickup_cancel_response.py +19 -0
- karrio/schemas/easyship/pickup_request.py +13 -0
- karrio/schemas/easyship/pickup_response.py +85 -0
- karrio/schemas/easyship/rate_request.py +100 -0
- karrio/schemas/easyship/rate_response.py +124 -0
- karrio/schemas/easyship/shipment_cancel_response.py +19 -0
- karrio/schemas/easyship/shipment_request.py +147 -0
- karrio/schemas/easyship/shipment_response.py +273 -0
- karrio/schemas/easyship/tracking_request.py +49 -0
- karrio/schemas/easyship/tracking_response.py +54 -0
- karrio_easyship-2025.5rc1.dist-info/METADATA +45 -0
- karrio_easyship-2025.5rc1.dist-info/RECORD +39 -0
- karrio_easyship-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_easyship-2025.5rc1.dist-info/entry_points.txt +2 -0
- karrio_easyship-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,306 @@
|
|
1
|
+
"""Karrio Easyship shipment API implementation."""
|
2
|
+
|
3
|
+
import karrio.schemas.easyship.shipment_request as easyship
|
4
|
+
import karrio.schemas.easyship.shipment_response as shipping
|
5
|
+
|
6
|
+
import typing
|
7
|
+
import karrio.lib as lib
|
8
|
+
import karrio.core.units as units
|
9
|
+
import karrio.core.models as models
|
10
|
+
import karrio.providers.easyship.error as error
|
11
|
+
import karrio.providers.easyship.utils as provider_utils
|
12
|
+
import karrio.providers.easyship.units as provider_units
|
13
|
+
|
14
|
+
|
15
|
+
def parse_shipment_response(
|
16
|
+
_response: lib.Deserializable[dict],
|
17
|
+
settings: provider_utils.Settings,
|
18
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
19
|
+
response = _response.deserialize()
|
20
|
+
|
21
|
+
messages = error.parse_error_response(response, settings)
|
22
|
+
shipment = lib.identity(
|
23
|
+
_extract_details(response, settings, ctx=_response.ctx)
|
24
|
+
if response.get("shipment")
|
25
|
+
and any(response["shipment"].get("shipping_documents") or [])
|
26
|
+
else None
|
27
|
+
)
|
28
|
+
|
29
|
+
return shipment, messages
|
30
|
+
|
31
|
+
|
32
|
+
def _extract_details(
|
33
|
+
data: dict,
|
34
|
+
settings: provider_utils.Settings,
|
35
|
+
ctx: dict,
|
36
|
+
) -> models.ShipmentDetails:
|
37
|
+
details = lib.to_object(shipping.ShipmentType, data["shipment"])
|
38
|
+
label_document = next(
|
39
|
+
(_ for _ in details.shipping_documents if _.category == "label"), None
|
40
|
+
)
|
41
|
+
label_type = (label_document.format or ctx.get("label_type") or "PDF").upper()
|
42
|
+
label = lib.bundle_base64(label_document.base64_encoded_strings, label_type)
|
43
|
+
tracking_numbers = [tracking.tracking_number for tracking in details.trackings]
|
44
|
+
tracking_number, *__ = tracking_numbers
|
45
|
+
|
46
|
+
return models.ShipmentDetails(
|
47
|
+
carrier_id=settings.carrier_id,
|
48
|
+
carrier_name=settings.carrier_name,
|
49
|
+
tracking_number=tracking_number,
|
50
|
+
shipment_identifier=details.easyship_shipment_id,
|
51
|
+
label_type=label_type,
|
52
|
+
docs=models.Documents(label=label),
|
53
|
+
meta=dict(
|
54
|
+
shipment_ids=[details.easyship_shipment_id],
|
55
|
+
tracking_numbers=tracking_numbers,
|
56
|
+
carrier=ctx["rate_provider"],
|
57
|
+
rate_provider=ctx["rate_provider"],
|
58
|
+
easyship_courier_id=ctx["courier_id"],
|
59
|
+
easyship_shipment_id=details.easyship_shipment_id,
|
60
|
+
easyship_courier_account_id=details.courier.id,
|
61
|
+
),
|
62
|
+
)
|
63
|
+
|
64
|
+
|
65
|
+
def shipment_request(
|
66
|
+
payload: models.ShipmentRequest,
|
67
|
+
settings: provider_utils.Settings,
|
68
|
+
) -> lib.Serializable:
|
69
|
+
shipper = lib.to_address(payload.shipper)
|
70
|
+
recipient = lib.to_address(payload.recipient)
|
71
|
+
return_address = lib.to_address(payload.return_address or payload.shipper)
|
72
|
+
packages = lib.to_packages(payload.parcels, options=payload.options)
|
73
|
+
weight_unit, dimension_unit = packages.compatible_units
|
74
|
+
options = lib.to_shipping_options(
|
75
|
+
payload.options,
|
76
|
+
package_options=packages.options,
|
77
|
+
initializer=provider_units.shipping_options_initializer,
|
78
|
+
)
|
79
|
+
service = lib.identity(
|
80
|
+
options.easyship_courier_id.state
|
81
|
+
or provider_units.ShippingServiceID.map(payload.service).name_or_key
|
82
|
+
)
|
83
|
+
courier = provider_units.ShippingCourierID.find(service)
|
84
|
+
customs = lib.to_customs_info(
|
85
|
+
payload.customs,
|
86
|
+
shipper=payload.shipper,
|
87
|
+
recipient=payload.recipient,
|
88
|
+
weight_unit=weight_unit.name,
|
89
|
+
)
|
90
|
+
incoterms = lib.identity(
|
91
|
+
options.easyship_incoterms.state or customs.options.incoterm.state or "DDU"
|
92
|
+
)
|
93
|
+
label_type = provider_units.LabelFormat.map(payload.label_type)
|
94
|
+
|
95
|
+
# map data to convert karrio model to easyship specific type
|
96
|
+
request = easyship.ShipmentRequestType(
|
97
|
+
buyer_regulatory_identifiers=lib.identity(
|
98
|
+
easyship.BuyerRegulatoryIdentifiersType(
|
99
|
+
ein=customs.duty_billing_address.tax_id,
|
100
|
+
vat_number=customs.options.vat_registration_number.state,
|
101
|
+
)
|
102
|
+
if any(
|
103
|
+
[
|
104
|
+
customs.options.vat_registration_number.state,
|
105
|
+
customs.duty_billing_address.tax_id,
|
106
|
+
]
|
107
|
+
)
|
108
|
+
else None
|
109
|
+
),
|
110
|
+
courier_selection=easyship.CourierSelectionType(
|
111
|
+
allow_courier_fallback=lib.identity(
|
112
|
+
options.easyship_allow_courier_fallback.state
|
113
|
+
if options.easyship_allow_courier_fallback.state is not None
|
114
|
+
else settings.connection_config.allow_courier_fallback.state
|
115
|
+
),
|
116
|
+
apply_shipping_rules=lib.identity(
|
117
|
+
options.easyship_apply_shipping_rules.state
|
118
|
+
if options.easyship_apply_shipping_rules.state is not None
|
119
|
+
else settings.connection_config.apply_shipping_rules.state
|
120
|
+
),
|
121
|
+
list_unavailable_couriers=lib.identity(
|
122
|
+
options.easyship_list_unavailable_couriers.state
|
123
|
+
if options.easyship_list_unavailable_couriers.state is not None
|
124
|
+
else False
|
125
|
+
),
|
126
|
+
selected_courier_id=service,
|
127
|
+
),
|
128
|
+
destination_address=easyship.AddressType(
|
129
|
+
city=recipient.city,
|
130
|
+
company_name=lib.text(recipient.company_name or "N/A", max=22),
|
131
|
+
contact_email=lib.identity(
|
132
|
+
recipient.email
|
133
|
+
or options.email_notification_to.state
|
134
|
+
or "user@mail.com"
|
135
|
+
),
|
136
|
+
contact_name=lib.text(recipient.person_name, max=22),
|
137
|
+
contact_phone=recipient.phone_number or "N/A",
|
138
|
+
country_alpha2=recipient.country_code,
|
139
|
+
line_1=recipient.address_line1,
|
140
|
+
line_2=recipient.address_line2,
|
141
|
+
postal_code=recipient.postal_code,
|
142
|
+
state=recipient.state_code,
|
143
|
+
),
|
144
|
+
consignee_tax_id=recipient.tax_id,
|
145
|
+
eei_reference=options.easyship_eei_reference.state,
|
146
|
+
incoterms=incoterms,
|
147
|
+
metadata=payload.metadata,
|
148
|
+
insurance=easyship.InsuranceType(
|
149
|
+
is_insured=options.insurance.state is not None
|
150
|
+
),
|
151
|
+
order_data=None,
|
152
|
+
origin_address=easyship.AddressType(
|
153
|
+
city=return_address.city,
|
154
|
+
company_name=lib.text(return_address.company_name or "N/A", max=22),
|
155
|
+
contact_email=lib.identity(
|
156
|
+
return_address.email
|
157
|
+
or options.email_notification_to.state
|
158
|
+
or "user@mail.com"
|
159
|
+
),
|
160
|
+
contact_name=lib.text(return_address.person_name, max=22),
|
161
|
+
contact_phone=return_address.phone_number or "N/A",
|
162
|
+
country_alpha2=return_address.country_code,
|
163
|
+
line_1=return_address.address_line1,
|
164
|
+
line_2=return_address.address_line2,
|
165
|
+
postal_code=return_address.postal_code,
|
166
|
+
state=return_address.state_code,
|
167
|
+
),
|
168
|
+
regulatory_identifiers=lib.identity(
|
169
|
+
easyship.RegulatoryIdentifiersType(
|
170
|
+
eori=customs.options.eori.state,
|
171
|
+
ioss=customs.options.ioss.state,
|
172
|
+
vat_number=customs.options.vat_registration_number.state,
|
173
|
+
)
|
174
|
+
if any(
|
175
|
+
[
|
176
|
+
customs.options.eori.state,
|
177
|
+
customs.options.vat_registration_number.state,
|
178
|
+
customs.duty_billing_address.tax_id,
|
179
|
+
]
|
180
|
+
)
|
181
|
+
else None
|
182
|
+
),
|
183
|
+
shipment_request_return=options.is_return.state,
|
184
|
+
return_address=easyship.AddressType(
|
185
|
+
city=return_address.city,
|
186
|
+
company_name=lib.text(return_address.company_name or "N/A", max=22),
|
187
|
+
contact_email=lib.identity(
|
188
|
+
return_address.email
|
189
|
+
or options.email_notification_to.state
|
190
|
+
or "user@mail.com"
|
191
|
+
),
|
192
|
+
contact_name=lib.text(return_address.person_name, max=22),
|
193
|
+
contact_phone=return_address.phone_number or "N/A",
|
194
|
+
country_alpha2=return_address.country_code,
|
195
|
+
line_1=return_address.address_line1,
|
196
|
+
line_2=return_address.address_line2,
|
197
|
+
postal_code=return_address.postal_code,
|
198
|
+
state=return_address.state_code,
|
199
|
+
),
|
200
|
+
return_address_id=options.easyship_return_address_id.state,
|
201
|
+
sender_address=easyship.AddressType(
|
202
|
+
city=shipper.city,
|
203
|
+
company_name=lib.text(shipper.company_name or "N/A", max=22),
|
204
|
+
contact_email=lib.identity(
|
205
|
+
shipper.email or options.email_notification_to.state or "user@mail.com"
|
206
|
+
),
|
207
|
+
contact_name=lib.text(shipper.person_name, max=22),
|
208
|
+
contact_phone=shipper.phone_number or "N/A",
|
209
|
+
country_alpha2=shipper.country_code,
|
210
|
+
line_1=shipper.address_line1,
|
211
|
+
line_2=shipper.address_line2,
|
212
|
+
postal_code=shipper.postal_code,
|
213
|
+
state=shipper.state_code,
|
214
|
+
),
|
215
|
+
sender_address_id=options.easyship_sender_address_id.state,
|
216
|
+
set_as_residential=recipient.residential,
|
217
|
+
shipping_settings=easyship.ShippingSettingsType(
|
218
|
+
additional_services=lib.identity(
|
219
|
+
easyship.AdditionalServicesType(
|
220
|
+
delivery_confirmation=None,
|
221
|
+
qr_code=None,
|
222
|
+
)
|
223
|
+
if any(
|
224
|
+
[
|
225
|
+
options.easyship_delivery_confirmation.state,
|
226
|
+
options.easyship_qr_code.state,
|
227
|
+
]
|
228
|
+
)
|
229
|
+
else None
|
230
|
+
),
|
231
|
+
b13_a_filing=None,
|
232
|
+
buy_label=True,
|
233
|
+
buy_label_synchronous=True,
|
234
|
+
printing_options=easyship.PrintingOptionsType(
|
235
|
+
commercial_invoice="A4",
|
236
|
+
format=label_type.value or "pdf",
|
237
|
+
label="4x6",
|
238
|
+
packing_slip=None,
|
239
|
+
remarks=payload.reference,
|
240
|
+
),
|
241
|
+
units=easyship.UnitsType(
|
242
|
+
dimensions=provider_units.DimensionUnit.map(dimension_unit.name).value,
|
243
|
+
weight=provider_units.WeightUnit.map(weight_unit.name).value,
|
244
|
+
),
|
245
|
+
),
|
246
|
+
parcels=[
|
247
|
+
easyship.ParcelType(
|
248
|
+
box=easyship.BoxType(
|
249
|
+
height=package.height.value,
|
250
|
+
length=package.length.value,
|
251
|
+
width=package.width.value,
|
252
|
+
slug=package.parcel.options.get("easyship_box_slug"),
|
253
|
+
),
|
254
|
+
items=[
|
255
|
+
easyship.ItemType(
|
256
|
+
dimensions=None,
|
257
|
+
declared_currency=lib.identity(
|
258
|
+
item.value_currency or options.currency.state or "USD"
|
259
|
+
),
|
260
|
+
origin_country_alpha2=lib.identity(
|
261
|
+
item.origin_country or shipper.country_code
|
262
|
+
),
|
263
|
+
quantity=item.quantity,
|
264
|
+
actual_weight=item.weight,
|
265
|
+
category=item.category or "bags_luggages",
|
266
|
+
declared_customs_value=item.value_amount,
|
267
|
+
description=item.description or item.title or "Item",
|
268
|
+
sku=item.sku or "N/A",
|
269
|
+
hs_code=item.hs_code or "N/A",
|
270
|
+
contains_liquids=item.metadata.get("contains_liquids"),
|
271
|
+
contains_battery_pi966=item.metadata.get(
|
272
|
+
"contains_battery_pi966"
|
273
|
+
),
|
274
|
+
contains_battery_pi967=item.metadata.get(
|
275
|
+
"contains_battery_pi967"
|
276
|
+
),
|
277
|
+
)
|
278
|
+
for item in lib.identity(
|
279
|
+
(package.items if any(package.items) else customs.commodities)
|
280
|
+
if any(package.items) or any(payload.customs or "")
|
281
|
+
else [
|
282
|
+
models.Commodity(
|
283
|
+
title=lib.text(package.description, max=35),
|
284
|
+
quantity=1,
|
285
|
+
value_amount=1.0,
|
286
|
+
)
|
287
|
+
]
|
288
|
+
)
|
289
|
+
],
|
290
|
+
total_actual_weight=package.weight.value,
|
291
|
+
)
|
292
|
+
for package in packages
|
293
|
+
],
|
294
|
+
)
|
295
|
+
|
296
|
+
return lib.Serializable(
|
297
|
+
request,
|
298
|
+
lambda _: lib.to_dict(
|
299
|
+
lib.to_json(_).replace("shipment_request_return", "return")
|
300
|
+
),
|
301
|
+
ctx=dict(
|
302
|
+
courier_id=service,
|
303
|
+
rate_provider=courier.name,
|
304
|
+
label_type=label_type.name or "PDF",
|
305
|
+
),
|
306
|
+
)
|
@@ -0,0 +1,110 @@
|
|
1
|
+
"""Karrio Easyship tracking API implementation."""
|
2
|
+
|
3
|
+
# import karrio.schemas.easyship.tracking_request as easyship
|
4
|
+
# import karrio.schemas.easyship.tracking_response as tracking
|
5
|
+
import karrio.schemas.easyship.shipment_response as shipping
|
6
|
+
|
7
|
+
import typing
|
8
|
+
import karrio.lib as lib
|
9
|
+
import karrio.core.units as units
|
10
|
+
import karrio.core.models as models
|
11
|
+
import karrio.providers.easyship.error as error
|
12
|
+
import karrio.providers.easyship.utils as provider_utils
|
13
|
+
import karrio.providers.easyship.units as provider_units
|
14
|
+
|
15
|
+
|
16
|
+
def parse_tracking_response(
|
17
|
+
_response: lib.Deserializable[typing.List[typing.Tuple[str, dict]]],
|
18
|
+
settings: provider_utils.Settings,
|
19
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
20
|
+
responses = _response.deserialize()
|
21
|
+
|
22
|
+
messages: typing.List[models.Message] = sum(
|
23
|
+
[
|
24
|
+
error.parse_error_response(response, settings, shipment_id=_)
|
25
|
+
for _, response in responses
|
26
|
+
],
|
27
|
+
start=[],
|
28
|
+
)
|
29
|
+
tracking_details = [
|
30
|
+
_extract_details(details, settings)
|
31
|
+
for _, details in responses
|
32
|
+
if details.get("shipment") is not None and any(details["shipment"]["trackings"])
|
33
|
+
]
|
34
|
+
|
35
|
+
return tracking_details, messages
|
36
|
+
|
37
|
+
|
38
|
+
def _extract_details(
|
39
|
+
data: dict,
|
40
|
+
settings: provider_utils.Settings,
|
41
|
+
) -> models.TrackingDetails:
|
42
|
+
details = lib.to_object(shipping.ShipmentType, data["shipment"])
|
43
|
+
master = details.trackings[0]
|
44
|
+
status = next(
|
45
|
+
(
|
46
|
+
status.name
|
47
|
+
for status in list(provider_units.TrackingStatus)
|
48
|
+
if getattr(master, "tracking_state", None) in status.value
|
49
|
+
),
|
50
|
+
provider_units.TrackingStatus.in_transit.name,
|
51
|
+
)
|
52
|
+
|
53
|
+
return models.TrackingDetails(
|
54
|
+
carrier_id=settings.carrier_id,
|
55
|
+
carrier_name=settings.carrier_name,
|
56
|
+
tracking_number=master.tracking_number,
|
57
|
+
delivered=status == "delivered",
|
58
|
+
status=status,
|
59
|
+
events=[
|
60
|
+
models.TrackingEvent(
|
61
|
+
code=str(master.leg_number),
|
62
|
+
date=lib.ftime(details.updated_at, "%Y-%m-%dT%H:%M:%SZ"),
|
63
|
+
time=lib.ftime(details.updated_at, "%Y-%m-%dT%H:%M:%SZ"),
|
64
|
+
description="",
|
65
|
+
)
|
66
|
+
],
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
def tracking_request(
|
71
|
+
payload: models.TrackingRequest,
|
72
|
+
settings: provider_utils.Settings,
|
73
|
+
) -> lib.Serializable:
|
74
|
+
"""Send one or multiple tracking request(s) to Easyship.
|
75
|
+
the payload must match the following schema:
|
76
|
+
{
|
77
|
+
"tracking_numbers": ["123456789"],
|
78
|
+
"shipment_ids": ["ESSG10006002"],
|
79
|
+
"options": {
|
80
|
+
"123456789": {
|
81
|
+
"carrier": "usps",
|
82
|
+
"easyship_shipment_id": "trk_xxxxxxxx", # optional
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
"""
|
87
|
+
|
88
|
+
requests: typing.List[dict] = []
|
89
|
+
|
90
|
+
for tracking_number in payload.tracking_numbers:
|
91
|
+
options = payload.options.get(tracking_number) or {}
|
92
|
+
shipment_id = lib.identity(
|
93
|
+
options.get("easyship_shipment_id")
|
94
|
+
or payload.options.get("easyship_shipment_id")
|
95
|
+
)
|
96
|
+
should_add = lib.identity(
|
97
|
+
shipment_id is not None
|
98
|
+
and not any(_.get("easyship_shipment_id") == shipment_id for _ in requests)
|
99
|
+
)
|
100
|
+
|
101
|
+
if should_add:
|
102
|
+
requests.append(
|
103
|
+
dict(
|
104
|
+
tracking_number=tracking_number,
|
105
|
+
shipment_id=shipment_id,
|
106
|
+
carrier=options.get("carrier"),
|
107
|
+
)
|
108
|
+
)
|
109
|
+
|
110
|
+
return lib.Serializable(requests, lib.to_dict)
|
@@ -0,0 +1,162 @@
|
|
1
|
+
import typing
|
2
|
+
import pathlib
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core.units as units
|
5
|
+
|
6
|
+
METADATA_JSON = lib.load_json(pathlib.Path(__file__).resolve().parent / "metadata.json")
|
7
|
+
EASYSHIP_CARRIER_METADATA = [_ for sublist in METADATA_JSON for _ in sublist]
|
8
|
+
KARRIO_CARRIER_MAPPING = {
|
9
|
+
"canada_post": "canadapost",
|
10
|
+
"dhl": "dhl_express",
|
11
|
+
}
|
12
|
+
|
13
|
+
|
14
|
+
class LabelFormat(lib.StrEnum):
|
15
|
+
"""Carrier specific label format"""
|
16
|
+
|
17
|
+
pdf = "pdf"
|
18
|
+
png = "png"
|
19
|
+
zpl = "zpl"
|
20
|
+
url = "url"
|
21
|
+
|
22
|
+
PDF = pdf
|
23
|
+
PNG = png
|
24
|
+
ZPL = zpl
|
25
|
+
|
26
|
+
|
27
|
+
class Incoterms(lib.StrEnum):
|
28
|
+
"""Carrier specific incoterms"""
|
29
|
+
|
30
|
+
DDU = "DDU"
|
31
|
+
DDP = "DDP"
|
32
|
+
|
33
|
+
|
34
|
+
class DimensionUnit(lib.StrEnum):
|
35
|
+
"""Carrier specific dimension unit"""
|
36
|
+
|
37
|
+
CM = "cm"
|
38
|
+
IN = "in"
|
39
|
+
|
40
|
+
|
41
|
+
class WeightUnit(lib.StrEnum):
|
42
|
+
"""Carrier specific weight unit"""
|
43
|
+
|
44
|
+
LB = "lb"
|
45
|
+
KG = "kg"
|
46
|
+
|
47
|
+
|
48
|
+
class PackagingType(lib.StrEnum):
|
49
|
+
"""Carrier specific packaging type"""
|
50
|
+
|
51
|
+
PACKAGE = "PACKAGE"
|
52
|
+
|
53
|
+
""" Unified Packaging type mapping """
|
54
|
+
envelope = PACKAGE
|
55
|
+
pak = PACKAGE
|
56
|
+
tube = PACKAGE
|
57
|
+
pallet = PACKAGE
|
58
|
+
small_box = PACKAGE
|
59
|
+
medium_box = PACKAGE
|
60
|
+
your_packaging = PACKAGE
|
61
|
+
|
62
|
+
|
63
|
+
class ShippingOption(lib.Enum):
|
64
|
+
"""Carrier specific options"""
|
65
|
+
|
66
|
+
# fmt: off
|
67
|
+
easyship_box_slug = lib.OptionEnum("box_slug")
|
68
|
+
easyship_courier_id = lib.OptionEnum("courier_id")
|
69
|
+
easyship_eei_reference = lib.OptionEnum("eei_reference")
|
70
|
+
easyship_incoterms = lib.OptionEnum("incoterms", Incoterms)
|
71
|
+
easyship_apply_shipping_rules = lib.OptionEnum("apply_shipping_rules", bool)
|
72
|
+
easyship_show_courier_logo_url = lib.OptionEnum("show_courier_logo_url", bool)
|
73
|
+
easyship_allow_courier_fallback = lib.OptionEnum("allow_courier_fallback", bool)
|
74
|
+
easyship_list_unavailable_couriers = lib.OptionEnum("list_unavailable_couriers", bool)
|
75
|
+
easyship_buyer_notes = lib.OptionEnum("buyer_notes")
|
76
|
+
easyship_seller_notes = lib.OptionEnum("seller_notes")
|
77
|
+
easyship_sender_address_id = lib.OptionEnum("sender_address_id")
|
78
|
+
easyship_return_address_id = lib.OptionEnum("return_address_id")
|
79
|
+
# fmt: on
|
80
|
+
|
81
|
+
|
82
|
+
def shipping_options_initializer(
|
83
|
+
options: dict,
|
84
|
+
package_options: units.ShippingOptions = None,
|
85
|
+
) -> units.ShippingOptions:
|
86
|
+
"""
|
87
|
+
Apply default values to the given options.
|
88
|
+
"""
|
89
|
+
|
90
|
+
if package_options is not None:
|
91
|
+
options.update(package_options.content)
|
92
|
+
|
93
|
+
def items_filter(key: str) -> bool:
|
94
|
+
return key in ShippingOption # type: ignore
|
95
|
+
|
96
|
+
return units.ShippingOptions(options, ShippingOption, items_filter=items_filter)
|
97
|
+
|
98
|
+
|
99
|
+
class TrackingStatus(lib.Enum):
|
100
|
+
on_hold = ["on_hold"]
|
101
|
+
delivered = ["delivered"]
|
102
|
+
in_transit = ["in_transit"]
|
103
|
+
delivery_failed = ["delivery_failed"]
|
104
|
+
delivery_delayed = ["delivery_delayed"]
|
105
|
+
out_for_delivery = ["out_for_delivery"]
|
106
|
+
ready_for_pickup = ["ready_for_pickup"]
|
107
|
+
|
108
|
+
|
109
|
+
def to_service_code(service: typing.Dict[str, str]) -> str:
|
110
|
+
return lib.to_slug(
|
111
|
+
f'easyship_{to_carrier_code(service)}_{lib.to_snake_case(service["service_name"])}'
|
112
|
+
)
|
113
|
+
|
114
|
+
|
115
|
+
def to_carrier_code(service: typing.Dict[str, str]) -> str:
|
116
|
+
code = lib.to_slug(service["umbrella_name"])
|
117
|
+
return KARRIO_CARRIER_MAPPING.get(code, code)
|
118
|
+
|
119
|
+
|
120
|
+
def find_courier(search: str):
|
121
|
+
courier: dict = next(
|
122
|
+
(
|
123
|
+
item
|
124
|
+
for item in EASYSHIP_CARRIER_METADATA
|
125
|
+
if item["name"] == search
|
126
|
+
or item["id"] == search
|
127
|
+
or item["umbrella_name"] == search
|
128
|
+
or to_service_code(item) == search
|
129
|
+
or to_carrier_code(item) == search
|
130
|
+
),
|
131
|
+
{},
|
132
|
+
)
|
133
|
+
if courier:
|
134
|
+
return ShippingCourierID.map(to_carrier_code(courier))
|
135
|
+
|
136
|
+
return ShippingCourierID.map(search)
|
137
|
+
|
138
|
+
|
139
|
+
ShippingService = lib.StrEnum(
|
140
|
+
"ShippingService",
|
141
|
+
{
|
142
|
+
to_service_code(service): service["service_name"]
|
143
|
+
for service in EASYSHIP_CARRIER_METADATA
|
144
|
+
},
|
145
|
+
)
|
146
|
+
|
147
|
+
ShippingServiceID = lib.StrEnum(
|
148
|
+
"ShippingServiceID",
|
149
|
+
{service["id"]: to_service_code(service) for service in EASYSHIP_CARRIER_METADATA},
|
150
|
+
)
|
151
|
+
|
152
|
+
ShippingCourierID = lib.StrEnum(
|
153
|
+
"ShippingCourierID",
|
154
|
+
{
|
155
|
+
to_carrier_code(courier): courier["name"]
|
156
|
+
for courier in {
|
157
|
+
_["umbrella_name"]: _ for _ in EASYSHIP_CARRIER_METADATA
|
158
|
+
}.values()
|
159
|
+
},
|
160
|
+
)
|
161
|
+
|
162
|
+
setattr(ShippingCourierID, "find", find_courier)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
import base64
|
2
|
+
import datetime
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core as core
|
5
|
+
import karrio.core.errors as errors
|
6
|
+
|
7
|
+
|
8
|
+
class Settings(core.Settings):
|
9
|
+
"""Easyship connection settings."""
|
10
|
+
|
11
|
+
# Add carrier specific api connection properties here
|
12
|
+
access_token: str
|
13
|
+
|
14
|
+
@property
|
15
|
+
def carrier_name(self):
|
16
|
+
return "easyship"
|
17
|
+
|
18
|
+
@property
|
19
|
+
def server_url(self):
|
20
|
+
return "https://api.easyship.com"
|
21
|
+
|
22
|
+
# """uncomment the following code block to expose a carrier tracking url."""
|
23
|
+
# @property
|
24
|
+
# def tracking_url(self):
|
25
|
+
# return "https://www.carrier.com/tracking?tracking-id={}"
|
26
|
+
|
27
|
+
# """uncomment the following code block to implement the Basic auth."""
|
28
|
+
# @property
|
29
|
+
# def authorization(self):
|
30
|
+
# pair = "%s:%s" % (self.username, self.password)
|
31
|
+
# return base64.b64encode(pair.encode("utf-8")).decode("ascii")
|
32
|
+
|
33
|
+
@property
|
34
|
+
def connection_config(self) -> lib.units.Options:
|
35
|
+
return lib.to_connection_config(
|
36
|
+
self.config or {},
|
37
|
+
option_type=ConnectionConfig,
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
# """uncomment the following code block to implement the oauth login."""
|
42
|
+
# @property
|
43
|
+
# def access_token(self):
|
44
|
+
# """Retrieve the access_token using the client_id|client_secret pair
|
45
|
+
# or collect it from the cache if an unexpired access_token exist.
|
46
|
+
# """
|
47
|
+
# cache_key = f"{self.carrier_name}|{self.client_id}|{self.client_secret}"
|
48
|
+
# now = datetime.datetime.now() + datetime.timedelta(minutes=30)
|
49
|
+
|
50
|
+
# auth = self.connection_cache.get(cache_key) or {}
|
51
|
+
# token = auth.get("access_token")
|
52
|
+
# expiry = lib.to_date(auth.get("expiry"), current_format="%Y-%m-%d %H:%M:%S")
|
53
|
+
|
54
|
+
# if token is not None and expiry is not None and expiry > now:
|
55
|
+
# return token
|
56
|
+
|
57
|
+
# self.connection_cache.set(cache_key, lambda: login(self))
|
58
|
+
# new_auth = self.connection_cache.get(cache_key)
|
59
|
+
|
60
|
+
# return new_auth["access_token"]
|
61
|
+
|
62
|
+
# """uncomment the following code block to implement the oauth login."""
|
63
|
+
# def login(settings: Settings):
|
64
|
+
# import karrio.providers.easyship.error as error
|
65
|
+
|
66
|
+
# result = lib.request(
|
67
|
+
# url=f"{settings.server_url}/oauth/token",
|
68
|
+
# method="POST",
|
69
|
+
# headers={"content-Type": "application/x-www-form-urlencoded"},
|
70
|
+
# data=lib.to_query_string(
|
71
|
+
# dict(
|
72
|
+
# grant_type="client_credentials",
|
73
|
+
# client_id=settings.client_id,
|
74
|
+
# client_secret=settings.client_secret,
|
75
|
+
# )
|
76
|
+
# ),
|
77
|
+
# )
|
78
|
+
|
79
|
+
# response = lib.to_dict(result)
|
80
|
+
# messages = error.parse_error_response(response, settings)
|
81
|
+
|
82
|
+
# if any(messages):
|
83
|
+
# raise errors.ParsedMessagesError(messages)
|
84
|
+
|
85
|
+
# expiry = datetime.datetime.now() + datetime.timedelta(
|
86
|
+
# seconds=float(response.get("expires_in", 0))
|
87
|
+
# )
|
88
|
+
# return {**response, "expiry": lib.fdatetime(expiry)}
|
89
|
+
|
90
|
+
|
91
|
+
class ConnectionConfig(lib.Enum):
|
92
|
+
"""Carrier specific connection configs"""
|
93
|
+
|
94
|
+
platform_name = lib.OptionEnum("platform_name")
|
95
|
+
apply_shipping_rules = lib.OptionEnum("apply_shipping_rules", bool)
|
96
|
+
allow_courier_fallback = lib.OptionEnum("allow_courier_fallback", bool)
|
97
|
+
shipping_options = lib.OptionEnum("shipping_options", list)
|
98
|
+
shipping_services = lib.OptionEnum("shipping_services", list)
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import attr
|
2
|
+
import jstruct
|
3
|
+
import typing
|
4
|
+
|
5
|
+
|
6
|
+
@attr.s(auto_attribs=True)
|
7
|
+
class ErrorType:
|
8
|
+
code: typing.Optional[str] = None
|
9
|
+
details: typing.Optional[typing.List[str]] = None
|
10
|
+
message: typing.Optional[str] = None
|
11
|
+
request_id: typing.Optional[str] = None
|
12
|
+
type: typing.Optional[str] = None
|
13
|
+
|
14
|
+
|
15
|
+
@attr.s(auto_attribs=True)
|
16
|
+
class ErrorResponseType:
|
17
|
+
error: typing.Optional[ErrorType] = jstruct.JStruct[ErrorType]
|