Skip to content

Commit 3d92f04

Browse files
authored
Merge pull request #2524 from ranaroussi/feature/expose-exceptions-v2
Improve exception handling
2 parents d482309 + f484539 commit 3d92f04

File tree

17 files changed

+182
-62
lines changed

17 files changed

+182
-62
lines changed

yfinance/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from .domain.industry import Industry
3333
from .domain.market import Market
3434
from .data import YfData
35+
from .config import YfConfig
3536

3637
from .screener.query import EquityQuery, FundQuery
3738
from .screener.screener import screen, PREDEFINED_SCREENER_QUERIES
@@ -48,7 +49,9 @@
4849

4950
# Config stuff:
5051
_NOTSET=object()
51-
def set_config(proxy=_NOTSET):
52+
def set_config(proxy=_NOTSET, hide_exceptions=_NOTSET):
5253
if proxy is not _NOTSET:
5354
YfData(proxy=proxy)
55+
if hide_exceptions is not _NOTSET:
56+
YfConfig(hide_exceptions=hide_exceptions)
5457
__all__ += ["set_config"]

yfinance/base.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
from . import utils, cache
3535
from .const import _MIC_TO_YAHOO_SUFFIX
3636
from .data import YfData
37-
from .exceptions import YFEarningsDateMissing, YFRateLimitError
37+
from .config import YfConfig
38+
from .exceptions import YFDataException, YFEarningsDateMissing, YFRateLimitError
3839
from .live import WebSocket
3940
from .scrapers.analysis import Analysis
4041
from .scrapers.fundamentals import Fundamentals
@@ -188,6 +189,8 @@ def _fetch_ticker_tz(self, timeout):
188189
# Must propagate this
189190
raise
190191
except Exception as e:
192+
if not YfConfig().hide_exceptions:
193+
raise
191194
logger.error(f"Failed to get ticker '{self.ticker}' reason: {e}")
192195
return None
193196
else:
@@ -199,6 +202,8 @@ def _fetch_ticker_tz(self, timeout):
199202
try:
200203
return data["chart"]["result"][0]["meta"]["exchangeTimezoneName"]
201204
except Exception as err:
205+
if not YfConfig().hide_exceptions:
206+
raise
202207
logger.error(f"Could not get exchangeTimezoneName for ticker '{self.ticker}' reason: {err}")
203208
logger.debug("Got response: ")
204209
logger.debug("-------------")
@@ -628,13 +633,17 @@ def get_shares_full(self, start=None, end=None, proxy=_SENTINEL_):
628633
json_data = self._data.cache_get(url=shares_url)
629634
json_data = json_data.json()
630635
except (_json.JSONDecodeError, requests.exceptions.RequestException):
636+
if not YfConfig().hide_exceptions:
637+
raise
631638
logger.error(f"{self.ticker}: Yahoo web request for share count failed")
632639
return None
633640
try:
634641
fail = json_data["finance"]["error"]["code"] == "Bad Request"
635642
except KeyError:
636643
fail = False
637644
if fail:
645+
if not YfConfig().hide_exceptions:
646+
raise requests.exceptions.HTTPError("Yahoo web request for share count returned 'Bad Request'")
638647
logger.error(f"{self.ticker}: Yahoo web request for share count failed")
639648
return None
640649

@@ -644,6 +653,8 @@ def get_shares_full(self, start=None, end=None, proxy=_SENTINEL_):
644653
try:
645654
df = pd.Series(shares_data[0]["shares_out"], index=pd.to_datetime(shares_data[0]["timestamp"], unit="s"))
646655
except Exception as e:
656+
if not YfConfig().hide_exceptions:
657+
raise
647658
logger.error(f"{self.ticker}: Failed to parse shares count data: {e}")
648659
return None
649660

@@ -722,12 +733,12 @@ def get_news(self, count=10, tab="news", proxy=_SENTINEL_) -> list:
722733

