"""Shared OpenBB result conversion utilities.""" import asyncio import logging from datetime import datetime, timedelta, timezone from typing import Any from openbb import obb logger = logging.getLogger(__name__) PROVIDER = "yfinance" def to_list(result: Any) -> list[dict[str, Any]]: """Convert OBBject result to list of dicts with serialized dates.""" if result is None or result.results is None: return [] items = result.results if not isinstance(items, list): items = [items] out = [] for item in items: if hasattr(item, "model_dump"): d = item.model_dump() else: raw = vars(item) d = dict(raw) if raw else {} d = { k: v.isoformat() if hasattr(v, "isoformat") else v for k, v in d.items() } out.append(d) return out def extract_single(result: Any) -> dict[str, Any]: """Extract data from an OBBject result (single model or list).""" if result is None: return {} items = getattr(result, "results", None) if items is None: return {} if hasattr(items, "model_dump"): return items.model_dump() if isinstance(items, list) and items: last = items[-1] return last.model_dump() if hasattr(last, "model_dump") else {} return {} def safe_last(result: Any) -> dict[str, Any] | None: """Get the last item from a list result, or None.""" if result is None: return None items = getattr(result, "results", None) if items is None or not isinstance(items, list) or not items: return None last = items[-1] return last.model_dump() if hasattr(last, "model_dump") else None def first_or_empty(result: Any) -> dict[str, Any]: """Get first result as dict, or empty dict.""" items = to_list(result) return items[0] if items else {} async def fetch_historical( symbol: str, days: int = 365, provider: str = PROVIDER, ) -> Any | None: """Fetch historical price data, returning the OBBject result or None.""" start = (datetime.now(tz=timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d") try: result = await asyncio.to_thread( obb.equity.price.historical, symbol, start_date=start, provider=provider, ) except Exception: logger.warning("Historical fetch failed for %s", symbol, exc_info=True) return None if result is None or result.results is None: return None return result