Skip to content
Open
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
12 changes: 8 additions & 4 deletions smartystreets_python_sdk/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@
PAYMENT_REQUIRED = "Payment Required: There is no active subscription\
for the account associated with the credentials submitted with the request."

FORBIDDEN = "Because the international service is currently in a limited release phase, only approved accounts" \
" may access the service."
FORBIDDEN = "Forbidden: The request contained valid data and was understood by the server, but the server is\
refusing action."

REQUEST_TIMEOUT = "Request timeout error."

REQUEST_ENTITY_TOO_LARGE = "Request Entity Too Large: The request body has exceeded the maximum size."

BAD_REQUEST = "Bad Request (Malformed Payload): A GET request lacked a street field or the request body of a\
BAD_REQUEST = "Bad Request (Malformed Payload): A GET request lacked a required field or the request body of a\
POST request contained malformed JSON."

UNPROCESSABLE_ENTITY = "GET request lacked required fields."

TOO_MANY_REQUESTS = "The rate limit has been exceeded."
TOO_MANY_REQUESTS = "Too Many Requests: The rate limit for your account has been exceeded."

INTERNAL_SERVER_ERROR = "Internal Server Error."

BAD_GATEWAY = "Bad Gateway error."

SERVICE_UNAVAILABLE = "Service Unavailable. Try again later."

GATEWAY_TIMEOUT = "The upstream data provider did not respond in a timely fashion and the request failed. " \
Expand Down
8 changes: 8 additions & 0 deletions smartystreets_python_sdk/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ class BadRequestError(SmartyException):
pass


class RequestTimeoutError(SmartyException):
pass


class BadGatewayError(SmartyException):
pass


class UnprocessableEntityError(SmartyException):
pass

Expand Down
31 changes: 21 additions & 10 deletions smartystreets_python_sdk/status_code_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ def send(self, request):
response.error = exceptions.NotModifiedError(errors.NOT_MODIFIED, response.find_header('etag'))
elif response.status_code == 429:
response.error = self.parse_rate_limit_response(response)
elif response.status_code in [400, 401, 402, 403, 413, 422]:
response.error = messageFrom(response, statuses.get(response.status_code))
else:
response.error = statuses.get(response.status_code)
elif response.status_code != 200:
response.error = messageFrom(response, fallback_error(response.status_code))

return response

def fallback_error(status_code):
error = statuses.get(status_code)
if error is None:
error = exceptions.SmartyException(
'The server returned an unexpected HTTP status code: {}'.format(status_code))
return error


def messageFrom(response, fallback):
message = extract_message(response)
if message:
Expand All @@ -51,10 +57,6 @@ def extract_message(response):
return message.rstrip()


def ok():
return None


def bad_credentials():
return exceptions.BadCredentialsError(errors.BAD_CREDENTIALS)

Expand All @@ -67,6 +69,10 @@ def forbidden():
return exceptions.ForbiddenError(errors.FORBIDDEN)


def request_timeout():
return exceptions.RequestTimeoutError(errors.REQUEST_TIMEOUT)


def request_entity_too_large():
return exceptions.RequestEntityTooLargeError(errors.REQUEST_ENTITY_TOO_LARGE)

Expand All @@ -83,6 +89,10 @@ def internal_server_error():
return exceptions.InternalServerError(errors.INTERNAL_SERVER_ERROR)


def bad_gateway():
return exceptions.BadGatewayError(errors.BAD_GATEWAY)


def service_unavailable():
return exceptions.ServiceUnavailableError(errors.SERVICE_UNAVAILABLE)

Expand All @@ -91,14 +101,15 @@ def gateway_timeout():
return exceptions.GatewayTimeoutError(errors.GATEWAY_TIMEOUT)


