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

Feature/bunq/sdk python#59 add response id to request error #64

Merged
Show file tree
Hide file tree
Changes from 10 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
33 changes: 31 additions & 2 deletions bunq/sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ class ApiClient(object):
:type _api_context: bunq.sdk.context.ApiContext
"""

# Error constants
_ERROR_COULD_NOT_DETERMINE_RESPONSE_ID_HEADER = ('The response header'
'"X-Bunq-Client-Response-'
'Id" or "x-bunq-client-'
'response-id" could not '
'be found.')

# Endpoints not requiring active session for the request to succeed.
_URL_DEVICE_SERVER = 'device-server'
_URI_INSTALLATION = 'installation'
Expand All @@ -39,6 +46,8 @@ class ApiClient(object):
HEADER_GEOLOCATION = 'X-Bunq-Geolocation'
HEADER_SIGNATURE = 'X-Bunq-Client-Signature'
HEADER_AUTHENTICATION = 'X-Bunq-Client-Authentication'
HEADER_RESPONSE_ID_UPPER_CASED = 'X-Bunq-Client-Response-Id'
HEADER_RESPONSE_ID_LOWER_CASED = 'x-bunq-client-response-id'

# Default header values
_USER_AGENT_BUNQ = 'bunq-sdk-python/0.12.4'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget bumping version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens during release 🙃

Expand Down Expand Up @@ -215,7 +224,8 @@ def _assert_response_success(self, response):
if response.status_code != self._STATUS_CODE_OK:
raise ExceptionFactory.create_exception_for_response(
response.status_code,
self._fetch_error_messages(response)
self._fetch_all_error_message(response),
self._fetch_response_id(response)
)

@classmethod
Expand All @@ -228,7 +238,7 @@ def _create_bunq_response_raw(cls, response):

return BunqResponseRaw(response.content, response.headers)

def _fetch_error_messages(self, response):
def _fetch_all_error_message(self, response):
"""
:type response: requests.Response

Expand Down Expand Up @@ -259,6 +269,25 @@ def _fetch_error_descriptions(self, error_dict):

return error_descriptions

def _fetch_response_id(self, response):
"""
:type response: requests.Response

:rtype: str
"""

headers = response.headers

if self.HEADER_RESPONSE_ID_UPPER_CASED in headers:
return headers[self.HEADER_RESPONSE_ID_UPPER_CASED]

if self.HEADER_RESPONSE_ID_LOWER_CASED in headers:
return headers[self.HEADER_RESPONSE_ID_LOWER_CASED]

return exception.BunqException(
self._ERROR_COULD_NOT_DETERMINE_RESPONSE_ID_HEADER
)

def put(self, uri_relative, request_bytes, custom_headers):
"""
:type uri_relative: str
Expand Down
12 changes: 11 additions & 1 deletion bunq/sdk/exception.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
class ApiException(Exception):
def __init__(self, message, response_code):
def __init__(self, message, response_code, response_id):
"""
:type response_id: str
:type message: str
:type response_code: int
"""

self._response_id = response_id
self._message = message
self._response_code = response_code

Expand All @@ -26,6 +28,14 @@ def response_code(self):

return self._response_code

@property
def response_id(self):
"""
:rtype: str
"""

return self._response_id


class BunqException(Exception):
def __init__(self, message):
Expand Down
82 changes: 67 additions & 15 deletions bunq/sdk/exception_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,86 @@ class ExceptionFactory:

# Constants for formatting messages
_FORMAT_RESPONSE_CODE_LINE = 'HTTP Response Code: {}'
_GLUE_ERROR_MESSAGES = '\n'
_FORMAT_RESPONSE_ID_LINE = 'The response id to help bunq debug: {}'
_FORMAT_ERROR_MESSAGE_LINE = 'Error message: {}'
_GLUE_ERROR_MESSAGE_NEW_LINE = '\n'
_GLUE_ERROR_MESSAGE_STRING_EMPTY = ''

@classmethod
def create_exception_for_response(cls, response_code, messages):
def create_exception_for_response(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arg list not formatted according to PEP 8: https://www.python.org/dev/peps/pep-0008/#indentation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, this is how we do it. The pep8 one is weird and ugly.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed 😝

cls,
response_code,
messages,
response_id
):
"""
:type response_id: str
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not ordered properly

