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.
Files changed (39) hide show
  1. karrio/mappers/easyship/__init__.py +3 -0
  2. karrio/mappers/easyship/mapper.py +94 -0
  3. karrio/mappers/easyship/proxy.py +155 -0
  4. karrio/mappers/easyship/settings.py +20 -0
  5. karrio/plugins/easyship/__init__.py +20 -0
  6. karrio/providers/easyship/__init__.py +25 -0
  7. karrio/providers/easyship/error.py +38 -0
  8. karrio/providers/easyship/manifest.py +72 -0
  9. karrio/providers/easyship/metadata.json +6985 -0
  10. karrio/providers/easyship/pickup/__init__.py +4 -0
  11. karrio/providers/easyship/pickup/cancel.py +42 -0
  12. karrio/providers/easyship/pickup/create.py +86 -0
  13. karrio/providers/easyship/pickup/update.py +88 -0
  14. karrio/providers/easyship/rate.py +215 -0
  15. karrio/providers/easyship/shipment/__init__.py +9 -0
  16. karrio/providers/easyship/shipment/cancel.py +39 -0
  17. karrio/providers/easyship/shipment/create.py +306 -0
  18. karrio/providers/easyship/tracking.py +110 -0
  19. karrio/providers/easyship/units.py +162 -0
  20. karrio/providers/easyship/utils.py +98 -0
  21. karrio/schemas/easyship/__init__.py +0 -0
  22. karrio/schemas/easyship/error_response.py +17 -0
  23. karrio/schemas/easyship/manifest_request.py +9 -0
  24. karrio/schemas/easyship/manifest_response.py +31 -0
  25. karrio/schemas/easyship/pickup_cancel_response.py +19 -0
  26. karrio/schemas/easyship/pickup_request.py +13 -0
  27. karrio/schemas/easyship/pickup_response.py +85 -0
  28. karrio/schemas/easyship/rate_request.py +100 -0
  29. karrio/schemas/easyship/rate_response.py +124 -0
  30. karrio/schemas/easyship/shipment_cancel_response.py +19 -0
  31. karrio/schemas/easyship/shipment_request.py +147 -0
  32. karrio/schemas/easyship/shipment_response.py +273 -0
  33. karrio/schemas/easyship/tracking_request.py +49 -0
  34. karrio/schemas/easyship/tracking_response.py +54 -0
  35. karrio_easyship-2025.5rc1.dist-info/METADATA +45 -0
  36. karrio_easyship-2025.5rc1.dist-info/RECORD +39 -0
  37. karrio_easyship-2025.5rc1.dist-info/WHEEL +5 -0
  38. karrio_easyship-2025.5rc1.dist-info/entry_points.txt +2 -0
  39. 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]