Make curl_cffi optional with graceful fallback to requests
Some platforms (e.g. older macOS, exotic Linux distros) cannot build the curl-impersonate binary that backs curl_cffi, leaving yfinance unusable. This change keeps curl_cffi as the preferred and default backend but no longer hard-requires it at runtime. - New `yfinance/_http.py` abstracts the HTTP backend. If `curl_cffi` is importable it is used as before; otherwise yfinance falls back to plain `requests` with a realistic Chrome User-Agent and logs a warning so the downgrade is explicit. - `data.py`, `base.py`, `multi.py`, `scrapers/history.py` build sessions via `_http.new_session()`. - Scrapers and screener catch `_http.HTTPError` instead of importing `curl_cffi.requests.exceptions.HTTPError` directly. - `is_supported_session()` accepts either backend; `cookie_jar()` papers over the small API difference between curl_cffi (`cookies.jar`) and requests (`cookies` is itself the jar). - New Advanced > Installation page documents the curl_cffi-free install recipe; README links to it just under the install instruction. Empirically verified that plain `requests` with a Chrome UA does not hit the 401-after-~10-requests issue that the earlier `curl_adapter` approach ran into; 15/15 quoteSummary calls succeed without rate limiting. `setup.py` is unchanged -- `curl_cffi>=0.15` remains the default install requirement (CVE-pinned). The fallback only activates when the import fails at runtime; moving curl_cffi to extras_require can be a follow-up decision for the maintainer.
This commit is contained in:
@@ -53,6 +53,8 @@ Install `yfinance` from PYPI using `pip`:
|
|||||||
$ pip install yfinance
|
$ pip install yfinance
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To install without `curl_cffi` for requests fallback, see [Advanced ▸ Installation](https://ranaroussi.github.io/yfinance/advanced/install.html).
|
||||||
|
|
||||||
### [yfinance relies on the community to investigate bugs and contribute code. Here's how you can help.](https://github.com/ranaroussi/yfinance/blob/main/CONTRIBUTING.md)
|
### [yfinance relies on the community to investigate bugs and contribute code. Here's how you can help.](https://github.com/ranaroussi/yfinance/blob/main/CONTRIBUTING.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Advanced
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
install
|
||||||
logging
|
logging
|
||||||
config
|
config
|
||||||
caching
|
caching
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
To install without ``curl_cffi`` for requests fallback:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/ranaroussi/yfinance/main/requirements.txt | grep -vi '^curl_cffi' | pip install -r /dev/stdin
|
||||||
|
pip install --no-deps yfinance
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Tests for the HTTP backend abstraction.
|
||||||
|
|
||||||
|
Verifies the fallback to plain ``requests`` when ``curl_cffi`` is
|
||||||
|
unavailable, and the ``YF_DISABLE_CURL_CFFI`` env-var opt-out.
|
||||||
|
"""
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
def _reload_http(curl_cffi_available: bool, disable_env: bool):
|
||||||
|
"""Reload ``yfinance._http`` simulating curl_cffi presence / opt-out."""
|
||||||
|
saved_modules = {k: sys.modules[k] for k in list(sys.modules) if k == "yfinance._http"}
|
||||||
|
saved_env = os.environ.get("YF_DISABLE_CURL_CFFI")
|
||||||
|
if disable_env:
|
||||||
|
os.environ["YF_DISABLE_CURL_CFFI"] = "1"
|
||||||
|
elif "YF_DISABLE_CURL_CFFI" in os.environ:
|
||||||
|
del os.environ["YF_DISABLE_CURL_CFFI"]
|
||||||
|
|
||||||
|
saved_curl = sys.modules.get("curl_cffi")
|
||||||
|
if not curl_cffi_available:
|
||||||
|
sys.modules["curl_cffi"] = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.modules.pop("yfinance._http", None)
|
||||||
|
return importlib.import_module("yfinance._http")
|
||||||
|
finally:
|
||||||
|
# Restore originals so other tests aren't affected.
|
||||||
|
if saved_curl is not None:
|
||||||
|
sys.modules["curl_cffi"] = saved_curl
|
||||||
|
elif not curl_cffi_available:
|
||||||
|
sys.modules.pop("curl_cffi", None)
|
||||||
|
if saved_env is None:
|
||||||
|
os.environ.pop("YF_DISABLE_CURL_CFFI", None)
|
||||||
|
else:
|
||||||
|
os.environ["YF_DISABLE_CURL_CFFI"] = saved_env
|
||||||
|
sys.modules.pop("yfinance._http", None)
|
||||||
|
for k, v in saved_modules.items():
|
||||||
|
sys.modules[k] = v
|
||||||
|
|
||||||
|
|
||||||
|
class TestHttpBackend(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_fallback_when_curl_cffi_missing(self):
|
||||||
|
import requests as _stdlib_requests
|
||||||
|
mod = _reload_http(curl_cffi_available=False, disable_env=False)
|
||||||
|
self.assertFalse(mod.HAS_CURL_CFFI)
|
||||||
|
session = mod.new_session()
|
||||||
|
self.assertIsInstance(session, _stdlib_requests.Session)
|
||||||
|
self.assertIn("Chrome", session.headers["User-Agent"])
|
||||||
|
|
||||||
|
def test_disable_env_var_forces_fallback(self):
|
||||||
|
import requests as _stdlib_requests
|
||||||
|
mod = _reload_http(curl_cffi_available=True, disable_env=True)
|
||||||
|
self.assertFalse(mod.HAS_CURL_CFFI)
|
||||||
|
self.assertIsInstance(mod.new_session(), _stdlib_requests.Session)
|
||||||
|
|
||||||
|
def test_cookie_jar_works_for_requests_session(self):
|
||||||
|
mod = _reload_http(curl_cffi_available=False, disable_env=False)
|
||||||
|
session = mod.new_session()
|
||||||
|
jar = mod.cookie_jar(session)
|
||||||
|
# requests.Session.cookies subclasses CookieJar directly.
|
||||||
|
from http.cookiejar import CookieJar
|
||||||
|
self.assertIsInstance(jar, CookieJar)
|
||||||
|
|
||||||
|
def test_is_supported_session_accepts_requests_session(self):
|
||||||
|
import requests as _stdlib_requests
|
||||||
|
mod = _reload_http(curl_cffi_available=False, disable_env=False)
|
||||||
|
self.assertTrue(mod.is_supported_session(_stdlib_requests.Session()))
|
||||||
|
self.assertFalse(mod.is_supported_session(object()))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""HTTP backend abstraction.
|
||||||
|
|
||||||
|
Prefers ``curl_cffi`` for browser TLS impersonation. Falls back to plain
|
||||||
|
``requests`` with a realistic ``User-Agent`` when ``curl_cffi`` cannot be
|
||||||
|
imported (e.g. binary not buildable on the host platform — see issue #2692)
|
||||||
|
or when ``YF_DISABLE_CURL_CFFI`` is set in the environment (downstream
|
||||||
|
packagers may ship without ``curl_cffi`` even if it is installed).
|
||||||
|
|
||||||
|
The fallback is best-effort: plain ``requests`` cannot replicate the
|
||||||
|
JA3/JA4 fingerprint and HTTP/2 settings that ``curl_cffi`` provides, so
|
||||||
|
Yahoo Finance may rate-limit or block this client. ``curl_cffi`` remains
|
||||||
|
the preferred backend and the default install dependency.
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
_DISABLE = os.environ.get("YF_DISABLE_CURL_CFFI", "").lower() in ("1", "true", "yes")
|
||||||
|
|
||||||
|
if not _DISABLE:
|
||||||
|
try:
|
||||||
|
from curl_cffi import requests as _curl_backend
|
||||||
|
_backend = _curl_backend
|
||||||
|
HAS_CURL_CFFI = True
|
||||||
|
except ImportError:
|
||||||
|
import requests as _requests_backend
|
||||||
|
_backend = _requests_backend
|
||||||
|
HAS_CURL_CFFI = False
|
||||||
|
else:
|
||||||
|
import requests as _requests_backend
|
||||||
|
_backend = _requests_backend
|
||||||
|
HAS_CURL_CFFI = False
|
||||||
|
|
||||||
|
requests = _backend
|
||||||
|
HTTPError = _backend.exceptions.HTTPError
|
||||||
|
|
||||||
|
_FALLBACK_USER_AGENT = (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
|
||||||
|
_fallback_warned = False
|
||||||
|
|
||||||
|
|
||||||
|
def _warn_once_on_fallback():
|
||||||
|
global _fallback_warned
|
||||||
|
if HAS_CURL_CFFI or _fallback_warned:
|
||||||
|
return
|
||||||
|
_fallback_warned = True
|
||||||
|
utils.get_yf_logger().warning(
|
||||||
|
"curl_cffi not available; falling back to requests without browser TLS "
|
||||||
|
"impersonation. Yahoo Finance may rate-limit or block this client. "
|
||||||
|
"Install curl_cffi (>=0.15) for the supported configuration."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def new_session():
|
||||||
|
"""Create a default Session for the active backend."""
|
||||||
|
if HAS_CURL_CFFI:
|
||||||
|
return _backend.Session(impersonate="chrome")
|
||||||
|
_warn_once_on_fallback()
|
||||||
|
s = _backend.Session()
|
||||||
|
s.headers.update({
|
||||||
|
"User-Agent": _FALLBACK_USER_AGENT,
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
|
})
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def cookie_jar(session):
|
||||||
|
"""Return the underlying ``http.cookiejar.CookieJar`` for either backend.
|
||||||
|
|
||||||
|
``curl_cffi`` exposes the jar as ``session.cookies.jar``; ``requests``
|
||||||
|
uses ``session.cookies`` directly (it subclasses ``CookieJar``).
|
||||||
|
"""
|
||||||
|
cookies = session.cookies
|
||||||
|
return getattr(cookies, "jar", cookies)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def _supported_session_classes() -> tuple:
|
||||||
|
classes = []
|
||||||
|
try:
|
||||||
|
from curl_cffi.requests.session import Session as _CurlSession
|
||||||
|
classes.append(_CurlSession)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
from requests.sessions import Session as _ReqSession
|
||||||
|
classes.append(_ReqSession)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
return tuple(classes)
|
||||||
|
|
||||||
|
|
||||||
|
def is_supported_session(obj) -> bool:
|
||||||
|
"""True if ``obj`` is a Session from either supported backend."""
|
||||||
|
classes = _supported_session_classes()
|
||||||
|
return bool(classes) and isinstance(obj, classes)
|
||||||
+2
-2
@@ -27,7 +27,7 @@ from urllib.parse import quote as urlencode
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from curl_cffi import requests
|
from ._http import requests, new_session
|
||||||
|
|
||||||
|
|
||||||
from . import utils, cache
|
from . import utils, cache
|
||||||
@@ -81,7 +81,7 @@ class TickerBase:
|
|||||||
ticker = base_symbol
|
ticker = base_symbol
|
||||||
|
|
||||||
self.ticker = ticker.upper()
|
self.ticker = ticker.upper()
|
||||||
self.session = session or requests.Session(impersonate="chrome")
|
self.session = session or new_session()
|
||||||
self._tz = None
|
self._tz = None
|
||||||
|
|
||||||
self._isin = None
|
self._isin = None
|
||||||
|
|||||||
+9
-8
@@ -4,7 +4,7 @@ import socket
|
|||||||
import time as _time
|
import time as _time
|
||||||
import json as _json
|
import json as _json
|
||||||
|
|
||||||
from curl_cffi import requests
|
from ._http import requests, new_session, is_supported_session, cookie_jar
|
||||||
from urllib.parse import urlsplit, urljoin
|
from urllib.parse import urlsplit, urljoin
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import datetime
|
import datetime
|
||||||
@@ -91,7 +91,7 @@ class YfData(metaclass=SingletonMeta):
|
|||||||
self._cookie_lock = threading.Lock()
|
self._cookie_lock = threading.Lock()
|
||||||
|
|
||||||
self._session = None
|
self._session = None
|
||||||
self._set_session(session or requests.Session(impersonate="chrome"))
|
self._set_session(session or new_session())
|
||||||
|
|
||||||
def set_login_cookies(self, cookie_t, cookie_y):
|
def set_login_cookies(self, cookie_t, cookie_y):
|
||||||
with self._cookie_lock:
|
with self._cookie_lock:
|
||||||
@@ -115,11 +115,12 @@ class YfData(metaclass=SingletonMeta):
|
|||||||
# Can't simply use a non-caching session to fetch cookie & crumb,
|
# Can't simply use a non-caching session to fetch cookie & crumb,
|
||||||
# because then the caching-session won't have cookie.
|
# because then the caching-session won't have cookie.
|
||||||
self._session_is_caching = True
|
self._session_is_caching = True
|
||||||
# But since switch to curl_cffi, can't use requests_cache with it.
|
# requests_cache wraps the stdlib Session; it doesn't work with
|
||||||
raise YFDataException("request_cache sessions don't work with curl_cffi, which is necessary now for Yahoo API. Solution: stop setting session, let YF handle.")
|
# curl_cffi and tends to miss anyway because the Yahoo crumb rotates.
|
||||||
|
raise YFDataException("Caching sessions (e.g. requests_cache) are not supported. Solution: stop setting session, let yfinance handle.")
|
||||||
|
|
||||||
if not isinstance(session, requests.session.Session):
|
if not is_supported_session(session):
|
||||||
raise YFDataException(f"Yahoo API requires curl_cffi session not {type(session)}. Solution: stop setting session, let YF handle.")
|
raise YFDataException(f"Unsupported session type {type(session)}; expected curl_cffi or requests Session. Solution: stop setting session, let yfinance handle.")
|
||||||
|
|
||||||
with self._cookie_lock:
|
with self._cookie_lock:
|
||||||
self._session = session
|
self._session = session
|
||||||
@@ -153,7 +154,7 @@ class YfData(metaclass=SingletonMeta):
|
|||||||
def _save_cookie_curlCffi(self):
|
def _save_cookie_curlCffi(self):
|
||||||
if self._session is None:
|
if self._session is None:
|
||||||
return False
|
return False
|
||||||
cookies = self._session.cookies.jar._cookies
|
cookies = cookie_jar(self._session)._cookies
|
||||||
if len(cookies) == 0:
|
if len(cookies) == 0:
|
||||||
return False
|
return False
|
||||||
yh_domains = [k for k in cookies.keys() if 'yahoo' in k]
|
yh_domains = [k for k in cookies.keys() if 'yahoo' in k]
|
||||||
@@ -189,7 +190,7 @@ class YfData(metaclass=SingletonMeta):
|
|||||||
if expired:
|
if expired:
|
||||||
utils.get_yf_logger().debug('cached cookie expired')
|
utils.get_yf_logger().debug('cached cookie expired')
|
||||||
return False
|
return False
|
||||||
self._session.cookies.jar._cookies.update(cookies)
|
cookie_jar(self._session)._cookies.update(cookies)
|
||||||
self._cookie = cookie
|
self._cookie = cookie
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -29,7 +29,7 @@ from typing import Union
|
|||||||
|
|
||||||
import multitasking as _multitasking
|
import multitasking as _multitasking
|
||||||
import pandas as _pd
|
import pandas as _pd
|
||||||
from curl_cffi import requests
|
from ._http import new_session
|
||||||
|
|
||||||
from . import Ticker, utils
|
from . import Ticker, utils
|
||||||
from .data import YfData
|
from .data import YfData
|
||||||
@@ -122,7 +122,7 @@ def _download_impl(ctx, tickers, start=None, end=None, actions=False, threads=Tr
|
|||||||
prepost=False, rounding=False, timeout=10, session=None,
|
prepost=False, rounding=False, timeout=10, session=None,
|
||||||
multi_level_index=True):
|
multi_level_index=True):
|
||||||
logger = utils.get_yf_logger()
|
logger = utils.get_yf_logger()
|
||||||
session = session or requests.Session(impersonate="chrome")
|
session = session or new_session()
|
||||||
|
|
||||||
YfData(session=session)
|
YfData(session=session)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import curl_cffi
|
from yfinance._http import HTTPError
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from yfinance import utils
|
from yfinance import utils
|
||||||
@@ -187,7 +187,7 @@ class Analysis:
|
|||||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": self._symbol}
|
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": self._symbol}
|
||||||
try:
|
try:
|
||||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", params=params_dict)
|
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", params=params_dict)
|
||||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
except HTTPError as e:
|
||||||
if not YfConfig.debug.hide_exceptions:
|
if not YfConfig.debug.hide_exceptions:
|
||||||
raise
|
raise
|
||||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from curl_cffi import requests
|
from yfinance._http import new_session
|
||||||
from math import isclose
|
from math import isclose
|
||||||
import bisect
|
import bisect
|
||||||
import datetime as _datetime
|
import datetime as _datetime
|
||||||
@@ -19,7 +19,7 @@ class PriceHistory:
|
|||||||
self._data = data
|
self._data = data
|
||||||
self.ticker = ticker.upper()
|
self.ticker = ticker.upper()
|
||||||
self.tz = tz
|
self.tz = tz
|
||||||
self.session = session or requests.Session(impersonate="chrome")
|
self.session = session or new_session()
|
||||||
|
|
||||||
self._history_cache = {}
|
self._history_cache = {}
|
||||||
self._history_metadata = None
|
self._history_metadata = None
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import curl_cffi
|
from yfinance._http import HTTPError
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from yfinance import utils
|
from yfinance import utils
|
||||||
@@ -71,7 +71,7 @@ class Holders:
|
|||||||
def _fetch_and_parse(self):
|
def _fetch_and_parse(self):
|
||||||
try:
|
try:
|
||||||
result = self._fetch()
|
result = self._fetch()
|
||||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
except HTTPError as e:
|
||||||
if not YfConfig.debug.hide_exceptions:
|
if not YfConfig.debug.hide_exceptions:
|
||||||
raise
|
raise
|
||||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import curl_cffi
|
from yfinance._http import HTTPError
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import numpy as _np
|
import numpy as _np
|
||||||
@@ -594,7 +594,7 @@ class Quote:
|
|||||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": self._symbol, "lang": YfConfig.locale.lang, "region": YfConfig.locale.region}
|
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": self._symbol, "lang": YfConfig.locale.lang, "region": YfConfig.locale.region}
|
||||||
try:
|
try:
|
||||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", params=params_dict)
|
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", params=params_dict)
|
||||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
except HTTPError as e:
|
||||||
if not YfConfig.debug.hide_exceptions:
|
if not YfConfig.debug.hide_exceptions:
|
||||||
raise
|
raise
|
||||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||||
@@ -605,7 +605,7 @@ class Quote:
|
|||||||
params_dict = {"symbols": self._symbol, "formatted": "false", "lang": YfConfig.locale.lang, "region": YfConfig.locale.region}
|
params_dict = {"symbols": self._symbol, "formatted": "false", "lang": YfConfig.locale.lang, "region": YfConfig.locale.region}
|
||||||
try:
|
try:
|
||||||
result = self._data.get_raw_json(f"{_QUERY1_URL_}/v7/finance/quote?", params=params_dict)
|
result = self._data.get_raw_json(f"{_QUERY1_URL_}/v7/finance/quote?", params=params_dict)
|
||||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
except HTTPError as e:
|
||||||
if not YfConfig.debug.hide_exceptions:
|
if not YfConfig.debug.hide_exceptions:
|
||||||
raise
|
raise
|
||||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import curl_cffi
|
from yfinance._http import HTTPError
|
||||||
from typing import Union
|
from typing import Union
|
||||||
import warnings
|
import warnings
|
||||||
from json import dumps
|
from json import dumps
|
||||||
@@ -176,7 +176,7 @@ def screen(query: Union[str, EquityQuery, FundQuery, ETFQuery],
|
|||||||
resp = _data.get(url=_PREDEFINED_URL_, params=params_dict)
|
resp = _data.get(url=_PREDEFINED_URL_, params=params_dict)
|
||||||
try:
|
try:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
except curl_cffi.requests.exceptions.HTTPError:
|
except HTTPError:
|
||||||
if query not in PREDEFINED_SCREENER_QUERIES:
|
if query not in PREDEFINED_SCREENER_QUERIES:
|
||||||
print(f"yfinance.screen: '{query}' is probably not a predefined query.")
|
print(f"yfinance.screen: '{query}' is probably not a predefined query.")
|
||||||
raise
|
raise
|
||||||
|
|||||||
Reference in New Issue
Block a user