Skip to content

Commit

Permalink
chore: improve exception messages
Browse files Browse the repository at this point in the history
  • Loading branch information
plankthom committed Mar 14, 2024
1 parent 1fae3fb commit e2bb4f1
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 76 deletions.
39 changes: 10 additions & 29 deletions src/waylay/sdk/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from typing import Any, Optional

from .http import Response as RESTResponse
from .http import Response

from ..exceptions import RequestError, RestResponseError
from ..exceptions import WaylayError, RequestError, RestResponseError


class ApiValueError(RequestError, ValueError):
Expand Down Expand Up @@ -32,54 +32,35 @@ def __init__(self, msg, path_to_item=None) -> None:
class ApiError(RestResponseError):
"""Exception class wrapping the response data of a REST call."""

response: RESTResponse
data: Optional[Any]

def __init__(
self,
response: RESTResponse,
response: Response,
data: Optional[Any],
) -> None:
"""Create an instance."""
self.response = response
super().__init__(response)
self.data = data

@classmethod
def from_response(
cls,
response: RESTResponse,
response: Response,
data: Optional[Any],
):
"""Create an instance from a REST exception response."""
return cls(response, data)

def __str__(self):
"""Get the string representation of the exception."""
resp = self.response
error_message = (
f"{self.__class__.__name__}({resp.status_code})\n"
f"Reason: {self.response.reason_phrase}\n"
)
if resp._request:
error_message += f"{resp.request.method} {resp.url}\n"

if resp.headers:
error_message += f"HTTP response headers: {resp.headers}\n"

if self.data:
error_message += f"HTTP response content: {self.data}\n"
elif resp._content:
error_message += (
f"HTTP response content: <bytes: len={len(resp._content)}>\n"
)
else:
error_message += (
f"HTTP response content: <streaming: len={resp.num_bytes_downloaded}>\n"
)
return error_message + "\n)"
error_message = super().__str__()
if self.data and self.data != self.response.content:
error_message += f"\nResponse data: {self.data}"
return error_message


class SyncCtxMgtNotSupportedError(TypeError):
class SyncCtxMgtNotSupportedError(WaylayError, TypeError):
"""Warn the user to use async context management."""

