karrio-sapient 2025.5.6__py3-none-any.whl → 2026.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/sapient/proxy.py +64 -10
- karrio/providers/sapient/utils.py +0 -95
- {karrio_sapient-2025.5.6.dist-info → karrio_sapient-2026.1.dist-info}/METADATA +1 -1
- {karrio_sapient-2025.5.6.dist-info → karrio_sapient-2026.1.dist-info}/RECORD +7 -7
- {karrio_sapient-2025.5.6.dist-info → karrio_sapient-2026.1.dist-info}/WHEEL +0 -0
- {karrio_sapient-2025.5.6.dist-info → karrio_sapient-2026.1.dist-info}/entry_points.txt +0 -0
- {karrio_sapient-2025.5.6.dist-info → karrio_sapient-2026.1.dist-info}/top_level.txt +0 -0
karrio/mappers/sapient/proxy.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Karrio SAPIENT client proxy."""
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
import karrio.lib as lib
|
|
4
5
|
import karrio.api.proxy as proxy
|
|
5
|
-
import karrio.
|
|
6
|
+
import karrio.core.errors as errors
|
|
7
|
+
import karrio.providers.sapient.error as provider_error
|
|
6
8
|
import karrio.mappers.sapient.settings as provider_settings
|
|
7
9
|
import karrio.universal.mappers.rating_proxy as rating_proxy
|
|
8
10
|
|
|
@@ -10,10 +12,55 @@ import karrio.universal.mappers.rating_proxy as rating_proxy
|
|
|
10
12
|
class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
|
|
11
13
|
settings: provider_settings.Settings
|
|
12
14
|
|
|
15
|
+
def authenticate(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
|
16
|
+
"""Retrieve the access_token using the client_id|client_secret pair
|
|
17
|
+
or collect it from the cache if an unexpired access_token exist.
|
|
18
|
+
"""
|
|
19
|
+
cache_key = (
|
|
20
|
+
f"{self.settings.carrier_name}|{self.settings.client_id}|{self.settings.client_secret}"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def get_token():
|
|
24
|
+
response = lib.request(
|
|
25
|
+
url=f"https://authentication.intersoftsapient.net/connect/token",
|
|
26
|
+
method="POST",
|
|
27
|
+
headers={
|
|
28
|
+
"content-Type": "application/x-www-form-urlencoded",
|
|
29
|
+
"user-agent": "Karrio/1.0",
|
|
30
|
+
},
|
|
31
|
+
data=lib.to_query_string(
|
|
32
|
+
dict(
|
|
33
|
+
grant_type="client_credentials",
|
|
34
|
+
client_id=self.settings.client_id,
|
|
35
|
+
client_secret=self.settings.client_secret,
|
|
36
|
+
)
|
|
37
|
+
),
|
|
38
|
+
decoder=lib.to_dict,
|
|
39
|
+
max_retries=2,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
messages = provider_error.parse_error_response(response, self.settings)
|
|
43
|
+
if any(messages):
|
|
44
|
+
raise errors.ParsedMessagesError(messages)
|
|
45
|
+
|
|
46
|
+
expiry = datetime.datetime.now() + datetime.timedelta(
|
|
47
|
+
seconds=float(response.get("expires_in", 0))
|
|
48
|
+
)
|
|
49
|
+
return {**response, "expiry": lib.fdatetime(expiry)}
|
|
50
|
+
|
|
51
|
+
token = self.settings.connection_cache.thread_safe(
|
|
52
|
+
refresh_func=get_token,
|
|
53
|
+
cache_key=cache_key,
|
|
54
|
+
buffer_minutes=10,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return lib.Deserializable(token.get_state())
|
|
58
|
+
|
|
13
59
|
def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
|
14
60
|
return super().get_rates(request)
|
|
15
61
|
|
|
16
62
|
def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
|
63
|
+
access_token = self.authenticate(request).deserialize()
|
|
17
64
|
response = lib.request(
|
|
18
65
|
url=f"{self.settings.server_url}/v4/shipments/{request.ctx['carrier_code']}",
|
|
19
66
|
data=lib.to_json(request.serialize()),
|
|
@@ -21,15 +68,17 @@ class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
|
|
|
21
68
|
method="POST",
|
|
22
69
|
headers={
|
|
23
70
|
"Content-Type": "application/json",
|
|
24
|
-
"Authorization": f"Bearer {
|
|
71
|
+
"Authorization": f"Bearer {access_token}",
|
|
25
72
|
"user-agent": "Karrio/1.0",
|
|
26
73
|
},
|
|
27
|
-
|
|
74
|
+
decoder=lib.to_dict_safe,
|
|
75
|
+
on_error=lambda b: lib.to_dict_safe(b.read()),
|
|
28
76
|
)
|
|
29
77
|
|
|
30
78
|
return lib.Deserializable(response, lib.to_dict, request.ctx)
|
|
31
79
|
|
|
32
80
|
def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
|
81
|
+
access_token = self.authenticate(request).deserialize()
|
|
33
82
|
response = lib.request(
|
|
34
83
|
url=f"{self.settings.server_url}/v4/shipments/status",
|
|
35
84
|
data=lib.to_json(request.serialize()),
|
|
@@ -37,16 +86,18 @@ class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
|
|
|
37
86
|
method="PUT",
|
|
38
87
|
headers={
|
|
39
88
|
"Content-Type": "application/json",
|
|
40
|
-
"Authorization": f"Bearer {
|
|
89
|
+
"Authorization": f"Bearer {access_token}",
|
|
41
90
|
"user-agent": "Karrio/1.0",
|
|
42
91
|
},
|
|
43
92
|
on_ok=lambda _: '{"ok": true}',
|
|
44
|
-
|
|
93
|
+
decoder=lib.to_dict_safe,
|
|
94
|
+
on_error=lambda b: lib.to_dict_safe(b.read()),
|
|
45
95
|
)
|
|
46
96
|
|
|
47
97
|
return lib.Deserializable(response, lib.to_dict)
|
|
48
98
|
|
|
49
99
|
def schedule_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
|
100
|
+
access_token = self.authenticate(request).deserialize()
|
|
50
101
|
response = lib.request(
|
|
51
102
|
url=f"{self.settings.server_url}/v4/collections/{request.ctx['carrier_code']}/{request.ctx['shipmentId']}",
|
|
52
103
|
data=lib.to_json(request.serialize()),
|
|
@@ -54,10 +105,11 @@ class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
|
|
|
54
105
|
method="POST",
|
|
55
106
|
headers={
|
|
56
107
|
"Content-Type": "application/json",
|
|
57
|
-
"Authorization": f"Bearer {
|
|
108
|
+
"Authorization": f"Bearer {access_token}",
|
|
58
109
|
"user-agent": "Karrio/1.0",
|
|
59
110
|
},
|
|
60
|
-
|
|
111
|
+
decoder=lib.to_dict_safe,
|
|
112
|
+
on_error=lambda b: lib.to_dict_safe(b.read()),
|
|
61
113
|
)
|
|
62
114
|
|
|
63
115
|
return lib.Deserializable(response, lib.to_dict, request.ctx)
|
|
@@ -65,12 +117,13 @@ class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
|
|
|
65
117
|
def modify_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
|
66
118
|
response = self.cancel_pickup(lib.Serializable(request.ctx))
|
|
67
119
|
|
|
68
|
-
if response.deserialize()
|
|
120
|
+
if response.deserialize().get("ok"):
|
|
69
121
|
response = self.schedule_pickup(request)
|
|
70
122
|
|
|
71
123
|
return lib.Deserializable(response, lib.to_dict, request.ctx)
|
|
72
124
|
|
|
73
125
|
def cancel_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
|
126
|
+
access_token = self.authenticate(request).deserialize()
|
|
74
127
|
payload = request.serialize()
|
|
75
128
|
response = lib.request(
|
|
76
129
|
url=f"{self.settings.server_url}/v4/collections/{payload['carrier_code']}/{payload['shipmentId']}/cancel",
|
|
@@ -78,10 +131,11 @@ class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
|
|
|
78
131
|
method="PUT",
|
|
79
132
|
headers={
|
|
80
133
|
"Content-Type": "application/json",
|
|
81
|
-
"Authorization": f"Bearer {
|
|
134
|
+
"Authorization": f"Bearer {access_token}",
|
|
82
135
|
"user-agent": "Karrio/1.0",
|
|
83
136
|
},
|
|
84
|
-
|
|
137
|
+
decoder=lib.to_dict_safe,
|
|
138
|
+
on_error=lambda b: lib.to_dict_safe(b.read()),
|
|
85
139
|
)
|
|
86
140
|
|
|
87
141
|
return lib.Deserializable(response, lib.to_dict)
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import karrio.lib as lib
|
|
3
2
|
import karrio.core as core
|
|
4
|
-
import karrio.core.errors as errors
|
|
5
|
-
import karrio.core.models as models
|
|
6
3
|
|
|
7
4
|
|
|
8
5
|
SapientCarrierCode = lib.units.create_enum(
|
|
@@ -46,100 +43,8 @@ class Settings(core.Settings):
|
|
|
46
43
|
option_type=ConnectionConfig,
|
|
47
44
|
)
|
|
48
45
|
|
|
49
|
-
@property
|
|
50
|
-
def access_token(self):
|
|
51
|
-
"""Retrieve the access_token using the client_id|client_secret pair
|
|
52
|
-
or collect it from the cache if an unexpired access_token exist.
|
|
53
|
-
"""
|
|
54
|
-
cache_key = f"{self.carrier_name}|{self.client_id}|{self.client_secret}"
|
|
55
|
-
|
|
56
|
-
return self.connection_cache.thread_safe(
|
|
57
|
-
refresh_func=lambda: login(self),
|
|
58
|
-
cache_key=cache_key,
|
|
59
|
-
buffer_minutes=30,
|
|
60
|
-
).get_state()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def login(settings: Settings):
|
|
64
|
-
import karrio.providers.sapient.error as error
|
|
65
|
-
|
|
66
|
-
result = lib.request(
|
|
67
|
-
url=f"https://authentication.intersoftsapient.net/connect/token",
|
|
68
|
-
method="POST",
|
|
69
|
-
headers={
|
|
70
|
-
"content-Type": "application/x-www-form-urlencoded",
|
|
71
|
-
"user-agent": "Karrio/1.0",
|
|
72
|
-
},
|
|
73
|
-
data=lib.to_query_string(
|
|
74
|
-
dict(
|
|
75
|
-
grant_type="client_credentials",
|
|
76
|
-
client_id=settings.client_id,
|
|
77
|
-
client_secret=settings.client_secret,
|
|
78
|
-
)
|
|
79
|
-
),
|
|
80
|
-
on_error=parse_error_response,
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
# Handle case where result is a plain string (error response)
|
|
84
|
-
# instead of JSON - parse_error_response may return non-JSON strings
|
|
85
|
-
response = lib.failsafe(lambda: lib.to_dict(result)) or {}
|
|
86
|
-
|
|
87
|
-
# If we couldn't parse as JSON, treat the result as an error message
|
|
88
|
-
if not response and isinstance(result, str):
|
|
89
|
-
response = {"error": result}
|
|
90
|
-
|
|
91
|
-
# Handle OAuth error response format (error, error_description)
|
|
92
|
-
if "error" in response:
|
|
93
|
-
raise errors.ParsedMessagesError(
|
|
94
|
-
messages=[
|
|
95
|
-
models.Message(
|
|
96
|
-
carrier_name=settings.carrier_name,
|
|
97
|
-
carrier_id=settings.carrier_id,
|
|
98
|
-
message=response.get("error_description", response.get("error")),
|
|
99
|
-
code=response.get("error", "AUTH_ERROR"),
|
|
100
|
-
)
|
|
101
|
-
]
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
messages = error.parse_error_response(response, settings)
|
|
105
|
-
|
|
106
|
-
if any(messages):
|
|
107
|
-
raise errors.ParsedMessagesError(messages)
|
|
108
|
-
|
|
109
|
-
# Validate that access_token is present in the response
|
|
110
|
-
if "access_token" not in response:
|
|
111
|
-
raise errors.ParsedMessagesError(
|
|
112
|
-
messages=[
|
|
113
|
-
models.Message(
|
|
114
|
-
carrier_name=settings.carrier_name,
|
|
115
|
-
carrier_id=settings.carrier_id,
|
|
116
|
-
message="Authentication failed: No access token received",
|
|
117
|
-
code="AUTH_ERROR",
|
|
118
|
-
)
|
|
119
|
-
]
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
expiry = datetime.datetime.now() + datetime.timedelta(
|
|
123
|
-
seconds=float(response.get("expires_in", 0))
|
|
124
|
-
)
|
|
125
|
-
return {**response, "expiry": lib.fdatetime(expiry)}
|
|
126
|
-
|
|
127
46
|
|
|
128
47
|
class ConnectionConfig(lib.Enum):
|
|
129
48
|
service_level = lib.OptionEnum("service_level", str)
|
|
130
49
|
shipping_options = lib.OptionEnum("shipping_options", list)
|
|
131
50
|
shipping_services = lib.OptionEnum("shipping_services", list)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def parse_error_response(response):
|
|
135
|
-
"""Parse the error response from the SAPIENT API."""
|
|
136
|
-
content = lib.failsafe(lambda: lib.decode(response.read()))
|
|
137
|
-
|
|
138
|
-
# If we have content, try to return it as-is (likely already JSON string)
|
|
139
|
-
if any(content or ""):
|
|
140
|
-
return content
|
|
141
|
-
|
|
142
|
-
# If no content, create a JSON error object
|
|
143
|
-
return lib.to_json(
|
|
144
|
-
dict(Errors=[dict(ErrorCode=str(response.code), Message=response.reason)])
|
|
145
|
-
)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
karrio/mappers/sapient/__init__.py,sha256=I4fWpxOlRnfyBNjL0LxvQlpwOk2sjrZhgtCO2XvSJVk,149
|
|
2
2
|
karrio/mappers/sapient/mapper.py,sha256=BDU1L2QW1W-0e8eYcgSf-eLkj5o7SS4kje-WObnZSUg,2866
|
|
3
|
-
karrio/mappers/sapient/proxy.py,sha256=
|
|
3
|
+
karrio/mappers/sapient/proxy.py,sha256=bhgLAYElw7JfC_t5-Vin_KG1OyNe7nZRjHJ-YsivcBU,5647
|
|
4
4
|
karrio/mappers/sapient/settings.py,sha256=a5xE-umxBbP1sHaN6SKkbt-ftsmx9pnWkMEUIeAhVLY,1095
|
|
5
5
|
karrio/plugins/sapient/__init__.py,sha256=_4xkqTPpUoq8KixCdNqNxcMVH52s5xH6L-s9c5fongE,537
|
|
6
6
|
karrio/providers/sapient/__init__.py,sha256=Zu7FIaPsCzXgIsrmEtluHQJSj3UnFDpz-oiB93P-xK0,476
|
|
7
7
|
karrio/providers/sapient/error.py,sha256=hkpy86gWOjKLNhtmBoe7NkHq330kRqrtgDxsspLaxGQ,870
|
|
8
8
|
karrio/providers/sapient/units.py,sha256=ZUGl0IVKOA8vLVRWJGY2X9JBOR8H5OBr9s17Fx22EcM,34880
|
|
9
|
-
karrio/providers/sapient/utils.py,sha256=
|
|
9
|
+
karrio/providers/sapient/utils.py,sha256=ZY35pktirmkkwsKBgK8TbK4mrmbAfae5GuPLO3T8nvc,1462
|
|
10
10
|
karrio/providers/sapient/pickup/__init__.py,sha256=qT64wGr8J6pkLNa9Y0Zou5mYshoI7W-R0Pp7nWEhQ08,296
|
|
11
11
|
karrio/providers/sapient/pickup/cancel.py,sha256=0laDCfdhjOYHpOEN7l6u1o79nexaxT8GXMM5pJU3Xow,1763
|
|
12
12
|
karrio/providers/sapient/pickup/create.py,sha256=5S-h9qQuDFqxvqrmd2l0-SGQMi86O38sfjfqRVPGgo8,2776
|
|
@@ -20,8 +20,8 @@ karrio/schemas/sapient/pickup_request.py,sha256=gzZZ3Bxr_pYWxw28vDRr-aDgtzJIQptU
|
|
|
20
20
|
karrio/schemas/sapient/pickup_response.py,sha256=xcl3ofB2L2RNksMSRccvFbBdBcK1hVDDPVB-oAu4I54,195
|
|
21
21
|
karrio/schemas/sapient/shipment_requests.py,sha256=3sjensnLiiF7mTIpwS4FyrdEe-xMh5YC1ekj69qI6XM,4618
|
|
22
22
|
karrio/schemas/sapient/shipment_response.py,sha256=457ULt-exxAJ0Drqn2Pnj9pl_4JWMZ-cesuJW9NWBAs,702
|
|
23
|
-
karrio_sapient-
|
|
24
|
-
karrio_sapient-
|
|
25
|
-
karrio_sapient-
|
|
26
|
-
karrio_sapient-
|
|
27
|
-
karrio_sapient-
|
|
23
|
+
karrio_sapient-2026.1.dist-info/METADATA,sha256=KZTqd47RcDyA3mcv5IYR1DiH2TcskhLs80zC6dJOGG8,987
|
|
24
|
+
karrio_sapient-2026.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
+
karrio_sapient-2026.1.dist-info/entry_points.txt,sha256=rPF-WjVKk-3suhX4-zi9hTkwr0Qxb4ZW-Tjoyzi_Cy8,59
|
|
26
|
+
karrio_sapient-2026.1.dist-info/top_level.txt,sha256=FZCY8Nwft8oEGHdl--xku8P3TrnOxu5dETEU_fWpRSM,20
|
|
27
|
+
karrio_sapient-2026.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|