5d10c0c48e
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.
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
"""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()
|