statuses = {200: ok(),
401: bad_credentials(),
statuses = {401: bad_credentials(),
402: payment_required(),
403: forbidden(),
408: request_timeout(),
413: request_entity_too_large(),
400: bad_request(),
422: unprocessable_entity(),
500: internal_server_error(),
502: bad_gateway(),
503: service_unavailable(),
504: gateway_timeout()
}
75 changes: 74 additions & 1 deletion test/status_code_sender_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest

from smartystreets_python_sdk import Response, errors
from smartystreets_python_sdk import Response, errors, exceptions
from smartystreets_python_sdk.exceptions import BadRequestError, NotModifiedError
from smartystreets_python_sdk.status_code_sender import StatusCodeSender
from test.mocks import MockSender
Expand Down Expand Up @@ -70,6 +70,79 @@ def test_client_error_falls_back_when_no_message(self):
self.assertIsInstance(response.error, BadRequestError)
self.assertEqual(errors.BAD_REQUEST, str(response.error))

def test_each_status_code_uses_api_message_when_present(self):
payload = '{"errors": [{"message": "API says no"}]}'
expected_types = {400: exceptions.BadRequestError,
401: exceptions.BadCredentialsError,
402: exceptions.PaymentRequiredError,
403: exceptions.ForbiddenError,
408: exceptions.RequestTimeoutError,
413: exceptions.RequestEntityTooLargeError,
422: exceptions.UnprocessableEntityError,
429: exceptions.TooManyRequestsError,
500: exceptions.InternalServerError,
502: exceptions.BadGatewayError,
503: exceptions.ServiceUnavailableError,
504: exceptions.GatewayTimeoutError}

for status_code, error_type in expected_types.items():
with self.subTest(status_code=status_code):
inner = MockSender(Response(payload, status_code, {}))

response = StatusCodeSender(inner).send(None)

self.assertIsInstance(response.error, error_type)
self.assertEqual('API says no', str(response.error))

def test_each_status_code_falls_back_to_standard_message(self):
expected_messages = {400: errors.BAD_REQUEST,
401: errors.BAD_CREDENTIALS,
402: errors.PAYMENT_REQUIRED,
403: errors.FORBIDDEN,
408: errors.REQUEST_TIMEOUT,
413: errors.REQUEST_ENTITY_TOO_LARGE,
422: errors.UNPROCESSABLE_ENTITY,
429: errors.TOO_MANY_REQUESTS,
500: errors.INTERNAL_SERVER_ERROR,
502: errors.BAD_GATEWAY,
503: errors.SERVICE_UNAVAILABLE,
504: errors.GATEWAY_TIMEOUT}

for status_code, message in expected_messages.items():
with self.subTest(status_code=status_code):
inner = MockSender(Response("", status_code, {}))

response = StatusCodeSender(inner).send(None)

self.assertEqual(message, str(response.error))

def test_standard_messages_match_shared_wording(self):
self.assertEqual("Bad Request (Malformed Payload): A GET request lacked a required field or the"
" request body of a POST request contained malformed JSON.", errors.BAD_REQUEST)
self.assertEqual("Forbidden: The request contained valid data and was understood by the server,"
" but the server is refusing action.", errors.FORBIDDEN)
self.assertEqual("Request timeout error.", errors.REQUEST_TIMEOUT)
self.assertEqual("Too Many Requests: The rate limit for your account has been exceeded.",
errors.TOO_MANY_REQUESTS)
self.assertEqual("Bad Gateway error.", errors.BAD_GATEWAY)

def test_unexpected_status_code_falls_back_to_standard_message(self):
inner = MockSender(Response("", 418, {}))

response = StatusCodeSender(inner).send(None)

self.assertIsInstance(response.error, exceptions.SmartyException)
self.assertEqual('The server returned an unexpected HTTP status code: 418', str(response.error))

def test_unexpected_status_code_uses_api_message_when_present(self):
payload = '{"errors": [{"message": "API teapot message"}]}'
inner = MockSender(Response(payload, 418, {}))

response = StatusCodeSender(inner).send(None)

self.assertIsInstance(response.error, exceptions.SmartyException)
self.assertEqual('API teapot message', str(response.error))


if __name__ == '__main__':
unittest.main()