Skip to content

Commit 4809c39

Browse files
Centralise error handling (#28)
* Centralise error handling * Use global error handling for validate_challenge * Remove response.raise_for_status() in methods as this is handled globally * Major version bump
1 parent 08eeb4e commit 4809c39

File tree

5 files changed

+83
-103
lines changed

5 files changed

+83
-103
lines changed
1.36 KB
Binary file not shown.

authsignal/client.py

+80-100
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,22 @@ def _remove_none_values(d: Dict[str, Any]) -> Dict[str, Any]:
5151
return {k: v for k, v in d.items() if v is not None}
5252

5353
def send(self, request, **kwargs):
54-
response = super().send(request, **kwargs)
55-
if response.headers.get('Content-Type') == 'application/json':
56-
try:
57-
data = response.json()
58-
if isinstance(data, dict) and 'actionCode' in data:
59-
del data['actionCode']
60-
response._content = json.dumps(humps.decamelize(data)).encode('utf-8')
61-
except json.JSONDecodeError:
62-
pass
63-
return response
54+
try:
55+
response = super().send(request, **kwargs)
56+
response.raise_for_status() # Raises HTTPError for 4xx/5xx responses
57+
58+
if response.headers.get('Content-Type') == 'application/json':
59+
try:
60+
data = response.json()
61+
if isinstance(data, dict) and 'actionCode' in data:
62+
del data['actionCode']
63+
response._content = json.dumps(humps.decamelize(data)).encode('utf-8')
64+
except json.JSONDecodeError:
65+
pass
66+
return response
67+
except requests.exceptions.RequestException as e:
68+
# Handle the exception globally
69+
raise ApiException(str(e), request.url, http_status_code=e.response.status_code if e.response else None) from e
6470

6571
class Client(object):
6672

@@ -111,19 +117,15 @@ def track(self, user_id, action, payload=None, path=None):
111117
params = {}
112118
timeout = self.timeout
113119

114-
try:
115-
response = self.session.post(
116-
path,
117-
data=json.dumps(payload if payload is not None else {}, cls=DecimalEncoder),
118-
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
119-
headers=headers,
120-
timeout=timeout,
121-
params=params)
122-
response.raise_for_status()
123-
124-
return response.json()
125-
except requests.exceptions.RequestException as e:
126-
raise ApiException(str(e), path) from e
120+
response = self.session.post(
121+
path,
122+
data=json.dumps(payload if payload is not None else {}, cls=DecimalEncoder),
123+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
124+
headers=headers,
125+
timeout=timeout,
126+
params=params)
127+
128+
return response.json()
127129

128130
def get_action(self, user_id, action, idempotency_key, path=None):
129131
"""Retrieves the action from authsignal, scoped to the user_id and action
@@ -142,18 +144,14 @@ def get_action(self, user_id, action, idempotency_key, path=None):
142144
params = {}
143145
timeout = self.timeout
144146

145-
try:
146-
response = self.session.get(
147-
path,
148-
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
149-
headers=headers,
150-
timeout=timeout,
151-
params=params)
152-
response.raise_for_status()
153-
154-
return response.json()
155-
except requests.exceptions.RequestException as e:
156-
raise ApiException(str(e), path) from e
147+
response = self.session.get(
148+
path,
149+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
150+
headers=headers,
151+
timeout=timeout,
152+
params=params)
153+
154+
return response.json()
157155

158156
def get_user(self, user_id, redirect_url=None, path=None):
159157
"""Retrieves the user from authsignal, and returns enrolment status, and self service url.
@@ -174,36 +172,29 @@ def get_user(self, user_id, redirect_url=None, path=None):
174172
_assert_non_empty_unicode(redirect_url, 'redirect_url')
175173
params.update({"redirectUrl": redirect_url})
176174

177-
try:
178-
response = self.session.get(
179-
path,
180-
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
181-
headers=headers,
182-
timeout=timeout,
183-
params=params)
184-
response.raise_for_status()
185-
186-
return response.json()
187-
except requests.exceptions.RequestException as e:
188-
raise ApiException(str(e), path) from e
175+
response = self.session.get(
176+
path,
177+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
178+
headers=headers,
179+
timeout=timeout,
180+
params=params)
181+
182+
return response.json()
189183

190184
def delete_user(self, user_id):
191185
_assert_non_empty_unicode(user_id, 'user_id')
192186

193187
path = self._delete_user_url(user_id)
194188
headers = self._default_headers()
195189

196-
try:
197-
response = self.session.delete(
198-
path,
199-
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
200-
headers=headers,
201-
timeout=self.timeout
202-
)
203-
response.raise_for_status()
204-
return response.json()
205-
except requests.exceptions.RequestException as e:
206-
raise ApiException(str(e), path) from e
190+
response = self.session.delete(
191+
path,
192+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
193+
headers=headers,
194+
timeout=self.timeout
195+
)
196+
197+
return response.json()
207198

208199
def delete_authenticator(self, user_id: str, user_authenticator_id: str) -> Dict[str, Any]:
209200
_assert_non_empty_unicode(user_id, 'user_id')
@@ -214,17 +205,14 @@ def delete_authenticator(self, user_id: str, user_authenticator_id: str) -> Dict
214205
path = f'{self.url}/v1/users/{user_id}/authenticators/{user_authenticator_id}'
215206
headers = self._default_headers()
216207

217-
try:
218-
response = self.session.delete(
219-
path,
220-
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
221-
headers=headers,
222-
timeout=self.timeout
223-
)
224-
response.raise_for_status()
225-
return response.json()
226-
except requests.exceptions.RequestException as e:
227-
raise ApiException(str(e), path) from e
208+
response = self.session.delete(
209+
path,
210+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
211+
headers=headers,
212+
timeout=self.timeout
213+
)
214+
215+
return response.json()
228216

229217
def update_user(self, user_id, data):
230218
user_id = urllib.parse.quote(user_id)
@@ -239,8 +227,6 @@ def update_user(self, user_id, data):
239227
auth=requests.auth.HTTPBasicAuth(self.api_key, '')
240228
)
241229

