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