1+ import functools
12import json
23import os
34import re
5+ import time
46
7+ from requests .exceptions import RequestException , HTTPError
58
69def write_dict_to_json_file (input , output_path ):
710 if os .path .exists (output_path ):
@@ -22,3 +25,58 @@ def get_uri_for_digest(uri, digest):
2225 """
2326 base_uri = re .split (r"[@:]" , uri , maxsplit = 1 )[0 ]
2427 return f"{ base_uri } @{ digest } "
28+
29+ def retry_on_error (max_retries = 3 , initial_delay = 1 , backoff_factor = 2 ,
30+ retryable_status_codes = (502 , 503 , 504 , 429 ), retryable_exceptions = (RequestException ,)):
31+ """
32+ A decorator for retrying functions that might fail due to transient network issues.
33+
34+ Args:
35+ max_retries: Maximum number of retry attempts
36+ initial_delay: Initial delay between retries in seconds
37+ backoff_factor: Factor by which the delay increases with each retry
38+ retryable_status_codes: HTTP status codes that trigger a retry
39+ retryable_exceptions: Exception types that trigger a retry
40+
41+ Returns:
42+ Decorated function with retry logic
43+ """
44+ def decorator (func ):
45+ @functools .wraps (func )
46+ def wrapper (* args , ** kwargs ):
47+ delay = initial_delay
48+ last_exception = None
49+
50+ for retry_count in range (max_retries + 1 ):
51+ try :
52+ if retry_count > 0 :
53+ logger .info (f"Retry attempt { retry_count } /{ max_retries } for { func .__name__ } " )
54+
55+ response = func (* args , ** kwargs )
56+
57+ # Check for retryable status codes in the response
58+ if hasattr (response , 'status_code' ) and response .status_code in retryable_status_codes :
59+ status_code = response .status_code
60+ logger .warning (f"Received status code { status_code } from { func .__name__ } , retrying..." )
61+ last_exception = HTTPError (f"HTTP Error { status_code } " )
62+ else :
63+ # Success, return the response
64+ return response
65+
66+ except retryable_exceptions as e :
67+ logger .warning (f"Request failed in { func .__name__ } : { str (e )} " )
68+ last_exception = e
69+
70+ # Don't sleep if this was the last attempt
71+ if retry_count < max_retries :
72+ sleep_time = delay * (backoff_factor ** retry_count )
73+ logger .info (f"Waiting { sleep_time :.2f} seconds before retry" )
74+ time .sleep (sleep_time )
75+
76+ # If we got here, all retries failed
77+ logger .error (f"All { max_retries } retries failed for { func .__name__ } " )
78+ if last_exception :
79+ raise last_exception
80+ return None
81+ return wrapper
82+ return decorator
0 commit comments