karrio-amazon-shipping 2025.5.7__py3-none-any.whl → 2026.1.1__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/amazon_shipping/mapper.py +47 -48
- karrio/mappers/amazon_shipping/proxy.py +115 -61
- karrio/mappers/amazon_shipping/settings.py +22 -9
- karrio/plugins/amazon_shipping/__init__.py +5 -2
- karrio/providers/amazon_shipping/error.py +22 -13
- karrio/providers/amazon_shipping/rate.py +126 -50
- karrio/providers/amazon_shipping/shipment/cancel.py +32 -19
- karrio/providers/amazon_shipping/shipment/create.py +126 -69
- karrio/providers/amazon_shipping/tracking.py +76 -23
- karrio/providers/amazon_shipping/units.py +125 -12
- karrio/providers/amazon_shipping/utils.py +55 -26
- karrio/schemas/amazon_shipping/cancel_shipment_response.py +8 -0
- karrio/schemas/amazon_shipping/one_click_shipment_request.py +61 -0
- karrio/schemas/amazon_shipping/one_click_shipment_response.py +60 -0
- karrio/schemas/amazon_shipping/purchase_shipment_request.py +15 -64
- karrio/schemas/amazon_shipping/purchase_shipment_response.py +18 -23
- karrio/schemas/amazon_shipping/rate_request.py +38 -24
- karrio/schemas/amazon_shipping/rate_response.py +70 -7
- karrio/schemas/amazon_shipping/tracking_response.py +24 -1
- {karrio_amazon_shipping-2025.5.7.dist-info → karrio_amazon_shipping-2026.1.1.dist-info}/METADATA +1 -1
- karrio_amazon_shipping-2026.1.1.dist-info/RECORD +29 -0
- {karrio_amazon_shipping-2025.5.7.dist-info → karrio_amazon_shipping-2026.1.1.dist-info}/top_level.txt +1 -0
- karrio/schemas/amazon_shipping/create_shipment_request.py +0 -69
- karrio/schemas/amazon_shipping/create_shipment_response.py +0 -37
- karrio/schemas/amazon_shipping/purchase_label_request.py +0 -15
- karrio/schemas/amazon_shipping/purchase_label_response.py +0 -56
- karrio/schemas/amazon_shipping/shipping_label.py +0 -15
- karrio_amazon_shipping-2025.5.7.dist-info/RECORD +0 -31
- {karrio_amazon_shipping-2025.5.7.dist-info → karrio_amazon_shipping-2026.1.1.dist-info}/WHEEL +0 -0
- {karrio_amazon_shipping-2025.5.7.dist-info → karrio_amazon_shipping-2026.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,67 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
from karrio.api.mapper import Mapper as BaseMapper
|
|
3
|
-
from karrio.mappers.amazon_shipping.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.amazon_shipping 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
|
-
)
|
|
1
|
+
"""Karrio Amazon Shipping mapper."""
|
|
26
2
|
|
|
3
|
+
import typing
|
|
4
|
+
import karrio.lib as lib
|
|
5
|
+
import karrio.api.mapper as mapper
|
|
6
|
+
import karrio.core.models as models
|
|
7
|
+
import karrio.providers.amazon_shipping as provider
|
|
8
|
+
import karrio.mappers.amazon_shipping.settings as provider_settings
|
|
27
9
|
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
|
|
11
|
+
class Mapper(mapper.Mapper):
|
|
12
|
+
"""Amazon Shipping API mapper."""
|
|
13
|
+
|
|
14
|
+
settings: provider_settings.Settings
|
|
30
15
|
|
|
31
16
|
# Request Mappers
|
|
32
17
|
|
|
33
|
-
def create_rate_request(
|
|
34
|
-
|
|
18
|
+
def create_rate_request(
|
|
19
|
+
self,
|
|
20
|
+
payload: models.RateRequest,
|
|
21
|
+
) -> lib.Serializable:
|
|
22
|
+
return provider.rate_request(payload, self.settings)
|
|
35
23
|
|
|
36
|
-
def create_shipment_request(
|
|
37
|
-
|
|
24
|
+
def create_shipment_request(
|
|
25
|
+
self,
|
|
26
|
+
payload: models.ShipmentRequest,
|
|
27
|
+
) -> lib.Serializable:
|
|
28
|
+
return provider.shipment_request(payload, self.settings)
|
|
38
29
|
|
|
39
30
|
def create_cancel_shipment_request(
|
|
40
|
-
self,
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
self,
|
|
32
|
+
payload: models.ShipmentCancelRequest,
|
|
33
|
+
) -> lib.Serializable:
|
|
34
|
+
return provider.shipment_cancel_request(payload, self.settings)
|
|
43
35
|
|
|
44
|
-
def create_tracking_request(
|
|
45
|
-
|
|
36
|
+
def create_tracking_request(
|
|
37
|
+
self,
|
|
38
|
+
payload: models.TrackingRequest,
|
|
39
|
+
) -> lib.Serializable:
|
|
40
|
+
return provider.tracking_request(payload, self.settings)
|
|
46
41
|
|
|
47
42
|
# Response Parsers
|
|
48
43
|
|
|
49
44
|
def parse_rate_response(
|
|
50
|
-
self,
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
self,
|
|
46
|
+
response: lib.Deserializable,
|
|
47
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
|
48
|
+
return provider.parse_rate_response(response, self.settings)
|
|
53
49
|
|
|
54
50
|
def parse_shipment_response(
|
|
55
|
-
self,
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
self,
|
|
52
|
+
response: lib.Deserializable,
|
|
53
|
+
) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
|
|
54
|
+
return provider.parse_shipment_response(response, self.settings)
|
|
58
55
|
|
|
59
56
|
def parse_cancel_shipment_response(
|
|
60
|
-
self,
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
self,
|
|
58
|
+
response: lib.Deserializable,
|
|
59
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
|
60
|
+
return provider.parse_shipment_cancel_response(response, self.settings)
|
|
63
61
|
|
|
64
62
|
def parse_tracking_response(
|
|
65
|
-
self,
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
self,
|
|
64
|
+
response: lib.Deserializable,
|
|
65
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
|
66
|
+
return provider.parse_tracking_response(response, self.settings)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
"""Karrio Amazon Shipping API proxy."""
|
|
2
|
+
|
|
1
3
|
import typing
|
|
2
4
|
import datetime
|
|
3
|
-
import urllib.parse
|
|
4
5
|
import karrio.lib as lib
|
|
5
6
|
import karrio.api.proxy as proxy
|
|
6
7
|
import karrio.core.errors as errors
|
|
@@ -10,132 +11,185 @@ from karrio.mappers.amazon_shipping.settings import Settings
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class Proxy(proxy.Proxy):
|
|
14
|
+
"""Amazon Shipping SP-API proxy."""
|
|
15
|
+
|
|
13
16
|
settings: Settings
|
|
14
17
|
|
|
15
18
|
def authenticate(self, _=None) -> lib.Deserializable[str]:
|
|
16
|
-
"""
|
|
17
|
-
|
|
19
|
+
"""Obtain access token using LWA OAuth2 refresh token flow.
|
|
20
|
+
|
|
21
|
+
The token is cached and refreshed when expired.
|
|
18
22
|
"""
|
|
19
|
-
cache_key = f"{self.settings.carrier_name}|{self.settings.
|
|
23
|
+
cache_key = f"{self.settings.carrier_name}|{self.settings.client_id}"
|
|
20
24
|
|
|
21
25
|
def get_token():
|
|
22
|
-
query = urllib.parse.urlencode(
|
|
23
|
-
dict(
|
|
24
|
-
developerId=self.settings.developer_id,
|
|
25
|
-
sellingPartnerId=self.settings.seller_id,
|
|
26
|
-
mwsAuthToken=self.settings.mws_auth_token,
|
|
27
|
-
)
|
|
28
|
-
)
|
|
29
26
|
result = lib.request(
|
|
30
|
-
url=
|
|
31
|
-
trace=self.
|
|
27
|
+
url=self.settings.token_url,
|
|
28
|
+
trace=self.trace_as("json"),
|
|
32
29
|
method="POST",
|
|
33
|
-
headers={"
|
|
30
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
31
|
+
data=lib.to_query_string(dict(
|
|
32
|
+
grant_type="refresh_token",
|
|
33
|
+
refresh_token=self.settings.refresh_token,
|
|
34
|
+
client_id=self.settings.client_id,
|
|
35
|
+
client_secret=self.settings.client_secret,
|
|
36
|
+
)),
|
|
34
37
|
max_retries=2,
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
response = lib.to_dict(result)
|
|
38
|
-
messages = provider_error.parse_error_response(response, self.settings)
|
|
39
41
|
|
|
40
|
-
if
|
|
41
|
-
raise errors.ParsedMessagesError(
|
|
42
|
+
if "error" in response:
|
|
43
|
+
raise errors.ParsedMessagesError(
|
|
44
|
+
messages=[
|
|
45
|
+
models.Message(
|
|
46
|
+
carrier_name=self.settings.carrier_name,
|
|
47
|
+
carrier_id=self.settings.carrier_id,
|
|
48
|
+
message=response.get("error_description", response["error"]),
|
|
49
|
+
code=response.get("error"),
|
|
50
|
+
)
|
|
51
|
+
]
|
|
52
|
+
)
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
lambda: response.get("payload", {}).get("authorizationCode")
|
|
46
|
-
)
|
|
47
|
-
if not authorization_code:
|
|
54
|
+
access_token = response.get("access_token")
|
|
55
|
+
if not access_token:
|
|
48
56
|
raise errors.ParsedMessagesError(
|
|
49
57
|
messages=[
|
|
50
58
|
models.Message(
|
|
51
59
|
carrier_name=self.settings.carrier_name,
|
|
52
60
|
carrier_id=self.settings.carrier_id,
|
|
53
|
-
message="Authentication failed: No
|
|
61
|
+
message="Authentication failed: No access token received",
|
|
54
62
|
code="AUTH_ERROR",
|
|
55
63
|
)
|
|
56
64
|
]
|
|
57
65
|
)
|
|
58
66
|
|
|
59
67
|
expiry = datetime.datetime.now() + datetime.timedelta(
|
|
60
|
-
seconds=float(response.get("expires_in",
|
|
68
|
+
seconds=float(response.get("expires_in", 3600))
|
|
61
69
|
)
|
|
62
70
|
|
|
63
71
|
return {
|
|
64
72
|
**response,
|
|
65
73
|
"expiry": lib.fdatetime(expiry),
|
|
66
|
-
"
|
|
74
|
+
"access_token": access_token,
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
token_state = self.settings.connection_cache.thread_safe(
|
|
70
78
|
refresh_func=get_token,
|
|
71
79
|
cache_key=cache_key,
|
|
72
|
-
buffer_minutes=
|
|
73
|
-
token_field="
|
|
80
|
+
buffer_minutes=5,
|
|
81
|
+
token_field="access_token",
|
|
74
82
|
)
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
# Handle both Token object and direct dict from cache
|
|
85
|
+
state = (
|
|
86
|
+
token_state.get_state()
|
|
87
|
+
if hasattr(token_state, "get_state")
|
|
88
|
+
else token_state
|
|
89
|
+
)
|
|
90
|
+
access_token = (
|
|
91
|
+
state.get("access_token")
|
|
92
|
+
if isinstance(state, dict)
|
|
93
|
+
else state
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return lib.Deserializable(access_token)
|
|
77
97
|
|
|
78
98
|
def get_rates(self, request: lib.Serializable) -> lib.Deserializable:
|
|
99
|
+
"""Get shipping rates using the v2 getRates API."""
|
|
79
100
|
response = self._send_request(
|
|
80
|
-
path="/shipping/
|
|
81
|
-
request=
|
|
101
|
+
path="/shipping/v2/shipments/rates",
|
|
102
|
+
request=request,
|
|
82
103
|
)
|
|
83
104
|
|
|
84
105
|
return lib.Deserializable(response, lib.to_dict)
|
|
85
106
|
|
|
86
107
|
def create_shipment(self, request: lib.Serializable) -> lib.Deserializable:
|
|
108
|
+
"""Create shipment using the oneClickShipment API for combined rate + purchase."""
|
|
87
109
|
response = self._send_request(
|
|
88
|
-
path="/shipping/
|
|
89
|
-
request=
|
|
110
|
+
path="/shipping/v2/oneClickShipment",
|
|
111
|
+
request=request,
|
|
90
112
|
)
|
|
91
113
|
|
|
92
114
|
return lib.Deserializable(response, lib.to_dict)
|
|
93
115
|
|
|
94
116
|
def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable:
|
|
117
|
+
"""Cancel shipment using the v2 cancelShipment API."""
|
|
118
|
+
shipment_id = request.serialize()
|
|
95
119
|
response = self._send_request(
|
|
96
|
-
path=f"/shipping/
|
|
120
|
+
path=f"/shipping/v2/shipments/{shipment_id}/cancel",
|
|
121
|
+
request=None,
|
|
122
|
+
method="PUT",
|
|
97
123
|
)
|
|
98
124
|
|
|
99
|
-
return lib.Deserializable(
|
|
125
|
+
return lib.Deserializable(
|
|
126
|
+
response if response.strip() else "{}",
|
|
127
|
+
lib.to_dict,
|
|
128
|
+
)
|
|
100
129
|
|
|
101
130
|
def get_tracking(self, request: lib.Serializable) -> lib.Deserializable:
|
|
131
|
+
"""Get tracking information using the v2 getTracking API."""
|
|
102
132
|
access_token = self.authenticate().deserialize()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
tracking_data = request.serialize()
|
|
134
|
+
|
|
135
|
+
def track(data: dict) -> typing.Tuple[str, str]:
|
|
136
|
+
tracking_id = data.get("tracking_id")
|
|
137
|
+
carrier_id = data.get("carrier_id", "AMZN_US")
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
tracking_id,
|
|
141
|
+
lib.request(
|
|
142
|
+
url=f"{self.settings.server_url}/shipping/v2/tracking",
|
|
143
|
+
trace=self.trace_as("json"),
|
|
144
|
+
method="GET",
|
|
145
|
+
headers=self._get_headers(access_token),
|
|
146
|
+
params=dict(
|
|
147
|
+
trackingId=tracking_id,
|
|
148
|
+
carrierId=carrier_id,
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
)
|
|
116
152
|
|
|
117
153
|
responses: typing.List[typing.Tuple[str, str]] = lib.run_asynchronously(
|
|
118
|
-
track,
|
|
154
|
+
track, tracking_data
|
|
119
155
|
)
|
|
156
|
+
|
|
120
157
|
return lib.Deserializable(
|
|
121
158
|
responses,
|
|
122
159
|
lambda res: [(key, lib.to_dict(response)) for key, response in res],
|
|
123
160
|
)
|
|
124
161
|
|
|
125
162
|
def _send_request(
|
|
126
|
-
self,
|
|
163
|
+
self,
|
|
164
|
+
path: str,
|
|
165
|
+
request: lib.Serializable = None,
|
|
166
|
+
method: str = "POST",
|
|
127
167
|
) -> str:
|
|
168
|
+
"""Send request to Amazon Shipping API."""
|
|
128
169
|
access_token = self.authenticate().deserialize()
|
|
129
|
-
data
|
|
170
|
+
data = dict(data=request.serialize()) if request is not None else {}
|
|
171
|
+
|
|
130
172
|
return lib.request(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"Content-Type": "application/json",
|
|
137
|
-
"x-amz-access-token": access_token,
|
|
138
|
-
},
|
|
139
|
-
**data,
|
|
140
|
-
}
|
|
173
|
+
url=f"{self.settings.server_url}{path}",
|
|
174
|
+
trace=self.trace_as("json"),
|
|
175
|
+
method=method,
|
|
176
|
+
headers=self._get_headers(access_token),
|
|
177
|
+
**data,
|
|
141
178
|
)
|
|
179
|
+
|
|
180
|
+
def _get_headers(self, access_token: str) -> dict:
|
|
181
|
+
"""Get request headers with authentication and business ID."""
|
|
182
|
+
headers = {
|
|
183
|
+
"Content-Type": "application/json",
|
|
184
|
+
"x-amz-access-token": access_token,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Add shipping business ID if configured
|
|
188
|
+
business_id = (
|
|
189
|
+
self.settings.shipping_business_id
|
|
190
|
+
or self.settings.connection_config.shipping_business_id.state
|
|
191
|
+
)
|
|
192
|
+
if business_id:
|
|
193
|
+
headers["x-amzn-shipping-business-id"] = business_id
|
|
194
|
+
|
|
195
|
+
return headers
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
"""Karrio Amazon Shipping connection settings."""
|
|
2
2
|
|
|
3
3
|
import attr
|
|
4
|
-
import
|
|
5
|
-
import karrio.lib as lib
|
|
6
|
-
from karrio.providers.amazon_shipping.utils import Settings as BaseSettings
|
|
4
|
+
import karrio.providers.amazon_shipping.utils as provider_utils
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
@attr.s(auto_attribs=True)
|
|
10
|
-
class Settings(
|
|
11
|
-
"""Amazon Shipping connection settings.
|
|
8
|
+
class Settings(provider_utils.Settings):
|
|
9
|
+
"""Amazon Shipping SP-API connection settings.
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
Credentials for Login with Amazon (LWA) OAuth2 authentication:
|
|
12
|
+
- client_id: LWA application client ID
|
|
13
|
+
- client_secret: LWA application client secret
|
|
14
|
+
- refresh_token: LWA refresh token for the authorized seller
|
|
15
|
+
|
|
16
|
+
Optional configuration:
|
|
17
|
+
- aws_region: AWS region (us-east-1, eu-west-1, us-west-2)
|
|
18
|
+
- shipping_business_id: Amazon business ID header (e.g., AmazonShipping_US)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# Required SP-API credentials
|
|
22
|
+
client_id: str
|
|
23
|
+
client_secret: str
|
|
24
|
+
refresh_token: str
|
|
25
|
+
|
|
26
|
+
# Optional configuration
|
|
16
27
|
aws_region: str = "us-east-1"
|
|
28
|
+
shipping_business_id: str = None
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
# Standard karrio settings
|
|
19
31
|
account_country_code: str = None
|
|
32
|
+
carrier_id: str = "amazon_shipping"
|
|
20
33
|
test_mode: bool = False
|
|
21
34
|
metadata: dict = {}
|
|
22
35
|
config: dict = {}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Karrio Amazon Shipping plugin."""
|
|
2
|
+
|
|
1
3
|
import karrio.core.metadata as metadata
|
|
2
4
|
import karrio.mappers.amazon_shipping as mappers
|
|
3
5
|
import karrio.providers.amazon_shipping.units as units
|
|
@@ -6,12 +8,13 @@ import karrio.providers.amazon_shipping.units as units
|
|
|
6
8
|
METADATA = metadata.PluginMetadata(
|
|
7
9
|
status="beta",
|
|
8
10
|
id="amazon_shipping",
|
|
9
|
-
label="
|
|
11
|
+
label="Amazon Shipping",
|
|
10
12
|
# Integrations
|
|
11
13
|
Mapper=mappers.Mapper,
|
|
12
14
|
Proxy=mappers.Proxy,
|
|
13
15
|
Settings=mappers.Settings,
|
|
14
16
|
# Data Units
|
|
15
|
-
services=units.
|
|
17
|
+
services=units.ShippingService,
|
|
18
|
+
options=units.ShippingOption,
|
|
16
19
|
has_intl_accounts=True,
|
|
17
20
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
"""Karrio Amazon Shipping error parser."""
|
|
2
|
+
|
|
2
3
|
import typing
|
|
3
4
|
import karrio.lib as lib
|
|
4
5
|
import karrio.core.models as models
|
|
@@ -6,24 +7,32 @@ import karrio.providers.amazon_shipping.utils as provider_utils
|
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def parse_error_response(
|
|
9
|
-
response: dict,
|
|
10
|
+
response: dict,
|
|
11
|
+
settings: provider_utils.Settings,
|
|
12
|
+
**kwargs,
|
|
10
13
|
) -> typing.List[models.Message]:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
"""Parse error response from Amazon Shipping API.
|
|
15
|
+
|
|
16
|
+
The v2 API returns errors in the format:
|
|
17
|
+
{
|
|
18
|
+
"errors": [
|
|
19
|
+
{"code": "InvalidRequest", "message": "...", "details": "..."}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
"""
|
|
23
|
+
errors = response.get("errors") or []
|
|
16
24
|
|
|
17
25
|
return [
|
|
18
26
|
models.Message(
|
|
19
27
|
carrier_id=settings.carrier_id,
|
|
20
28
|
carrier_name=settings.carrier_name,
|
|
21
|
-
code=error.code,
|
|
22
|
-
message=error.message,
|
|
29
|
+
code=error.get("code"),
|
|
30
|
+
message=error.get("message"),
|
|
23
31
|
details={
|
|
24
|
-
**
|
|
25
|
-
**({} if error.details
|
|
26
|
-
},
|
|
32
|
+
**kwargs,
|
|
33
|
+
**({"note": error.get("details")} if error.get("details") else {}),
|
|
34
|
+
} or None,
|
|
27
35
|
)
|
|
28
|
-
for error in errors
|
|
36
|
+
for error in errors
|
|
37
|
+
if error.get("code") or error.get("message")
|
|
29
38
|
]
|