mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 11:15:24 +03:00
fix(dataflows): keep vendor API keys out of request errors
- FRED and Alpha Vantage authenticate with a query parameter, and requests quotes the full URL in HTTP, connection and timeout errors, so the key was written into any log or traceback that recorded one - route both vendors through one request helper that re-raises a requests error as the same class with the key removed, carrying no request, response or exception chain, each of which still held the URL #1324
This commit is contained in:
@@ -13,9 +13,12 @@ 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
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
status_code = 200
|
||||
|
||||
def __init__(self, text):
|
||||
self.text = text
|
||||
|
||||
@@ -34,7 +37,7 @@ def _patched_get(body, capture=None):
|
||||
@pytest.mark.unit
|
||||
def test_request_passes_timeout(monkeypatch):
|
||||
captured = {}
|
||||
monkeypatch.setattr(av.requests, "get", _patched_get("Date,Close\n2025-01-02,1.0", captured))
|
||||
monkeypatch.setattr(utils.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
|
||||
|
||||
@@ -42,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(av.requests, "get", _patched_get(body))
|
||||
monkeypatch.setattr(utils.requests, "get", _patched_get(body))
|
||||
with pytest.raises(av.AlphaVantageRateLimitError):
|
||||
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
|
||||
|
||||
@@ -53,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(av.requests, "get", _patched_get(body))
|
||||
monkeypatch.setattr(utils.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(av.requests, "get", _patched_get('{"Note": "API call frequency is 5 calls per minute."}'))
|
||||
monkeypatch.setattr(utils.requests, "get", _patched_get('{"Note": "API call frequency is 5 calls per minute."}'))
|
||||
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
|
||||
|
||||
|
||||
@@ -133,3 +136,18 @@ def test_unparseable_body_is_never_served_untrimmed(monkeypatch):
|
||||
def test_empty_body_still_passes_through(monkeypatch):
|
||||
monkeypatch.setattr(avs, "_make_api_request", lambda *a, **k: "")
|
||||
assert avs.get_stock("IBM", "2024-05-09", "2024-05-10") == ""
|
||||
|
||||
|
||||
def test_request_error_message_carries_no_key(monkeypatch):
|
||||
# Alpha Vantage also sends its key in the URL (#1324).
|
||||
import requests
|
||||
key = "AVKEY1234567890XYZ"
|
||||
monkeypatch.setenv("ALPHA_VANTAGE_API_KEY", key)
|
||||
|
||||
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)
|
||||
with pytest.raises(requests.Timeout) as caught:
|
||||
av._make_api_request("OVERVIEW", {"symbol": "IBM"})
|
||||
assert key not in str(caught.value)
|
||||
|
||||
@@ -8,6 +8,7 @@ import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
import tradingagents.dataflows.config as config_module
|
||||
import tradingagents.default_config as default_config
|
||||
@@ -233,3 +234,53 @@ class FredRoutingTests(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
_KEY = "abcdef0123456789abcdef0123456789"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestKeyKeptOutOfErrors:
|
||||
"""The key travels as a query parameter, and requests quotes the full URL in
|
||||
its error messages, so any log or traceback would carry it (#1324)."""
|
||||
|
||||
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), \
|
||||
pytest.raises(requests.RequestException) as caught:
|
||||
fred._request("series", {"series_id": "DGS10"})
|
||||
return caught.value
|
||||
|
||||
def test_http_error_message_carries_no_key(self):
|
||||
response = mock.Mock(status_code=502)
|
||||
response.raise_for_status.side_effect = requests.HTTPError(
|
||||
f"502 Server Error for url: https://api.stlouisfed.org/fred/series?api_key={_KEY}",
|
||||
response=response,
|
||||
)
|
||||
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
|
||||
mock.patch("tradingagents.dataflows.utils.requests.get", return_value=response), \
|
||||
pytest.raises(requests.HTTPError) as caught:
|
||||
fred._request("series", {"series_id": "DGS10"})
|
||||
exc = caught.value
|
||||
assert _KEY not in str(exc) and _KEY not in repr(exc)
|
||||
# The response and request carry the full URL, so they are not attached.
|
||||
assert exc.response is None and exc.request is None
|
||||
assert exc.__cause__ is None and exc.__context__ is None # no chain holds the key
|
||||
|
||||
def test_connection_error_before_any_response_carries_no_key(self):
|
||||
exc = self._raises(requests.ConnectionError(
|
||||
f"Max retries exceeded with url: /fred/series?series_id=DGS10&api_key={_KEY}"))
|
||||
assert isinstance(exc, requests.ConnectionError)
|
||||
assert _KEY not in str(exc) and exc.__context__ is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_error_without_the_key_in_its_message_still_drops_the_request():
|
||||
# Some timeout messages omit the URL, but the attached request still has it.
|
||||
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)), \
|
||||
pytest.raises(rq.Timeout) as caught:
|
||||
fred._request("series", {"series_id": "DGS10"})
|
||||
assert caught.value.request is None
|
||||
|
||||
Reference in New Issue
Block a user