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,67 @@
|
|
1
|
+
from typing import List, Tuple
|
2
|
+
from karrio.api.mapper import Mapper as BaseMapper
|
3
|
+
from karrio.mappers.easypost.settings import Settings
|
4
|
+
from karrio.core.utils.serializable import Deserializable, Serializable
|
5
|
+
from karrio.core.models import (
|
6
|
+
RateRequest,
|
7
|
+
ShipmentRequest,
|
8
|
+
ShipmentDetails,
|
9
|
+
ShipmentCancelRequest,
|
10
|
+
RateDetails,
|
11
|
+
Message,
|
12
|
+
ConfirmationDetails,
|
13
|
+
TrackingDetails,
|
14
|
+
TrackingRequest,
|
15
|
+
)
|
16
|
+
from karrio.providers.easypost import (
|
17
|
+
parse_shipment_cancel_response,
|
18
|
+
parse_tracking_response,
|
19
|
+
parse_shipment_response,
|
20
|
+
parse_rate_response,
|
21
|
+
shipment_cancel_request,
|
22
|
+
tracking_request,
|
23
|
+
shipment_request,
|
24
|
+
rate_request,
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
class Mapper(BaseMapper):
|
29
|
+
settings: Settings
|
30
|
+
|
31
|
+
# Request Mappers
|
32
|
+
|
33
|
+
def create_rate_request(self, payload: RateRequest) -> Serializable:
|
34
|
+
return rate_request(payload, self.settings)
|
35
|
+
|
36
|
+
def create_shipment_request(self, payload: ShipmentRequest) -> Serializable:
|
37
|
+
return shipment_request(payload, self.settings)
|
38
|
+
|
39
|
+
def create_cancel_shipment_request(
|
40
|
+
self, payload: ShipmentCancelRequest
|
41
|
+
) -> Serializable:
|
42
|
+
return shipment_cancel_request(payload, self.settings)
|
43
|
+
|
44
|
+
def create_tracking_request(self, payload: TrackingRequest) -> Serializable:
|
45
|
+
return tracking_request(payload, self.settings)
|
46
|
+
|
47
|
+
# Response Parsers
|
48
|
+
|
49
|
+
def parse_rate_response(
|
50
|
+
self, response: Deserializable
|
51
|
+
) -> Tuple[List[RateDetails], List[Message]]:
|
52
|
+
return parse_rate_response(response, self.settings)
|
53
|
+
|
54
|
+
def parse_shipment_response(
|
55
|
+
self, response: Deserializable
|
56
|
+
) -> Tuple[ShipmentDetails, List[Message]]:
|
57
|
+
return parse_shipment_response(response, self.settings)
|
58
|
+
|
59
|
+
def parse_cancel_shipment_response(
|
60
|
+
self, response: Deserializable
|
61
|
+
) -> Tuple[ConfirmationDetails, List[Message]]:
|
62
|
+
return parse_shipment_cancel_response(response, self.settings)
|
63
|
+
|
64
|
+
def parse_tracking_response(
|
65
|
+
self, response: Deserializable
|
66
|
+
) -> Tuple[List[TrackingDetails], List[Message]]:
|
67
|
+
return parse_tracking_response(response, self.settings)
|
@@ -0,0 +1,100 @@
|
|
1
|
+
import typing
|
2
|
+
import karrio.lib as lib
|
3
|
+
import karrio.api.proxy as base
|
4
|
+
import karrio.mappers.easypost.settings as provider_settings
|
5
|
+
|
6
|
+
|
7
|
+
class Proxy(base.Proxy):
|
8
|
+
settings: provider_settings.Settings
|
9
|
+
|
10
|
+
def get_rates(self, request: lib.Serializable) -> lib.Deserializable:
|
11
|
+
response = self._send_request(
|
12
|
+
path="/shipments",
|
13
|
+
request=lib.Serializable(request.serialize(), lib.to_json),
|
14
|
+
)
|
15
|
+
|
16
|
+
return lib.Deserializable(response, lib.to_dict)
|
17
|
+
|
18
|
+
def create_shipment(self, request: lib.Serializable) -> lib.Deserializable:
|
19
|
+
payload = request.serialize()
|
20
|
+
|
21
|
+
def create(request) -> str:
|
22
|
+
response = lib.to_dict(
|
23
|
+
self._send_request(
|
24
|
+
path="/shipments", request=lib.Serializable(request, lib.to_json)
|
25
|
+
)
|
26
|
+
)
|
27
|
+
|
28
|
+
if "error" in response:
|
29
|
+
return response
|
30
|
+
|
31
|
+
# retrieve rate with the selected service.
|
32
|
+
rate_id = next(
|
33
|
+
(
|
34
|
+
rate["id"]
|
35
|
+
for rate in response.get("rates", [])
|
36
|
+
if rate["service"] == payload["service"]
|
37
|
+
),
|
38
|
+
None,
|
39
|
+
)
|
40
|
+
data = lib.to_dict(
|
41
|
+
{
|
42
|
+
"rate": {"id": rate_id},
|
43
|
+
"insurance": payload.get("insurance"),
|
44
|
+
}
|
45
|
+
)
|
46
|
+
|
47
|
+
if rate_id is None:
|
48
|
+
raise Exception("No rate found for the given service.")
|
49
|
+
|
50
|
+
return self._send_request(
|
51
|
+
path=f"/shipments/{response['id']}/buy",
|
52
|
+
request=lib.Serializable(data, lib.to_json),
|
53
|
+
)
|
54
|
+
|
55
|
+
response = create(payload["data"])
|
56
|
+
return lib.Deserializable(response, lib.to_dict)
|
57
|
+
|
58
|
+
def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable:
|
59
|
+
response = self._send_request(path=f"/shipments/{request.serialize()}/refund")
|
60
|
+
|
61
|
+
return lib.Deserializable(response, lib.to_dict)
|
62
|
+
|
63
|
+
def get_tracking(self, requests: lib.Serializable) -> lib.Deserializable:
|
64
|
+
track = lambda request: (
|
65
|
+
request["tracking_code"],
|
66
|
+
self._send_request(
|
67
|
+
**(
|
68
|
+
dict(
|
69
|
+
path="/trackers", request=lib.Serializable(request, lib.to_json)
|
70
|
+
)
|
71
|
+
if request.get("tracker_id") is None
|
72
|
+
else dict(path=f"/trackers/{request['tracker_id']}", method="GET")
|
73
|
+
)
|
74
|
+
),
|
75
|
+
)
|
76
|
+
|
77
|
+
responses: typing.List[typing.Tuple[str, str]] = lib.run_asynchronously(
|
78
|
+
track, requests.serialize()
|
79
|
+
)
|
80
|
+
return lib.Deserializable(
|
81
|
+
responses,
|
82
|
+
lambda res: [(key, lib.to_dict(response)) for key, response in res],
|
83
|
+
)
|
84
|
+
|
85
|
+
def _send_request(
|
86
|
+
self, path: str, request: lib.Serializable = None, method: str = "POST"
|
87
|
+
) -> str:
|
88
|
+
data: dict = dict(data=request.serialize()) if request is not None else dict()
|
89
|
+
return lib.request(
|
90
|
+
**{
|
91
|
+
"url": f"{self.settings.server_url}{path}",
|
92
|
+
"trace": self.trace_as("json"),
|
93
|
+
"method": method,
|
94
|
+
"headers": {
|
95
|
+
"Content-Type": "application/json",
|
96
|
+
"Authorization": f"Basic {self.settings.authorization}",
|
97
|
+
},
|
98
|
+
**data,
|
99
|
+
}
|
100
|
+
)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"""Karrio EasyPost connection settings."""
|
2
|
+
|
3
|
+
import attr
|
4
|
+
from karrio.providers.easypost.utils import Settings as BaseSettings
|
5
|
+
|
6
|
+
|
7
|
+
@attr.s(auto_attribs=True)
|
8
|
+
class Settings(BaseSettings):
|
9
|
+
"""EasyPost connection settings."""
|
10
|
+
|
11
|
+
api_key: str
|
12
|
+
|
13
|
+
id: str = None
|
14
|
+
test_mode: bool = False
|
15
|
+
carrier_id: str = "easypost"
|
16
|
+
account_country_code: str = None
|
17
|
+
metadata: dict = {}
|
18
|
+
config: dict = {}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import karrio.core.metadata as metadata
|
2
|
+
import karrio.mappers.easypost as mappers
|
3
|
+
import karrio.providers.easypost.units as units
|
4
|
+
|
5
|
+
|
6
|
+
METADATA = metadata.PluginMetadata(
|
7
|
+
status="production-ready",
|
8
|
+
id="easypost",
|
9
|
+
label="EasyPost",
|
10
|
+
is_hub=True,
|
11
|
+
# Integrations
|
12
|
+
Mapper=mappers.Mapper,
|
13
|
+
Proxy=mappers.Proxy,
|
14
|
+
Settings=mappers.Settings,
|
15
|
+
# Data Units
|
16
|
+
options=units.ShippingOption,
|
17
|
+
services=units.Service,
|
18
|
+
hub_carriers=units.CarrierId.to_dict(),
|
19
|
+
)
|
@@ -0,0 +1,8 @@
|
|
1
|
+
from karrio.providers.easypost.rate import parse_rate_response, rate_request
|
2
|
+
from karrio.providers.easypost.shipment import (
|
3
|
+
parse_shipment_cancel_response,
|
4
|
+
parse_shipment_response,
|
5
|
+
shipment_cancel_request,
|
6
|
+
shipment_request,
|
7
|
+
)
|
8
|
+
from karrio.providers.easypost.tracking import parse_tracking_response, tracking_request
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import typing
|
2
|
+
import karrio.lib as lib
|
3
|
+
import karrio.core.models as models
|
4
|
+
import karrio.providers.easypost.utils as provider_utils
|
5
|
+
|
6
|
+
|
7
|
+
def parse_error_response(
|
8
|
+
response: dict, settings: provider_utils.Settings, **kwargs
|
9
|
+
) -> typing.List[models.Message]:
|
10
|
+
errors = [
|
11
|
+
*response.get("messages", []),
|
12
|
+
*([response.get("error")] if "error" in response else []),
|
13
|
+
]
|
14
|
+
|
15
|
+
return [
|
16
|
+
models.Message(
|
17
|
+
carrier_id=settings.carrier_id,
|
18
|
+
carrier_name=settings.carrier_name,
|
19
|
+
code=(error.get("code") or error.get("type")),
|
20
|
+
message=error.get("message"),
|
21
|
+
details=lib.to_dict(
|
22
|
+
{
|
23
|
+
"errors": error.get("errors"),
|
24
|
+
"carrier": error.get("carrier"),
|
25
|
+
"carrier_account_id": error.get("carrier_account_id"),
|
26
|
+
**kwargs,
|
27
|
+
}
|
28
|
+
),
|
29
|
+
)
|
30
|
+
for error in errors
|
31
|
+
]
|
@@ -0,0 +1,194 @@
|
|
1
|
+
import karrio.schemas.easypost.shipment_request as easypost
|
2
|
+
from karrio.schemas.easypost.shipments_response import Shipment
|
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_rate_response(
|
14
|
+
_response: lib.Deserializable[dict],
|
15
|
+
settings: provider_utils.Settings,
|
16
|
+
) -> typing.Tuple[models.RateDetails, typing.List[models.Message]]:
|
17
|
+
response = _response.deserialize()
|
18
|
+
errors = provider_error.parse_error_response(response, settings)
|
19
|
+
rates = _extract_details(response, settings) if "error" not in response else []
|
20
|
+
|
21
|
+
return rates, errors
|
22
|
+
|
23
|
+
|
24
|
+
def _extract_details(
|
25
|
+
response: dict,
|
26
|
+
settings: provider_utils.Settings,
|
27
|
+
) -> typing.List[models.RateDetails]:
|
28
|
+
rates = lib.to_object(Shipment, response).rates
|
29
|
+
|
30
|
+
return [
|
31
|
+
(
|
32
|
+
lambda rate_provider, service, service_name: models.RateDetails(
|
33
|
+
carrier_id=settings.carrier_id,
|
34
|
+
carrier_name=settings.carrier_name,
|
35
|
+
service=service,
|
36
|
+
currency=rate.currency,
|
37
|
+
total_charge=lib.to_decimal(rate.rate),
|
38
|
+
transit_days=rate.delivery_days,
|
39
|
+
meta=dict(
|
40
|
+
service_name=service_name,
|
41
|
+
rate_provider=rate_provider,
|
42
|
+
),
|
43
|
+
)
|
44
|
+
)(*provider_units.Service.info(rate.service, rate.carrier))
|
45
|
+
for rate in rates
|
46
|
+
]
|
47
|
+
|
48
|
+
|
49
|
+
def rate_request(payload: models.RateRequest, _) -> lib.Serializable:
|
50
|
+
shipper = lib.to_address(payload.shipper)
|
51
|
+
recipient = lib.to_address(payload.recipient)
|
52
|
+
return_address = lib.to_address(payload.return_address)
|
53
|
+
billing_address = lib.to_address(payload.billing_address)
|
54
|
+
package = lib.to_packages(
|
55
|
+
payload.parcels,
|
56
|
+
package_option_type=provider_units.ShippingOption,
|
57
|
+
).single
|
58
|
+
options = lib.to_shipping_options(
|
59
|
+
payload,
|
60
|
+
package_options=package.options,
|
61
|
+
initializer=provider_units.shipping_options_initializer,
|
62
|
+
)
|
63
|
+
is_intl = shipper.country_code != recipient.country_code
|
64
|
+
customs = (
|
65
|
+
models.Customs(
|
66
|
+
commodities=(
|
67
|
+
package.parcel.items
|
68
|
+
if any(package.parcel.items)
|
69
|
+
else [
|
70
|
+
models.Commodity(
|
71
|
+
sku="0000",
|
72
|
+
quantity=1,
|
73
|
+
weight=package.weight.value,
|
74
|
+
weight_unit=package.weight_unit.value,
|
75
|
+
)
|
76
|
+
]
|
77
|
+
)
|
78
|
+
)
|
79
|
+
if is_intl
|
80
|
+
else None
|
81
|
+
)
|
82
|
+
|
83
|
+
requests = easypost.ShipmentRequest(
|
84
|
+
shipment=easypost.Shipment(
|
85
|
+
reference=payload.reference,
|
86
|
+
to_address=easypost.Address(
|
87
|
+
company=recipient.company_name,
|
88
|
+
street1=recipient.street,
|
89
|
+
street2=recipient.address_line2,
|
90
|
+
city=recipient.city,
|
91
|
+
state=recipient.state_code,
|
92
|
+
zip=recipient.postal_code,
|
93
|
+
country=recipient.country_code,
|
94
|
+
residential=recipient.residential,
|
95
|
+
name=recipient.person_name,
|
96
|
+
phone=recipient.phone_number,
|
97
|
+
email=recipient.email,
|
98
|
+
federal_tax_id=recipient.federal_tax_id,
|
99
|
+
state_tax_id=recipient.state_tax_id,
|
100
|
+
),
|
101
|
+
from_address=easypost.Address(
|
102
|
+
company=shipper.company_name,
|
103
|
+
street1=shipper.street,
|
104
|
+
street2=shipper.address_line2,
|
105
|
+
city=shipper.city,
|
106
|
+
state=shipper.state_code,
|
107
|
+
zip=shipper.postal_code,
|
108
|
+
country=shipper.country_code,
|
109
|
+
residential=shipper.residential,
|
110
|
+
name=shipper.person_name,
|
111
|
+
phone=shipper.phone_number,
|
112
|
+
email=shipper.email,
|
113
|
+
federal_tax_id=shipper.federal_tax_id,
|
114
|
+
state_tax_id=shipper.state_tax_id,
|
115
|
+
),
|
116
|
+
return_address=lib.identity(
|
117
|
+
easypost.Address(
|
118
|
+
company=return_address.company_name,
|
119
|
+
street1=return_address.street,
|
120
|
+
street2=return_address.address_line2,
|
121
|
+
city=return_address.city,
|
122
|
+
state=return_address.state_code,
|
123
|
+
zip=return_address.postal_code,
|
124
|
+
country=return_address.country_code,
|
125
|
+
residential=return_address.residential,
|
126
|
+
name=return_address.person_name,
|
127
|
+
phone=return_address.phone_number,
|
128
|
+
email=return_address.email,
|
129
|
+
federal_tax_id=return_address.federal_tax_id,
|
130
|
+
state_tax_id=return_address.state_tax_id,
|
131
|
+
)
|
132
|
+
if payload.return_address
|
133
|
+
else None
|
134
|
+
),
|
135
|
+
buyer_address=lib.identity(
|
136
|
+
easypost.Address(
|
137
|
+
company=billing_address.company_name,
|
138
|
+
street1=billing_address.street,
|
139
|
+
street2=billing_address.address_line2,
|
140
|
+
city=billing_address.city,
|
141
|
+
state=billing_address.state_code,
|
142
|
+
zip=billing_address.postal_code,
|
143
|
+
country=billing_address.country_code,
|
144
|
+
residential=billing_address.residential,
|
145
|
+
name=billing_address.person_name,
|
146
|
+
phone=billing_address.phone_number,
|
147
|
+
email=billing_address.email,
|
148
|
+
federal_tax_id=billing_address.federal_tax_id,
|
149
|
+
state_tax_id=billing_address.state_tax_id,
|
150
|
+
)
|
151
|
+
if payload.billing_address
|
152
|
+
else None
|
153
|
+
),
|
154
|
+
parcel=easypost.Parcel(
|
155
|
+
length=package.length.IN,
|
156
|
+
width=package.width.IN,
|
157
|
+
height=package.height.IN,
|
158
|
+
weight=package.weight.OZ,
|
159
|
+
predefined_package=provider_units.PackagingType.map(
|
160
|
+
package.packaging_type
|
161
|
+
).value,
|
162
|
+
),
|
163
|
+
options={option.code: option.state for _, option in options.items()},
|
164
|
+
customs_info=(
|
165
|
+
easypost.CustomsInfo(
|
166
|
+
contents_type="other",
|
167
|
+
customs_certify=True,
|
168
|
+
customs_signer=shipper.person_name,
|
169
|
+
customs_items=[
|
170
|
+
easypost.CustomsItem(
|
171
|
+
description=lib.text(
|
172
|
+
item.description or item.title or "N/A"
|
173
|
+
),
|
174
|
+
origin_country=item.origin_country,
|
175
|
+
quantity=item.quantity,
|
176
|
+
value=item.value_amount,
|
177
|
+
weight=units.Weight(item.weight, item.weight_unit).OZ,
|
178
|
+
code=item.sku,
|
179
|
+
manufacturer=None,
|
180
|
+
currency=item.value_currency,
|
181
|
+
eccn=(item.metadata or {}).get("eccn"),
|
182
|
+
printed_commodity_identifier=(item.sku or item.id),
|
183
|
+
hs_tariff_number=item.hs_code,
|
184
|
+
)
|
185
|
+
for item in customs.commodities
|
186
|
+
],
|
187
|
+
)
|
188
|
+
if customs
|
189
|
+
else None
|
190
|
+
),
|
191
|
+
)
|
192
|
+
)
|
193
|
+
|
194
|
+
return lib.Serializable(requests, lib.to_dict)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import typing
|
2
|
+
import karrio.lib as lib
|
3
|
+
import karrio.core.units as units
|
4
|
+
import karrio.core.models as models
|
5
|
+
import karrio.providers.easypost.error as provider_error
|
6
|
+
import karrio.providers.easypost.units as provider_units
|
7
|
+
import karrio.providers.easypost.utils as provider_utils
|
8
|
+
|
9
|
+
|
10
|
+
def parse_shipment_cancel_response(
|
11
|
+
_response: lib.Deserializable[dict],
|
12
|
+
settings: provider_utils.Settings,
|
13
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
14
|
+
response = _response.deserialize()
|
15
|
+
status = response.get("status")
|
16
|
+
errors = provider_error.parse_error_response(response, settings)
|
17
|
+
|
18
|
+
details = models.ConfirmationDetails(
|
19
|
+
carrier_id=settings.carrier_id,
|
20
|
+
carrier_name=settings.carrier_name,
|
21
|
+
success=status != "rejected",
|
22
|
+
operation="cancel shipment",
|
23
|
+
)
|
24
|
+
|
25
|
+
return details, errors
|
26
|
+
|
27
|
+
|
28
|
+
def shipment_cancel_request(
|
29
|
+
payload: models.ShipmentCancelRequest, _
|
30
|
+
) -> lib.Serializable:
|
31
|
+
return lib.Serializable(payload.shipment_identifier)
|