mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-03 16:12:44 +02:00
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:
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user