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,3 @@
1
+ from karrio.mappers.easypost.mapper import Mapper
2
+ from karrio.mappers.easypost.proxy import Proxy
3
+ from karrio.mappers.easypost.settings import Settings
@@ -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,8 @@
1
+ from karrio.providers.easypost.shipment.create import (
2
+ parse_shipment_response,
3
+ shipment_request,
4
+ )
5
+ from karrio.providers.easypost.shipment.cancel import (
6
+ parse_shipment_cancel_response,
7
+ shipment_cancel_request,
8
+ )
@@ -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)