Skip to content

Commit 3d53d27

Browse files
committed
add as_pandas() support to the all endpoints
1 parent 43d5062 commit 3d53d27

4 files changed

Lines changed: 177 additions & 25 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ docs/_build/*
4848
cover/*
4949
MANIFEST
5050
.claude/
51+
.env
5152

5253
# Per-project virtualenvs
5354
.venv*/

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,14 @@ With batch requests up to 120 symbols might be returned per single API call. The
171171
```python
172172
# 1. Pass instruments symbols as a string delimited by comma (,)
173173
ts = td.time_series(
174-
symbol="V, RY, AUD/CAD, BTC/USD:Huobi"
174+
symbol="V, RY, AUD/CAD, BTC/USD",
175+
interval="1day",
175176
)
176177

177178
# 2. Pass as a list of symbols
178179
ts = td.time_series(
179-
symbol=["V", "RY", "AUD/CAD", "BTC/USD:Huobi"]
180+
symbol=["V", "RY", "AUD/CAD", "BTC/USD"],
181+
interval="1day",
180182
)
181183
```
182184

src/twelvedata/mixins.py

Lines changed: 171 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# coding: utf-8
22

33
import csv
4+
from .exceptions import (
5+
BadRequestError,
6+
InternalServerError,
7+
InvalidApiKeyError,
8+
TwelveDataError,
9+
)
410
from .utils import convert_collection_to_pandas, convert_collection_to_pandas_multi_index, convert_pandas_to_plotly
511

612

@@ -50,15 +56,16 @@ def as_json(self):
5056
json = resp.json()
5157
if hasattr(self, 'is_batch') and self.is_batch:
5258
return json
53-
if isinstance(json, dict) and json.get("status") == "ok":
54-
if 'result' in json and isinstance(json['result'], dict) and 'list' in json['result'] \
55-
and isinstance(json['result']['list'], list):
56-
return json['result']['list']
57-
for key in self._JSON_PAYLOAD_KEYS:
58-
value = json.get(key)
59-
if value:
60-
return value
61-
return []
59+
if not isinstance(json, dict):
60+
return json
61+
if json.get("status") == "error":
62+
return json
63+
if 'result' in json and isinstance(json['result'], dict) and 'list' in json['result'] \
64+
and isinstance(json['result']['list'], list):
65+
return json['result']['list']
66+
for key in self._JSON_PAYLOAD_KEYS:
67+
if key in json:
68+
return json[key]
6269
return json
6370

6471
def as_raw_json(self):
@@ -79,28 +86,170 @@ def as_raw_csv(self):
7986
return resp.text
8087

8188

89+
# Strategy -> (kind, index_field). kind in {"datetime", "date", "grouped", "flat", "single"}.
90+
# Endpoints not listed and without `is_indicator` fall through to ("flat", None) — a plain
91+
# DataFrame with no forced index. Technical indicators are routed to the "datetime" strategy
92+
# via the `is_indicator` flag set in utils.patch_endpoints_meta.
93+
_PANDAS_STRATEGIES = {
94+
# explicit anchors for the datetime path (behavior unchanged)
95+
"time_series": ("datetime", "datetime"),
96+
"time_series_cross": ("datetime", "datetime"),
97+
"quote": ("datetime", "datetime"),
98+
"eod": ("datetime", "datetime"),
99+
"exchange_rate": ("single", None),
100+
"currency_conversion": ("single", None),
101+
"earliest_timestamp": ("datetime", "datetime"),
102+
"press_releases": ("datetime", "datetime"),
103+
"edgar_filings_archive": ("date", "filed_at"),
104+
105+
# date-indexed lists of records
106+
"splits": ("date", "date"),
107+
"dividends": ("date", "ex_date"),
108+
"earnings": ("date", "date"),
109+
"splits_calendar": ("date", "date"),
110+
"dividends_calendar": ("date", "ex_date"),
111+
"balance_sheet": ("date", "fiscal_date"),
112+
"balance_sheet_consolidated": ("date", "fiscal_date"),
113+
"cash_flow": ("date", "fiscal_date"),
114+
"cash_flow_consolidated": ("date", "fiscal_date"),
115+
"income_statement": ("date", "fiscal_date"),
116+
"income_statement_consolidated": ("date", "fiscal_date"),
117+
"earnings_estimate": ("date", "date"),
118+
"revenue_estimate": ("date", "date"),
119+
"eps_trend": ("date", "date"),
120+
"eps_revisions": ("date", "date"),
121+
"market_cap": ("date", "date"),
122+
"analyst_ratings_light": ("date", "date"),
123+
"analyst_ratings_us_equities": ("date", "date"),
124+
"insider_transactions": ("date", "date_reported"),
125+
"institutional_holders": ("date", "date_reported"),
126+
"fund_holders": ("date", "date_reported"),
127+
"direct_holders": ("date", "date_reported"),
128+
129+
# dict[date_str -> list[record]] — flatten then date-index
130+
"earnings_calendar": ("grouped", "date"),
131+
"ipo_calendar": ("grouped", "date"),
132+
133+
# list of records, no temporal key
134+
"stocks": ("flat", None),
135+
"stock_exchanges": ("flat", None),
136+
"forex_pairs": ("flat", None),
137+
"cryptocurrencies": ("flat", None),
138+
"etfs": ("flat", None),
139+
"indices": ("flat", None),
140+
"funds": ("flat", None),
141+
"bonds": ("flat", None),
142+
"commodities": ("flat", None),
143+
"exchanges": ("flat", None),
144+
"cryptocurrency_exchanges": ("flat", None),
145+
"exchange_schedule": ("flat", None),
146+
"countries": ("flat", None),
147+
"cross_listings": ("flat", None),
148+
"intervals": ("flat", None),
149+
"instrument_type": ("flat", None),
150+
"symbol_search": ("flat", None),
151+
"technical_indicators": ("flat", None),
152+
"market_state": ("flat", None),
153+
"market_movers_market": ("flat", None),
154+
"last_change_endpoint": ("flat", None),
155+
"sanctions_source": ("flat", None),
156+
"etfs_list": ("flat", None),
157+
"etfs_type": ("flat", None),
158+
"etfs_family": ("flat", None),
159+
"mutual_funds_list": ("flat", None),
160+
"mutual_funds_type": ("flat", None),
161+
"mutual_funds_family": ("flat", None),
162+
"key_executives": ("flat", None),
163+
"options_expiration": ("flat", None),
164+
"options_chain": ("flat", None),
165+
166+
# single-record / metadata payload — wrap in 1-row DataFrame
167+
"profile": ("single", None),
168+
"statistics": ("single", None),
169+
"logo": ("single", None),
170+
"tax_info": ("single", None),
171+
"recommendations": ("single", None),
172+
"price_target": ("single", None),
173+
"growth_estimates": ("single", None),
174+
"price": ("single", None),
175+
"api_usage": ("single", None),
176+
"etfs_world": ("single", None),
177+
"etfs_world_summary": ("single", None),
178+
"etfs_world_composition": ("single", None),
179+
"etfs_world_performance": ("single", None),
180+
"etfs_world_risk": ("single", None),
181+
"mutual_funds_world": ("single", None),
182+
"mutual_funds_world_summary": ("single", None),
183+
"mutual_funds_world_composition": ("single", None),
184+
"mutual_funds_world_performance": ("single", None),
185+
"mutual_funds_world_risk": ("single", None),
186+
"mutual_funds_world_ratings": ("single", None),
187+
"mutual_funds_world_purchase_info": ("single", None),
188+
"mutual_funds_world_sustainability": ("single", None),
189+
}
190+
191+
82192
class AsPandasMixin(object):
83193
def as_pandas(self, **kwargs):
84194
import pandas as pd
85195

