refactor(data): unify vendor errors under a VendorError hierarchy
Every condition where a vendor cannot return usable data now derives from a single VendorError base (errors.py): NoMarketDataError, VendorRateLimitError, and VendorNotConfiguredError (still a ValueError for back-compat). Vendor-named errors subclass the generic bases, and the router catches the base types, so a new vendor needs no new except clause. Not-configured now has explicit try-next-vendor handling instead of falling through the generic catch-all. The number of error types tracks the number of distinct router reactions, not the number of causes.
This commit is contained in:
7 files changed
+226
-64
No files matched your search
@@ -1,35 +1,34 @@
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
# Import from vendor-specific modules
|
||||
from .y_finance import (
|
||||
get_YFin_data_online,
|
||||
get_stock_stats_indicators_window,
|
||||
get_fundamentals as get_yfinance_fundamentals,
|
||||
get_balance_sheet as get_yfinance_balance_sheet,
|
||||
get_cashflow as get_yfinance_cashflow,
|
||||
get_income_statement as get_yfinance_income_statement,
|
||||
get_insider_transactions as get_yfinance_insider_transactions,
|
||||
)
|
||||
from .yfinance_news import get_news_yfinance, get_global_news_yfinance
|
||||
from .alpha_vantage import (
|
||||
get_stock as get_alpha_vantage_stock,
|
||||
get_indicator as get_alpha_vantage_indicator,
|
||||
get_fundamentals as get_alpha_vantage_fundamentals,
|
||||
get_balance_sheet as get_alpha_vantage_balance_sheet,
|
||||
get_cashflow as get_alpha_vantage_cashflow,
|
||||
get_fundamentals as get_alpha_vantage_fundamentals,
|
||||
get_global_news as get_alpha_vantage_global_news,
|
||||
get_income_statement as get_alpha_vantage_income_statement,
|
||||
get_indicator as get_alpha_vantage_indicator,
|
||||
get_insider_transactions as get_alpha_vantage_insider_transactions,
|
||||
get_news as get_alpha_vantage_news,
|
||||
get_global_news as get_alpha_vantage_global_news,
|
||||
get_stock as get_alpha_vantage_stock,
|
||||
)
|
||||
from .config import get_config
|
||||
from .errors import (
|
||||
NoMarketDataError,
|
||||
VendorNotConfiguredError,
|
||||
VendorRateLimitError,
|
||||
)
|
||||
from .alpha_vantage_common import AlphaVantageRateLimitError
|
||||
from .fred import get_macro_data as get_fred_macro_data
|
||||
from .polymarket import get_prediction_markets as get_polymarket_prediction_markets
|
||||
from .symbol_utils import NoMarketDataError
|
||||
|
||||
# Configuration and routing logic
|
||||
from .config import get_config
|
||||
from .y_finance import (
|
||||
get_balance_sheet as get_yfinance_balance_sheet,
|
||||
get_cashflow as get_yfinance_cashflow,
|
||||
get_fundamentals as get_yfinance_fundamentals,
|
||||
get_income_statement as get_yfinance_income_statement,
|
||||
get_insider_transactions as get_yfinance_insider_transactions,
|
||||
get_stock_stats_indicators_window,
|
||||
get_YFin_data_online,
|
||||
)
|
||||
from .yfinance_news import get_global_news_yfinance, get_news_yfinance
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -194,9 +193,14 @@ def route_to_vendor(method: str, *args, **kwargs):
|
||||
|
||||
try:
|
||||
return impl_func(*args, **kwargs)
|
||||
except AlphaVantageRateLimitError:
|
||||
except VendorRateLimitError:
|
||||
logger.warning("Vendor %r rate-limited for %s; trying next vendor.", vendor, method)
|
||||
continue
|
||||
except VendorNotConfiguredError as e:
|
||||
logger.warning("Vendor %r not configured for %s; trying next vendor.", vendor, method)
|
||||
if first_error is None:
|
||||
first_error = e # Surface it if no other vendor can serve the call.
|
||||
continue
|
||||
except NoMarketDataError as e:
|
||||
last_no_data = e # No data here; another configured vendor may have it
|
||||
continue
|
||||
@@ -224,10 +228,14 @@ def route_to_vendor(method: str, *args, **kwargs):
|
||||
sym = last_no_data.symbol
|
||||
canonical = last_no_data.canonical
|
||||
resolved = "" if canonical == sym else f" (resolved to '{canonical}')"
|
||||
# Surface the typed error's detail (e.g. "latest row is 2025-06-11 ...
|
||||
# stale") so the agent sees the specific reason — invalid symbol, no
|
||||
# coverage, or stale data — not just a generic "unavailable".
|
||||
reason = f" ({last_no_data.detail})" if last_no_data.detail else ""
|
||||
return (
|
||||
f"NO_DATA_AVAILABLE: No market data found for '{sym}'{resolved} from "
|
||||
f"any configured vendor. The symbol may be invalid, delisted, or not "
|
||||
f"covered by Yahoo Finance / Alpha Vantage. Do not estimate or "
|
||||
f"NO_DATA_AVAILABLE: No usable market data for '{sym}'{resolved} from "
|
||||
f"any configured vendor{reason}. The symbol may be invalid, delisted, "
|
||||
f"not covered, or the vendor returned stale data. Do not estimate or "
|
||||
f"fabricate values — report that data is unavailable for this symbol."
|
||||
)
|
||||
|
||||
@@ -236,4 +244,4 @@ def route_to_vendor(method: str, *args, **kwargs):
|
||||
if first_error is not None:
|
||||
raise first_error
|
||||
|
||||
raise RuntimeError(f"No available vendor for '{method}'")
|
||||
raise RuntimeError(f"No available vendor for '{method}'")
|
||||
Reference in new issue
Block a user