refactor(dataflows): name the shared modules by what they hold

- interface -> router; symbol_utils -> symbols, which also takes safe_ticker_component
- utils is split: get_current_date to date_window, the HTTP helpers to net
- dataflows imports are absolute; the NoMarketDataError re-export from symbols is gone
This commit is contained in:
Yijia-Xiao
2026-09-24 04:31:05 +00:00
parent a58aa613fc
commit c42a2f2c61
47 changed files with 217 additions and 219 deletions
+6 -6
View File
@@ -13,7 +13,7 @@ import pytest
import tradingagents.dataflows.alpha_vantage_common as av
import tradingagents.dataflows.alpha_vantage_fundamentals as avf
import tradingagents.dataflows.alpha_vantage_stock as avs
import tradingagents.dataflows.utils as utils
import tradingagents.dataflows.net as net
class _FakeResponse:
@@ -37,7 +37,7 @@ def _patched_get(body, capture=None):
@pytest.mark.unit
def test_request_passes_timeout(monkeypatch):
captured = {}
monkeypatch.setattr(utils.requests, "get", _patched_get("Date,Close\n2025-01-02,1.0", captured))
monkeypatch.setattr(net.requests, "get", _patched_get("Date,Close\n2025-01-02,1.0", captured))
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
assert captured.get("timeout") == av.REQUEST_TIMEOUT # #990
@@ -45,7 +45,7 @@ def test_request_passes_timeout(monkeypatch):
@pytest.mark.unit
def test_rate_limit_detected(monkeypatch):
body = '{"Information": "Our standard API rate limit is 25 requests per day. ... your API key ..."}'
monkeypatch.setattr(utils.requests, "get", _patched_get(body))
monkeypatch.setattr(net.requests, "get", _patched_get(body))
with pytest.raises(av.AlphaVantageRateLimitError):
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
@@ -56,11 +56,11 @@ def test_invalid_key_not_mislabeled_as_rate_limit(monkeypatch):
# (transient) rate limit, but surface as a real configuration error (#991).
body = ('{"Information": "the parameter apikey is invalid or missing. '
'Please claim your free API key on (https://www.alphavantage.co/support/#api-key)."}')
monkeypatch.setattr(utils.requests, "get", _patched_get(body))
monkeypatch.setattr(net.requests, "get", _patched_get(body))
with pytest.raises(av.AlphaVantageNotConfiguredError):
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
with pytest.raises(av.AlphaVantageRateLimitError): # sanity: rate-limit path still distinct
monkeypatch.setattr(utils.requests, "get", _patched_get('{"Note": "API call frequency is 5 calls per minute."}'))
monkeypatch.setattr(net.requests, "get", _patched_get('{"Note": "API call frequency is 5 calls per minute."}'))
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
@@ -147,7 +147,7 @@ def test_request_error_message_carries_no_key(monkeypatch):
def boom(*a, **k):
raise requests.Timeout(f"Read timed out. url: https://www.alphavantage.co/query?apikey={key}")
monkeypatch.setattr(utils.requests, "get", boom)
monkeypatch.setattr(net.requests, "get", boom)
with pytest.raises(requests.Timeout) as caught:
av._make_api_request("OVERVIEW", {"symbol": "IBM"})
assert key not in str(caught.value)
+1 -1
View File
@@ -7,7 +7,7 @@ import pytest
from cli.models import AssetType
from cli.utils import detect_asset_type, is_valid_ticker_input, normalize_ticker_symbol
from tradingagents.dataflows.symbol_utils import normalize_symbol
from tradingagents.dataflows.symbols import normalize_symbol
# --- #982: stablecoin-quoted crypto normalizes to Yahoo's -USD pair ---
+4 -4
View File
@@ -73,7 +73,7 @@ def _graph(config):
def _vendors_seen_by_a_run(graph, ticker="AAPL"):
from tradingagents.dataflows.interface import get_vendor
from tradingagents.dataflows.router import get_vendor
seen = []
@@ -121,7 +121,7 @@ def test_concurrent_runs_each_read_their_own_config():
config = copy.deepcopy(default_config.DEFAULT_CONFIG)
config["tool_vendors"] = {"get_balance_sheet": vendor}
graph = _graph(config)
from tradingagents.dataflows.interface import get_vendor
from tradingagents.dataflows.router import get_vendor
def _run(*a, **k):
barrier.wait(timeout=5) # both runs are in flight
@@ -141,7 +141,7 @@ def test_concurrent_runs_each_read_their_own_config():
@pytest.mark.unit
def test_settling_reads_the_graphs_own_config():
from tradingagents.dataflows.interface import get_vendor
from tradingagents.dataflows.router import get_vendor
config = copy.deepcopy(default_config.DEFAULT_CONFIG)
config["tool_vendors"] = {"get_stock_data": "alpha_vantage"}
@@ -164,7 +164,7 @@ def test_tools_inside_a_langgraph_run_see_the_run_config():
from langgraph.prebuilt import ToolNode
from tradingagents.dataflows.config import run_config
from tradingagents.dataflows.interface import get_vendor
from tradingagents.dataflows.router import get_vendor
@tool
def probe() -> str:
+9 -9
View File
@@ -12,7 +12,7 @@ import requests
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import fred, interface
from tradingagents.dataflows import fred, router
from tradingagents.dataflows.config import set_config
# A small, stable set of observations to format against.
@@ -203,15 +203,15 @@ class FredRoutingTests(unittest.TestCase):
def test_macro_category_routes_to_fred(self):
self.assertEqual(
interface.get_category_for_method("get_macro_indicators"), "macro_data"
router.get_category_for_method("get_macro_indicators"), "macro_data"
)
set_config({"data_vendors": {"macro_data": "fred"}})
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_macro_indicators": {"fred": lambda *a, **k: "MACRO_OK"}},
clear=False,
):
out = interface.route_to_vendor("get_macro_indicators", "cpi", "2026-06-01", 365)
out = router.route_to_vendor("get_macro_indicators", "cpi", "2026-06-01", 365)
self.assertEqual(out, "MACRO_OK")
def test_not_configured_degrades_gracefully(self):
@@ -224,11 +224,11 @@ class FredRoutingTests(unittest.TestCase):
raise fred.FredNotConfiguredError("FRED_API_KEY not set")
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_macro_indicators": {"fred": _unconfigured}},
clear=False,
):
out = interface.route_to_vendor("get_macro_indicators", "cpi", "2026-06-01", 365)
out = router.route_to_vendor("get_macro_indicators", "cpi", "2026-06-01", 365)
self.assertIn("DATA_UNAVAILABLE", out)
@@ -246,7 +246,7 @@ class TestKeyKeptOutOfErrors:
def _raises(self, side_effect):
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", side_effect=side_effect), \
mock.patch("tradingagents.dataflows.net.requests.get", side_effect=side_effect), \
pytest.raises(requests.RequestException) as caught:
fred._request("series", {"series_id": "DGS10"})
return caught.value
@@ -258,7 +258,7 @@ class TestKeyKeptOutOfErrors:
response=response,
)
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", return_value=response), \
mock.patch("tradingagents.dataflows.net.requests.get", return_value=response), \
pytest.raises(requests.HTTPError) as caught:
fred._request("series", {"series_id": "DGS10"})
exc = caught.value
@@ -280,7 +280,7 @@ def test_error_without_the_key_in_its_message_still_drops_the_request():
import requests as rq
req = rq.Request("GET", f"https://api.stlouisfed.org/fred/series?api_key={_KEY}").prepare()
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", side_effect=rq.Timeout("Read timed out.", request=req)), \
mock.patch("tradingagents.dataflows.net.requests.get", side_effect=rq.Timeout("Read timed out.", request=req)), \
pytest.raises(rq.Timeout) as caught:
fred._request("series", {"series_id": "DGS10"})
assert caught.value.request is None
+1 -1
View File
@@ -123,7 +123,7 @@ class TestNoUsableFieldsStillRaises:
def test_stub_payload_raises_no_market_data(self):
# yfinance returns {"trailingPegRatio": None} for unknown symbols; on a
# live run that must stay a hard "no data", not a bare header.
from tradingagents.dataflows.symbol_utils import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError
with pytest.raises(NoMarketDataError):
_yf(_TODAY, info={"trailingPegRatio": None})
+2 -2
View File
@@ -20,7 +20,7 @@ from pydantic import Field
from tradingagents.agents import schemas
from tradingagents.agents.analysts import sentiment_analyst
from tradingagents.agents.utils import agent_utils
from tradingagents.dataflows import interface, market_data_validator, y_finance
from tradingagents.dataflows import market_data_validator, router, y_finance
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.graph import trading_graph
@@ -94,7 +94,7 @@ class _Client:
def offline(monkeypatch, tmp_path):
"""Every vendor answers offline; returns the set of router methods called."""
called: set[str] = set()
for method, vendors in interface.VENDOR_METHODS.items():
for method, vendors in router.VENDOR_METHODS.items():
for vendor in vendors:
monkeypatch.setitem(vendors, vendor,
lambda *a, _m=method, **k: called.add(_m) or f"{_m} data")
+6 -6
View File
@@ -14,9 +14,9 @@ from unittest import mock
import pandas as pd
import pytest
from tradingagents.dataflows import interface, stockstats_utils
from tradingagents.dataflows import router, stockstats_utils
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.symbol_utils import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError
@pytest.mark.unit
@@ -54,9 +54,9 @@ class TestRouteToVendorSentinel(unittest.TestCase):
patched = {"yfinance": raises_no_data, "alpha_vantage": raises_no_data}
with mock.patch.dict(
interface.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
router.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
):
result = interface.route_to_vendor(
result = router.route_to_vendor(
"get_stock_data", "XAUUSD+", "2026-01-01", "2026-01-10"
)
self.assertIn("NO_DATA_AVAILABLE", result)
@@ -76,9 +76,9 @@ class TestRouteToVendorSentinel(unittest.TestCase):
patched = {"yfinance": raises_no_data, "alpha_vantage": raises_unavailable}
with mock.patch.dict(
interface.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
router.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
):
result = interface.route_to_vendor(
result = router.route_to_vendor(
"get_stock_data", "FAKE", "2026-01-01", "2026-01-10"
)
self.assertIn("NO_DATA_AVAILABLE", result)
+1 -1
View File
@@ -19,7 +19,7 @@ import pandas as pd
import pytest
from tradingagents.dataflows import stockstats_utils as su
from tradingagents.dataflows.symbol_utils import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError
def _stamp(path, ts):
+4 -4
View File
@@ -12,7 +12,7 @@ import requests
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import interface, polymarket
from tradingagents.dataflows import polymarket, router
from tradingagents.dataflows.config import set_config
@@ -112,16 +112,16 @@ class PolymarketRoutingTests(unittest.TestCase):
def test_category_routes_to_polymarket(self):
self.assertEqual(
interface.get_category_for_method("get_prediction_markets"),
router.get_category_for_method("get_prediction_markets"),
"prediction_markets",
)
set_config({"data_vendors": {"prediction_markets": "polymarket"}})
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_prediction_markets": {"polymarket": lambda *a, **k: "POLY_OK"}},
clear=False,
):
out = interface.route_to_vendor("get_prediction_markets", "fed", 5)
out = router.route_to_vendor("get_prediction_markets", "fed", 5)
self.assertEqual(out, "POLY_OK")
+1 -1
View File
@@ -5,7 +5,7 @@ import unittest
import pytest
from tradingagents.dataflows.utils import safe_ticker_component
from tradingagents.dataflows.symbols import safe_ticker_component
@pytest.mark.unit
@@ -4,11 +4,8 @@ import unittest
import pytest
from tradingagents.dataflows.symbol_utils import (
NoMarketDataError,
crypto_base,
normalize_symbol,
)
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.symbols import crypto_base, normalize_symbol
@pytest.mark.unit
+9 -6
View File
@@ -106,7 +106,7 @@ def test_a_historical_run_is_told_the_identity_is_current(monkeypatch):
@pytest.mark.unit
def test_a_current_run_is_not_cluttered_with_a_vintage_note(monkeypatch):
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.dataflows.utils import get_current_date
from tradingagents.dataflows.date_window import get_current_date
today = build_instrument_context("EXMP", "stock", {"company_name": "Example Corp"},
curr_date=get_current_date())
@@ -214,15 +214,15 @@ def test_an_unreachable_vendor_is_not_reported_as_a_missing_symbol(monkeypatch):
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 interface
from tradingagents.dataflows import router
from tradingagents.dataflows.errors import VendorRateLimitError
def _down(*a, **k):
raise VendorRateLimitError("Yahoo Finance is unreachable")
monkeypatch.setitem(interface.VENDOR_METHODS["get_balance_sheet"], "yfinance", _down)
monkeypatch.setitem(router.VENDOR_METHODS["get_balance_sheet"], "yfinance", _down)
out = interface.route_to_vendor("get_balance_sheet", "AAPL", "annual", "2026-09-01")
out = router.route_to_vendor("get_balance_sheet", "AAPL", "annual", "2026-09-01")
assert "unavailable" in out.lower() and "unreachable" in out.lower()
assert "delisted" not in out.lower() # not a claim about the symbol
@@ -283,8 +283,11 @@ def test_an_unavailable_notice_names_no_date_after_the_run():
"""A notice explaining why data is missing named where the vendor's coverage
starts or today's date, both after a historical run's date."""
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.dataflows.date_window import coverage_gap, withhold_live_profile
from tradingagents.dataflows.utils import get_current_date
from tradingagents.dataflows.date_window import (
coverage_gap,
get_current_date,
withhold_live_profile,
)
today = get_current_date()
notices = [
+7 -13
View File
@@ -10,7 +10,7 @@ import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import interface
from tradingagents.dataflows import router
from tradingagents.dataflows.alpha_vantage_common import (
AlphaVantageNotConfiguredError,
AlphaVantageRateLimitError,
@@ -42,12 +42,6 @@ class HierarchyTests(unittest.TestCase):
# ... and therefore still ValueErrors
self.assertTrue(issubclass(FredNotConfiguredError, ValueError))
def test_symbol_utils_reexports_no_market_data_error(self):
from tradingagents.dataflows.symbol_utils import (
NoMarketDataError as ReExported,
)
self.assertIs(ReExported, NoMarketDataError)
@pytest.mark.unit
class RouterHandlesBaseTypesTests(unittest.TestCase):
@@ -65,11 +59,11 @@ class RouterHandlesBaseTypesTests(unittest.TestCase):
raise AlphaVantageRateLimitError("slow down")
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _throttled, "yfinance": lambda *a, **k: "YF"}},
clear=False,
):
out = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
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):
@@ -79,11 +73,11 @@ class RouterHandlesBaseTypesTests(unittest.TestCase):
raise AlphaVantageNotConfiguredError("no key")
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _unconfigured, "yfinance": lambda *a, **k: "YF"}},
clear=False,
):
out = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
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):
@@ -94,11 +88,11 @@ class RouterHandlesBaseTypesTests(unittest.TestCase):
raise AlphaVantageNotConfiguredError("no key")
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _unconfigured}},
clear=False,
), self.assertRaises(AlphaVantageNotConfiguredError):
interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
if __name__ == "__main__":
+12 -12
View File
@@ -13,9 +13,9 @@ import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import interface
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.symbol_utils import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError
def _reset_config():
@@ -50,7 +50,7 @@ class VendorRoutingTests(unittest.TestCase):
def _route(self, vendors_for_get_stock_data):
return mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": vendors_for_get_stock_data},
clear=False,
)
@@ -60,7 +60,7 @@ class VendorRoutingTests(unittest.TestCase):
set_config({"data_vendors": {"core_stock_apis": "yfinance"}})
av = mock.Mock(side_effect=_returns("AV_DATA"))
with self._route({"yfinance": _no_data, "alpha_vantage": av}):
result = interface.route_to_vendor("get_stock_data", "FAKE", "2026-01-01", "2026-01-10")
result = router.route_to_vendor("get_stock_data", "FAKE", "2026-01-01", "2026-01-10")
self.assertIn("NO_DATA_AVAILABLE", result)
av.assert_not_called() # the unchosen vendor was never tried
@@ -68,7 +68,7 @@ class VendorRoutingTests(unittest.TestCase):
# Listing both vendors opts in to ordered fallback.
set_config({"data_vendors": {"core_stock_apis": "yfinance,alpha_vantage"}})
with self._route({"yfinance": _no_data, "alpha_vantage": _returns("AV_DATA")}):
result = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
result = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertEqual(result, "AV_DATA")
def test_primary_error_is_logged_not_masked(self):
@@ -76,8 +76,8 @@ class VendorRoutingTests(unittest.TestCase):
# must be visible in logs (broken primary not hidden).
set_config({"data_vendors": {"core_stock_apis": "yfinance,alpha_vantage"}})
with self._route({"yfinance": _raises(ValueError("boom")), "alpha_vantage": _no_data}), \
self.assertLogs("tradingagents.dataflows.interface", level="WARNING") as cm:
result = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertLogs("tradingagents.dataflows.router", level="WARNING") as cm:
result = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertIn("NO_DATA_AVAILABLE", result)
joined = "\n".join(cm.output)
self.assertIn("boom", joined) # the real error surfaced in logs
@@ -86,18 +86,18 @@ class VendorRoutingTests(unittest.TestCase):
def test_unknown_configured_vendor_raises(self):
set_config({"data_vendors": {"core_stock_apis": "bogus_vendor"}})
with self.assertRaises(ValueError) as ctx:
interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertIn("bogus_vendor", str(ctx.exception))
def test_default_sentinel_uses_all_vendors(self):
# No explicit choice ("default") keeps the resilient full-chain behavior.
set_config({"data_vendors": {"core_stock_apis": "default"}})
with self._route({"yfinance": _no_data, "alpha_vantage": _returns("AV_DATA")}):
result = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
result = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertEqual(result, "AV_DATA")
def _route_method(self, method, vendors):
return mock.patch.dict(interface.VENDOR_METHODS, {method: vendors}, clear=False)
return mock.patch.dict(router.VENDOR_METHODS, {method: vendors}, clear=False)
def test_optional_category_degrades_instead_of_raising(self):
# An optional enrichment vendor (FRED macro) that raises must NOT abort
@@ -106,7 +106,7 @@ class VendorRoutingTests(unittest.TestCase):
with self._route_method(
"get_macro_indicators", {"fred": _raises(ValueError("FRED 400: bad series"))}
):
result = interface.route_to_vendor("get_macro_indicators", "cpi", "2026-01-01")
result = router.route_to_vendor("get_macro_indicators", "cpi", "2026-01-01")
self.assertIn("DATA_UNAVAILABLE", result)
self.assertIn("macro_data", result)
@@ -116,7 +116,7 @@ class VendorRoutingTests(unittest.TestCase):
set_config({"data_vendors": {"core_stock_apis": "yfinance"}})
with self._route({"yfinance": _raises(ValueError("boom"))}), \
self.assertRaises(ValueError):
interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
if __name__ == "__main__":
+4 -4
View File
@@ -15,10 +15,10 @@ import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.dataflows.y_finance as y_finance
import tradingagents.default_config as default_config
from tradingagents.dataflows import interface
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.stockstats_utils import _assert_ohlcv_not_stale
from tradingagents.dataflows.symbol_utils import NoMarketDataError
def _frame(date):
@@ -98,11 +98,11 @@ class StaleGuardRoutingTests(unittest.TestCase):
)
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": {"yfinance": _stale}},
clear=False,
):
out = interface.route_to_vendor(
out = router.route_to_vendor(
"get_stock_data", "CB", "2026-06-01", "2026-06-11"
)
self.assertIn("NO_DATA_AVAILABLE", out)