86196
assert hasattr(self, "as_json")
87197

88198
data = self.as_json()
89-
if hasattr(self, "is_batch") and self.is_batch:
90-
df = convert_collection_to_pandas_multi_index(data)
91-
elif hasattr(self, "method") and self.method == "earnings":
92-
df = self.create_basic_df(data, pd, index_column="date", **kwargs)
93-
elif hasattr(self, "method") and self.method == "earnings_calendar":
94-
modified_data = []
95-
for date, row in data.items():
96-
for earning in row:
97-
earning["date"] = date
98-
modified_data.append(earning)
99-
100-
df = self.create_basic_df(modified_data, pd, index_column="date", **kwargs)
199+
200+
if isinstance(data, dict) and data.get("status") == "error":
201+
code = data.get("code")
202+
message = data.get("message", "API error")
203+
if code == 401:
204+
raise InvalidApiKeyError(message)
205+
if code == 400:
206+
raise BadRequestError(message)
207+
if isinstance(code, int) and code >= 500:
208+
raise InternalServerError(message)
209+
raise TwelveDataError(message)
210+
211+
if getattr(self, "is_batch", False):
212+
return convert_collection_to_pandas_multi_index(data)
213+
214+
if not data:
215+
return pd.DataFrame()
216+
217+
if getattr(self, "is_indicator", False):
218+
kind, index_field = ("datetime", "datetime")
101219
else:
102-
df = self.create_basic_df(data, pd, **kwargs)
220+
kind, index_field = _PANDAS_STRATEGIES.get(
221+
getattr(self, "_name", "") or "", ("flat", None)
222+
)
103223

224+
if kind == "datetime":
225+
return self.create_basic_df(data, pd, index_column="datetime", **kwargs)
226+
if kind == "date":
227+
return self.create_basic_df(data, pd, index_column=index_field, **kwargs)
228+
if kind == "grouped":
229+
rows = []
230+
for key, items in (data or {}).items():
231+
for item in items:
232+
item[index_field] = key
233+
rows.append(item)
234+
return self.create_basic_df(rows, pd, index_column=index_field, **kwargs)
235+
if kind == "single":
236+
if isinstance(data, list):
237+
rows = data
238+
elif isinstance(data, dict):
239+
rows = [data]
240+
else:
241+
rows = [{"value": data}]
242+
return pd.DataFrame(rows)
243+
244+
# "flat" (default): list of records, no temporal index
245+
if isinstance(data, list) and data and not isinstance(data[0], dict):
246+
return pd.DataFrame({"value": data})
247+
df = convert_collection_to_pandas(data, **kwargs)
248+
for col in df.columns:
249+
try:
250+
df[col] = pd.to_numeric(df[col])
251+
except (ValueError, TypeError):
252+
pass
104253
return df
105254

106255
@staticmethod

src/twelvedata/renders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def render_plotly(self, ctx, df, **kwargs):
6060

6161
if self.volume and self.volume in df:
6262
volume_colors = [
63-
COLOR_UP if i != 0 and df[closes][i] > df[opens][i] else COLOR_DOWN
63+
COLOR_UP if i != 0 and df[closes].iloc[i] > df[opens].iloc[i] else COLOR_DOWN
6464
for i in range(len(df[closes]))
6565
]
6666

0 commit comments

Comments
 (0)