:type response_code: int
:type messages: list[str]

:return: The exception according to the status code.
:rtype: ApiException
"""

error_message = cls._generate_message_error(response_code, messages)
error_message = cls._generate_message_error(
response_code,
messages,
response_id
)

if response_code == cls._HTTP_RESPONSE_CODE_BAD_REQUEST:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider creating a mapping (response_code => exception type) for all these cases

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has not been modified in this pr, please create a follow up issue for this.

If so, this must also be refactored in the other SDK's

return BadRequestException(error_message, response_code)
return BadRequestException(
error_message,
response_code,
response_id
)
if response_code == cls._HTTP_RESPONSE_CODE_UNAUTHORIZED:
return UnauthorizedException(error_message, response_code)
return UnauthorizedException(
error_message,
response_code,
response_id
)
if response_code == cls._HTTP_RESPONSE_CODE_FORBIDDEN:
return ForbiddenException(error_message, response_code)
return ForbiddenException(
error_message,
response_code,
response_id
)
if response_code == cls._HTTP_RESPONSE_CODE_NOT_FOUND:
return NotFoundException(error_message, response_code)
return NotFoundException(
error_message,
response_code,
response_id
)
if response_code == cls._HTTP_RESPONSE_CODE_METHOD_NOT_ALLOWED:
return MethodNotAllowedException(error_message, response_code)
return MethodNotAllowedException(
error_message,
response_code,
response_id
)
if response_code == cls._HTTP_RESPONSE_CODE_TOO_MANY_REQUESTS:
return TooManyRequestsException(error_message, response_code)
return TooManyRequestsException(
error_message,
response_code,
response_id
)
if response_code == cls._HTTP_RESPONSE_CODE_INTERNAL_SERVER_ERROR:
return PleaseContactBunqException(error_message, response_code)
return PleaseContactBunqException(
error_message,
response_code,
response_id
)

return UnknownApiErrorException(error_message, response_code)
return UnknownApiErrorException(
error_message,
response_code,
response_id
)

@classmethod
def _generate_message_error(cls, response_code, messages):
def _generate_message_error(cls, response_code, messages, response_id):
"""
:type response_id: str
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not ordered properly

:type response_code: int
:type messages: list[str]

Expand All @@ -63,15 +109,21 @@ def _generate_message_error(cls, response_code, messages):

line_response_code = cls._FORMAT_RESPONSE_CODE_LINE \
.format(response_code)
line_response_id = cls._FORMAT_RESPONSE_ID_LINE.format(response_id)
line_error_message = cls._FORMAT_ERROR_MESSAGE_LINE.format(
cls._GLUE_ERROR_MESSAGE_STRING_EMPTY.join(messages)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider whether _GLUE_ERROR_MESSAGE would be sufficient as a name here. Having the name imply it's an empty string is redundant information and not relevant for the consumer IMHO

)

return cls._glue_messages([line_response_code] + messages)
return cls._glue_all_error_message(
[line_response_code, line_response_id, line_error_message]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No trailing comma?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only in multi line

)

@classmethod
def _glue_messages(cls, messages):
def _glue_all_error_message(cls, messages):
"""
:type messages: list[str]

:rtype: str
"""

return cls._GLUE_ERROR_MESSAGES.join(messages)
return cls._GLUE_ERROR_MESSAGE_NEW_LINE.join(messages)
Empty file added tests/http/__init__.py
Empty file.
File renamed without changes.
29 changes: 29 additions & 0 deletions tests/http/test_bad_request_with_response_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from bunq.sdk.exception import BadRequestException
from bunq.sdk.model.generated.endpoint import UserPerson
from tests.bunq_test import BunqSdkTestCase


class TestPagination(BunqSdkTestCase):
"""
Tests if the response id from a failed request can be retrieved
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring should start on the same line as the opening """

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also how we do it 😁 we ignore pep8 's suggestion to begin doctoring right behind """

successfully.
"""

_INVALID_USER_PERSON_ID = 0

def test_bad_request_with_response_id(self):
"""
"""

caught_exception = None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


try:
UserPerson.get(
self._get_api_context(),
self._INVALID_USER_PERSON_ID
)
except BadRequestException as exception:
caught_exception = exception

self.assertIsNotNone(caught_exception)
self.assertIsNotNone(caught_exception.response_id)
File renamed without changes.