Skip to content

Commit

Permalink
Merge pull request #69 from CiscoDevNet/enhance-exceptions
Browse files Browse the repository at this point in the history
Simplify ApiError messages and add data attributes
  • Loading branch information
cmlccie committed Jan 3, 2019
2 parents f028c06 + 50cd1d8 commit 719b586
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 118 deletions.
165 changes: 52 additions & 113 deletions webexteamssdk/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,93 +30,15 @@
unicode_literals,
)

import sys
import textwrap
import logging
from builtins import *

import requests
from past.builtins import basestring

from .response_codes import RESPONSE_CODES


def _to_unicode(string):
"""Convert a string (bytes, str or unicode) to unicode."""
assert isinstance(string, basestring)
if sys.version_info[0] >= 3:
if isinstance(string, bytes):
return string.decode('utf-8')
else:
return string
else:
if isinstance(string, str):
return string.decode('utf-8')
else:
return string


def _sanitize(header_tuple):
"""Sanitize request headers.
Remove authentication `Bearer` token.
"""
header, value = header_tuple

if (header.lower().strip() == "Authorization".lower().strip()
and "Bearer".lower().strip() in value.lower().strip()):
return header, "Bearer <redacted>"

else:
return header_tuple


def _response_to_string(response):
"""Render a response object as a human readable string."""
assert isinstance(response, requests.Response)
request = response.request

section_header = "{title:-^79}"

# Prepare request components
req = textwrap.fill("{} {}".format(request.method, request.url),
width=79,
subsequent_indent=' ' * (len(request.method) + 1))
req_headers = [
textwrap.fill("{}: {}".format(*_sanitize(header)),
width=79,
subsequent_indent=' ' * 4)
for header in request.headers.items()
]
req_body = (textwrap.fill(_to_unicode(request.body), width=79)
if request.body else "")

# Prepare response components
resp = textwrap.fill("{} {}".format(response.status_code,
response.reason
if response.reason else ""),
width=79,
subsequent_indent=' ' * (len(request.method) + 1))
resp_headers = [
textwrap.fill("{}: {}".format(*header), width=79,
subsequent_indent=' ' * 4)
for header in response.headers.items()
]
resp_body = textwrap.fill(response.text, width=79) if response.text else ""

# Return the combined string
return "\n".join([
section_header.format(title="Request"),
req,
"\n".join(req_headers),
"",
req_body,
"",
section_header.format(title="Response"),
resp,
"\n".join(resp_headers),
"",
resp_body,
])
logger = logging.getLogger(__name__)


class webexteamssdkException(Exception):
Expand All @@ -130,41 +52,55 @@ class AccessTokenError(webexteamssdkException):


class ApiError(webexteamssdkException):
"""Errors returned by requests to the Webex Teams cloud APIs."""
"""Errors returned in response to requests sent to the Webex Teams APIs.
Several data attributes are available for inspection.
"""

def __init__(self, response):
assert isinstance(response, requests.Response)

# Extended exception data attributes
self.request = response.request
"""The :class:`requests.PreparedRequest` of the API call."""

# Extended exception attributes
self.response = response
"""The :class:`requests.Response` object returned from the API call."""

# Error message
response_code = response.status_code
response_reason = " " + response.reason if response.reason else ""
description = RESPONSE_CODES.get(
response_code,
"Unknown Response Code",
)
detail = _response_to_string(response)
self.request = self.response.request
"""The :class:`requests.PreparedRequest` of the API call."""

self.status_code = self.response.status_code
"""The HTTP status code from the API response."""

self.status = self.response.reason
"""The HTTP status from the API response."""

self.details = None
"""The parsed JSON details from the API response."""
if "application/json" in \
self.response.headers.get("Content-Type", "").lower():
try:
self.details = self.response.json()
except ValueError:
logger.warning("Error parsing JSON response body")

self.message = self.details.get("message") if self.details else None
"""The error message from the parsed API response."""

self.description = RESPONSE_CODES.get(self.status_code)
"""A description of the HTTP Response Code from the API docs."""

super(ApiError, self).__init__(
"Response Code [{}]{} - {}\n{}"
"".format(
response_code,
response_reason,
description,
detail
"[{status_code}]{status} - {message}".format(
status_code=self.status_code,
status=" " + self.status if self.status else "",
message=self.message or self.description or "Unknown Error",
)
)


class MalformedResponse(webexteamssdkException):
"""Raised when a malformed response is received from Webex Teams."""
pass
def __repr__(self):
return "<{exception_name} [{status_code}]>".format(
exception_name=self.__class__.__name__,
status_code=self.status_code,
)


class RateLimitError(ApiError):
Expand All @@ -175,18 +111,19 @@ class RateLimitError(ApiError):
"""

def __init__(self, response):
super(RateLimitError, self).__init__(response)
assert isinstance(response, requests.Response)

# Extended exception data attributes
# Extended exception attributes
self.retry_after = max(1, int(response.headers.get('Retry-After', 15)))
"""The `Retry-After` time period (in seconds) provided by Webex Teams.
Defaults to 15 seconds if the response `Retry-After` header isn't
present in the response headers, and defaults to a minimum wait time of
1 second if Webex Teams returns a `Retry-After` header of 0 seconds.
"""

super(RateLimitError, self).__init__(response)


class RateLimitWarning(UserWarning):
"""Webex Teams rate-limit exceeded warning.
Expand All @@ -196,18 +133,20 @@ class RateLimitWarning(UserWarning):
"""

def __init__(self, response):
super(RateLimitWarning, self).__init__()
assert isinstance(response, requests.Response)

# Extended warning attributes
self.retry_after = max(1, int(response.headers.get('Retry-After', 15)))
"""The `Retry-After` time period (in seconds) provided by Webex Teams.
Defaults to 15 seconds if the response `Retry-After` header isn't
present in the response headers, and defaults to a minimum wait time of
1 second if Webex Teams returns a `Retry-After` header of 0 seconds.
"""

def __str__(self):
"""Webex Teams rate-limit exceeded warning message."""
return "Rate-limit response received; the request will " \
"automatically be retried in {0} seconds." \
"".format(self.retry_after)
super(RateLimitWarning, self).__init__()


class MalformedResponse(webexteamssdkException):
"""Raised when a malformed response is received from Webex Teams."""
pass
13 changes: 8 additions & 5 deletions webexteamssdk/response_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@


RESPONSE_CODES = {
200: "OK",
204: "Member deleted.",
400: "The request was invalid or cannot be otherwise served. An "
"accompanying error message will explain further.",
200: "Successful request with body content.",
204: "Successful request without body content.",
400: "The request was invalid or cannot be otherwise served.",
401: "Authentication credentials were missing or incorrect.",
403: "The request is understood, but it has been refused or access is not "
"allowed.",
Expand All @@ -47,11 +46,15 @@
409: "The request could not be processed because it conflicts with some "
"established rule of the system. For example, a person may not be "
"added to a room more than once.",
415: "The request was made to a resource without specifying a media type "
"or used a media type that is not supported.",
429: "Too many requests have been sent in a given amount of time and the "
"request has been rate limited. A Retry-After header should be "
"present that specifies how many seconds you need to wait before a "
"successful request can be made.",
500: "Something went wrong on the server.",
500: "Something went wrong on the server. If the issue persists, feel "
"free to contact the Webex Developer Support team "
"(https://developer.webex.com/support).",
502: "The server received an invalid response from an upstream server "
"while processing the request. Try again later.",
503: "Server is overloaded with requests. Try again later."
Expand Down

0 comments on commit 719b586

Please sign in to comment.