Skip to content

Commit 6f6f32e

Browse files
committed
Refactor Response Codes, Exceptions & Automatic Rate-Limit Handling
Move the response code “data” as distributed throughout the package to a dedicated response-code module. Refactor the package exceptions to enable greater reuse through inheritance and create a new SparkRateLimitError to be used when catching rate-limit errors. Refactor the automatic rate limit handling to be controlled with a simple wait_on_rate_limit on/off setting, and to use the new SparkRateLimitError for catching rate limit messages.
1 parent b826f57 commit 6f6f32e

File tree

8 files changed

+241
-165
lines changed

8 files changed

+241
-165
lines changed

ciscosparkapi/__init__.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from ciscosparkapi.exceptions import ciscosparkapiException, SparkApiError
1919
from ciscosparkapi.restsession import (
2020
DEFAULT_SINGLE_REQUEST_TIMEOUT,
21-
DEFAULT_RATE_LIMIT_TIMEOUT,
21+
DEFAULT_WAIT_ON_RATE_LIMIT,
2222
RestSession,
2323
)
2424
from ciscosparkapi.api.people import Person, PeopleAPI
@@ -97,7 +97,7 @@ class CiscoSparkAPI(object):
9797
def __init__(self, access_token=None, base_url=DEFAULT_BASE_URL,
9898
timeout=None,
9999
single_request_timeout=DEFAULT_SINGLE_REQUEST_TIMEOUT,
100-
rate_limit_timeout=DEFAULT_RATE_LIMIT_TIMEOUT):
100+
wait_on_rate_limit=DEFAULT_WAIT_ON_RATE_LIMIT):
101101
"""Create a new CiscoSparkAPI object.
102102
103103
An access token must be used when interacting with the Cisco Spark API.
@@ -119,8 +119,13 @@ def __init__(self, access_token=None, base_url=DEFAULT_BASE_URL,
119119
base_url(basestring): The base URL to be prefixed to the
120120
individual API endpoint suffixes.
121121
Defaults to ciscosparkapi.DEFAULT_BASE_URL.
122-
timeout(int): Timeout (in seconds) for RESTful HTTP requests.
123-
Defaults to ciscosparkapi.DEFAULT_TIMEOUT.
122+
timeout(int): [deprecated] Timeout (in seconds) for RESTful HTTP
123+
requests. Defaults to ciscosparkapi.DEFAULT_TIMEOUT.
124+
single_request_timeout(int): Timeout (in seconds) for RESTful HTTP
125+
requests. Defaults to
126+
ciscosparkapi.DEFAULT_SINGLE_REQUEST_TIMEOUT.
127+
wait_on_rate_limit(bool): Enables or disables automatic rate-limit
128+
handling. Defaults to ciscosparkapi.DEFAULT_WAIT_ON_RATE_LIMIT.
124129
125130
Returns:
126131
CiscoSparkAPI: A new CiscoSparkAPI object.
@@ -151,7 +156,7 @@ def __init__(self, access_token=None, base_url=DEFAULT_BASE_URL,
151156
base_url,
152157
timeout=timeout,
153158
single_request_timeout=single_request_timeout,
154-
rate_limit_timeout=rate_limit_timeout,
159+
wait_on_rate_limit=wait_on_rate_limit
155160
)
156161

157162
# Spark API wrappers
@@ -184,5 +189,5 @@ def single_request_timeout(self):
184189
return self._session.single_request_timeout
185190

186191
@property
187-
def rate_limit_timeout(self):
188-
return self._session.rate_limit_timeout
192+
def wait_on_rate_limit(self):
193+
return self._session.wait_on_rate_limit

ciscosparkapi/api/accesstokens.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@
2828

2929
from ciscosparkapi.sparkdata import SparkData
3030
from ciscosparkapi.utils import (
31-
ERC,
3231
validate_base_url,
3332
check_response_code,
3433
extract_and_parse_json,
3534
)
35+
from ciscosparkapi.responsecodes import EXPECTED_RESPONSE_CODE
3636

3737

3838
__author__ = "Chris Lunsford"
@@ -156,7 +156,7 @@ def get(self, client_id, client_secret, code, redirect_uri):
156156
# API request
157157
response = requests.post(self._endpoint_url, data=data,
158158
**self._request_kwargs)
159-
check_response_code(response, ERC['POST'])
159+
check_response_code(response, EXPECTED_RESPONSE_CODE['POST'])
160160
json_data = extract_and_parse_json(response)
161161
# Return a AccessToken object created from the response JSON data
162162
return AccessToken(json_data)
@@ -194,7 +194,7 @@ def refresh(self, client_id, client_secret, refresh_token):
194194
# API request
195195
response = requests.post(self._endpoint_url, data=data,
196196
**self._request_kwargs)
197-
check_response_code(response, ERC['POST'])
197+
check_response_code(response, EXPECTED_RESPONSE_CODE['POST'])
198198
json_data = extract_and_parse_json(response)
199199
# Return a AccessToken object created from the response JSON data
200200
return AccessToken(json_data)

ciscosparkapi/exceptions.py

Lines changed: 86 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,57 +12,105 @@
1212
from builtins import *
1313
from past.builtins import basestring
1414

15+
import json
16+
17+
import requests
18+
19+
from collections import OrderedDict
20+
21+
from ciscosparkapi.responsecodes import SPARK_RESPONSE_CODES
22+
1523

1624
__author__ = "Chris Lunsford"
1725
__author_email__ = "[email protected]"
1826
__copyright__ = "Copyright (c) 2016 Cisco Systems, Inc."
1927
__license__ = "MIT"
2028

2129

22-
SPARK_RESPONSE_CODES = {
23-
200: "OK",
24-
204: "Member deleted.",
25-
400: "The request was invalid or cannot be otherwise served. An "
26-
"accompanying error message will explain further.",
27-
401: "Authentication credentials were missing or incorrect.",
28-
403: "The request is understood, but it has been refused or access is not "
29-
"allowed.",
30-
404: "The URI requested is invalid or the resource requested, such as a "
31-
"user, does not exist. Also returned when the requested format is "
32-
"not supported by the requested method.",
33-
409: "The request could not be processed because it conflicts with some "
34-
"established rule of the system. For example, a person may not be "
35-
"added to a room more than once.",
36-
429: "Too many requests have been sent in a given amount of time and the "
37-
"request has been rate limited. A Retry-After header should be "
38-
"present that specifies how many seconds you need to wait before a "
39-
"successful request can be made.",
40-
500: "Something went wrong on the server.",
41-
503: "Server is overloaded with requests. Try again later."
42-
}
43-
44-
4530
class ciscosparkapiException(Exception):
4631
"""Base class for all ciscosparkapi package exceptions."""
4732

48-
def __init__(self, *args, **kwargs):
49-
super(ciscosparkapiException, self).__init__(*args, **kwargs)
33+
def __init__(self, *error_message_args, **error_data):
34+
super(ciscosparkapiException, self).__init__()
35+
36+
self.error_message_args = error_message_args
37+
self.error_data = OrderedDict(error_data)
38+
39+
@property
40+
def error_message(self):
41+
"""The error message created from the error message arguments."""
42+
if not self.error_message_args:
43+
return ""
44+
elif len(self.error_message_args) == 1:
45+
return str(self.error_message_args[0])
46+
elif len(self.error_message_args) > 1 \
47+
and isinstance(self.error_message_args[0], basestring):
48+
return self.error_message_args[0] % self.error_message_args[1:]
49+
else:
50+
return "; ".join(self.error_message_args)
51+
52+
def __repr__(self):
53+
"""String representation of the exception."""
54+
arg_list = self.error_message_args
55+
kwarg_list = [str(key) + "=" + repr(value)
56+
for key, value in self.error_data.items()]
57+
arg_string = ", ".join(arg_list + kwarg_list)
58+
59+
return self.__class__.__name__ + "(" + arg_string + ")"
60+
61+
def __str__(self):
62+
"""Human readable string representation of the exception."""
63+
return self.error_message + '\n' + \
64+
json.dumps(self.error_data, indent=4)
5065

5166

5267
class SparkApiError(ciscosparkapiException):
5368
"""Errors returned by requests to the Cisco Spark cloud APIs."""
5469

55-
def __init__(self, response_code, request=None, response=None):
56-
assert isinstance(response_code, int)
57-
self.response_code = response_code
58-
self.request = request
70+
def __init__(self, response):
71+
assert isinstance(response, requests.Response)
72+
73+
super(SparkApiError, self).__init__()
74+
75+
# Convenience data attributes
76+
self.request = response.request
5977
self.response = response
60-
response_text = SPARK_RESPONSE_CODES.get(response_code)
61-
if response_text:
62-
self.response_text = response_text
63-
error_message = "Response Code [{!s}] - {}".format(response_code,
64-
response_text)
65-
else:
66-
error_message = "Response Code [{!s}] - " \
67-
"Unknown Response Code".format(response_code)
68-
super(SparkApiError, self).__init__(error_message)
78+
self.response_code = response.status_code
79+
self.response_text = SPARK_RESPONSE_CODES.get(self.response_code,
80+
"Unknown Response Code")
81+
82+
# Error message and parameters
83+
self.error_message_args = [
84+
"Response Code [%s] - %s",
85+
self.response_code,
86+
self.response_text
87+
]
88+
89+
# Error Data
90+
self.error_data["response_code"] = self.response_code
91+
self.error_data["description"] = self.response_text
92+
if response.text:
93+
try:
94+
response_data = json.loads(response.text,
95+
object_pairs_hook=OrderedDict)
96+
except ValueError:
97+
self.error_data["response_body"] = response.text
98+
else:
99+
self.error_data["response_body"] = response_data
100+
101+
102+
class SparkRateLimitError(SparkApiError):
103+
"""Cisco Spark Rate-Limit exceeded Error."""
104+
105+
def __init__(self, response):
106+
assert isinstance(response, requests.Response)
107+
108+
super(SparkRateLimitError, self).__init__(response)
109+
110+
retry_after = response.headers.get('Retry-After')
111+
if retry_after:
112+
# Convenience data attributes
113+
self.retry_after = float(retry_after)
114+
115+
# Error Data
116+
self.error_data["retry_after"] = self.retry_after

ciscosparkapi/responsecodes.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# -*- coding: utf-8 -*-
2+
"""Cisco Spark Response Codes."""
3+
4+
5+
# Use future for Python v2 and v3 compatibility
6+
from __future__ import (
7+
absolute_import,
8+
division,
9+
print_function,
10+
unicode_literals,
11+
)
12+
from builtins import *
13+
from past.builtins import basestring
14+
15+
16+
SPARK_RESPONSE_CODES = {
17+
200: "OK",
18+
204: "Member deleted.",
19+
400: "The request was invalid or cannot be otherwise served. An "
20+
"accompanying error message will explain further.",
21+
401: "Authentication credentials were missing or incorrect.",
22+
403: "The request is understood, but it has been refused or access is not "
23+
"allowed.",
24+
404: "The URI requested is invalid or the resource requested, such as a "
25+
"user, does not exist. Also returned when the requested format is "
26+
"not supported by the requested method.",
27+
409: "The request could not be processed because it conflicts with some "
28+
"established rule of the system. For example, a person may not be "
29+
"added to a room more than once.",
30+
429: "Too many requests have been sent in a given amount of time and the "
31+
"request has been rate limited. A Retry-After header should be "
32+
"present that specifies how many seconds you need to wait before a "
33+
"successful request can be made.",
34+
500: "Something went wrong on the server.",
35+
503: "Server is overloaded with requests. Try again later."
36+
}
37+
38+
RATE_LIMIT_RESPONSE_CODE = 429
39+
40+
EXPECTED_RESPONSE_CODE = {
41+
'GET': 200,
42+
'POST': 200,
43+
'PUT': 200,
44+
'DELETE': 204
45+
}

0 commit comments

Comments
 (0)