Skip to content

Commit 719b586

Browse files
authored
Merge pull request #69 from CiscoDevNet/enhance-exceptions
Simplify ApiError messages and add data attributes
2 parents f028c06 + 50cd1d8 commit 719b586

File tree

2 files changed

+60
-118
lines changed

2 files changed

+60
-118
lines changed

webexteamssdk/exceptions.py

Lines changed: 52 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -30,93 +30,15 @@
3030
unicode_literals,
3131
)
3232

33-
import sys
34-
import textwrap
33+
import logging
3534
from builtins import *
3635

3736
import requests
38-
from past.builtins import basestring
3937

4038
from .response_codes import RESPONSE_CODES
4139

4240

43-
def _to_unicode(string):
44-
"""Convert a string (bytes, str or unicode) to unicode."""
45-
assert isinstance(string, basestring)
46-
if sys.version_info[0] >= 3:
47-
if isinstance(string, bytes):
48-
return string.decode('utf-8')
49-
else:
50-
return string
51-
else:
52-
if isinstance(string, str):
53-
return string.decode('utf-8')
54-
else:
55-
return string
56-
57-
58-
def _sanitize(header_tuple):
59-
"""Sanitize request headers.
60-
61-
Remove authentication `Bearer` token.
62-
"""
63-
header, value = header_tuple
64-
65-
if (header.lower().strip() == "Authorization".lower().strip()
66-
and "Bearer".lower().strip() in value.lower().strip()):
67-
return header, "Bearer <redacted>"
68-
69-
else:
70-
return header_tuple
71-
72-
73-
def _response_to_string(response):
74-
"""Render a response object as a human readable string."""
75-
assert isinstance(response, requests.Response)
76-
request = response.request
77-
78-
section_header = "{title:-^79}"
79-
80-
# Prepare request components
81-
req = textwrap.fill("{} {}".format(request.method, request.url),
82-
width=79,
83-
subsequent_indent=' ' * (len(request.method) + 1))
84-
req_headers = [
85-
textwrap.fill("{}: {}".format(*_sanitize(header)),
86-
width=79,
87-
subsequent_indent=' ' * 4)
88-
for header in request.headers.items()
89-
]
90-
req_body = (textwrap.fill(_to_unicode(request.body), width=79)
91-
if request.body else "")
92-
93-
# Prepare response components
94-
resp = textwrap.fill("{} {}".format(response.status_code,
95-
response.reason
96-
if response.reason else ""),
97-
width=79,
98-
subsequent_indent=' ' * (len(request.method) + 1))
99-
resp_headers = [
100-
textwrap.fill("{}: {}".format(*header), width=79,
101-
subsequent_indent=' ' * 4)
102-
for header in response.headers.items()
103-
]
104-
resp_body = textwrap.fill(response.text, width=79) if response.text else ""
105-
106-
# Return the combined string
107-
return "\n".join([
108-
section_header.format(title="Request"),
109-
req,
110-
"\n".join(req_headers),
111-
"",
112-
req_body,
113-
"",
114-
section_header.format(title="Response"),
115-
resp,
116-
"\n".join(resp_headers),
117-
"",
118-
resp_body,
119-
])
41+
logger = logging.getLogger(__name__)
12042

12143

12244
class webexteamssdkException(Exception):
@@ -130,41 +52,55 @@ class AccessTokenError(webexteamssdkException):
13052

13153

13254
class ApiError(webexteamssdkException):
133-
"""Errors returned by requests to the Webex Teams cloud APIs."""
55+
"""Errors returned in response to requests sent to the Webex Teams APIs.
56+
57+
Several data attributes are available for inspection.
58+
"""
13459

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

138-
# Extended exception data attributes
139-
self.request = response.request
140-
"""The :class:`requests.PreparedRequest` of the API call."""
141-
63+
# Extended exception attributes
14264
self.response = response
14365
"""The :class:`requests.Response` object returned from the API call."""
14466

145-
# Error message
146-
response_code = response.status_code
147-
response_reason = " " + response.reason if response.reason else ""
148-
description = RESPONSE_CODES.get(
149-
response_code,
150-
"Unknown Response Code",
151-
)
152-
detail = _response_to_string(response)
67+
self.request = self.response.request
68+
"""The :class:`requests.PreparedRequest` of the API call."""
69+
70+
self.status_code = self.response.status_code
71+
"""The HTTP status code from the API response."""
72+
73+
self.status = self.response.reason
74+
"""The HTTP status from the API response."""
75+
76+
self.details = None
77+
"""The parsed JSON details from the API response."""
78+
if "application/json" in \
79+
self.response.headers.get("Content-Type", "").lower():
80+
try:
81+
self.details = self.response.json()
82+
except ValueError:
83+
logger.warning("Error parsing JSON response body")
84+
85+
self.message = self.details.get("message") if self.details else None
86+
"""The error message from the parsed API response."""
87+
88+
self.description = RESPONSE_CODES.get(self.status_code)
89+
"""A description of the HTTP Response Code from the API docs."""
15390

15491
super(ApiError, self).__init__(
155-
"Response Code [{}]{} - {}\n{}"
156-
"".format(
157-
response_code,
158-
response_reason,
159-
description,
160-
detail
92+
"[{status_code}]{status} - {message}".format(
93+
status_code=self.status_code,
94+
status=" " + self.status if self.status else "",
95+
message=self.message or self.description or "Unknown Error",
16196
)
16297
)
16398

164-
165-
class MalformedResponse(webexteamssdkException):
166-
"""Raised when a malformed response is received from Webex Teams."""
167-
pass
99+
def __repr__(self):
100+
return "<{exception_name} [{status_code}]>".format(
101+
exception_name=self.__class__.__name__,
102+
status_code=self.status_code,
103+
)
168104

169105

170106
class RateLimitError(ApiError):
@@ -175,18 +111,19 @@ class RateLimitError(ApiError):
175111
"""
176112

177113
def __init__(self, response):
178-
super(RateLimitError, self).__init__(response)
114+
assert isinstance(response, requests.Response)
179115

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

125+
super(RateLimitError, self).__init__(response)
126+
190127

191128
class RateLimitWarning(UserWarning):
192129
"""Webex Teams rate-limit exceeded warning.
@@ -196,18 +133,20 @@ class RateLimitWarning(UserWarning):
196133
"""
197134

198135
def __init__(self, response):
199-
super(RateLimitWarning, self).__init__()
136+
assert isinstance(response, requests.Response)
137+
138+
# Extended warning attributes
200139
self.retry_after = max(1, int(response.headers.get('Retry-After', 15)))
201140
"""The `Retry-After` time period (in seconds) provided by Webex Teams.
202141
203142
Defaults to 15 seconds if the response `Retry-After` header isn't
204143
present in the response headers, and defaults to a minimum wait time of
205144
1 second if Webex Teams returns a `Retry-After` header of 0 seconds.
206-
207145
"""
208146

209-
def __str__(self):
210-
"""Webex Teams rate-limit exceeded warning message."""
211-
return "Rate-limit response received; the request will " \
212-
"automatically be retried in {0} seconds." \
213-
"".format(self.retry_after)
147+
super(RateLimitWarning, self).__init__()
148+
149+
150+
class MalformedResponse(webexteamssdkException):
151+
"""Raised when a malformed response is received from Webex Teams."""
152+
pass

webexteamssdk/response_codes.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@
3232

3333

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

0 commit comments

Comments
 (0)