1515
1616import requests
1717
18+ from requests .adapters import HTTPAdapter
19+ from urllib3 .util .retry import Retry
20+
1821logger = logging .getLogger (__name__ )
1922
2023
24+ def create_retry_session (max_retries : int = 3 ) -> requests .Session :
25+ """Create a requests session with retry configuration."""
26+ session = requests .Session ()
27+
28+ retry_strategy = Retry (
29+ total = max_retries ,
30+ backoff_factor = 0.1 ,
31+ status_forcelist = [429 , 500 , 502 , 503 , 504 ], # Retry on these HTTP status codes
32+ allowed_methods = ["HEAD" , "GET" , "OPTIONS" , "POST" , "PUT" , "DELETE" ],
33+ raise_on_status = False , # Don't raise on HTTP errors, let our code handle them
34+ )
35+
36+ adapter = HTTPAdapter (max_retries = retry_strategy )
37+ session .mount ("http://" , adapter )
38+ session .mount ("https://" , adapter )
39+
40+ return session
41+
42+
2143class SentryClient :
2244 """Client for authenticated API calls to the Sentry monolith."""
2345
@@ -27,36 +49,34 @@ def __init__(self, base_url: str) -> None:
2749 if not self .shared_secret :
2850 raise RuntimeError ("LAUNCHPAD_RPC_SHARED_SECRET must be provided or set as environment variable" )
2951
52+ self .session = create_retry_session ()
53+
3054 def download_artifact (self , org : str , project : str , artifact_id : str ) -> Dict [str , Any ]:
3155 """Download preprod artifact."""
3256 endpoint = f"/api/0/internal/{ org } /{ project } /files/preprodartifacts/{ artifact_id } /"
3357 url = self ._build_url (endpoint )
3458
35- try :
36- logger .debug (f"GET { url } " )
37- response = requests .get (url , headers = self ._get_auth_headers (), timeout = 120 , stream = True )
38-
39- if response .status_code != 200 :
40- return self ._handle_error_response (response , "Download" )
41-
42- # Read content with size limit
43- content = b""
44- for chunk in response .iter_content (chunk_size = 8192 ):
45- if chunk :
46- content += chunk
47- if len (content ) > 5 * 1024 * 1024 * 1024 : # 5GB limit
48- logger .warning ("Download truncated at 5GB" )
49- break
50-
51- return {
52- "success" : True ,
53- "file_content" : content ,
54- "file_size_bytes" : len (content ),
55- "headers" : dict (response .headers ),
56- }
57- except Exception as e :
58- logger .error (f"Download failed: { e } " )
59- return {"error" : str (e )}
59+ logger .debug (f"GET { url } " )
60+ response = self .session .get (url , headers = self ._get_auth_headers (), timeout = 120 , stream = True )
61+
62+ if response .status_code != 200 :
63+ return self ._handle_error_response (response , "Download" )
64+
65+ # Read content with size limit
66+ content = b""
67+ for chunk in response .iter_content (chunk_size = 8192 ):
68+ if chunk :
69+ content += chunk
70+ if len (content ) > 5 * 1024 * 1024 * 1024 : # 5GB limit
71+ logger .warning ("Download truncated at 5GB" )
72+ break
73+
74+ return {
75+ "success" : True ,
76+ "file_content" : content ,
77+ "file_size_bytes" : len (content ),
78+ "headers" : dict (response .headers ),
79+ }
6080
6181 def update_artifact (self , org : str , project : str , artifact_id : str , data : Dict [str , Any ]) -> Dict [str , Any ]:
6282 """Update preprod artifact."""
@@ -174,7 +194,7 @@ def _make_json_request(
174194 operation = operation or f"{ method } { endpoint } "
175195
176196 logger .debug (f"{ method } { url } " )
177- response = requests .request (
197+ response = self . session .request (
178198 method = method ,
179199 url = url ,
180200 data = body or None ,
@@ -249,7 +269,7 @@ def _upload_chunk(self, org: str, chunk: Dict[str, Any]) -> bool:
249269 }
250270
251271 try :
252- response = requests .post (url , data = body , headers = headers , timeout = 60 )
272+ response = self . session .post (url , data = body , headers = headers , timeout = 60 )
253273
254274 success = response .status_code in [200 , 201 , 409 ] # 409 = already exists
255275 if not success :
0 commit comments