723734
data = self._data.post(url, body=payload)
724735
if data is None or "Will be right back" in data.text:
725-
raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
726-
"Our engineers are working quickly to resolve "
727-
"the issue. Thank you for your patience.")
736+
raise YFDataException("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***")
728737
try:
729738
data = data.json()
730739
except _json.JSONDecodeError:
740+
if not YfConfig().hide_exceptions:
741+
raise
731742
logger.error(f"{self.ticker}: Failed to retrieve the news and received faulty response instead.")
732743
data = {}
733744

yfinance/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import threading
2+
3+
class SingletonMeta(type):
4+
"""
5+
Metaclass that creates a Singleton instance.
6+
"""
7+
_instances = {}
8+
_lock = threading.Lock()
9+
10+
def __call__(cls, *args, **kwargs):
11+
with cls._lock:
12+
if cls not in cls._instances:
13+
instance = super().__call__(*args, **kwargs)
14+
cls._instances[cls] = instance
15+
else:
16+
# Update the existing instance
17+
if 'hide_exceptions' in kwargs or (args and len(args) > 0):
18+
hide_exceptions = kwargs.get('hide_exceptions') if 'hide_exceptions' in kwargs else args[0]
19+
cls._instances[cls]._set_hide_exceptions(hide_exceptions)
20+
return cls._instances[cls]
21+
22+
23+
class YfConfig(metaclass=SingletonMeta):
24+
def __init__(self, hide_exceptions=True):
25+
self._hide_exceptions = hide_exceptions
26+
27+
def _set_hide_exceptions(self, hide_exceptions):
28+
self._hide_exceptions = hide_exceptions
29+
30+
@property
31+
def hide_exceptions(self):
32+
return self._hide_exceptions
33+

yfinance/data.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from . import utils, cache
1212
import threading
1313

14-
from .exceptions import YFRateLimitError, YFDataException
14+
from .exceptions import YFException, YFDataException, YFRateLimitError
1515

1616
cache_maxsize = 64
1717

@@ -198,8 +198,10 @@ def _get_cookie_basic(self, timeout=30):
198198
url='https://fc.yahoo.com',
199199
timeout=timeout,
200200
allow_redirects=True)
201-
except requests.exceptions.DNSError:
202-
# Possible because url on some privacy/ad blocklists
201+
except requests.exceptions.DNSError as e:
202+
# Possible because url on some privacy/ad blocklists.
203+
# Can ignore because have second strategy.
204+
utils.get_yf_logger().debug("Handling DNS error on cookie fetch: " + str(e))
203205
return False
204206
self._save_cookie_curlCffi()
205207
return True
@@ -397,7 +399,7 @@ def _make_request(self, url, request_method, body=None, params=None, timeout=30)
397399
if params is None:
398400
params = {}
399401
if 'crumb' in params:
400-
raise Exception("Don't manually add 'crumb' to params dict, let data.py handle it")
402+
raise YFException("Don't manually add 'crumb' to params dict, let data.py handle it")
401403

402404
crumb, strategy = self._get_cookie_and_crumb()
403405
if crumb is not None:

yfinance/domain/industry.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import warnings
66

77
from .. import utils
8+
from ..config import YfConfig
89
from ..const import _SENTINEL_
910
from ..data import YfData
1011

@@ -147,6 +148,8 @@ def _fetch_and_parse(self) -> None:
147148

148149
return result
149150
except Exception as e:
151+
if not YfConfig().hide_exceptions:
152+
raise
150153
logger = utils.get_yf_logger()
151154
logger.error(f"Failed to get industry data for '{self._key}' reason: {e}")
152155
logger.debug("Got response: ")

yfinance/domain/market.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
import json as _json
33
import warnings
44

5+
from ..config import YfConfig
56
from ..const import _QUERY1_URL_, _SENTINEL_
67
from ..data import utils, YfData
8+
from ..exceptions import YFDataException
79

