1717import requests
1818import json
1919import logging
20+ import time
2021
2122from requests import Response
2223
3233
3334logger = logging .getLogger (__name__ )
3435
36+ """Default retry count to be used when RetryRequests is True but no other config is supplied"""
37+ DEFAULT_RETRY_COUNT = 10
38+ """Default retry interval to be used when RetryRequests is True but no other config is supplied"""
39+ DEFAULT_RETRY_INTERVAL = 1000
40+
3541
3642def log_http (fn ):
3743 """
38- Decorates requests to log the request and response when debugging is enabled
44+ Decorates requests to log the request and response when debugging is enabled
3945 on the underlying client context.\n
4046 To log requests, set `debug=True` when creating your initial SDK context.
4147 :param fn: TODO DOCS
@@ -80,7 +86,7 @@ def _wrapper(self, *args, **kwargs):
8086
8187class BaseClient (object ):
8288 """
83- The BaseClient class encapsulates conventions, authorization, and URL handling
89+ The BaseClient class encapsulates conventions, authorization, and URL handling
8490 to make basic requests against the Splunk Cloud Platform. You can use this class
8591 to access a feature that has not implemented in the SDK.\n
8692
@@ -89,14 +95,15 @@ class BaseClient(object):
8995 bc.get(bc.build_url("/identity/v2/validate")) #=> HTTP response (presuming that v2 of the validate service is deployed)
9096 """
9197
92- def __init__ (self , context : Context , auth_manager : AuthManager , requests_hooks = None ):
98+ def __init__ (self , context : Context , auth_manager : AuthManager , retry_config = None , requests_hooks = None ):
9399 self .context = context
94100 self ._session = requests .Session ()
95101 self ._session .headers .update ({
96102 'Content-Type' : 'application/json' })
97103 self ._session .headers .update ({
98104 'Splunk-Client' : 'client-python/{}' .format (__version__ )})
99105 self ._auth_manager = auth_manager
106+ self ._retry_config = retry_config
100107
101108 self ._session .hooks [REQUESTS_HOOK_NAME_RESPONSE ].extend (requests_hooks or [])
102109
@@ -117,9 +124,12 @@ def get(self, url: str, **kwargs) -> requests.Response:
117124 :param kwargs: TODO DOCS
118125 :return: TODO DOCS
119126 """
127+
120128 self .update_auth ()
121129 # Params are used for querystring vars
122- return self ._session .get (url , ** kwargs )
130+ response = self ._session .get (url , ** kwargs )
131+
132+ return self .handle_error_response ("GET" , response , url , self ._retry_config , ** kwargs )
123133
124134 @log_http
125135 def options (self , url : str , ** kwargs ) -> requests .Response :
@@ -129,8 +139,12 @@ def options(self, url: str, **kwargs) -> requests.Response:
129139 :param kwargs: TODO DOCS
130140 :return: TODO DOCS
131141 """
142+
132143 self .update_auth ()
133- return self ._session .options (url , ** kwargs )
144+
145+ response = self ._session .options (url , ** kwargs )
146+
147+ return self .handle_error_response ("OPTIONS" , response , url , self ._retry_config , ** kwargs )
134148
135149 @log_http
136150 def head (self , url , ** kwargs ) -> requests .Response :
@@ -140,8 +154,12 @@ def head(self, url, **kwargs) -> requests.Response:
140154 :param kwargs: TODO DOCS
141155 :return: TODO DOCS
142156 """
157+
143158 self .update_auth ()
144- return self ._session .head (url , ** kwargs )
159+
160+ response = self ._session .head (url , ** kwargs )
161+
162+ return self .handle_error_response ("HEAD" , response , url , self ._retry_config , ** kwargs )
145163
146164 @log_http
147165 @preprocess_body
@@ -154,8 +172,12 @@ def post(self, url, data=None, json=None, **kwargs) -> requests.Response:
154172 :param kwargs: TODO DOCS
155173 :return: TODO DOCS
156174 """
175+
157176 self .update_auth ()
158- return self ._session .post (url , data , json , ** kwargs )
177+
178+ response = self ._session .post (url , data , json , ** kwargs )
179+
180+ return self .handle_error_response ("POST" , response , url , data , json , self ._retry_config , ** kwargs )
159181
160182 @log_http
161183 @preprocess_body
@@ -167,8 +189,12 @@ def put(self, url, data=None, **kwargs) -> requests.Response:
167189 :param kwargs: TODO DOCS
168190 :return: TODO DOCS
169191 """
192+
170193 self .update_auth ()
171- return self ._session .put (url , data , ** kwargs )
194+
195+ response = self ._session .put (url , data , ** kwargs )
196+
197+ return self .handle_error_response ("PUT" , response , url , data , self ._retry_config , ** kwargs )
172198
173199 @log_http
174200 @preprocess_body
@@ -180,8 +206,12 @@ def patch(self, url, data=None, **kwargs) -> requests.Response:
180206 :param kwargs:
181207 :return:
182208 """
209+
183210 self .update_auth ()
184- return self ._session .patch (url , data , ** kwargs )
211+
212+ response = self ._session .patch (url , data , ** kwargs )
213+
214+ return self .handle_error_response ("PATCH" , response , url , data , self ._retry_config , ** kwargs )
185215
186216 @log_http
187217 def delete (self , url : str , ** kwargs ) -> requests .Response :
@@ -192,14 +222,17 @@ def delete(self, url: str, **kwargs) -> requests.Response:
192222 :return: TODO DOCS
193223 """
194224 self .update_auth ()
195- return self ._session .delete (url , ** kwargs )
225+
226+ response = self ._session .delete (url , ** kwargs )
227+
228+ return self .handle_error_response ("DELETE" , response , url , self ._retry_config , ** kwargs )
196229
197230 def build_url (self , route : str , ** kwargs ) -> str :
198231 """
199232 Builds a full URL from the specified path template by adding the current
200- tenant (if the path does not start with '/system') and the configured host,
233+ tenant (if the path does not start with '/system') and the configured host,
201234 and applying any `kwargs` to the path template. \n
202- You can pass the returned URL to GET, PUT, POST, PATCH, DELETE, OPTIONS,
235+ You can pass the returned URL to GET, PUT, POST, PATCH, DELETE, OPTIONS,
203236 and HEAD requests.
204237 :param route: TODO DOCS
205238 :param kwargs: TODO DOCS
@@ -227,6 +260,20 @@ def get_tenant(self) -> str:
227260 """
228261 return self .context .tenant
229262
263+ def handle_error_response (self , method : str , response : Response , url , data = None , json_data = None , retry_config = None , ** kwargs ) -> requests .Response :
264+
265+ if response .status_code != 429 or retry_config is None or (retry_config is not None and retry_config .retry_requests_enabled is not True ):
266+ return response
267+
268+ retry_count = 0
269+ success_response = self ._retry_config .handle_response (self , method , url , retry_count , data , json_data , ** kwargs )
270+ while (success_response is not None and success_response .status_code == 429 ) and retry_count < self ._retry_config .retry_count :
271+ retry_count += 1
272+ success_response = self ._retry_config .handle_response (self , method , url , retry_count , data , json_data , ** kwargs )
273+ if success_response is not None and success_response .status_code != 429 :
274+ response = success_response
275+
276+ return response
230277
231278def inflate (data , model , is_collection : bool ):
232279 """ Handles deserializing responses from services into model objects."""
@@ -246,8 +293,8 @@ def inflate(data, model, is_collection: bool):
246293
247294def dictify (obj ):
248295 """
249- Private. Serializes the model into JSON. The naming conventions for the services
250- differ from Python naming conventions, so serialization involves changing from
296+ Private. Serializes the model into JSON. The naming conventions for the services
297+ differ from Python naming conventions, so serialization involves changing from
251298 Python conventions to those defined by the Splunk Cloud services.
252299 :param obj: TODO DOCS
253300 :return: TODO DOCS
@@ -262,9 +309,11 @@ def dictify(obj):
262309 return obj
263310
264311
265- def get_client (context , auth_manager , requests_hooks = None ):
312+ def get_client (context , auth_manager , retry_config = None , requests_hooks = None ):
266313 """Returns a Service Client object for the specified authorization manager."""
267- return BaseClient (context , auth_manager , requests_hooks = requests_hooks )
314+ client = BaseClient (context , auth_manager , retry_config = retry_config , requests_hooks = requests_hooks )
315+ client .update_auth ()
316+ return client
268317
269318
270319# TODO: refactor this helper away and make handle_response cleaner
@@ -323,6 +372,69 @@ def handle_response(response: Response, klass=None, key=None):
323372 raise HTTPError (response .status_code , response .text )
324373
325374
375+ class RetryConfig (object ):
376+ """The RetryConfig class wraps around the configuration values for retrying requests that fail
377+ when a 429 is encountered at the server."""
378+
379+ def __init__ (self , retry_requests_enabled : bool , retry_count = None , retry_interval = None ):
380+ self ._retry_requests_enabled = retry_requests_enabled
381+ if retry_count is not None :
382+ self ._retry_count = retry_count
383+ else :
384+ self ._retry_count = DEFAULT_RETRY_COUNT
385+
386+ if retry_interval is not None :
387+ self ._retry_interval = retry_interval
388+ else :
389+ self ._retry_interval = DEFAULT_RETRY_INTERVAL
390+
391+ @property
392+ def retry_requests_enabled (self ) -> bool :
393+ return self ._retry_requests_enabled
394+
395+ @retry_requests_enabled .setter
396+ def retry_requests_enabled (self , retry_requests_enabled : bool ):
397+ self ._retry_requests_enabled = retry_requests_enabled
398+
399+ @property
400+ def retry_count (self ) -> int :
401+ return self ._retry_count
402+
403+ @retry_count .setter
404+ def retry_count (self , retry_count : int ):
405+ self ._retry_count = retry_count
406+
407+ @property
408+ def retry_interval (self ) -> bool :
409+ return self ._retry_interval
410+
411+ @retry_interval .setter
412+ def retry_interval (self , retry_interval : bool ):
413+ self ._retry_interval = retry_interval
414+
415+ # implement exponential back off by increasing the waiting time between retries after each retry failure.
416+ def handle_response (self , client , method , url , retry_count , data = None , json_data = None , ** kwargs ) -> requests .Response :
417+ response = None
418+ backOffSeconds = ((1 << retry_count ) * self ._retry_interval ) / 1000
419+ time .sleep (backOffSeconds )
420+
421+ if method == "POST" :
422+ response = client ._session .post (url , data , json_data , ** kwargs )
423+ elif method == "GET" :
424+ response = self ._session .get (url , ** kwargs )
425+ elif method == "DELETE" :
426+ response = self ._session .delete (url , ** kwargs )
427+ elif method == "OPTIONS" :
428+ response = self ._session .options (url , ** kwargs )
429+ elif method == "HEAD" :
430+ response = self ._session .head (url , ** kwargs )
431+ elif method == "PUT" :
432+ response = self ._session .put (url , data , ** kwargs )
433+ elif method == "PATCH" :
434+ response = self ._session .patch (url , data , ** kwargs )
435+
436+ return response
437+
326438class HTTPError (Exception ):
327439 """The HTTPError class provides an exception wrapper for HTTP error responses."""
328440
0 commit comments