diff --git a/yahooquery/base.py b/yahooquery/base.py index de66d30..6cc4da6 100644 --- a/yahooquery/base.py +++ b/yahooquery/base.py @@ -328,3 +328,59 @@ def _construct_data(self, json, response_field, **kwargs): except TypeError: data = json return data + + def _normalize_quote_summary_response(self, data, module_name="quoteSummary"): + """ + Normalize quoteSummary response to ensure consistent structure across all modules. + + Many quoteSummary endpoints can return either: + 1. A dictionary with nested module data (normal case) + 2. A string error message when no data is found (error case) + + This method ensures that error cases return a consistent structure + instead of a plain string, making it easier for consumers to handle + the response predictably across all quoteSummary-based properties. + + Parameters + ---------- + data : dict + Raw response from _quote_summary + module_name : str, optional + Name of the module being normalized (for logging/debugging) + + Returns + ------- + dict + Normalized response where each symbol maps to either: + - A dict with module data (success case) + - A dict with error information (error case): + { + "error": { + "code": 404, + "type": "NotFoundError", + "message": "No fundamentals data found for symbol: EAI", + "symbol": "EAI" + } + } + """ + if not isinstance(data, dict): + return data + + normalized_data = {} + for symbol, module_data in data.items(): + + if isinstance(module_data, str): + # Convert string error messages to a consistent error structure + normalized_data[symbol] = { + "error": { + "code": 404, + "type": "NotFoundError", + "message": module_data, + "symbol": symbol + } + } + else: + # Keep successful responses as-is + normalized_data[symbol] = module_data + + return normalized_data diff --git a/yahooquery/constants.py b/yahooquery/constants.py index d99eb5a..b9d64ea 100644 --- a/yahooquery/constants.py +++ b/yahooquery/constants.py @@ -587,15 +587,15 @@ "UnrealizedGainLossOnInvestmentSecurities", ], "valuation": [ + "MarketCap", + "EnterpriseValue", + "PeRatio", "ForwardPeRatio", + "PegRatio", "PsRatio", "PbRatio", - "EnterprisesValueEBITDARatio", "EnterprisesValueRevenueRatio", - "PeRatio", - "MarketCap", - "EnterpriseValue", - "PegRatio", + "EnterprisesValueEBITDARatio", ], } diff --git a/yahooquery/ticker.py b/yahooquery/ticker.py index 1c45246..fa18d36 100644 --- a/yahooquery/ticker.py +++ b/yahooquery/ticker.py @@ -195,7 +195,8 @@ def get_modules(self, modules): One of {} is not a valid value. Valid values are {}. """.format(", ".join(modules), ", ".join(all_modules)) ) - return self._quote_summary(modules) + data = self._quote_summary(modules) + return self._normalize_quote_summary_response(data, "quoteSummary") @property def asset_profile(self): @@ -208,7 +209,8 @@ def asset_profile(self): dict assetProfile module data """ - return self._quote_summary(["assetProfile"]) + data = self._quote_summary(["assetProfile"]) + return self._normalize_quote_summary_response(data, "asset_profile") @property def calendar_events(self): @@ -222,7 +224,8 @@ def calendar_events(self): dict calendarEvents module data """ - return self._quote_summary(["calendarEvents"]) + data = self._quote_summary(["calendarEvents"]) + return self._normalize_quote_summary_response(data, "calendar_events") @property def earnings(self): @@ -235,7 +238,8 @@ def earnings(self): dict earnings module data """ - return self._quote_summary(["earnings"]) + data = self._quote_summary(["earnings"]) + return self._normalize_quote_summary_response(data, "earnings") @property def earnings_trend(self): @@ -249,7 +253,8 @@ def earnings_trend(self): dict earningsTrend module data """ - return self._quote_summary(["earningsTrend"]) + data = self._quote_summary(["earningsTrend"]) + return self._normalize_quote_summary_response(data, "earnings_trend") @property def esg_scores(self): @@ -263,7 +268,8 @@ def esg_scores(self): dict esgScores module data """ - return self._quote_summary(["esgScores"]) + data = self._quote_summary(["esgScores"]) + return self._normalize_quote_summary_response(data, "esg_scores") @property def financial_data(self): @@ -276,7 +282,8 @@ def financial_data(self): dict financialData module data """ - return self._quote_summary(["financialData"]) + data = self._quote_summary(["financialData"]) + return self._normalize_quote_summary_response(data, "financial_data") def news(self, count=25, start=None): """News articles related to given symbol(s) @@ -320,7 +327,8 @@ def index_trend(self): dict indexTrend module data """ - return self._quote_summary(["indexTrend"]) + data = self._quote_summary(["indexTrend"]) + return self._normalize_quote_summary_response(data, "index_trend") @property def industry_trend(self): @@ -333,7 +341,8 @@ def industry_trend(self): dict industryTrend module data """ - return self._quote_summary(["industryTrend"]) + data = self._quote_summary(["industryTrend"]) + return self._normalize_quote_summary_response(data, "industry_trend") @property def key_stats(self): @@ -346,7 +355,8 @@ def key_stats(self): dict defaultKeyStatistics module data """ - return self._quote_summary(["defaultKeyStatistics"]) + data = self._quote_summary(["defaultKeyStatistics"]) + return self._normalize_quote_summary_response(data, "key_stats") @property def major_holders(self): @@ -360,7 +370,8 @@ def major_holders(self): dict majorHoldersBreakdown module data """ - return self._quote_summary(["majorHoldersBreakdown"]) + data = self._quote_summary(["majorHoldersBreakdown"]) + return self._normalize_quote_summary_response(data, "major_holders") @property def page_views(self): @@ -373,7 +384,8 @@ def page_views(self): dict pageViews module data """ - return self._quote_summary(["pageViews"]) + data = self._quote_summary(["pageViews"]) + return self._normalize_quote_summary_response(data, "page_views") @property def price(self): @@ -387,7 +399,8 @@ def price(self): dict price module data """ - return self._quote_summary(["price"]) + data = self._quote_summary(["price"]) + return self._normalize_quote_summary_response(data, "price") @property def quote_type(self): @@ -446,7 +459,8 @@ def share_purchase_activity(self): dict netSharePurchaseActivity module data """ - return self._quote_summary(["netSharePurchaseActivity"]) + data = self._quote_summary(["netSharePurchaseActivity"]) + return self._normalize_quote_summary_response(data, "share_purchase_activity") @property def summary_detail(self): @@ -459,7 +473,8 @@ def summary_detail(self): dict summaryDetail module data """ - return self._quote_summary(["summaryDetail"]) + data = self._quote_summary(["summaryDetail"]) + return self._normalize_quote_summary_response(data, "summary_detail") @property def summary_profile(self): @@ -472,7 +487,8 @@ def summary_profile(self): dict summaryProfile module data """ - return self._quote_summary(["summaryProfile"]) + data = self._quote_summary(["summaryProfile"]) + return self._normalize_quote_summary_response(data, "summary_profile") @property def technical_insights(self): @@ -557,7 +573,7 @@ def _financials_dataframes(self, data, period_type): df = pd.DataFrame.from_records(data[data_type]) if period_type: df["reportedValue"] = df["reportedValue"].apply( - lambda x: x.get("raw") if isinstance(x, dict) else x + lambda x: self._extract_reported_value(x) ) df["dataType"] = data_type df["symbol"] = symbol @@ -571,6 +587,22 @@ def _financials_dataframes(self, data, period_type): # No data is available for that type pass + def _extract_reported_value(self, reported_value): + """Extract the raw value from reportedValue, handling nested structures""" + if isinstance(reported_value, dict): + raw_value = reported_value.get("raw") + if isinstance(raw_value, dict): + # Handle nested raw structure (e.g., for MarketCap, EnterpriseValue) + raw_value = raw_value.get("parsedValue", raw_value.get("source")) + if isinstance(raw_value, str): + try: + return float(raw_value) + except (ValueError, TypeError): + pass + return raw_value + return reported_value + + def all_financial_data(self, frequency="a"): """ Retrieve all financial data, including income statement, @@ -635,17 +667,29 @@ def corporate_guidance(self): trailing=False, ) - @property - def valuation_measures(self): + def valuation_measures(self, frequency="q", trailing=True): """Valuation Measures - Retrieves valuation measures for most recent four quarters as well - as the most recent date + + Retrieves valuation measures for most recent quarters or years Notes ----- Only quarterly data is available for non-premium subscribers + + Parameters + ---------- + frequency: str, default 'q', optional + Specify either annual or quarterly. Value should be 'a' or 'q'. + trailing: bool, default True, optional + Specify whether or not you'd like trailing twelve month (TTM) + data returned + + Returns + ------- + pandas.DataFrame """ - return self._financials("valuation", "q") + return self._financials("valuation", frequency, trailing=trailing) + def balance_sheet(self, frequency="a"): """Balance Sheet @@ -1170,12 +1214,26 @@ def p_ideas(self, idea_id): def p_technical_events(self): return self._get_data("technical_events") - def p_valuation_measures(self, frequency="q"): + def p_valuation_measures(self, frequency="q", trailing=True): """Valuation Measures + Retrieves valuation measures for all available dates for given symbol(s) + + Parameters + ---------- + frequency: str, default 'q', optional + Specify either annual or quarterly. Value should be 'a' or 'q'. + trailing: bool, default True, optional + Specify whether or not you'd like trailing twelve month (TTM) + data returned + + Returns + ------- + pandas.DataFrame """ - return self._financials("valuation", frequency, premium=True) + return self._financials("valuation", frequency, premium=True, trailing=trailing) + @property def p_value_analyzer(self):