def __init__(self, target):
Expand Down
59 changes: 56 additions & 3 deletions src/waylay/sdk/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Base exception class hierarchy for errors in the waylay client."""

from typing import Optional
from .api.http import Request, Response


class WaylayError(Exception):
"""Root class for all exceptions raised by this module."""
Expand Down Expand Up @@ -28,10 +31,60 @@ class RestError(WaylayError):
class RestRequestError(RestError):
"""Exception class for failures to prepare a REST call."""


class RestResponseError(RestError):
request: Optional[Request]

def __init__(
self,
request: Optional[Request],
):
"""Create an instance."""
self.request = request

def __str__(self):
"""Get the string representation of the exception."""
error_message = f"{self.__class__.__name__}"
if self.request is None:
return error_message
req = self.request
error_message += f"\nRequest: {req.method} {req.url}"
if req.headers:
error_message += f"\nRequest headers: {req.headers}"
if hasattr(req, "_content"):
error_message += f"\nRequest content: <bytes: len={len(req._content)}>"
else:
error_message += f"\nRequest content: <streaming: {req.stream}>"
return error_message


class RestResponseError(RestRequestError):
"""Exception class wrapping the response data of a REST call."""

response: Response

def __init__(
self,
response: Response,
) -> None:
"""Create an instance."""
super().__init__(response._request)
self.response = response

def __str__(self):
"""Get the string representation of the exception."""
error_message = super().__str__()
resp = self.response
error_message += f"\nStatus: {resp.status_code}"
error_message += f"\nReason: {resp.reason_phrase}"
if resp.headers:
error_message += f"\nResponse headers: {resp.headers}"
if hasattr(self.response, "_content"):
error_message += f"\nResponse content: <bytes: len={len(resp._content)}>"
else:
error_message += (
f"\nResponse content: <streaming: len={resp.num_bytes_downloaded}>"
)
return error_message


class RestResponseParseError(RestResponseError):
"""Exception raised when a successfull http request (200) could not be parsed succesfully."""
"""Exception raised when a successfull http request (2XX) could not be parsed succesfully."""
109 changes: 69 additions & 40 deletions test/unit/api/__snapshots__/api_client_test.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -662,12 +662,12 @@
# name: test_deserialize_error_responses[response_kwargs0-response_type_map0][{'404': typing.Dict[str, str]}]
tuple(
'''
ApiError(404)
ApiError
Status: 404
Reason: Not Found
HTTP response headers: Headers({'content-length': '67', 'content-type': 'application/json'})
HTTP response content: {'message': 'some not found message', 'code': 'RESOURCE_NOT_FOUND'}

)
Response headers: Headers({'content-length': '67', 'content-type': 'application/json'})
Response content: <bytes: len=67>
Response data: {'message': 'some not found message', 'code': 'RESOURCE_NOT_FOUND'}
''',
'dict',
dict({
Expand All @@ -679,12 +679,12 @@
# name: test_deserialize_error_responses[response_kwargs1-response_type_map1][{'4XX': <class 'dict'>}]
tuple(
'''
ApiError(404)
ApiError
Status: 404
Reason: Not Found
HTTP response headers: Headers({'content-length': '67', 'content-type': 'application/json'})
HTTP response content: {'message': 'some not found message', 'code': 'RESOURCE_NOT_FOUND'}

)
Response headers: Headers({'content-length': '67', 'content-type': 'application/json'})
Response content: <bytes: len=67>
Response data: {'message': 'some not found message', 'code': 'RESOURCE_NOT_FOUND'}
''',
'dict',
dict({
Expand All @@ -696,12 +696,12 @@
# name: test_deserialize_error_responses[response_kwargs2-response_type_map2][{'4XX': typing.Any}]
tuple(
'''
ApiError(404)
ApiError
Status: 404
Reason: Not Found
HTTP response headers: Headers({'content-length': '67', 'content-type': 'application/json'})
HTTP response content: {'message': 'some not found message', 'code': 'RESOURCE_NOT_FOUND'}

)
Response headers: Headers({'content-length': '67', 'content-type': 'application/json'})
Response content: <bytes: len=67>
Response data: {'message': 'some not found message', 'code': 'RESOURCE_NOT_FOUND'}
''',
'dict',
dict({
Expand All @@ -713,12 +713,12 @@
# name: test_deserialize_error_responses[response_kwargs3-response_type_map3][{'4XX': typing.Any}]
tuple(
'''
ApiError(404)
ApiError
Status: 404
Reason: Not Found
HTTP response headers: Headers({'content-length': '22', 'content-type': 'text/plain; charset=utf-8'})
HTTP response content: some not found message

)
Response headers: Headers({'content-length': '22', 'content-type': 'text/plain; charset=utf-8'})
Response content: <bytes: len=22>
Response data: some not found message
''',
'str',
'some not found message',
Expand All @@ -727,12 +727,12 @@
# name: test_deserialize_error_responses[response_kwargs4-response_type_map4][{}]
tuple(
'''
ApiError(404)
ApiError
Status: 404
Reason: Not Found
HTTP response headers: Headers({'content-length': '67', 'content-type': 'application/json'})
HTTP response content: message='some not found message' code='RESOURCE_NOT_FOUND'

)
Response headers: Headers({'content-length': '67', 'content-type': 'application/json'})
Response content: <bytes: len=67>
Response data: message='some not found message' code='RESOURCE_NOT_FOUND'
''',
'_Model',
_Model(message='some not found message', code='RESOURCE_NOT_FOUND'),
Expand All @@ -741,12 +741,12 @@
# name: test_deserialize_error_responses[response_kwargs5-response_type_map5][{'400': <class 'unit.api.example.pet_model.Pet'>}]
tuple(
'''
ApiError(400)
ApiError
Status: 400
Reason: Bad Request
HTTP response headers: Headers({'content-length': '95', 'content-type': 'application/json'})
HTTP response content: name='Lord Biscuit, Master of Naps' owner=PetOwner(id=123, name='Simon') tag='doggo'

)
Response headers: Headers({'content-length': '95', 'content-type': 'application/json'})
Response content: <bytes: len=95>
Response data: name='Lord Biscuit, Master of Naps' owner=PetOwner(id=123, name='Simon') tag='doggo'
''',
'Pet',
Pet(name='Lord Biscuit, Master of Naps', owner=PetOwner(id=123, name='Simon'), tag='doggo'),
Expand All @@ -755,12 +755,12 @@
# name: test_deserialize_error_responses[response_kwargs6-response_type_map6][{'default': typing.Any}]
tuple(
'''
ApiError(400)
ApiError
Status: 400
Reason: Bad Request
HTTP response headers: Headers({'content-length': '87'})
HTTP response content: {'name': 'Lord Biscuit, Master of Naps', 'owner': {'id': 123, 'name': 'Simon'}, 'tag': 'doggo'}

)
Response headers: Headers({'content-length': '87'})
Response content: <bytes: len=87>
Response data: {'name': 'Lord Biscuit, Master of Naps', 'owner': {'id': 123, 'name': 'Simon'}, 'tag': 'doggo'}
''',
'dict',
dict({
Expand All @@ -776,17 +776,46 @@
# name: test_deserialize_error_responses[response_kwargs7-response_type_map7][{}]
tuple(
'''
ApiError(400)
ApiError
Status: 400
Reason: Bad Request
HTTP response headers: Headers({'content-length': '95', 'content-type': 'application/json'})
HTTP response content: name='Lord Biscuit, Master of Naps' owner=_Model(id=123, name='Simon') tag='doggo'

)
Response headers: Headers({'content-length': '95', 'content-type': 'application/json'})
Response content: <bytes: len=95>
Response data: name='Lord Biscuit, Master of Naps' owner=_Model(id=123, name='Simon') tag='doggo'
''',
'_Model',
_Model(name='Lord Biscuit, Master of Naps', owner=_Model(id=123, name='Simon'), tag='doggo'),
)
# ---
# name: test_deserialize_error_responses[response_kwargs8-response_type_map8][{}]
tuple(
'''
ApiError
Request: GET http://all?debug=true
Request headers: Headers({'host': 'all', 'x-feature': 'flagged'})
Request content: <bytes: len=0>
Status: 400
Reason: Bad Request
Response headers: Headers({'content-length': '3'})
Response content: <streaming: len=0>
''',
'NoneType',
None,
)
# ---
# name: test_deserialize_partially_fetched_error_stream
tuple(
'''
ApiError
Status: 400
Reason: Bad Request
Response headers: Headers({'content-length': '3'})
Response content: <streaming: len=1>
''',
'NoneType',
None,
)
# ---
# name: test_serialize_and_call[binary_body]
dict({
'_content': b'..some binary content..',
Expand Down
Loading

0 comments on commit e2bb4f1

Please sign in to comment.