diff --git a/cli/main.py b/cli/main.py index 376017a63..99fde1aad 100644 --- a/cli/main.py +++ b/cli/main.py @@ -44,7 +44,7 @@ from cli.utils import ( ) from tradingagents.agents.utils.rating import is_review from tradingagents.backtest import iter_grid, run_backtest, summarize -from tradingagents.dataflows.utils import safe_ticker_component +from tradingagents.dataflows.symbols import safe_ticker_component from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.graph.analyst_execution import ( AnalystWallTimeTracker, diff --git a/cli/utils.py b/cli/utils.py index 1559379f0..03f63e6d5 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -71,7 +71,7 @@ def normalize_ticker_symbol(ticker: str) -> str: plain upper-case if the data layer is unavailable. """ try: - from tradingagents.dataflows.symbol_utils import normalize_symbol + from tradingagents.dataflows.symbols import normalize_symbol return normalize_symbol(ticker) except Exception: diff --git a/pyproject.toml b/pyproject.toml index 1b15d65b6..b906ed027 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,5 +78,5 @@ ignore = ["E501"] [tool.ruff.lint.isort] # Keep multiple aliased names from one module in a single combined import block -# (e.g. the vendor re-exports in interface.py) instead of one statement per name. +# (e.g. the vendor imports in router.py) instead of one statement per name. combine-as-imports = true diff --git a/tests/test_alpha_vantage_hardening.py b/tests/test_alpha_vantage_hardening.py index 3bc90164f..cc9aaf210 100644 --- a/tests/test_alpha_vantage_hardening.py +++ b/tests/test_alpha_vantage_hardening.py @@ -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) diff --git a/tests/test_cli_symbol_handling.py b/tests/test_cli_symbol_handling.py index 950468cdc..3c1f499e0 100644 --- a/tests/test_cli_symbol_handling.py +++ b/tests/test_cli_symbol_handling.py @@ -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 --- diff --git a/tests/test_dataflows_config.py b/tests/test_dataflows_config.py index 480793782..e4a8113c6 100644 --- a/tests/test_dataflows_config.py +++ b/tests/test_dataflows_config.py @@ -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: diff --git a/tests/test_fred.py b/tests/test_fred.py index a4ec29962..e393ac327 100644 --- a/tests/test_fred.py +++ b/tests/test_fred.py @@ -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 diff --git a/tests/test_fundamentals_lookahead.py b/tests/test_fundamentals_lookahead.py index 81757346e..bbd91f2ca 100644 --- a/tests/test_fundamentals_lookahead.py +++ b/tests/test_fundamentals_lookahead.py @@ -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}) diff --git a/tests/test_graph_end_to_end.py b/tests/test_graph_end_to_end.py index 865af0d73..003a692c4 100644 --- a/tests/test_graph_end_to_end.py +++ b/tests/test_graph_end_to_end.py @@ -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") diff --git a/tests/test_no_data_handling.py b/tests/test_no_data_handling.py index e4bc7845b..fe85a3373 100644 --- a/tests/test_no_data_handling.py +++ b/tests/test_no_data_handling.py @@ -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) diff --git a/tests/test_ohlcv_latest_bar.py b/tests/test_ohlcv_latest_bar.py index 1534fa36d..3975e06ba 100644 --- a/tests/test_ohlcv_latest_bar.py +++ b/tests/test_ohlcv_latest_bar.py @@ -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): diff --git a/tests/test_polymarket.py b/tests/test_polymarket.py index e1b185218..1db11dc9e 100644 --- a/tests/test_polymarket.py +++ b/tests/test_polymarket.py @@ -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") diff --git a/tests/test_safe_ticker_component.py b/tests/test_safe_ticker_component.py index 5af601725..947f9a87b 100644 --- a/tests/test_safe_ticker_component.py +++ b/tests/test_safe_ticker_component.py @@ -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 diff --git a/tests/test_symbol_utils.py b/tests/test_symbols.py similarity index 97% rename from tests/test_symbol_utils.py rename to tests/test_symbols.py index 2a6b9cb31..c27df3493 100644 --- a/tests/test_symbol_utils.py +++ b/tests/test_symbols.py @@ -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 diff --git a/tests/test_undated_tools_as_of.py b/tests/test_undated_tools_as_of.py index 742631b08..304c55682 100644 --- a/tests/test_undated_tools_as_of.py +++ b/tests/test_undated_tools_as_of.py @@ -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 = [ diff --git a/tests/test_vendor_errors.py b/tests/test_vendor_errors.py index 9df641e0d..7b31334ea 100644 --- a/tests/test_vendor_errors.py +++ b/tests/test_vendor_errors.py @@ -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__": diff --git a/tests/test_vendor_routing.py b/tests/test_vendor_routing.py index 757fc288b..85db0ed3f 100644 --- a/tests/test_vendor_routing.py +++ b/tests/test_vendor_routing.py @@ -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__": diff --git a/tests/test_yfinance_stale_ohlcv_guard.py b/tests/test_yfinance_stale_ohlcv_guard.py index 4ab425f2d..f1faaa3d4 100644 --- a/tests/test_yfinance_stale_ohlcv_guard.py +++ b/tests/test_yfinance_stale_ohlcv_guard.py @@ -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) diff --git a/tradingagents/agents/utils/agent_utils.py b/tradingagents/agents/utils/agent_utils.py index bb47097cc..04841a1eb 100644 --- a/tradingagents/agents/utils/agent_utils.py +++ b/tradingagents/agents/utils/agent_utils.py @@ -22,6 +22,7 @@ from tradingagents.agents.utils.news_data_tools import ( ) from tradingagents.agents.utils.prediction_markets_tools import get_prediction_markets from tradingagents.agents.utils.technical_indicators_tools import get_indicators +from tradingagents.dataflows.date_window import get_current_date from tradingagents.dataflows.y_finance import get_company_profile # Public surface: the data tools are imported here so agents and the graph @@ -48,8 +49,6 @@ __all__ = [ logger = logging.getLogger(__name__) -from tradingagents.dataflows.utils import get_current_date # noqa: E402 - def get_language_instruction() -> str: """Return a prompt instruction for the configured output language. diff --git a/tradingagents/agents/utils/core_stock_tools.py b/tradingagents/agents/utils/core_stock_tools.py index eb97d982c..16e04d830 100644 --- a/tradingagents/agents/utils/core_stock_tools.py +++ b/tradingagents/agents/utils/core_stock_tools.py @@ -4,7 +4,7 @@ from langchain_core.tools import tool from langgraph.prebuilt import InjectedState from tradingagents.dataflows.date_window import as_of_window -from tradingagents.dataflows.interface import route_to_vendor +from tradingagents.dataflows.router import route_to_vendor @tool diff --git a/tradingagents/agents/utils/fundamental_data_tools.py b/tradingagents/agents/utils/fundamental_data_tools.py index ffe76decc..96b15fcca 100644 --- a/tradingagents/agents/utils/fundamental_data_tools.py +++ b/tradingagents/agents/utils/fundamental_data_tools.py @@ -4,7 +4,7 @@ from langchain_core.tools import tool from langgraph.prebuilt import InjectedState from tradingagents.dataflows.date_window import as_of -from tradingagents.dataflows.interface import route_to_vendor +from tradingagents.dataflows.router import route_to_vendor @tool diff --git a/tradingagents/agents/utils/macro_data_tools.py b/tradingagents/agents/utils/macro_data_tools.py index f44809e6c..90c8bd08d 100644 --- a/tradingagents/agents/utils/macro_data_tools.py +++ b/tradingagents/agents/utils/macro_data_tools.py @@ -4,7 +4,7 @@ from langchain_core.tools import tool from langgraph.prebuilt import InjectedState from tradingagents.dataflows.date_window import as_of -from tradingagents.dataflows.interface import route_to_vendor +from tradingagents.dataflows.router import route_to_vendor @tool diff --git a/tradingagents/agents/utils/news_data_tools.py b/tradingagents/agents/utils/news_data_tools.py index 122240a5a..60ed7a4d7 100644 --- a/tradingagents/agents/utils/news_data_tools.py +++ b/tradingagents/agents/utils/news_data_tools.py @@ -4,7 +4,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.interface import route_to_vendor +from tradingagents.dataflows.router import route_to_vendor @tool diff --git a/tradingagents/agents/utils/prediction_markets_tools.py b/tradingagents/agents/utils/prediction_markets_tools.py index dfa50f9b9..b127582d8 100644 --- a/tradingagents/agents/utils/prediction_markets_tools.py +++ b/tradingagents/agents/utils/prediction_markets_tools.py @@ -3,7 +3,7 @@ from typing import Annotated from langchain_core.tools import tool from langgraph.prebuilt import InjectedState -from tradingagents.dataflows.interface import route_to_vendor +from tradingagents.dataflows.router import route_to_vendor @tool diff --git a/tradingagents/agents/utils/technical_indicators_tools.py b/tradingagents/agents/utils/technical_indicators_tools.py index c6baecf37..cc97c13ad 100644 --- a/tradingagents/agents/utils/technical_indicators_tools.py +++ b/tradingagents/agents/utils/technical_indicators_tools.py @@ -4,7 +4,7 @@ from langchain_core.tools import tool from langgraph.prebuilt import InjectedState from tradingagents.dataflows.date_window import as_of -from tradingagents.dataflows.interface import route_to_vendor +from tradingagents.dataflows.router import route_to_vendor @tool diff --git a/tradingagents/backtest.py b/tradingagents/backtest.py index 99d6874a4..5a7afef4d 100644 --- a/tradingagents/backtest.py +++ b/tradingagents/backtest.py @@ -23,7 +23,8 @@ from pathlib import Path from tradingagents.agents.utils.memory import TradingMemoryLog from tradingagents.agents.utils.rating import RATING_REVIEW -from tradingagents.dataflows.utils import get_current_date, safe_ticker_component +from tradingagents.dataflows.date_window import get_current_date +from tradingagents.dataflows.symbols import safe_ticker_component from tradingagents.graph.trading_graph import TradingAgentsGraph logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/alpha_vantage.py b/tradingagents/dataflows/alpha_vantage.py index 90032a72b..68ead2f8a 100644 --- a/tradingagents/dataflows/alpha_vantage.py +++ b/tradingagents/dataflows/alpha_vantage.py @@ -1,14 +1,18 @@ # Aggregates the per-category Alpha Vantage implementations into one module the # vendor router imports from; the imports below are the public surface. -from .alpha_vantage_fundamentals import ( +from tradingagents.dataflows.alpha_vantage_fundamentals import ( get_balance_sheet, get_cashflow, get_fundamentals, get_income_statement, ) -from .alpha_vantage_indicator import get_indicator -from .alpha_vantage_news import get_global_news, get_insider_transactions, get_news -from .alpha_vantage_stock import get_stock +from tradingagents.dataflows.alpha_vantage_indicator import get_indicator +from tradingagents.dataflows.alpha_vantage_news import ( + get_global_news, + get_insider_transactions, + get_news, +) +from tradingagents.dataflows.alpha_vantage_stock import get_stock __all__ = [ "get_balance_sheet", diff --git a/tradingagents/dataflows/alpha_vantage_common.py b/tradingagents/dataflows/alpha_vantage_common.py index 7f07af9b1..640cf7a24 100644 --- a/tradingagents/dataflows/alpha_vantage_common.py +++ b/tradingagents/dataflows/alpha_vantage_common.py @@ -5,8 +5,8 @@ from io import StringIO import pandas as pd -from .errors import VendorNotConfiguredError, VendorRateLimitError -from .utils import get_scrubbed +from tradingagents.dataflows.errors import VendorNotConfiguredError, VendorRateLimitError +from tradingagents.dataflows.net import get_scrubbed API_BASE_URL = "https://www.alphavantage.co/query" diff --git a/tradingagents/dataflows/alpha_vantage_fundamentals.py b/tradingagents/dataflows/alpha_vantage_fundamentals.py index 56525b3ff..e92d2c5fa 100644 --- a/tradingagents/dataflows/alpha_vantage_fundamentals.py +++ b/tradingagents/dataflows/alpha_vantage_fundamentals.py @@ -1,7 +1,7 @@ import json -from .alpha_vantage_common import _make_api_request -from .date_window import withhold_live_profile +from tradingagents.dataflows.alpha_vantage_common import _make_api_request +from tradingagents.dataflows.date_window import withhold_live_profile def _filter_reports_by_date(result, curr_date: str): diff --git a/tradingagents/dataflows/alpha_vantage_indicator.py b/tradingagents/dataflows/alpha_vantage_indicator.py index c53d91a5c..46e401576 100644 --- a/tradingagents/dataflows/alpha_vantage_indicator.py +++ b/tradingagents/dataflows/alpha_vantage_indicator.py @@ -1,7 +1,7 @@ import logging -from .alpha_vantage_common import _make_api_request -from .errors import NoMarketDataError, VendorError +from tradingagents.dataflows.alpha_vantage_common import _make_api_request +from tradingagents.dataflows.errors import NoMarketDataError, VendorError logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/alpha_vantage_news.py b/tradingagents/dataflows/alpha_vantage_news.py index e323e4900..4b446a15c 100644 --- a/tradingagents/dataflows/alpha_vantage_news.py +++ b/tradingagents/dataflows/alpha_vantage_news.py @@ -1,7 +1,7 @@ import json -from .alpha_vantage_common import _make_api_request, format_datetime_for_api -from .config import get_config +from tradingagents.dataflows.alpha_vantage_common import _make_api_request, format_datetime_for_api +from tradingagents.dataflows.config import get_config def get_news(ticker, start_date, end_date) -> dict[str, str] | str: diff --git a/tradingagents/dataflows/alpha_vantage_stock.py b/tradingagents/dataflows/alpha_vantage_stock.py index 43d693b9c..50b050911 100644 --- a/tradingagents/dataflows/alpha_vantage_stock.py +++ b/tradingagents/dataflows/alpha_vantage_stock.py @@ -1,6 +1,9 @@ from datetime import datetime -from .alpha_vantage_common import _filter_csv_by_date_range, _make_api_request +from tradingagents.dataflows.alpha_vantage_common import ( + _filter_csv_by_date_range, + _make_api_request, +) def get_stock( diff --git a/tradingagents/dataflows/date_window.py b/tradingagents/dataflows/date_window.py index 607a1cd46..d84e43887 100644 --- a/tradingagents/dataflows/date_window.py +++ b/tradingagents/dataflows/date_window.py @@ -11,9 +11,7 @@ in a backtest we can't prove it isn't future. from __future__ import annotations -from datetime import datetime, timedelta, timezone - -from .utils import get_current_date +from datetime import date, datetime, timedelta, timezone def to_utc(dt: datetime) -> datetime: @@ -32,6 +30,11 @@ def in_window(pub_dt: datetime | None, start_dt: datetime, end_dt: datetime) -> return end >= datetime.now(timezone.utc) - timedelta(days=1) +def get_current_date() -> str: + """Today's date, YYYY-MM-DD.""" + return date.today().strftime("%Y-%m-%d") + + def coverage_gap( dates, start_date: str, end_date: str, source: str, subject: str ) -> str | None: diff --git a/tradingagents/dataflows/fred.py b/tradingagents/dataflows/fred.py index 28312e434..611d8d919 100644 --- a/tradingagents/dataflows/fred.py +++ b/tradingagents/dataflows/fred.py @@ -14,8 +14,8 @@ from datetime import datetime, timedelta import pytz -from .errors import VendorNotConfiguredError -from .utils import get_scrubbed +from tradingagents.dataflows.errors import VendorNotConfiguredError +from tradingagents.dataflows.net import get_scrubbed logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/net.py b/tradingagents/dataflows/net.py new file mode 100644 index 000000000..4f39a73de --- /dev/null +++ b/tradingagents/dataflows/net.py @@ -0,0 +1,37 @@ +"""HTTP helpers shared by the vendors.""" + +import requests + + +def get_scrubbed(url: str, *, params: dict, timeout: float, secret: str, passthrough=()): + """``requests.get`` plus ``raise_for_status``, with ``secret`` kept out of errors. + + Vendors that authenticate with a query parameter put the key in the URL, and + requests quotes the full URL in HTTP, connection and timeout errors, so any + log or traceback that records one would carry the key (#1324). A requests + error is re-raised as the same class with the key replaced and nothing + attached: no request or response (both hold the URL) and no exception chain, + which is why this raises after the ``except`` block rather than inside it. + Statuses in ``passthrough`` are returned for the caller to handle. + """ + try: + response = requests.get(url, params=params, timeout=timeout) + if response.status_code not in passthrough: + response.raise_for_status() + return response + except requests.RequestException as exc: + error = type(exc)(str(exc).replace(secret, "***")) if secret else exc + raise error + + +def vendor_reachable(url: str, timeout: float = 5.0) -> bool: + """Whether the vendor answers at all, for telling silence from an outage. + + A client that returns an empty result instead of raising leaves those two + cases indistinguishable. Called only when a result is empty. + """ + try: + requests.head(url, timeout=timeout, allow_redirects=True) + return True + except requests.RequestException: + return False diff --git a/tradingagents/dataflows/polymarket.py b/tradingagents/dataflows/polymarket.py index 3188ac034..96a352b0b 100644 --- a/tradingagents/dataflows/polymarket.py +++ b/tradingagents/dataflows/polymarket.py @@ -15,7 +15,7 @@ from datetime import datetime, timezone import requests -from .utils import get_current_date +from tradingagents.dataflows.date_window import get_current_date logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/reddit.py b/tradingagents/dataflows/reddit.py index e01442853..bfb0e6fef 100644 --- a/tradingagents/dataflows/reddit.py +++ b/tradingagents/dataflows/reddit.py @@ -29,8 +29,8 @@ from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import Request, urlopen -from .date_window import coverage_gap, in_window -from .symbol_utils import crypto_base +from tradingagents.dataflows.date_window import coverage_gap, in_window +from tradingagents.dataflows.symbols import crypto_base logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/interface.py b/tradingagents/dataflows/router.py similarity index 95% rename from tradingagents/dataflows/interface.py rename to tradingagents/dataflows/router.py index 61fd74e1a..6b3deecd8 100644 --- a/tradingagents/dataflows/interface.py +++ b/tradingagents/dataflows/router.py @@ -1,6 +1,6 @@ import logging -from .alpha_vantage import ( +from tradingagents.dataflows.alpha_vantage import ( get_balance_sheet as get_alpha_vantage_balance_sheet, get_cashflow as get_alpha_vantage_cashflow, get_fundamentals as get_alpha_vantage_fundamentals, @@ -11,20 +11,22 @@ from .alpha_vantage import ( get_news as get_alpha_vantage_news, get_stock as get_alpha_vantage_stock, ) -from .config import get_config -from .errors import ( +from tradingagents.dataflows.config import get_config +from tradingagents.dataflows.errors import ( NoMarketDataError, VendorNotConfiguredError, VendorRateLimitError, ) -from .fred import get_macro_data as get_fred_macro_data -from .polymarket import get_prediction_markets as get_polymarket_prediction_markets -from .sec_edgar import ( +from tradingagents.dataflows.fred import get_macro_data as get_fred_macro_data +from tradingagents.dataflows.polymarket import ( + get_prediction_markets as get_polymarket_prediction_markets, +) +from tradingagents.dataflows.sec_edgar import ( get_balance_sheet as get_sec_edgar_balance_sheet, get_cashflow as get_sec_edgar_cashflow, get_income_statement as get_sec_edgar_income_statement, ) -from .y_finance import ( +from tradingagents.dataflows.y_finance import ( get_balance_sheet as get_yfinance_balance_sheet, get_cashflow as get_yfinance_cashflow, get_fundamentals as get_yfinance_fundamentals, @@ -33,7 +35,7 @@ from .y_finance import ( get_stock_stats_indicators_window, get_YFin_data_online, ) -from .yfinance_news import get_global_news_yfinance, get_news_yfinance +from tradingagents.dataflows.yfinance_news import get_global_news_yfinance, get_news_yfinance logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/sec_edgar.py b/tradingagents/dataflows/sec_edgar.py index 44161e0d5..e096748d4 100644 --- a/tradingagents/dataflows/sec_edgar.py +++ b/tradingagents/dataflows/sec_edgar.py @@ -27,8 +27,8 @@ from pathlib import Path import requests -from .config import get_config -from .errors import NoMarketDataError, VendorRateLimitError +from tradingagents.dataflows.config import get_config +from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/stockstats_utils.py b/tradingagents/dataflows/stockstats_utils.py index 5625f2e08..843b12575 100644 --- a/tradingagents/dataflows/stockstats_utils.py +++ b/tradingagents/dataflows/stockstats_utils.py @@ -8,10 +8,10 @@ import yfinance as yf from stockstats import wrap from yfinance.exceptions import YFRateLimitError -from .config import get_config -from .errors import VendorRateLimitError -from .symbol_utils import NoMarketDataError, normalize_symbol -from .utils import safe_ticker_component, vendor_reachable +from tradingagents.dataflows.config import get_config +from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError +from tradingagents.dataflows.net import vendor_reachable +from tradingagents.dataflows.symbols import normalize_symbol, safe_ticker_component logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/stocktwits.py b/tradingagents/dataflows/stocktwits.py index 49700c9ae..987a279d6 100644 --- a/tradingagents/dataflows/stocktwits.py +++ b/tradingagents/dataflows/stocktwits.py @@ -21,8 +21,8 @@ import logging from datetime import datetime from urllib.request import Request, urlopen -from .date_window import coverage_gap, in_window -from .symbol_utils import crypto_base +from tradingagents.dataflows.date_window import coverage_gap, in_window +from tradingagents.dataflows.symbols import crypto_base logger = logging.getLogger(__name__) diff --git a/tradingagents/dataflows/symbol_utils.py b/tradingagents/dataflows/symbols.py similarity index 76% rename from tradingagents/dataflows/symbol_utils.py rename to tradingagents/dataflows/symbols.py index bacababc0..c33ba5736 100644 --- a/tradingagents/dataflows/symbol_utils.py +++ b/tradingagents/dataflows/symbols.py @@ -1,4 +1,4 @@ -"""Symbol normalization and market-data error types for vendor calls. +"""Symbol normalization for vendor calls, and ticker values safe to use in a path. Yahoo Finance (the default vendor) uses specific ticker conventions that differ from the broker / TradingView / MT5 style symbols users often type: @@ -25,10 +25,6 @@ from __future__ import annotations import logging import re -# NoMarketDataError lives in the vendor-error taxonomy (errors.py); re-exported -# here for the many call sites that import it alongside normalize_symbol. -from .errors import NoMarketDataError as NoMarketDataError - logger = logging.getLogger(__name__) @@ -147,3 +143,38 @@ def normalize_symbol(raw: str) -> str: logger.info("Resolved symbol %r to Yahoo symbol %r", raw, canonical) return canonical + +# Tickers can contain letters, digits, dot, dash, underscore, caret +# (index symbols like ^GSPC), equals (futures like GC=F), and plus +# (forex/CFD symbols like XAUUSD+). None of these enable directory +# traversal, so the value never escapes a containing directory when +# interpolated into a path. Anything else is rejected. +_TICKER_PATH_RE = re.compile(r"^[A-Za-z0-9._\-\^=+]+$") + + +def safe_ticker_component(value: str, *, max_len: int = 32) -> str: + """Validate ``value`` is safe to interpolate into a filesystem path. + + Tickers come from user CLI input or from LLM tool calls, both of which + can be influenced by attacker-controlled content (e.g. prompt injection + embedded in fetched news). Without validation, a value like + ``"../../../etc/foo"`` flows into ``os.path.join`` / ``Path /`` and + escapes the configured cache, checkpoint, or results directory. + + Returns ``value`` unchanged when it matches the allowed pattern; raises + ``ValueError`` otherwise. + """ + if not isinstance(value, str) or not value: + raise ValueError(f"ticker must be a non-empty string, got {value!r}") + if len(value) > max_len: + raise ValueError(f"ticker exceeds {max_len} chars: {value!r}") + if not _TICKER_PATH_RE.fullmatch(value): + raise ValueError( + f"ticker contains characters not allowed in a filesystem path: {value!r}" + ) + # The regex above allows '.', so values like '.', '..', '...' would pass, + # and as a path component they traverse the parent directory. Reject any + # value that's only dots. + if set(value) == {"."}: + raise ValueError(f"ticker cannot consist solely of dots: {value!r}") + return value diff --git a/tradingagents/dataflows/utils.py b/tradingagents/dataflows/utils.py deleted file mode 100644 index 1bff0273c..000000000 --- a/tradingagents/dataflows/utils.py +++ /dev/null @@ -1,77 +0,0 @@ -import re -from datetime import date - -import requests - -# Tickers can contain letters, digits, dot, dash, underscore, caret -# (index symbols like ^GSPC), equals (futures like GC=F), and plus -# (forex/CFD symbols like XAUUSD+). None of these enable directory -# traversal, so the value never escapes a containing directory when -# interpolated into a path. Anything else is rejected. -_TICKER_PATH_RE = re.compile(r"^[A-Za-z0-9._\-\^=+]+$") - - -def safe_ticker_component(value: str, *, max_len: int = 32) -> str: - """Validate ``value`` is safe to interpolate into a filesystem path. - - Tickers come from user CLI input or from LLM tool calls, both of which - can be influenced by attacker-controlled content (e.g. prompt injection - embedded in fetched news). Without validation, a value like - ``"../../../etc/foo"`` flows into ``os.path.join`` / ``Path /`` and - escapes the configured cache, checkpoint, or results directory. - - Returns ``value`` unchanged when it matches the allowed pattern; raises - ``ValueError`` otherwise. - """ - if not isinstance(value, str) or not value: - raise ValueError(f"ticker must be a non-empty string, got {value!r}") - if len(value) > max_len: - raise ValueError(f"ticker exceeds {max_len} chars: {value!r}") - if not _TICKER_PATH_RE.fullmatch(value): - raise ValueError( - f"ticker contains characters not allowed in a filesystem path: {value!r}" - ) - # The regex above allows '.', so values like '.', '..', '...' would pass, - # and as a path component they traverse the parent directory. Reject any - # value that's only dots. - if set(value) == {"."}: - raise ValueError(f"ticker cannot consist solely of dots: {value!r}") - return value - - -def get_current_date(): - return date.today().strftime("%Y-%m-%d") - - -def get_scrubbed(url: str, *, params: dict, timeout: float, secret: str, passthrough=()): - """``requests.get`` plus ``raise_for_status``, with ``secret`` kept out of errors. - - Vendors that authenticate with a query parameter put the key in the URL, and - requests quotes the full URL in HTTP, connection and timeout errors, so any - log or traceback that records one would carry the key (#1324). A requests - error is re-raised as the same class with the key replaced and nothing - attached: no request or response (both hold the URL) and no exception chain, - which is why this raises after the ``except`` block rather than inside it. - Statuses in ``passthrough`` are returned for the caller to handle. - """ - try: - response = requests.get(url, params=params, timeout=timeout) - if response.status_code not in passthrough: - response.raise_for_status() - return response - except requests.RequestException as exc: - error = type(exc)(str(exc).replace(secret, "***")) if secret else exc - raise error - - -def vendor_reachable(url: str, timeout: float = 5.0) -> bool: - """Whether the vendor answers at all, for telling silence from an outage. - - A client that returns an empty result instead of raising leaves those two - cases indistinguishable. Called only when a result is empty. - """ - try: - requests.head(url, timeout=timeout, allow_redirects=True) - return True - except requests.RequestException: - return False diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index d326ffdf5..251eb9d7b 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -6,9 +6,10 @@ import pandas as pd import yfinance as yf from dateutil.relativedelta import relativedelta -from .date_window import withhold_live_profile -from .errors import VendorError, VendorRateLimitError -from .stockstats_utils import ( +from tradingagents.dataflows.date_window import withhold_live_profile +from tradingagents.dataflows.errors import NoMarketDataError, VendorError, VendorRateLimitError +from tradingagents.dataflows.net import vendor_reachable +from tradingagents.dataflows.stockstats_utils import ( StockstatsUtils, _assert_ohlcv_not_stale, filter_financials_by_date, @@ -16,8 +17,7 @@ from .stockstats_utils import ( raise_for_empty, yf_retry, ) -from .symbol_utils import NoMarketDataError, normalize_symbol -from .utils import vendor_reachable +from tradingagents.dataflows.symbols import normalize_symbol _YAHOO_HOST = "https://query2.finance.yahoo.com" diff --git a/tradingagents/dataflows/yfinance_news.py b/tradingagents/dataflows/yfinance_news.py index 3b2efce51..b47f29a07 100644 --- a/tradingagents/dataflows/yfinance_news.py +++ b/tradingagents/dataflows/yfinance_news.py @@ -6,11 +6,11 @@ from datetime import datetime, timezone import yfinance as yf from dateutil.relativedelta import relativedelta -from .config import get_config -from .date_window import coverage_gap, in_window -from .errors import NoMarketDataError -from .stockstats_utils import yf_retry -from .symbol_utils import normalize_symbol +from tradingagents.dataflows.config import get_config +from tradingagents.dataflows.date_window import coverage_gap, in_window +from tradingagents.dataflows.errors import NoMarketDataError +from tradingagents.dataflows.stockstats_utils import yf_retry +from tradingagents.dataflows.symbols import normalize_symbol def _extract_article_data(article: dict) -> dict: diff --git a/tradingagents/graph/checkpointer.py b/tradingagents/graph/checkpointer.py index abe5cea87..7fc994dc4 100644 --- a/tradingagents/graph/checkpointer.py +++ b/tradingagents/graph/checkpointer.py @@ -13,7 +13,7 @@ from pathlib import Path from langgraph.checkpoint.sqlite import SqliteSaver -from tradingagents.dataflows.utils import safe_ticker_component +from tradingagents.dataflows.symbols import safe_ticker_component def _db_path(data_dir: str | Path, ticker: str) -> Path: diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 64c7099af..ba185be3d 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -15,7 +15,8 @@ from tradingagents.agents.utils.agent_utils import ( from tradingagents.agents.utils.memory import TradingMemoryLog from tradingagents.agents.utils.rating import parse_rating from tradingagents.dataflows.config import run_config, set_config -from tradingagents.dataflows.utils import get_current_date, safe_ticker_component +from tradingagents.dataflows.date_window import get_current_date +from tradingagents.dataflows.symbols import safe_ticker_component from tradingagents.dataflows.y_finance import get_closes from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.llm_clients import create_llm_client @@ -207,7 +208,7 @@ class TradingAgentsGraph: entry, which is the right default because the alpha calculation works in USD. """ - from tradingagents.dataflows.symbol_utils import normalize_symbol + from tradingagents.dataflows.symbols import normalize_symbol explicit = self.config.get("benchmark_ticker") if explicit: