refactor(dataflows): name the unavailable-vendor error for what it covers

- VendorRateLimitError is VendorUnavailableError: a vendor that throttled or failed the request
This commit is contained in:
Yijia-Xiao
2026-09-29 05:57:18 +00:00
parent a6318b31cc
commit 00924bb203
12 changed files with 42 additions and 39 deletions
+2 -2
View File
@@ -95,11 +95,11 @@ if __name__ == "__main__":
@pytest.mark.unit
def test_an_unreachable_yahoo_is_not_reported_as_a_symbol_without_insider_data():
from tradingagents.dataflows.errors import VendorRateLimitError
from tradingagents.dataflows.errors import VendorUnavailableError
from tradingagents.dataflows.vendors.yahoo import fundamentals
ticker = type("T", (), {"insider_transactions": pd.DataFrame()})()
with mock.patch.object(fundamentals.yf, "Ticker", return_value=ticker), \
mock.patch.object(fundamentals, "vendor_reachable", return_value=False), \
pytest.raises(VendorRateLimitError):
pytest.raises(VendorUnavailableError):
fundamentals.get_insider_transactions("AAPL", as_of_date="2026-09-21")
+2 -2
View File
@@ -144,13 +144,13 @@ def test_a_throttle_lets_the_next_vendor_try(monkeypatch):
"""SEC throttles by refusing the request; the router then tries yfinance."""
import requests
from tradingagents.dataflows.errors import VendorRateLimitError
from tradingagents.dataflows.errors import VendorUnavailableError
def _throttled(*a, **k):
raise requests.HTTPError(response=mock.Mock(status_code=429))
monkeypatch.setattr(sec_edgar.requests, "get", _throttled)
with pytest.raises(VendorRateLimitError):
with pytest.raises(VendorUnavailableError):
_REAL_FETCH("https://data.sec.gov/api/xbrl/companyfacts/CIK0000320193.json")
+6 -6
View File
@@ -196,14 +196,14 @@ def test_an_unreachable_vendor_is_not_reported_as_a_missing_symbol(monkeypatch):
company has no balance sheet, when the truth is we could not ask."""
import pandas as pd
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError
from tradingagents.dataflows.vendors.yahoo import ohlcv
empty = mock.Mock(quarterly_balance_sheet=pd.DataFrame(), balance_sheet=pd.DataFrame())
monkeypatch.setattr(yahoo_market.yf, "Ticker", lambda s: empty)
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: False)
with pytest.raises(VendorRateLimitError, match="unreachable"):
with pytest.raises(VendorUnavailableError, match="unreachable"):
yahoo_fundamentals.get_balance_sheet("AAPL", "annual", "2026-09-01")
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: True)
@@ -216,10 +216,10 @@ def test_every_vendor_unavailable_says_so_rather_than_crashing(monkeypatch):
"""A throttled or unreachable chain used to raise RuntimeError('No available
vendor'), which ends the run, and never said the vendor was the problem."""
from tradingagents.dataflows import router
from tradingagents.dataflows.errors import VendorRateLimitError
from tradingagents.dataflows.errors import VendorUnavailableError
def _down(*a, **k):
raise VendorRateLimitError("Yahoo Finance is unreachable")
raise VendorUnavailableError("Yahoo Finance is unreachable")
monkeypatch.setitem(router.VENDOR_METHODS["get_balance_sheet"], "yfinance", _down)
@@ -235,13 +235,13 @@ def test_the_price_path_also_tells_an_outage_from_an_unknown_symbol(monkeypatch)
delisted symbol either."""
import pandas as pd
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError
from tradingagents.dataflows.vendors.yahoo import ohlcv
monkeypatch.setattr(yahoo_market.yf, "Ticker", lambda s: mock.Mock(history=lambda **k: pd.DataFrame()))
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: False)
with pytest.raises(VendorRateLimitError, match="unreachable"):
with pytest.raises(VendorUnavailableError, match="unreachable"):
yahoo_market.get_YFin_data_online("AAPL", "2026-09-01", "2026-09-10")
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: True)
+3 -3
View File
@@ -16,7 +16,7 @@ from tradingagents.dataflows.errors import (
NoMarketDataError,
VendorError,
VendorNotConfiguredError,
VendorRateLimitError,
VendorUnavailableError,
)
from tradingagents.dataflows.vendors.alpha_vantage.common import (
AlphaVantageNotConfiguredError,
@@ -28,7 +28,7 @@ from tradingagents.dataflows.vendors.fred import FredNotConfiguredError
@pytest.mark.unit
class HierarchyTests(unittest.TestCase):
def test_all_conditions_derive_from_vendor_error(self):
for cls in (NoMarketDataError, VendorRateLimitError, VendorNotConfiguredError):
for cls in (NoMarketDataError, VendorUnavailableError, VendorNotConfiguredError):
self.assertTrue(issubclass(cls, VendorError))
def test_not_configured_is_still_a_value_error(self):
@@ -36,7 +36,7 @@ class HierarchyTests(unittest.TestCase):
self.assertTrue(issubclass(VendorNotConfiguredError, ValueError))
def test_vendor_named_errors_subclass_the_generic_bases(self):
self.assertTrue(issubclass(AlphaVantageRateLimitError, VendorRateLimitError))
self.assertTrue(issubclass(AlphaVantageRateLimitError, VendorUnavailableError))
self.assertTrue(issubclass(AlphaVantageNotConfiguredError, VendorNotConfiguredError))
self.assertTrue(issubclass(FredNotConfiguredError, VendorNotConfiguredError))
# ... and therefore still ValueErrors
+5 -5
View File
@@ -21,7 +21,7 @@ from tradingagents.agents.tools import (
)
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.errors import VendorRateLimitError
from tradingagents.dataflows.errors import VendorUnavailableError
from tradingagents.dataflows.vendors.yahoo import fundamentals, ohlcv
DAY = "2026-09-18"
@@ -84,7 +84,7 @@ def test_the_indicator_path_retries_a_rate_limit(yahoo):
def test_yfinance_raises_the_rate_limit_from_history(yahoo):
yahoo.setattr(YfData, "_make_request", _rate_limited)
with pytest.raises(VendorRateLimitError, match="rate limited"):
with pytest.raises(VendorUnavailableError, match="rate limited"):
ohlcv.load_ohlcv("AAPL", DAY)
@@ -122,7 +122,7 @@ def test_a_price_request_that_raises_is_unavailable_whether_or_not_yahoo_answers
yahoo.setattr(yf.Ticker, "history", refused)
for reachable in (False, True):
yahoo.setattr(ohlcv, "vendor_reachable", lambda url, _r=reachable: _r)
with pytest.raises(VendorRateLimitError, match="request failed"):
with pytest.raises(VendorUnavailableError, match="request failed"):
ohlcv.load_ohlcv("AAPL", DAY)
@@ -136,7 +136,7 @@ def test_a_price_request_that_fails_is_unavailable_even_when_yahoo_answers(monke
monkeypatch.setattr(ohlcv.yf, "Ticker", lambda s: type("T", (), {"history": lambda self, **k: timed_out(**k)})())
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: True)
with pytest.raises(VendorRateLimitError):
with pytest.raises(VendorUnavailableError):
ohlcv.load_ohlcv("NVDA", "2026-09-23")
@@ -174,5 +174,5 @@ def test_a_yahoo_request_that_fails_is_unavailable_not_absent(yahoo, call):
yahoo.setattr(yf, "Ticker", _Unreachable)
yahoo.setattr(yf, "Search", _Unreachable)
with pytest.raises(VendorRateLimitError):
with pytest.raises(VendorUnavailableError):
call()
+2 -2
View File
@@ -10,7 +10,7 @@ from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.date_window import as_of, as_of_window
from tradingagents.dataflows.errors import VendorRateLimitError
from tradingagents.dataflows.errors import VendorUnavailableError
from tradingagents.dataflows.router import route_to_vendor, vendor_unavailable
from tradingagents.dataflows.vendors.yahoo.snapshot import build_verified_market_snapshot
@@ -85,7 +85,7 @@ def get_verified_market_snapshot(
# An exception out of a tool would end the run.
try:
return build_verified_market_snapshot(symbol, as_of(curr_date, trade_date), look_back_days)
except VendorRateLimitError as exc:
except VendorUnavailableError as exc:
return vendor_unavailable("get_verified_market_snapshot", exc)
+6 -3
View File
@@ -7,7 +7,7 @@ these (or a thin vendor-named subclass) and needs no new ``except`` clause.
VendorError
├── NoMarketDataError no usable rows (empty result OR stale data)
├── VendorRateLimitError transient throttle -> skip to next vendor
├── VendorUnavailableError throttled or failed request -> skip to next vendor
└── VendorNotConfiguredError missing API key/config -> vendor unavailable
The number of types is the number of distinct router reactions, not the number
@@ -43,8 +43,11 @@ class NoMarketDataError(VendorError):
super().__init__(msg)
class VendorRateLimitError(VendorError):
"""A vendor throttled the request; the router skips to the next vendor."""
class VendorUnavailableError(VendorError):
"""A vendor could not answer: it throttled or failed the request.
Says nothing about the instrument; the router skips to the next vendor.
"""
class VendorNotConfiguredError(VendorError, ValueError):
+4 -4
View File
@@ -4,7 +4,7 @@ from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.errors import (
NoMarketDataError,
VendorNotConfiguredError,
VendorRateLimitError,
VendorUnavailableError,
)
from tradingagents.dataflows.vendors.alpha_vantage import (
get_balance_sheet as get_alpha_vantage_balance_sheet,
@@ -173,7 +173,7 @@ def get_vendor(category: str, method: str = None) -> str:
return config.get("data_vendors", {}).get(category, "default")
def vendor_unavailable(method: str, error: VendorRateLimitError) -> str:
def vendor_unavailable(method: str, error: VendorUnavailableError) -> str:
"""What a call returns when every vendor was throttled or unreachable."""
return (
f"DATA_UNAVAILABLE: no configured vendor could serve {method} right now "
@@ -210,7 +210,7 @@ def route_to_vendor(method: str, *args, **kwargs):
vendor_chain = all_available_vendors
last_no_data: NoMarketDataError | None = None
last_unavailable: VendorRateLimitError | None = None
last_unavailable: VendorUnavailableError | None = None
first_error: Exception | None = None
for vendor in vendor_chain:
vendor_impl = VENDOR_METHODS[method][vendor]
@@ -218,7 +218,7 @@ def route_to_vendor(method: str, *args, **kwargs):
try:
return impl_func(*args, **kwargs)
except VendorRateLimitError as e:
except VendorUnavailableError as e:
logger.warning("Vendor %r unavailable for %s: %s; trying next vendor.", vendor, method, e)
# Kept so an all-unavailable chain can say the vendor was the
# problem, rather than reporting nothing about the symbol.
+2 -2
View File
@@ -5,7 +5,7 @@ from io import StringIO
import pandas as pd
from tradingagents.dataflows.errors import VendorNotConfiguredError, VendorRateLimitError
from tradingagents.dataflows.errors import VendorNotConfiguredError, VendorUnavailableError
from tradingagents.dataflows.net import get_scrubbed
API_BASE_URL = "https://www.alphavantage.co/query"
@@ -62,7 +62,7 @@ def format_datetime_for_api(date_input, end_of_day: bool = False) -> str:
raise ValueError(f"Date must be string or datetime object, got {type(date_input)}")
class AlphaVantageRateLimitError(VendorRateLimitError):
class AlphaVantageRateLimitError(VendorUnavailableError):
"""Raised when the Alpha Vantage API rate limit is exceeded."""
pass
+3 -3
View File
@@ -28,7 +28,7 @@ import requests
from tradingagents import __version__
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError
from tradingagents.dataflows.files import replace_file
logger = logging.getLogger(__name__)
@@ -109,9 +109,9 @@ def _fetch_json(url: str) -> dict:
status = getattr(getattr(exc, "response", None), "status_code", None)
# Every failure here is "this vendor cannot serve it now", so the router
# moves on instead of seeing a transport exception it has no rule for.
raise VendorRateLimitError(f"SEC EDGAR request failed ({status or type(exc).__name__})") from exc
raise VendorUnavailableError(f"SEC EDGAR request failed ({status or type(exc).__name__})") from exc
except ValueError as exc:
raise VendorRateLimitError("SEC EDGAR returned an unreadable response") from exc
raise VendorUnavailableError("SEC EDGAR returned an unreadable response") from exc
def _cached_json(url: str, name: str) -> dict:
+2 -2
View File
@@ -4,7 +4,7 @@ import pandas as pd
import yfinance as yf
from tradingagents.dataflows.date_window import withhold_live_profile
from tradingagents.dataflows.errors import VendorRateLimitError
from tradingagents.dataflows.errors import VendorUnavailableError
from tradingagents.dataflows.net import vendor_reachable
from tradingagents.dataflows.symbols import normalize_symbol
from tradingagents.dataflows.vendors.yahoo.ohlcv import (
@@ -151,7 +151,7 @@ def get_insider_transactions(
# so report it plainly rather than treating the symbol as invalid.
if data is None or data.empty:
if not vendor_reachable(YAHOO_HOST):
raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved")
raise VendorUnavailableError("Yahoo Finance is unreachable; insider filings were not retrieved")
return f"No insider transactions reported for symbol '{canonical}'"
if as_of_date:
+5 -5
View File
@@ -7,7 +7,7 @@ import yfinance as yf
from yfinance.exceptions import YFRateLimitError
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError
from tradingagents.dataflows.files import replace_file
from tradingagents.dataflows.net import vendor_reachable
from tradingagents.dataflows.symbols import normalize_symbol, safe_ticker_component
@@ -35,7 +35,7 @@ def raise_for_empty(symbol: str, canonical: str, what: str) -> None:
without this a Yahoo outage reads as "this symbol has no {what}".
"""
if not vendor_reachable(YAHOO_HOST):
raise VendorRateLimitError(f"Yahoo Finance is unreachable; no {what} was retrieved")
raise VendorUnavailableError(f"Yahoo Finance is unreachable; no {what} was retrieved")
raise NoMarketDataError(symbol, canonical, f"no {what}")
@@ -45,7 +45,7 @@ def yf_retry(func, max_retries=3, base_delay=2.0):
yfinance raises YFRateLimitError on HTTP 429 responses but does not
retry them internally, so this wrapper retries them. A rate limit that
outlasts the retries, or any other exception, is raised as
VendorRateLimitError: Yahoo answers an unknown symbol with an empty
VendorUnavailableError: Yahoo answers an unknown symbol with an empty
result, so a request that raised failed in transit and says nothing about
the symbol. ``func`` should build its own Ticker and make the request
itself: a Ticker keeps a failed ``info`` fetch as done, so asking the same
@@ -60,11 +60,11 @@ def yf_retry(func, max_retries=3, base_delay=2.0):
logger.warning(f"Yahoo Finance rate limited, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries})")
time.sleep(delay)
else:
raise VendorRateLimitError(
raise VendorUnavailableError(
f"Yahoo Finance rate limited after {max_retries} retries: {exc}"
) from exc
except Exception as exc:
raise VendorRateLimitError(f"Yahoo Finance request failed: {type(exc).__name__}") from exc
raise VendorUnavailableError(f"Yahoo Finance request failed: {type(exc).__name__}") from exc
def _ensure_date_column(data: pd.DataFrame) -> pd.DataFrame: