55import json
66import logging
77from ssl import SSLContext
8- from typing import Any
8+ from typing import Any , List
99from typing import Dict , Optional
1010
1111import aiohttp
1818 get_user_agent ,
1919)
2020from .response import AuditLogsResponse
21+ from slack_sdk .http_retry .async_handler import AsyncRetryHandler
22+ from slack_sdk .http_retry .builtin_async_handlers import async_default_handlers
23+ from slack_sdk .http_retry .request import HttpRequest as RetryHttpRequest
24+ from slack_sdk .http_retry .response import HttpResponse as RetryHttpResponse
25+ from slack_sdk .http_retry .state import RetryState
2126from ...proxy_env_variable_loader import load_http_proxy_from_env
2227
2328
@@ -34,6 +39,7 @@ class AsyncAuditLogsClient:
3439 auth : Optional [BasicAuth ]
3540 default_headers : Dict [str , str ]
3641 logger : logging .Logger
42+ retry_handlers : List [AsyncRetryHandler ]
3743
3844 def __init__ (
3945 self ,
@@ -49,6 +55,7 @@ def __init__(
4955 user_agent_prefix : Optional [str ] = None ,
5056 user_agent_suffix : Optional [str ] = None ,
5157 logger : Optional [logging .Logger ] = None ,
58+ retry_handlers : List [AsyncRetryHandler ] = async_default_handlers ,
5259 ):
5360 """API client for Audit Logs API
5461 See https://api.slack.com/admins/audit-logs for more details
@@ -66,6 +73,7 @@ def __init__(
6673 user_agent_prefix: Prefix for User-Agent header value
6774 user_agent_suffix: Suffix for User-Agent header value
6875 logger: Custom logger
76+ retry_handlers: Retry handlers
6977 """
7078 self .token = token
7179 self .timeout = timeout
@@ -80,6 +88,7 @@ def __init__(
8088 user_agent_prefix , user_agent_suffix
8189 )
8290 self .logger = logger if logger is not None else logging .getLogger (__name__ )
91+ self .retry_handlers = retry_handlers
8392
8493 if self .proxy is None or len (self .proxy .strip ()) == 0 :
8594 env_variable = load_http_proxy_from_env (self .logger )
@@ -218,18 +227,6 @@ async def _perform_http_request(
218227 body_params = json .dumps (body_params )
219228 headers ["Content-Type" ] = "application/json;charset=utf-8"
220229
221- if self .logger .level <= logging .DEBUG :
222- headers_for_logging = {
223- k : "(redacted)" if k .lower () == "authorization" else v
224- for k , v in headers .items ()
225- }
226- self .logger .debug (
227- f"Sending a request - "
228- f"url: { url } , "
229- f"params: { query_params } , "
230- f"body: { body_params } , "
231- f"headers: { headers_for_logging } "
232- )
233230 session : Optional [ClientSession ] = None
234231 use_running_session = self .session and not self .session .closed
235232 if use_running_session :
@@ -241,7 +238,8 @@ async def _perform_http_request(
241238 trust_env = self .trust_env_in_session ,
242239 )
243240
244- resp : AuditLogsResponse
241+ last_error = None
242+ resp : Optional [AuditLogsResponse ] = None
245243 try :
246244 request_kwargs = {
247245 "headers" : headers ,
@@ -250,25 +248,112 @@ async def _perform_http_request(
250248 "ssl" : self .ssl ,
251249 "proxy" : self .proxy ,
252250 }
253- async with session .request (http_verb , url , ** request_kwargs ) as res :
254- response_body = {}
255- try :
256- response_body = await res .text ()
257- except aiohttp .ContentTypeError :
251+ retry_request = RetryHttpRequest (
252+ method = http_verb ,
253+ url = url ,
254+ headers = headers ,
255+ body_params = body_params ,
256+ )
257+
258+ retry_state = RetryState ()
259+ counter_for_safety = 0
260+ while counter_for_safety < 100 :
261+ counter_for_safety += 1
262+ # If this is a retry, the next try started here. We can reset the flag.
263+ retry_state .next_attempt_requested = False
264+ retry_response : Optional [RetryHttpResponse ] = None
265+ response_body = ""
266+
267+ if self .logger .level <= logging .DEBUG :
268+ headers_for_logging = {
269+ k : "(redacted)" if k .lower () == "authorization" else v
270+ for k , v in headers .items ()
271+ }
258272 self .logger .debug (
259- f"No response data returned from the following API call: { url } ."
273+ f"Sending a request - "
274+ f"url: { url } , "
275+ f"params: { query_params } , "
276+ f"body: { body_params } , "
277+ f"headers: { headers_for_logging } "
260278 )
261- except json .decoder .JSONDecodeError as e :
262- message = f"Failed to parse the response body: { str (e )} "
263- raise SlackApiError (message , res )
264-
265- resp = AuditLogsResponse (
266- url = url ,
267- status_code = res .status ,
268- raw_body = response_body ,
269- headers = res .headers ,
270- )
271- _debug_log_response (self .logger , resp )
279+
280+ try :
281+ async with session .request (http_verb , url , ** request_kwargs ) as res :
282+ try :
283+ response_body = await res .text ()
284+ retry_response = RetryHttpResponse (
285+ status_code = res .status ,
286+ headers = res .headers ,
287+ data = response_body .encode ("utf-8" )
288+ if response_body is not None
289+ else None ,
290+ )
291+ except aiohttp .ContentTypeError :
292+ self .logger .debug (
293+ f"No response data returned from the following API call: { url } ."
294+ )
295+ except json .decoder .JSONDecodeError as e :
296+ message = f"Failed to parse the response body: { str (e )} "
297+ raise SlackApiError (message , res )
298+
299+ if res .status == 429 :
300+ for handler in self .retry_handlers :
301+ if await handler .can_retry_async (
302+ state = retry_state ,
303+ request = retry_request ,
304+ response = retry_response ,
305+ ):
306+ if self .logger .level <= logging .DEBUG :
307+ self .logger .info (
308+ f"A retry handler found: { type (handler ).__name__ } "
309+ f"for { http_verb } { url } - rate_limited"
310+ )
311+ await handler .prepare_for_next_attempt_async (
312+ state = retry_state ,
313+ request = retry_request ,
314+ response = retry_response ,
315+ )
316+ break
317+
318+ if retry_state .next_attempt_requested is False :
319+ resp = AuditLogsResponse (
320+ url = url ,
321+ status_code = res .status ,
322+ raw_body = response_body ,
323+ headers = res .headers ,
324+ )
325+ _debug_log_response (self .logger , resp )
326+ return resp
327+
328+ except Exception as e :
329+ last_error = e
330+ for handler in self .retry_handlers :
331+ if await handler .can_retry_async (
332+ state = retry_state ,
333+ request = retry_request ,
334+ response = retry_response ,
335+ error = e ,
336+ ):
337+ if self .logger .level <= logging .DEBUG :
338+ self .logger .info (
339+ f"A retry handler found: { type (handler ).__name__ } "
340+ f"for { http_verb } { url } - { e } "
341+ )
342+ await handler .prepare_for_next_attempt_async (
343+ state = retry_state ,
344+ request = retry_request ,
345+ response = retry_response ,
346+ error = e ,
347+ )
348+ break
349+
350+ if retry_state .next_attempt_requested is False :
351+ raise last_error
352+
353+ if resp is not None :
354+ return resp
355+ raise last_error
356+
272357 finally :
273358 if not use_running_session :
274359 await session .close ()
0 commit comments