Skip to content

Commit 5b4cdab

Browse files
committed
[SAC-27535] Updated http.py to improve rate limit handling
1 parent ddc8dde commit 5b4cdab

File tree

1 file changed

+55
-41
lines changed

1 file changed

+55
-41
lines changed

tap_frontapp/http.py

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""HTTP client for FrontApp tap with rate limit handling."""
2+
13
import json
24
import time
35
import requests
@@ -7,85 +9,97 @@
79

810
LOGGER = singer.get_logger()
911

12+
1013
class RateLimitException(Exception):
11-
pass
14+
"""Exception raised when rate limit is exceeded."""
15+
1216

1317
class MetricsRateLimitException(Exception):
14-
pass
18+
"""Exception raised for 423 locked error specific to metrics."""
19+
1520

16-
class Client(object):
17-
BASE_URL = 'https://api2.frontapp.com'
21+
class Client:
22+
"""Client to make authenticated requests to the FrontApp API."""
23+
BASE_URL = "https://api2.frontapp.com"
1824

1925
def __init__(self, config):
20-
self.token = 'Bearer ' + config.get('token')
26+
self.token = "Bearer " + config.get("token")
2127
self.session = requests.Session()
2228
self.calls_remaining = None
2329
self.limit_reset = None
2430

2531
def url(self, path):
32+
"""Return full API URL."""
2633
return self.BASE_URL + path
2734

28-
# Log backoff retries
29-
def log_backoff(details):
30-
LOGGER.warning(f"[Backoff] Retrying {details['target'].__name__} in {details['wait']}s due to {details['exception']}")
31-
32-
# Retry strategy: Max 5 retries or 60 seconds total
33-
@backoff.on_exception(backoff.expo,
34-
RateLimitException,
35-
max_tries=5,
36-
max_time=60,
37-
on_backoff=log_backoff)
35+
def log_backoff(self, details):
36+
"""
37+
Logs backoff retry information.
38+
39+
Args:
40+
details (dict): Backoff details including 'wait' and 'value'.
41+
"""
42+
wait_time = details.get("wait", "unknown")
43+
reason = details.get("value", "unknown")
44+
LOGGER.warning("Retrying request in %ss due to %s", wait_time, reason)
45+
46+
@backoff.on_exception(
47+
backoff.expo,
48+
RateLimitException,
49+
max_tries=5,
50+
max_time=60,
51+
on_backoff=lambda details: Client.log_backoff(Client, details),
52+
)
3853
def request(self, method, url, **kwargs):
39-
if self.calls_remaining is not None and self.calls_remaining == 0:
54+
"""Make an HTTP request with retry and rate limit handling."""
55+
if self.calls_remaining == 0 and self.limit_reset:
4056
wait = self.limit_reset - int(time.monotonic())
4157
if 0 < wait <= 300:
4258
time.sleep(wait)
4359

44-
if 'headers' not in kwargs:
45-
kwargs['headers'] = {}
46-
if self.token:
47-
kwargs['headers']['Authorization'] = self.token
48-
49-
kwargs['headers']['Content-Type'] = 'application/json'
60+
kwargs.setdefault("headers", {})
61+
kwargs["headers"]["Authorization"] = self.token
62+
kwargs["headers"]["Content-Type"] = "application/json"
5063

51-
if 'endpoint' in kwargs:
52-
endpoint = kwargs['endpoint']
53-
del kwargs['endpoint']
64+
if "endpoint" in kwargs:
65+
endpoint = kwargs.pop("endpoint")
5466
with metrics.http_request_timer(endpoint) as timer:
55-
response = requests.request(method, url, **kwargs)
67+
response = requests.request(method, url, timeout=30, **kwargs)
5668
timer.tags[metrics.Tag.http_status_code] = response.status_code
5769
else:
58-
response = requests.request(method, url, **kwargs)
70+
response = requests.request(method, url, timeout=30, **kwargs)
5971

60-
#Handled missing headers to avoid KeyError
61-
self.calls_remaining = int(response.headers.get('X-Ratelimit-Remaining', 1))
62-
self.limit_reset = int(float(response.headers.get('X-Ratelimit-Reset', time.monotonic() + 1)))
72+
self.calls_remaining = int(response.headers.get("X-Ratelimit-Remaining", 0))
73+
self.limit_reset = int(float(
74+
response.headers.get("X-Ratelimit-Reset", time.monotonic() + 60)))
6375

6476
if response.status_code in [429, 503]:
6577
raise RateLimitException(response.text)
6678
if response.status_code == 423:
6779
raise MetricsRateLimitException()
80+
6881
try:
6982
response.raise_for_status()
70-
except:
71-
LOGGER.error('{} - {}'.format(response.status_code, response.text))
83+
except requests.HTTPError:
84+
LOGGER.error("HTTP %s - %s", response.status_code, response.text)
7285
raise
7386

7487
return response
7588

7689
def get_report_metrics(self, url, **kwargs):
77-
response = self.request('get', url, **kwargs)
78-
return response.json().get('metrics', [])
90+
"""Fetch analytics metrics from a report endpoint."""
91+
response = self.request("get", url, **kwargs)
92+
return response.json().get("metrics", [])
7993

8094
def create_report(self, path, data, **kwargs):
95+
"""Create a report on FrontApp and return its URL."""
8196
url = self.url(path)
82-
kwargs['data'] = json.dumps(data)
83-
response = self.request('post', url, **kwargs)
84-
if response.json().get('_links', {}).get('self'):
85-
return response.json()['_links']['self']
86-
return {}
97+
kwargs["data"] = json.dumps(data)
98+
response = self.request("post", url, **kwargs)
99+
return response.json().get("_links", {}).get("self", {})
87100

88101
def list_metrics(self, path, **kwargs):
102+
"""List available metrics from an endpoint."""
89103
url = self.url(path)
90-
response = self.request('get', url, **kwargs)
91-
return response.json().get('_results', [])
104+
response = self.request("get", url, **kwargs)
105+
return response.json().get("_results", [])

0 commit comments

Comments
 (0)