30
30
unicode_literals ,
31
31
)
32
32
33
- import sys
34
- import textwrap
33
+ import logging
35
34
from builtins import *
36
35
37
36
import requests
38
- from past .builtins import basestring
39
37
40
38
from .response_codes import RESPONSE_CODES
41
39
42
40
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__ )
120
42
121
43
122
44
class webexteamssdkException (Exception ):
@@ -130,41 +52,55 @@ class AccessTokenError(webexteamssdkException):
130
52
131
53
132
54
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
+ """
134
59
135
60
def __init__ (self , response ):
136
61
assert isinstance (response , requests .Response )
137
62
138
- # Extended exception data attributes
139
- self .request = response .request
140
- """The :class:`requests.PreparedRequest` of the API call."""
141
-
63
+ # Extended exception attributes
142
64
self .response = response
143
65
"""The :class:`requests.Response` object returned from the API call."""
144
66
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."""
153
90
154
91
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" ,
161
96
)
162
97
)
163
98
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
+ )
168
104
169
105
170
106
class RateLimitError (ApiError ):
@@ -175,18 +111,19 @@ class RateLimitError(ApiError):
175
111
"""
176
112
177
113
def __init__ (self , response ):
178
- super ( RateLimitError , self ). __init__ ( response )
114
+ assert isinstance ( response , requests . Response )
179
115
180
- # Extended exception data attributes
116
+ # Extended exception attributes
181
117
self .retry_after = max (1 , int (response .headers .get ('Retry-After' , 15 )))
182
118
"""The `Retry-After` time period (in seconds) provided by Webex Teams.
183
119
184
120
Defaults to 15 seconds if the response `Retry-After` header isn't
185
121
present in the response headers, and defaults to a minimum wait time of
186
122
1 second if Webex Teams returns a `Retry-After` header of 0 seconds.
187
-
188
123
"""
189
124
125
+ super (RateLimitError , self ).__init__ (response )
126
+
190
127
191
128
class RateLimitWarning (UserWarning ):
192
129
"""Webex Teams rate-limit exceeded warning.
@@ -196,18 +133,20 @@ class RateLimitWarning(UserWarning):
196
133
"""
197
134
198
135
def __init__ (self , response ):
199
- super (RateLimitWarning , self ).__init__ ()
136
+ assert isinstance (response , requests .Response )
137
+
138
+ # Extended warning attributes
200
139
self .retry_after = max (1 , int (response .headers .get ('Retry-After' , 15 )))
201
140
"""The `Retry-After` time period (in seconds) provided by Webex Teams.
202
141
203
142
Defaults to 15 seconds if the response `Retry-After` header isn't
204
143
present in the response headers, and defaults to a minimum wait time of
205
144
1 second if Webex Teams returns a `Retry-After` header of 0 seconds.
206
-
207
145
"""
208
146
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