810
class Market:
911
def __init__(self, market:'str', session=None, proxy=_SENTINEL_, timeout=30):
@@ -24,12 +26,12 @@ def __init__(self, market:'str', session=None, proxy=_SENTINEL_, timeout=30):
2426
def _fetch_json(self, url, params):
2527
data = self._data.cache_get(url=url, params=params, timeout=self.timeout)
2628
if data is None or "Will be right back" in data.text:
27-
raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
28-
"Our engineers are working quickly to resolve "
29-
"the issue. Thank you for your patience.")
29+
raise YFDataException("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***")
3030
try:
3131
return data.json()
3232
except _json.JSONDecodeError:
33+
if not YfConfig().hide_exceptions:
34+
raise
3335
self._logger.error(f"{self.market}: Failed to retrieve market data and recieved faulty data.")
3436
return {}
3537

@@ -66,6 +68,8 @@ def _parse_data(self):
6668
self._summary = self._summary['marketSummaryResponse']['result']
6769
self._summary = {x['exchange']:x for x in self._summary}
6870
except Exception as e:
71+
if not YfConfig().hide_exceptions:
72+
raise
6973
self._logger.error(f"{self.market}: Failed to parse market summary")
7074
self._logger.debug(f"{type(e)}: {e}")
7175

@@ -75,18 +79,22 @@ def _parse_data(self):
7579
self._status = self._status['finance']['marketTimes'][0]['marketTime'][0]
7680
self._status['timezone'] = self._status['timezone'][0]
7781
del self._status['time'] # redundant
78-
try:
79-
self._status.update({
80-
"open": dt.datetime.fromisoformat(self._status["open"]),
81-
"close": dt.datetime.fromisoformat(self._status["close"]),
82-
"tz": dt.timezone(dt.timedelta(hours=int(self._status["timezone"]["gmtoffset"]))/1000, self._status["timezone"]["short"])
83-
})
84-
except Exception as e:
85-
self._logger.error(f"{self.market}: Failed to update market status")
86-
self._logger.debug(f"{type(e)}: {e}")
8782
except Exception as e:
83+
if not YfConfig().hide_exceptions:
84+
raise
8885
self._logger.error(f"{self.market}: Failed to parse market status")
8986
self._logger.debug(f"{type(e)}: {e}")
87+
try:
88+
self._status.update({
89+
"open": dt.datetime.fromisoformat(self._status["open"]),
90+
"close": dt.datetime.fromisoformat(self._status["close"]),
91+
"tz": dt.timezone(dt.timedelta(hours=int(self._status["timezone"]["gmtoffset"]))/1000, self._status["timezone"]["short"])
92+
})
93+
except Exception as e:
94+
if not YfConfig().hide_exceptions:
95+
raise
96+
self._logger.error(f"{self.market}: Failed to update market status")
97+
self._logger.debug(f"{type(e)}: {e}")
9098

9199

92100

yfinance/domain/sector.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Dict, Optional
55
import warnings
66

7+
from ..config import YfConfig
78
from ..const import SECTOR_INDUSTY_MAPPING_LC, _SENTINEL_
89
from ..data import YfData
910
from ..utils import dynamic_docstring, generate_list_table_from_dict, get_yf_logger
@@ -148,6 +149,8 @@ def _fetch_and_parse(self) -> None:
148149
self._industries = self._parse_industries(data.get('industries', {}))
149150

150151
except Exception as e:
152+
if not YfConfig().hide_exceptions:
153+
raise
151154
logger = get_yf_logger()
152155
logger.error(f"Failed to get sector data for '{self._key}' reason: {e}")
153156
logger.debug("Got response: ")

yfinance/live.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from websockets.asyncio.client import connect as async_connect
88

99
from yfinance import utils
10+
from yfinance.config import YfConfig
1011
from yfinance.pricing_pb2 import PricingData
1112
from google.protobuf.json_format import MessageToDict
1213

