Skip to content

Commit ecd58c3

Browse files
committed
fix: repeat requests after timeout with too_many_requests (#37)
1 parent 4ccc96b commit ecd58c3

3 files changed

Lines changed: 30 additions & 52 deletions

File tree

churchtools_api/churchtools_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ def __init__(
6262
6363
"""
6464
super().__init__()
65-
self.session = None
66-
self.domain = domain
65+
self.session : None | RateLimitedSession = None
66+
self.domain : str = domain
6767

6868
if ct_token is not None:
6969
self.login_ct_rest_api(ct_token=ct_token)

churchtools_api/ratelimitedsession.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""This code is used to wrap the most common requests into a rate limited model.
22
3-
ChurchTools API usually accepts X / sec requests and will return an error if exceeded
3+
ChurchTools API usually responds code 429 on excessive use
4+
- repeating request after timeout will suceed
45
"""
5-
#TODO@bensteUEM: inquiry to CT Team 147987 for configured thresholds
6-
# https://github.com/bensteUEM/ChurchToolsAPI/issues/37
7-
6+
import logging
7+
from time import sleep
88
from typing import override
99

1010
import requests
11-
from ratelimit import limits, sleep_and_retry
11+
12+
logger = logging.getLogger(__name__)
1213

1314

1415
class RateLimitedSession(requests.Session):
@@ -17,18 +18,21 @@ class RateLimitedSession(requests.Session):
1718
with rate limits and retry
1819
"""
1920

20-
MAX_CALLS = 50
21-
LIMIT_PERIOD_SECONDS = 2
22-
2321
def __init__(self) -> None:
2422
"""Inits session with additional params."""
23+
logger.debug("init rate limited session")
2524
super().__init__()
2625

27-
@sleep_and_retry
28-
@limits(calls=MAX_CALLS, period=LIMIT_PERIOD_SECONDS)
2926
def _rate_limited_request(self, method, url, **kwargs) -> requests.Response: # noqa: ANN001, ANN003
3027
"""Rate limiting execution of original request method."""
31-
return super().request(method, url, **kwargs)
28+
result = super().request(method, url, **kwargs)
29+
30+
while result.status_code == requests.codes.too_many_requests:
31+
logger.info("rate limit reached - waiting 15 sec before repeating request")
32+
sleep(15.0)
33+
result = self._rate_limited_request(method=method, url=url, **kwargs)
34+
35+
return result
3236

3337
@override
3438
def request(self, method, url, **kwargs) -> requests.Response: # noqa: ANN001, ANN003

tests/test_ratelimitedsession.py

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
"""This test module is used to verify integrity of the Ratelimited session."""
2-
#TODO@bensteUEM: inquiry to CT Team 147987 for configured threshold
3-
# https://github.com/bensteUEM/ChurchToolsAPI/issues/37
42

53
import json
64
import logging
75
import logging.config
8-
import time
96
from pathlib import Path
107

118
import pytest
12-
import requests
139

14-
from churchtools_api.ratelimitedsession import RateLimitedSession
10+
from tests.test_churchtools_api_abstract import TestsChurchToolsApiAbstract
1511

1612
logger = logging.getLogger(__name__)
1713

@@ -23,41 +19,19 @@
2319
log_directory.mkdir(parents=True)
2420
logging.config.dictConfig(config=logging_config)
2521

26-
# Make sure test values are adjusted
27-
# - e.g. 250* google.com with 10 req per 2seconds ~= 50 sec
28-
# - e.g. 250* google.com with no threshold ~= 30 sec
29-
SAMPLE_URL = "https://google.com"
30-
EXPECTED_TIME = 30
3122

23+
class TestsRateLimitedSession(TestsChurchToolsApiAbstract):
24+
"""Test for Rate limits."""
3225

33-
@pytest.mark.skip("execute only if ratelimit is decreased")
34-
def test_no_rate_limit() -> None:
35-
"""Reference time for 250 requests without throttle using google.com.
26+
def test_no_rate_limit(self, caplog: pytest.LogCaptureFixture) -> None:
27+
"""Reference time for 250 requests without throttle using google.com."""
28+
SAMPLE_SONG_ID = 408
3629

37-
Actual time might depend on internet speed!
30+
with caplog.at_level(logging.INFO, logger="ratelimitedsession"):
31+
for _i in range(750):
32+
self.api.get_songs(song_id=SAMPLE_SONG_ID)
33+
EXPECTED_MESSAGES = [
34+
"rate limit reached - waiting 15 sec before repeating request"
35+
]
3836

39-
Only checks GET request for simplification.
40-
"""
41-
session = requests.Session()
42-
43-
start = time.perf_counter()
44-
for _i in range(250):
45-
session.get(url=SAMPLE_URL)
46-
end = time.perf_counter()
47-
48-
assert end - start < EXPECTED_TIME
49-
50-
@pytest.mark.skip("execute only if ratelimit is decreased")
51-
def test_rate_limited() -> None:
52-
"""Reference time for 250 requests with throttle using google.com.
53-
54-
Only checks GET request for simplification.
55-
"""
56-
session = RateLimitedSession()
57-
58-
start = time.perf_counter()
59-
for _i in range(250):
60-
session.get(url=SAMPLE_URL)
61-
end = time.perf_counter()
62-
63-
assert end-start > EXPECTED_TIME
37+
assert caplog.messages == EXPECTED_MESSAGES

0 commit comments

Comments
 (0)