Skip to content

Commit 339a372

Browse files
committed
Add SparkRateLimitWarning and use it for rate-limit test
Add a SparkRateLimitWarning class to warn users when a rate-limit event has been detected, and use this new warning (in place of the logging detection) to detect when a rate-limit event has occured in the rate-limit test.
1 parent b16acd1 commit 339a372

File tree

4 files changed

+53
-51
lines changed

4 files changed

+53
-51
lines changed

ciscosparkapi/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
from .api.teams import TeamsAPI as _TeamsAPI
2828
from .api.webhooks import WebhooksAPI as _WebhooksAPI
2929
from .exceptions import (
30-
SparkApiError, SparkRateLimitError, ciscosparkapiException,
30+
SparkApiError, SparkRateLimitError, SparkRateLimitWarning,
31+
ciscosparkapiException,
3132
)
3233
from .models import (
3334
AccessToken, License, Membership, Message, Organization, Person, Role,

ciscosparkapi/exceptions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,23 @@ def __init__(self, response):
152152
present in the response headers.
153153
154154
"""
155+
156+
157+
class SparkRateLimitWarning(UserWarning):
158+
"""Cisco Spark rate-limit exceeded warning; the request will be retried."""
159+
160+
def __init__(self, response):
161+
super(SparkRateLimitWarning, self).__init__()
162+
self.retry_after = int(response.headers.get('Retry-After', 200))
163+
"""The `Retry-After` time period (in seconds) provided by Cisco Spark.
164+
165+
Defaults to 200 seconds if the response `Retry-After` header isn't
166+
present in the response headers.
167+
168+
"""
169+
170+
def __str__(self):
171+
"""Spark rate-limit exceeded warning message."""
172+
return "Rate-limit response received; the request will " \
173+
"automatically be retried in {0} seconds." \
174+
"".format(self.retry_after)

ciscosparkapi/restsession.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
from past.builtins import basestring
2222
import requests
2323

24-
from .exceptions import SparkRateLimitError, ciscosparkapiException
24+
from .exceptions import (
25+
SparkRateLimitError, SparkRateLimitWarning, ciscosparkapiException
26+
)
2527
from .response_codes import EXPECTED_RESPONSE_CODE
2628
from .utils import (
2729
check_response_code, extract_and_parse_json, validate_base_url,
@@ -58,24 +60,25 @@ def _fix_next_url(next_url):
5860
5961
Raises:
6062
AssertionError: If the parameter types are incorrect.
61-
ciscosparkapiException: If 'next_url' does not contain a valid API
62-
endpoint URL (scheme, netloc and path).
63+
ValueError: If 'next_url' does not contain a valid API endpoint URL
64+
(scheme, netloc and path).
6365
6466
"""
6567
next_url = str(next_url)
6668
parsed_url = urllib.parse.urlparse(next_url)
6769

6870
if not parsed_url.scheme or not parsed_url.netloc or not parsed_url.path:
69-
error_message = "'next_url' must be a valid API endpoint URL, " \
70-
"minimally containing a scheme, netloc and path."
71-
raise ciscosparkapiException(error_message)
71+
raise ValueError(
72+
"'next_url' must be a valid API endpoint URL, minimally "
73+
"containing a scheme, netloc and path."
74+
)
7275

7376
if parsed_url.query:
7477
query_list = parsed_url.query.split('&')
7578
if 'max=null' in query_list:
7679
query_list.remove('max=null')
7780
warnings.warn("`max=null` still present in next-URL returned "
78-
"from Cisco Spark", Warning)
81+
"from Cisco Spark", RuntimeWarning)
7982
new_query = '&'.join(query_list)
8083
parsed_url = list(parsed_url)
8184
parsed_url[4] = new_query
@@ -275,9 +278,7 @@ def request(self, method, url, erc, **kwargs):
275278

276279
# Wait and retry if automatic rate-limit handling is enabled
277280
if self.wait_on_rate_limit and e.retry_after:
278-
logger.info("Received rate-limit message; "
279-
"waiting {0} seconds."
280-
"".format(e.retry_after))
281+
warnings.warn(SparkRateLimitWarning(response))
281282
time.sleep(e.retry_after)
282283
continue
283284

tests/test_restsession.py

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,30 @@
33

44

55
import logging
6+
import warnings
67

78
import pytest
89

10+
import ciscosparkapi
11+
912

1013
__author__ = "Chris Lunsford"
1114
__author_email__ = "[email protected]"
1215
__copyright__ = "Copyright (c) 2016-2018 Cisco and/or its affiliates."
1316
__license__ = "MIT"
1417

1518

16-
# Helper Classes
17-
class RateLimitDetector(logging.Handler):
18-
"""Detects occurrences of rate limiting."""
19-
20-
def __init__(self):
21-
super(RateLimitDetector, self).__init__()
22-
23-
self.rate_limit_detected = False
19+
logging.captureWarnings(True)
2420

25-
def emit(self, record):
26-
"""Check record to see if it is a rate-limit message."""
27-
assert isinstance(record, logging.LogRecord)
2821

29-
if "Received rate-limit message" in record.msg:
30-
self.rate_limit_detected = True
22+
# Helper Functions
23+
def rate_limit_detected(w):
24+
"""Check to see if a rate-limit warning is in the warnings list."""
25+
while w:
26+
if issubclass(w.pop().category, ciscosparkapi.SparkRateLimitWarning):
27+
return True
28+
break
29+
return False
3130

3231

3332
# CiscoSparkAPI Tests
@@ -36,35 +35,16 @@ class TestRestSession:
3635

3736
@pytest.mark.ratelimit
3837
def test_rate_limit_retry(self, api, rooms_list, add_rooms):
39-
logger = logging.getLogger(__name__)
40-
4138
# Save state and initialize test setup
4239
original_wait_on_rate_limit = api._session.wait_on_rate_limit
4340
api._session.wait_on_rate_limit = True
4441

45-
# Add log handler
46-
root_logger = logging.getLogger()
47-
rate_limit_detector = RateLimitDetector()
48-
root_logger.addHandler(rate_limit_detector)
49-
logger = logging.getLogger(__name__)
50-
logger.info("Starting Rate Limit Testing")
51-
52-
try:
53-
# Try and trigger a rate-limit
54-
rooms = api.rooms.list(max=1)
55-
request_count = 0
56-
while not rate_limit_detector.rate_limit_detected:
57-
for room in rooms:
58-
request_count += 1
59-
if rate_limit_detector.rate_limit_detected:
60-
break
61-
62-
finally:
63-
logger.info("Rate-limit reached with approximately %s requests.",
64-
request_count)
65-
# Remove the log handler and restore the pre-test state
66-
root_logger.removeHandler(rate_limit_detector)
67-
api._session.wait_on_rate_limit = original_wait_on_rate_limit
42+
with warnings.catch_warnings(record=True) as w:
43+
rooms = api.rooms.list()
44+
while True:
45+
# Try and trigger a rate-limit
46+
list(rooms)
47+
if rate_limit_detected(w):
48+
break
6849

69-
# Assert test condition
70-
assert rate_limit_detector.rate_limit_detected == True
50+
api._session.wait_on_rate_limit = original_wait_on_rate_limit

0 commit comments

Comments
 (0)