Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Centralise error handling #28

Merged
merged 4 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified authsignal/__pycache__/client.cpython-311.pyc
Binary file not shown.
180 changes: 80 additions & 100 deletions authsignal/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,22 @@ def _remove_none_values(d: Dict[str, Any]) -> Dict[str, Any]:
return {k: v for k, v in d.items() if v is not None}

def send(self, request, **kwargs):
response = super().send(request, **kwargs)
if response.headers.get('Content-Type') == 'application/json':
try:
data = response.json()
if isinstance(data, dict) and 'actionCode' in data:
del data['actionCode']
response._content = json.dumps(humps.decamelize(data)).encode('utf-8')
except json.JSONDecodeError:
pass
return response
try:
response = super().send(request, **kwargs)
response.raise_for_status() # Raises HTTPError for 4xx/5xx responses

if response.headers.get('Content-Type') == 'application/json':
try:
data = response.json()
if isinstance(data, dict) and 'actionCode' in data:
del data['actionCode']
response._content = json.dumps(humps.decamelize(data)).encode('utf-8')
except json.JSONDecodeError:
pass
return response
except requests.exceptions.RequestException as e:
# Handle the exception globally
raise ApiException(str(e), request.url, http_status_code=e.response.status_code if e.response else None) from e

class Client(object):

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

try:
response = self.session.post(
path,
data=json.dumps(payload if payload is not None else {}, cls=DecimalEncoder),
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=timeout,
params=params)
response.raise_for_status()

return response.json()
except requests.exceptions.RequestException as e:
raise ApiException(str(e), path) from e
response = self.session.post(
path,
data=json.dumps(payload if payload is not None else {}, cls=DecimalEncoder),
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=timeout,
params=params)

return response.json()

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

try:
response = self.session.get(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=timeout,
params=params)
response.raise_for_status()

return response.json()
except requests.exceptions.RequestException as e:
raise ApiException(str(e), path) from e
response = self.session.get(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=timeout,
params=params)

return response.json()

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

try:
response = self.session.get(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=timeout,
params=params)
response.raise_for_status()

return response.json()
except requests.exceptions.RequestException as e:
raise ApiException(str(e), path) from e
response = self.session.get(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=timeout,
params=params)

return response.json()

def delete_user(self, user_id):
_assert_non_empty_unicode(user_id, 'user_id')

path = self._delete_user_url(user_id)
headers = self._default_headers()

try:
response = self.session.delete(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise ApiException(str(e), path) from e
response = self.session.delete(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=self.timeout
)

return response.json()

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

try:
response = self.session.delete(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise ApiException(str(e), path) from e
response = self.session.delete(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers=headers,
timeout=self.timeout
)

return response.json()

def update_user(self, user_id, data):
user_id = urllib.parse.quote(user_id)
Expand All @@ -239,8 +227,6 @@ def update_user(self, user_id, data):
auth=requests.auth.HTTPBasicAuth(self.api_key, '')
)

response.raise_for_status()

return response.json()

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

try:
response = self.session.post(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
data=json.dumps(authenticator_payload),
headers=headers,
timeout=timeout,
params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise ApiException(str(e), path) from e
response = self.session.post(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
data=json.dumps(authenticator_payload),
headers=headers,
timeout=timeout,
params=params)

return response.json()

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


try:
response = self.session.post(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
data=json.dumps({'token': token, 'userId': user_id, 'action': action}),
headers=headers,
timeout=self.timeout
)

response_data = response.json()

return response_data
except requests.exceptions.RequestException as e:
raise ApiException(str(e), path) from e
response = self.session.post(
path,
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
data=json.dumps({'token': token, 'userId': user_id, 'action': action}),
headers=headers,
timeout=self.timeout
)

return response.json()

def _default_headers(self):
return {'Content-type': 'application/json',
Expand Down Expand Up @@ -336,15 +313,18 @@ def _ensure_versioned_path(self, path):
class ApiException(Exception):
def __init__(self, message, url, http_status_code=None, body=None, api_status=None,
api_error_message=None, request=None):
Exception.__init__(self, message)

super().__init__(message)
self.url = url
self.http_status_code = http_status_code
self.body = body
self.api_status = api_status
self.api_error_message = api_error_message
self.request = request

def __str__(self):
return (f"{super().__str__()} status: {self.http_status_code}, "
f"error: {self.api_error_message}, description: {self.body}")

def _assert_non_empty_unicode(val, name, error_cls=None):
error = False
if not isinstance(val, _UNICODE_STRING):
Expand Down
2 changes: 1 addition & 1 deletion authsignal/client_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def test_delete_authenticator(self):
def test_validate_challenge_returns_success_false_if_user_id_is_incorrect(self):
responses.add(responses.POST, f"{base_url}/validate",
json={'isValid': False, 'error': 'User is invalid.'},
status=400
status=200
)

response = self.authsignal_client.validate_challenge(user_id="spoofed_id", token=self.jwt_token)
Expand Down
2 changes: 1 addition & 1 deletion authsignal/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = '2.0.7'
VERSION = '3.0.0'
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "authsignal"
version = "2.0.7"
version = "3.0.0"
description = "Authsignal Python SDK for Passwordless Step Up Authentication"
authors = ["justinsoong <[email protected]>"]
license = "MIT"
Expand Down
Loading