From 16360c00fe0fd61693b7bdef6229021a3f1d8b99 Mon Sep 17 00:00:00 2001 From: alexpipipi Date: Tue, 16 Jun 2026 14:28:35 -0400 Subject: [PATCH 1/2] fix: handle free-tier 'warning' column in get_historical_data (#66) A free API key requesting more than one year of historical data receives an extra 'warning' column in the response. get_historical_data relabels columns by fixed position, so the extra column caused a confusing pandas error: "ValueError: Length mismatch: Expected axis has 9 elements, new values have 8". Added _strip_free_tier_warning(): it surfaces the API warning message via the console and drops the 'warning' column before relabelling, so the call returns the available (one-year) data with a clear log message instead of crashing. Applied to both the EOD and intraday branches. Added regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- eodhd/apiclient.py | 14 +++++ tests/test_historical_free_tier_warning.py | 65 ++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 tests/test_historical_free_tier_warning.py diff --git a/eodhd/apiclient.py b/eodhd/apiclient.py index bece975..0060157 100644 --- a/eodhd/apiclient.py +++ b/eodhd/apiclient.py @@ -186,6 +186,18 @@ def _rest_get(self, endpoint: str = "", uri: str = "", querystring: str = "") -> else: return pd.DataFrame(json_data, index=[0]) + def _strip_free_tier_warning(self, df_data: pd.DataFrame) -> pd.DataFrame: + """Free API keys append a 'warning' column (e.g. data limited to one year) + to historical responses. Surface the message and drop the column so the + downstream fixed-width column relabelling does not raise a confusing + "Length mismatch" pandas error (see issue #66).""" + if "warning" in df_data.columns: + messages = df_data["warning"].dropna() + if not messages.empty: + self.console.log("EODHD API warning:", messages.iloc[-1]) + df_data = df_data.drop(columns=["warning"]) + return df_data + def get_exchanges(self) -> pd.DataFrame: """Get supported exchanges""" @@ -313,6 +325,7 @@ def get_historical_data( sys.exit() df_data = self._rest_get("eod", symbol, f"&period={interval}&from={str(date_from)}&to={str(date_to)}") + df_data = self._strip_free_tier_warning(df_data) if len(df_data) == 0: columns_eod = [ @@ -424,6 +437,7 @@ def get_historical_data( sys.exit() df_data = self._rest_get("intraday", symbol, f"&interval={interval}&from={str(date_from)}&to={str(date_to)}") + df_data = self._strip_free_tier_warning(df_data) if len(df_data) == 0: columns_eod = [ diff --git a/tests/test_historical_free_tier_warning.py b/tests/test_historical_free_tier_warning.py new file mode 100644 index 0000000..03d5a23 --- /dev/null +++ b/tests/test_historical_free_tier_warning.py @@ -0,0 +1,65 @@ +"""Regression test for issue #66. + +A free API key requesting more than one year of historical data gets an extra +'warning' column appended to the response. The fixed-width column relabelling in +get_historical_data then raised a confusing pandas "Length mismatch" error. +The warning must be surfaced and the column dropped instead. +""" + +from unittest.mock import patch + +import pandas as pd +import pytest + +from eodhd.apiclient import APIClient + + +@pytest.fixture +def client(): + with patch("eodhd.apiclient.requests.Session"): + yield APIClient(api_key="demo1234567890123456") + + +def _free_tier_eod_frame(): + """Mimics the EOD response a free key returns for a >1y request: a trailing + 'warning' column that is NaN on every row except the last.""" + return pd.DataFrame( + { + "date": ["2025-04-22", "2025-04-23"], + "open": [14347.8896, 14404.0498], + "high": [14414.0996, 14645.2598], + "low": [14293.0303, 14403.5498], + "close": [14404.0498, 14549.4199], + "adjusted_close": [14404.0498, 14549.4199], + "volume": [0, 0], + "warning": [None, "Data is limited by one year as you have free subscription."], + } + ) + + +def test_free_tier_warning_does_not_raise(client): + with patch.object(client, "_rest_get", return_value=_free_tier_eod_frame()): + df = client.get_historical_data("BUKAC.INDX", interval="d", results=10000) + + # Previously raised: ValueError: Length mismatch: Expected axis has 9 elements... + assert "warning" not in df.columns + assert list(df.columns) == [ + "symbol", + "interval", + "open", + "high", + "low", + "close", + "adjusted_close", + "volume", + ] + assert len(df) == 2 + + +def test_free_tier_warning_is_logged(client): + with patch.object(client, "_rest_get", return_value=_free_tier_eod_frame()): + with patch.object(client.console, "log") as mock_log: + client.get_historical_data("BUKAC.INDX", interval="d", results=10000) + + logged = " ".join(str(a) for call in mock_log.call_args_list for a in call.args) + assert "warning" in logged.lower() From e1cbad0bf775ca2bf31ecd4cdedb5bb49134e532 Mon Sep 17 00:00:00 2001 From: alexpipipi Date: Thu, 18 Jun 2026 11:59:32 -0400 Subject: [PATCH 2/2] test: cover intraday branch + warning-only response for free-tier warning (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to PR #73 review (Codex GPT-5.4 + Gemini Pro): - guard _strip_free_tier_warning against a warning-only response (no market data): after dropping the column, an empty-of-columns frame is returned as 0 rows so the caller's len()==0 early-return handles it instead of crashing on a missing date/OHLCV column. - add intraday-branch regression test (interval=1h) — the fix touches both the EOD and intraday branches, only EOD was covered. - add warning-only-response test. Co-Authored-By: Claude Opus 4.8 (1M context) --- eodhd/apiclient.py | 6 +++ tests/test_historical_free_tier_warning.py | 57 ++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/eodhd/apiclient.py b/eodhd/apiclient.py index 0060157..996d151 100644 --- a/eodhd/apiclient.py +++ b/eodhd/apiclient.py @@ -196,6 +196,12 @@ def _strip_free_tier_warning(self, df_data: pd.DataFrame) -> pd.DataFrame: if not messages.empty: self.console.log("EODHD API warning:", messages.iloc[-1]) df_data = df_data.drop(columns=["warning"]) + # Degenerate case: the response carried only the 'warning' column and + # no market data. Dropping it leaves a column-less frame; return it + # empty (0 rows) so the caller's `len(df_data) == 0` guard handles it + # instead of crashing later on a missing date/OHLCV column (#66). + if df_data.shape[1] == 0: + return df_data.iloc[0:0] return df_data def get_exchanges(self) -> pd.DataFrame: diff --git a/tests/test_historical_free_tier_warning.py b/tests/test_historical_free_tier_warning.py index 03d5a23..800339c 100644 --- a/tests/test_historical_free_tier_warning.py +++ b/tests/test_historical_free_tier_warning.py @@ -63,3 +63,60 @@ def test_free_tier_warning_is_logged(client): logged = " ".join(str(a) for call in mock_log.call_args_list for a in call.args) assert "warning" in logged.lower() + + +def _free_tier_intraday_frame(): + """Mimics the intraday response a free key returns for an over-range request: + the same trailing 'warning' column, on the intraday column shape + (timestamp/gmtoffset/datetime/OHLC/volume). Column order matters — the client + relabels positionally after dropping 'datetime'.""" + return pd.DataFrame( + { + "timestamp": [1745312400, 1745316000], + "gmtoffset": [0, 0], + "datetime": ["2025-04-22 09:00:00", "2025-04-22 10:00:00"], + "open": [14347.8896, 14404.0498], + "high": [14414.0996, 14645.2598], + "low": [14293.0303, 14403.5498], + "close": [14404.0498, 14549.4199], + "volume": [0, 0], + "warning": [None, "Data is limited by one year as you have free subscription."], + } + ) + + +def test_free_tier_warning_intraday_does_not_raise(client): + """The fix is applied in BOTH the EOD and the intraday branch of + get_historical_data — guard the intraday branch too (#66).""" + with patch.object(client, "_rest_get", return_value=_free_tier_intraday_frame()): + df = client.get_historical_data("BUKAC.INDX", interval="1h", results=10000) + + assert "warning" not in df.columns + assert list(df.columns) == [ + "epoch", + "gmtoffset", + "symbol", + "interval", + "open", + "high", + "low", + "close", + "volume", + ] + assert len(df) == 2 + + +def _warning_only_frame(): + """Degenerate free-tier response: a single 'warning' row and no market data + at all. After dropping the column the frame must be treated as empty rather + than crashing on a missing date/OHLCV column.""" + return pd.DataFrame({"warning": ["Data is limited by one year as you have free subscription."]}) + + +def test_free_tier_warning_only_response_returns_empty(client): + with patch.object(client, "_rest_get", return_value=_warning_only_frame()): + df = client.get_historical_data("BUKAC.INDX", interval="d", results=10000) + + # No crash; empty result with the standard EOD column set. + assert "warning" not in df.columns + assert len(df) == 0