karrio-sapient 2025.5.5__py3-none-any.whl → 2025.5.7__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.
@@ -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.providers.sapient.utils as provider_utils
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 {self.settings.access_token}",
71
+ "Authorization": f"Bearer {access_token}",
25
72
  "user-agent": "Karrio/1.0",
26
73
  },
27
- on_error=provider_utils.parse_error_response,
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 {self.settings.access_token}",
89
+ "Authorization": f"Bearer {access_token}",
41
90
  "user-agent": "Karrio/1.0",
42
91
  },
43
92
  on_ok=lambda _: '{"ok": true}',
44
- on_error=provider_utils.parse_error_response,
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 {self.settings.access_token}",
108
+ "Authorization": f"Bearer {access_token}",
58
109
  "user-agent": "Karrio/1.0",
59
110
  },
60
- on_error=provider_utils.parse_error_response,
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()["ok"]:
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 {self.settings.access_token}",
134
+ "Authorization": f"Bearer {access_token}",
82
135
  "user-agent": "Karrio/1.0",
83
136
  },
84
- on_error=provider_utils.parse_error_response,
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karrio_sapient
3
- Version: 2025.5.5
3
+ Version: 2025.5.7
4
4
  Summary: Karrio - SAPIENT Shipping Extension
5
5
  Author-email: karrio <hello@karrio.io>
6
6
  License-Expression: LGPL-3.0
@@ -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=vzfErbI6i5_uVA5UUIjCyKpxNeZu6eupDpSkvQT-P9E,3537
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=4lRjA5gDiHwJjadHfTeE5eP64g-rkbBhKwjyUnSRx90,4714
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-2025.5.5.dist-info/METADATA,sha256=Mz9p5aPk8dAhO-kifzkl47s7OfKJGfzVmKLjcYoG65Y,989
24
- karrio_sapient-2025.5.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- karrio_sapient-2025.5.5.dist-info/entry_points.txt,sha256=rPF-WjVKk-3suhX4-zi9hTkwr0Qxb4ZW-Tjoyzi_Cy8,59
26
- karrio_sapient-2025.5.5.dist-info/top_level.txt,sha256=FZCY8Nwft8oEGHdl--xku8P3TrnOxu5dETEU_fWpRSM,20
27
- karrio_sapient-2025.5.5.dist-info/RECORD,,
23
+ karrio_sapient-2025.5.7.dist-info/METADATA,sha256=i1dcTyHN19IuI37aZxemo2FkSzoCeJwxL_ecT5DZxG8,989
24
+ karrio_sapient-2025.5.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ karrio_sapient-2025.5.7.dist-info/entry_points.txt,sha256=rPF-WjVKk-3suhX4-zi9hTkwr0Qxb4ZW-Tjoyzi_Cy8,59
26
+ karrio_sapient-2025.5.7.dist-info/top_level.txt,sha256=FZCY8Nwft8oEGHdl--xku8P3TrnOxu5dETEU_fWpRSM,20
27
+ karrio_sapient-2025.5.7.dist-info/RECORD,,