242-
response.raise_for_status()
243-
244230
return response.json()
245231

246232
def enroll_verified_authenticator(self, user_id, authenticator_payload, path=None):
@@ -259,18 +245,15 @@ def enroll_verified_authenticator(self, user_id, authenticator_payload, path=No
259245
params = {}
260246
timeout = self.timeout
261247

262-
try:
263-
response = self.session.post(
264-
path,
265-
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
266-
data=json.dumps(authenticator_payload),
267-
headers=headers,
268-
timeout=timeout,
269-
params=params)
270-
response.raise_for_status()
271-
return response.json()
272-
except requests.exceptions.RequestException as e:
273-
raise ApiException(str(e), path) from e
248+
response = self.session.post(
249+
path,
250+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
251+
data=json.dumps(authenticator_payload),
252+
headers=headers,
253+
timeout=timeout,
254+
params=params)
255+
256+
return response.json()
274257

275258
def validate_challenge(self, token: str, user_id: Optional[str] = None, action: Optional[str] = None) -> Dict[str, Any]:
276259
path = self._validate_challenge_url()
@@ -279,21 +262,15 @@ def validate_challenge(self, token: str, user_id: Optional[str] = None, action:
279262
'Accept': 'application/json'
280263
}
281264

282-
283-
try:
284-
response = self.session.post(
285-
path,
286-
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
287-
data=json.dumps({'token': token, 'userId': user_id, 'action': action}),
288-
headers=headers,
289-
timeout=self.timeout
290-
)
291-
292-
response_data = response.json()
293-
294-
return response_data
295-
except requests.exceptions.RequestException as e:
296-
raise ApiException(str(e), path) from e
265+
response = self.session.post(
266+
path,
267+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
268+
data=json.dumps({'token': token, 'userId': user_id, 'action': action}),
269+
headers=headers,
270+
timeout=self.timeout
271+
)
272+
273+
return response.json()
297274

298275
def _default_headers(self):
299276
return {'Content-type': 'application/json',
@@ -336,15 +313,18 @@ def _ensure_versioned_path(self, path):
336313
class ApiException(Exception):
337314
def __init__(self, message, url, http_status_code=None, body=None, api_status=None,
338315
api_error_message=None, request=None):
339-
Exception.__init__(self, message)
340-
316+
super().__init__(message)
341317
self.url = url
342318
self.http_status_code = http_status_code
343319
self.body = body
344320
self.api_status = api_status
345321
self.api_error_message = api_error_message
346322
self.request = request
347323

324+
def __str__(self):
325+
return (f"{super().__str__()} status: {self.http_status_code}, "
326+
f"error: {self.api_error_message}, description: {self.body}")
327+
348328
def _assert_non_empty_unicode(val, name, error_cls=None):
349329
error = False
350330
if not isinstance(val, _UNICODE_STRING):

authsignal/client_tests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def test_delete_authenticator(self):
170170
def test_validate_challenge_returns_success_false_if_user_id_is_incorrect(self):
171171
responses.add(responses.POST, f"{base_url}/validate",
172172
json={'isValid': False, 'error': 'User is invalid.'},
173-
status=400
173+
status=200
174174
)
175175

176176
response = self.authsignal_client.validate_challenge(user_id="spoofed_id", token=self.jwt_token)

authsignal/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = '2.0.7'
1+
VERSION = '3.0.0'

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "authsignal"
3-
version = "2.0.7"
3+
version = "3.0.0"
44
description = "Authsignal Python SDK for Passwordless Step Up Authentication"
55
authors = ["justinsoong <[email protected]>"]
66
license = "MIT"

0 commit comments

Comments
 (0)