Skip to content

Commit ec5f1c2

Browse files
committed
Add optional retry mechanism for transient network errors
1 parent 3d92f04 commit ec5f1c2

File tree

6 files changed

+68
-6
lines changed

6 files changed

+68
-6
lines changed

doc/source/advanced/config.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,15 @@ Set proxy once in config, affects all yfinance data fetches.
1313
1414
import yfinance as yf
1515
yf.set_config(proxy="PROXY_SERVER")
16+
17+
Retries
18+
-------
19+
20+
Configure automatic retry for transient network errors. The retry mechanism uses exponential backoff (1s, 2s, 4s...).
21+
22+
.. code-block:: python
23+
24+
import yfinance as yf
25+
yf.set_config(retries=2)
26+
27+
Set to 0 to disable retries (default behavior).

tests/test_prices.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from tests.context import session_gbl
33

44
import unittest
5+
import socket
56

67
import datetime as _dt
78
import pytz as _tz
@@ -456,6 +457,21 @@ def test_aggregate_capital_gains(self):
456457

457458
dat.history(start=start, end=end, interval=interval)
458459

460+
def test_transient_error_detection(self):
461+
"""Test that _is_transient_error correctly identifies transient vs permanent errors"""
462+
from yfinance.data import _is_transient_error
463+
from yfinance.exceptions import YFPricesMissingError
464+
465+
# Transient errors (should retry)
466+
self.assertTrue(_is_transient_error(socket.error("Network error")))
467+
self.assertTrue(_is_transient_error(TimeoutError("Timeout")))
468+
self.assertTrue(_is_transient_error(OSError("OS error")))
469+
470+
# Permanent errors (should NOT retry)
471+
self.assertFalse(_is_transient_error(ValueError("Invalid")))
472+
self.assertFalse(_is_transient_error(YFPricesMissingError('INVALID', '')))
473+
self.assertFalse(_is_transient_error(KeyError("key")))
474+
459475

460476
if __name__ == '__main__':
461477
unittest.main()

yfinance/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@
4949

5050
# Config stuff:
5151
_NOTSET=object()
52-
def set_config(proxy=_NOTSET, hide_exceptions=_NOTSET):
52+
def set_config(proxy=_NOTSET, retries=_NOTSET, hide_exceptions=_NOTSET):
5353
if proxy is not _NOTSET:
5454
YfData(proxy=proxy)
55+
if retries is not _NOTSET:
56+
YfConfig(retries=retries)
5557
if hide_exceptions is not _NOTSET:
5658
YfConfig(hide_exceptions=hide_exceptions)
5759
__all__ += ["set_config"]

yfinance/config.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,28 @@ def __call__(cls, *args, **kwargs):
1717
if 'hide_exceptions' in kwargs or (args and len(args) > 0):
1818
hide_exceptions = kwargs.get('hide_exceptions') if 'hide_exceptions' in kwargs else args[0]
1919
cls._instances[cls]._set_hide_exceptions(hide_exceptions)
20+
if 'retries' in kwargs or (args and len(args) > 1):
21+
retries = kwargs.get('retries') if 'retries' in kwargs else args[1]
22+
cls._instances[cls]._set_retries(retries)
2023
return cls._instances[cls]
2124

2225

2326
class YfConfig(metaclass=SingletonMeta):
24-
def __init__(self, hide_exceptions=True):
27+
def __init__(self, hide_exceptions=True, retries=0):
2528
self._hide_exceptions = hide_exceptions
29+
self._retries = retries
2630

2731
def _set_hide_exceptions(self, hide_exceptions):
2832
self._hide_exceptions = hide_exceptions
2933

34+
def _set_retries(self, retries):
35+
self._retries = retries
36+
3037
@property
3138
def hide_exceptions(self):
3239
return self._hide_exceptions
40+
41+
@property
42+
def retries(self):
43+
return self._retries
3344

yfinance/data.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import functools
22
from functools import lru_cache
3+
import socket
4+
import time as _time
35

46
from curl_cffi import requests
57
from urllib.parse import urlsplit, urljoin
@@ -9,10 +11,23 @@
911
from frozendict import frozendict
1012

1113
from . import utils, cache
14+
from .config import YfConfig
1215
import threading
1316

1417
from .exceptions import YFException, YFDataException, YFRateLimitError
1518

19+
20+
def _is_transient_error(exception):
21+
"""Check if error is transient (network/timeout) and should be retried."""
22+
if isinstance(exception, (TimeoutError, socket.error, OSError)):
23+
return True
24+
error_type_name = type(exception).__name__
25+
transient_error_types = {
26+
'Timeout', 'TimeoutError', 'ConnectionError', 'ConnectTimeout',
27+
'ReadTimeout', 'ChunkedEncodingError', 'RemoteDisconnected',
28+
}
29+
return error_type_name in transient_error_types
30+
1631
cache_maxsize = 64
1732

1833

@@ -416,7 +431,15 @@ def _make_request(self, url, request_method, body=None, params=None, timeout=30)
416431
if body:
417432
request_args['json'] = body
418433

419-
response = request_method(**request_args)
434+
for attempt in range(YfConfig().retries + 1):
435+
try:
436+
response = request_method(**request_args)
437+
break
438+
except Exception as e:
439+
if _is_transient_error(e) and attempt < YfConfig().retries:
440+
_time.sleep(2 ** attempt)
441+
else:
442+
raise
420443
utils.get_yf_logger().debug(f'response code={response.status_code}')
421444
if response.status_code >= 400:
422445
# Retry with other cookie strategy

yfinance/multi.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,10 @@ def _download_one(ticker, start=None, end=None,
286286
rounding=rounding, keepna=keepna, timeout=timeout,
287287
raise_errors=True
288288
)
289+
shared._DFS[ticker.upper()] = data
289290
except Exception as e:
290-
# glob try/except needed as current thead implementation breaks if exception is raised.
291291
shared._DFS[ticker.upper()] = utils.empty_df()
292292
shared._ERRORS[ticker.upper()] = repr(e)
293293
shared._TRACEBACKS[ticker.upper()] = traceback.format_exc()
294-
else:
295-
shared._DFS[ticker.upper()] = data
296294

297295
return data

0 commit comments

Comments
 (0)