Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/ml4t/data/providers/fxmacrodata.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,14 +593,17 @@ def fetch_endpoint(

def _request_json(self, path: str, params: dict[str, Any]) -> Any:
clean_params = {key: value for key, value in params.items() if value is not None}
if self.api_key:
clean_params["api_key"] = self.api_key
# Send the key as a header rather than a query parameter. FXMacroData
# accepts both, but documents the header as the preferred transport for
# server-side clients: a key in the query string is recorded verbatim by
# proxies, CDNs and server access logs along the whole request path.
headers = {"X-API-Key": self.api_key} if self.api_key else None

url = f"{self.base_url}/{path.lstrip('/')}"
self._acquire_rate_limit()

try:
response = self.session.get(url, params=clean_params)
response = self.session.get(url, params=clean_params, headers=headers)
except httpx.RequestError as err:
raise NetworkError(provider=self.name, message=f"Request failed: {err}") from err

Expand Down
29 changes: 28 additions & 1 deletion tests/test_fxmacrodata_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,39 @@ def test_fetch_announcements_preserves_key_fields_and_metadata(self, provider):

url = get.call_args.args[0]
params = get.call_args.kwargs["params"]
headers = get.call_args.kwargs["headers"]
assert url == "https://api.fxmacrodata.com/v1/announcements/usd/inflation"
assert params["api_key"] == "test_key"
assert headers["X-API-Key"] == "test_key"
assert "api_key" not in params
assert params["start_date"] == "2025-01-01"
assert params["limit"] == 1
assert "end_date" not in params

def test_api_key_is_never_sent_as_a_query_parameter(self, provider):
response = _response(payload=ANNOUNCEMENT_PAYLOAD)

with patch.object(provider.session, "get", return_value=response) as get:
provider.fetch_announcements("USD", "inflation", limit=1)

params = get.call_args.kwargs["params"]
url = get.call_args.args[0]
assert "test_key" not in url
assert not any(value == "test_key" for value in params.values())

def test_no_auth_header_is_sent_without_a_key(self):
# clear=True matches the init tests: without it an ambient
# FXMACRODATA_API_KEY/FXMD_API_KEY in the developer's environment would
# be picked up and the assertion would pass for the wrong reason.
with patch.dict("os.environ", {}, clear=True):
provider = FXMacroDataProvider(rate_limit=(1000, 1.0))
try:
response = _response(payload=ANNOUNCEMENT_PAYLOAD)
with patch.object(provider.session, "get", return_value=response) as get:
provider.fetch_announcements("USD", "inflation", limit=1)
assert get.call_args.kwargs["headers"] is None
finally:
provider.close()

def test_fetch_catalogue_reshapes_indicator_mapping(self, provider):
payload = {
"gdp": {
Expand Down