@@ -27,6 +28,8 @@ def _decode_message(self, base64_message: str) -> dict:
2728
pricing_data.ParseFromString(decoded_bytes)
2829
return MessageToDict(pricing_data, preserving_proto_field_name=True)
2930
except Exception as e:
31+
if not YfConfig().hide_exceptions:
32+
raise
3033
self.logger.error("Failed to decode message: %s", e, exc_info=True)
3134
if self.verbose:
3235
print("Failed to decode message: %s", e)
@@ -61,6 +64,8 @@ async def _connect(self):
6164
if self.verbose:
6265
print("Connected to WebSocket.")
6366
except Exception as e:
67+
if not YfConfig().hide_exceptions:
68+
raise
6469
self.logger.error("Failed to connect to WebSocket: %s", e, exc_info=True)
6570
if self.verbose:
6671
print(f"Failed to connect to WebSocket: {e}")
@@ -79,6 +84,8 @@ async def _periodic_subscribe(self):
7984
if self.verbose:
8085
print(f"Heartbeat subscription sent for symbols: {self._subscriptions}")
8186
except Exception as e:
87+
if not YfConfig().hide_exceptions:
88+
raise
8289
self.logger.error("Error in heartbeat subscription: %s", e, exc_info=True)
8390
if self.verbose:
8491
print(f"Error in heartbeat subscription: {e}")
@@ -162,6 +169,8 @@ async def listen(self, message_handler=None):
162169
else:
163170
self._message_handler(decoded_message)
164171
except Exception as handler_exception:
172+
if not YfConfig().hide_exceptions:
173+
raise
165174
self.logger.error("Error in message handler: %s", handler_exception, exc_info=True)
166175
if self.verbose:
167176
print("Error in message handler:", handler_exception)
@@ -176,6 +185,8 @@ async def listen(self, message_handler=None):
176185
break
177186

178187
except Exception as e:
188+
if not YfConfig().hide_exceptions:
189+
raise
179190
self.logger.error("Error while listening to messages: %s", e, exc_info=True)
180191
if self.verbose:
181192
print("Error while listening to messages: %s", e)
@@ -301,6 +312,8 @@ def listen(self, message_handler: Optional[Callable[[dict], None]] = None):
301312
try:
302313
message_handler(decoded_message)
303314
except Exception as handler_exception:
315+
if not YfConfig().hide_exceptions:
316+
raise
304317
self.logger.error("Error in message handler: %s", handler_exception, exc_info=True)
305318
if self.verbose:
306319
print("Error in message handler:", handler_exception)
@@ -314,6 +327,8 @@ def listen(self, message_handler: Optional[Callable[[dict], None]] = None):
314327
break
315328

316329
except Exception as e:
330+
if not YfConfig().hide_exceptions:
331+
raise
317332
self.logger.error("Error while listening to messages: %s", e, exc_info=True)
318333
if self.verbose:
319334
print("Error while listening to messages: %s", e)

yfinance/lookup.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
import warnings
2525

2626
from . import utils
27+
from .config import YfConfig
2728
from .const import _QUERY1_URL_, _SENTINEL_
2829
from .data import YfData
29-
from .exceptions import YFException
30+
from .exceptions import YFDataException
3031

3132
LOOKUP_TYPES = ["all", "equity", "mutualfund", "etf", "index", "future", "currency", "cryptocurrency"]
3233

@@ -81,18 +82,19 @@ def _fetch_lookup(self, lookup_type="all", count=25) -> dict:
8182

8283
data = self._data.get(url=url, params=params, timeout=self.timeout)
8384
if data is None or "Will be right back" in data.text:
84-
raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
85-
"Our engineers are working quickly to resolve "
86-
"the issue. Thank you for your patience.")
85+
raise YFDataException("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***")
8786
try:
8887
data = data.json()
8988
except _json.JSONDecodeError:
90-
self._logger.error(f"{self.query}: Failed to retrieve lookup results and received faulty response instead.")
89+
if not YfConfig().hide_exceptions:
90+
raise
91+
self._logger.error(f"{self.ticker}: 'lookup' fetch received faulty data")
9192
data = {}
9293

9394
# Error returned
9495
if data.get("finance", {}).get("error", {}):
95-
raise YFException(data.get("finance", {}).get("error", {}))
96+
error = data.get("finance", {}).get("error", {})
97+
raise YFDataException(f"{self.ticker}: 'lookup' fetch returned error: {error}")
9698

9799
self._cache[cache_key] = data
98100
return data

0 commit comments

Comments
 (0)