@@ -47,6 +47,9 @@ class YAsyncClient:
4747
4848 _DEFAULT_TIMEOUT : Final [float ] = 5.0
4949 _READ_TIMEOUT : Final [float ] = 15.0
50+ _REQUEST_ATTEMPTS : Final [int ] = 3
51+ _RETRYABLE_STATUS_CODES : Final [frozenset [int ]] = frozenset ({429 , 502 , 503 , 504 })
52+ _RETRY_DELAY_SECONDS : Final [float ] = 0.25
5053
5154 def __init__ (self , timeout : httpx .Timeout | None = None ) -> None :
5255 """Initialize the async Yahoo! Finance API client.
@@ -98,15 +101,35 @@ async def _request_or_raise(
98101 """
99102
100103 request = self ._client .get if method == "GET" else self ._client .post
101- start = time .perf_counter ()
102- try :
103- response = await request (url , ** kwargs )
104- response .raise_for_status ()
105- except httpx .HTTPStatusError as exc :
106- if exc .response .is_error :
104+ attempt = 1
105+ while True :
106+ start = time .perf_counter ()
107+ try :
108+ response = await request (url , ** kwargs )
109+ if response .is_error :
110+ response .raise_for_status ()
111+ except httpx .HTTPStatusError as exc :
107112 status_code = exc .response .status_code if exc .response else - 1
108113 reason = exc .response .reason_phrase if exc .response else "unknown"
109114 url_str = str (exc .request .url )
115+ if (
116+ method == "GET"
117+ and status_code in self ._RETRYABLE_STATUS_CODES
118+ and attempt < self ._REQUEST_ATTEMPTS
119+ ):
120+ self ._logger .warning (
121+ "Transient HTTP error for '%s': Status %s - %s. "
122+ "Retrying attempt %s/%s." ,
123+ context ,
124+ status_code ,
125+ reason ,
126+ attempt + 1 ,
127+ self ._REQUEST_ATTEMPTS ,
128+ )
129+ await asyncio .sleep (self ._RETRY_DELAY_SECONDS * attempt )
130+ attempt += 1
131+ continue
132+
110133 self ._logger .exception (
111134 "HTTP error for '%s': Status %s - %s. "
112135 "URL: %s. "
@@ -118,33 +141,46 @@ async def _request_or_raise(
118141 url_str ,
119142 )
120143 raise MarketDataRequestError (status_code , url_str ) from exc
121- except httpx .TransportError as exc :
122- self ._logger .exception (
123- "Transport error for '%s'. "
124- "Potential causes: network connectivity issues, "
125- "DNS resolution failure, or timeout. "
126- "Check your internet connection." ,
127- context ,
128- )
129- raise MarketDataUnavailableError (context ) from exc
130- except asyncio .CancelledError :
131- self ._logger .info (
132- "Request cancelled for '%s'. "
133- "Typically occurs during application shutdown "
134- "or when a timeout is exceeded." ,
135- context ,
136- )
137- raise
138- finally :
139- elapsed_ms = (time .perf_counter () - start ) * 1_000.0
140- self ._logger .debug (
141- "Request timing for '%s': %.1f ms (method=%s url=%s)" ,
142- context ,
143- elapsed_ms ,
144- method ,
145- url ,
146- )
147- return response # type: ignore reportPossiblyUnboundVariable
144+ except httpx .TransportError as exc :
145+ if method == "GET" and attempt < self ._REQUEST_ATTEMPTS :
146+ self ._logger .warning (
147+ "Transient transport error for '%s'. Retrying attempt %s/%s." ,
148+ context ,
149+ attempt + 1 ,
150+ self ._REQUEST_ATTEMPTS ,
151+ )
152+ await asyncio .sleep (self ._RETRY_DELAY_SECONDS * attempt )
153+ attempt += 1
154+ continue
155+
156+ self ._logger .exception (
157+ "Transport error for '%s'. "
158+ "Potential causes: network connectivity issues, "
159+ "DNS resolution failure, or timeout. "
160+ "Check your internet connection." ,
161+ context ,
162+ )
163+ raise MarketDataUnavailableError (context ) from exc
164+ except asyncio .CancelledError :
165+ self ._logger .info (
166+ "Request cancelled for '%s'. "
167+ "Typically occurs during application shutdown "
168+ "or when a timeout is exceeded." ,
169+ context ,
170+ )
171+ raise
172+ else :
173+ return response
174+ finally :
175+ elapsed_ms = (time .perf_counter () - start ) * 1_000.0
176+ self ._logger .debug (
177+ "Request timing for '%s': %.1f ms (method=%s url=%s attempt=%s)" ,
178+ context ,
179+ elapsed_ms ,
180+ method ,
181+ url ,
182+ attempt ,
183+ )
148184
149185 async def _refresh_cookies (self ) -> None :
150186 """Log into Yahoo! finance and set required cookies.
0 commit comments