1- import json
21import time
32
43import requests
54from requests .exceptions import RequestException
65from tenacity import retry
7- from tenacity import retry_if_exception_type
86from tenacity import stop_after_attempt
97from tenacity import wait_random_exponential
108
9+ DEFAULT_API_URL = "https://rest.iad-02.braze.com"
10+ USER_TRACK_ENDPOINT = "/users/track"
11+ USER_DELETE_ENDPOINT = "/users/delete"
12+ MAX_RETRIES = 3
13+ # Max time to wait between API call retries
14+ MAX_WAIT_SECONDS = 1.25
15+
1116
1217class BrazeRateLimitError (Exception ):
13- pass
18+ def __init__ (self , reset_epoch_s ):
19+ """
20+ A rate limit error was encountered.
21+
22+ :param float reset_epoch_s: Unix timestamp for when the API may be called again.
23+ """
24+ self .reset_epoch_s = reset_epoch_s
25+ super (BrazeRateLimitError , self ).__init__ ("BrazeRateLimitError" )
1426
1527
1628class BrazeInternalServerError (Exception ):
1729 pass
1830
1931
32+ def _wait_random_exp_or_rate_limit ():
33+ """Creates a tenacity wait callback that accounts for explicit rate limits."""
34+ random_exp = wait_random_exponential (multiplier = 1 , max = MAX_WAIT_SECONDS )
35+
36+ def check (retry_state ):
37+ """
38+ Waits with either a random exponential backoff or attempts to obey rate limits
39+ that Braze returns.
40+
41+ :param tenacity.RetryCallState retry_state: Info about current retry invocation
42+ :raises BrazeRateLimitError: If the rate limit reset time is too long
43+ :returns: Time to wait, in seconds.
44+ :rtype: float
45+ """
46+ exc = retry_state .outcome .exception ()
47+ if isinstance (exc , BrazeRateLimitError ):
48+ sec_to_reset = exc .reset_epoch_s - float (time .time ())
49+ if sec_to_reset >= MAX_WAIT_SECONDS :
50+ raise exc
51+ return max (0.0 , sec_to_reset )
52+ return random_exp (retry_state = retry_state )
53+
54+ return check
55+
56+
2057class BrazeClient (object ):
2158 """
2259 Client for Appboy public API. Support user_track.
@@ -42,15 +79,9 @@ class BrazeClient(object):
4279 print r['errors']
4380 """
4481
45- DEFAULT_API_URL = "https://rest.iad-02.braze.com"
46- USER_TRACK_ENDPOINT = "/users/track"
47- USER_DELETE_ENDPOINT = "/users/delete"
48- MAX_RETRIES = 3
49- MAX_WAIT_SECONDS = 1.25
50-
5182 def __init__ (self , api_key , api_url = None ):
5283 self .api_key = api_key
53- self .api_url = api_url or self . DEFAULT_API_URL
84+ self .api_url = api_url or DEFAULT_API_URL
5485 self .request_url = ""
5586
5687 def user_track (self , attributes , events , purchases ):
@@ -61,7 +92,7 @@ def user_track(self, attributes, events, purchases):
6192 :param purchases: dict or list of user purchases dict (external_id, app_id, product_id, currency, price)
6293 :return: json dict response, for example: {"message": "success", "errors": [], "client_error": ""}
6394 """
64- self .request_url = self .api_url + self . USER_TRACK_ENDPOINT
95+ self .request_url = self .api_url + USER_TRACK_ENDPOINT
6596
6697 payload = {}
6798
@@ -83,7 +114,7 @@ def user_delete(self, external_ids, appboy_ids):
83114 :param appboy_ids: dict or list of user braze ids
84115 :return: json dict response, for example: {"message": "success", "errors": [], "client_error": ""}
85116 """
86- self .request_url = self .api_url + self . USER_DELETE_ENDPOINT
117+ self .request_url = self .api_url + USER_DELETE_ENDPOINT
87118
88119 payload = {}
89120
@@ -129,35 +160,19 @@ def __create_request(self, payload):
129160
130161 @retry (
131162 reraise = True ,
132- wait = wait_random_exponential ( multiplier = 1 , max = MAX_WAIT_SECONDS ),
163+ wait = _wait_random_exp_or_rate_limit ( ),
133164 stop = stop_after_attempt (MAX_RETRIES ),
134- retry = (
135- retry_if_exception_type (BrazeInternalServerError )
136- | retry_if_exception_type (RequestException )
137- ),
138165 )
139- def _post_request_with_retries (self , payload , retry_attempt = 0 ):
166+ def _post_request_with_retries (self , payload ):
140167 """
141168 :param dict payload:
142- :param int retry_attempt: current retry attempt number
143- :rtype: dict
169+ :rtype: requests.Response
144170 """
145- headers = {"Content-Type" : "application/json" }
146- r = requests .post (
147- self .request_url , data = json .dumps (payload ), headers = headers , timeout = 2
148- )
149- if retry_attempt >= self .MAX_RETRIES :
150- raise BrazeRateLimitError ("BrazeRateLimitError" )
151-
171+ r = requests .post (self .request_url , json = payload , timeout = 2 )
152172 # https://www.braze.com/docs/developer_guide/rest_api/messaging/#fatal-errors
153173 if r .status_code == 429 :
154- reset_epoch_seconds = float (r .headers .get ("X-RateLimit-Reset" ))
155- sec_to_reset = reset_epoch_seconds - float (time .time ())
156- if sec_to_reset < self .MAX_WAIT_SECONDS :
157- time .sleep (sec_to_reset )
158- return self ._post_request_with_retries (payload , retry_attempt + 1 )
159- else :
160- raise BrazeRateLimitError ("BrazeRateLimitError" )
174+ reset_epoch_s = float (r .headers .get ("X-RateLimit-Reset" , 0 ))
175+ raise BrazeRateLimitError (reset_epoch_s )
161176 elif str (r .status_code ).startswith ("5" ):
162177 raise BrazeInternalServerError ("BrazeInternalServerError" )
163178 return r
0 commit comments