11# coding: utf-8
22
33import csv
4+ from .exceptions import (
5+ BadRequestError ,
6+ InternalServerError ,
7+ InvalidApiKeyError ,
8+ TwelveDataError ,
9+ )
410from .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+
82192class 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
0 commit comments