3030 unicode_literals ,
3131)
3232
33- import sys
34- import textwrap
33+ import logging
3534from builtins import *
3635
3736import requests
38- from past .builtins import basestring
3937
4038from .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
12244class webexteamssdkException (Exception ):
@@ -130,41 +52,55 @@ class AccessTokenError(webexteamssdkException):
13052
13153
13254class 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
170106class 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
191128class 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
0 commit comments