Files
tradingagents/tests/test_vendor_errors.py
Yijia-Xiao ec2ecc79c3 fix(router): report a vendor chain with an unavailable vendor as unavailable, not as no data
- a vendor that throttled or failed the request never said whether it has the symbol, so another vendor's "no data" no longer speaks for the chain
2026-09-29 05:57:18 +00:00

120 lines
4.9 KiB
Python

"""The vendor data-error hierarchy: every "vendor couldn't return usable data"
condition derives from VendorError, so the router catches base types and any
vendor slots in without new handling.
"""
import copy
import unittest
from unittest import mock
import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.errors import (
NoMarketDataError,
VendorError,
VendorNotConfiguredError,
VendorUnavailableError,
)
from tradingagents.dataflows.vendors.alpha_vantage.common import (
AlphaVantageNotConfiguredError,
AlphaVantageRateLimitError,
)
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, VendorUnavailableError, VendorNotConfiguredError):
self.assertTrue(issubclass(cls, VendorError))
def test_not_configured_is_still_a_value_error(self):
# Back-compat: existing `except ValueError` callers keep working.
self.assertTrue(issubclass(VendorNotConfiguredError, ValueError))
def test_vendor_named_errors_subclass_the_generic_bases(self):
self.assertTrue(issubclass(AlphaVantageRateLimitError, VendorUnavailableError))
self.assertTrue(issubclass(AlphaVantageNotConfiguredError, VendorNotConfiguredError))
self.assertTrue(issubclass(FredNotConfiguredError, VendorNotConfiguredError))
# ... and therefore still ValueErrors
self.assertTrue(issubclass(FredNotConfiguredError, ValueError))
@pytest.mark.unit
class RouterHandlesBaseTypesTests(unittest.TestCase):
def setUp(self):
config_module._config = copy.deepcopy(default_config.DEFAULT_CONFIG)
def tearDown(self):
config_module._config = copy.deepcopy(default_config.DEFAULT_CONFIG)
def test_rate_limit_subclass_caught_by_base(self):
# A vendor-named rate-limit error skips to the next vendor in the chain.
set_config({"data_vendors": {"core_stock_apis": "alpha_vantage,yfinance"}})
def _throttled(*a, **k):
raise AlphaVantageRateLimitError("slow down")
with mock.patch.dict(
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _throttled, "yfinance": lambda *a, **k: "YF"}},
clear=False,
):
out = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertEqual(out, "YF")
def test_not_configured_falls_through_to_next_vendor(self):
set_config({"data_vendors": {"core_stock_apis": "alpha_vantage,yfinance"}})
def _unconfigured(*a, **k):
raise AlphaVantageNotConfiguredError("no key")
with mock.patch.dict(
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _unconfigured, "yfinance": lambda *a, **k: "YF"}},
clear=False,
):
out = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertEqual(out, "YF")
def test_sole_unconfigured_vendor_surfaces_the_error(self):
# With no fallback, the not-configured condition must surface (not vanish).
set_config({"data_vendors": {"core_stock_apis": "alpha_vantage"}})
def _unconfigured(*a, **k):
raise AlphaVantageNotConfiguredError("no key")
with mock.patch.dict(
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _unconfigured}},
clear=False,
), self.assertRaises(AlphaVantageNotConfiguredError):
router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
def test_an_unavailable_vendor_keeps_absence_from_being_claimed(self):
# The throttled vendor's coverage was never learned, so "no usable data
# from any configured vendor" would be a claim nobody checked.
def throttled(*a, **k):
raise VendorUnavailableError("Yahoo HTTP 429")
def timed_out(*a, **k):
raise TimeoutError("read timed out") # a vendor that does not type its failures
def not_covered(*a, **k):
raise NoMarketDataError("AAPL", "AAPL", "not covered")
for chain in ({"yfinance": throttled, "alpha_vantage": not_covered},
{"yfinance": not_covered, "alpha_vantage": throttled},
{"alpha_vantage": timed_out, "yfinance": not_covered}):
set_config({"data_vendors": {"core_stock_apis": ",".join(chain)}})
with mock.patch.dict(router.VENDOR_METHODS, {"get_stock_data": chain}, clear=False):
out = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertTrue(out.startswith("DATA_UNAVAILABLE"), out)
if __name__ == "__main__":
unittest.main()