Files
openbb-invest-api/obb_utils.py
Yaojia Wang 0f7341b158 refactor: address architect review findings (6 items)
R1: Extend @safe to catch ValueError->400, simplify routes_backtest
    (eliminated 4 copies of duplicated try/except)
R2: Consolidate PROVIDER constant into obb_utils.py (single source)
R3: Add days_ago() helper to obb_utils.py, replace 8+ duplications
R4: Extract Reddit/ApeWisdom into reddit_service.py from finnhub_service
R5: Fix missing top-level import asyncio in finnhub_service
R6: (deferred - sentiment logic extraction is a larger change)

All 561 tests passing.
2026-03-19 23:15:00 +01:00

89 lines
2.6 KiB
Python

"""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 {}
def days_ago(days: int) -> str:
"""Return a YYYY-MM-DD date string for N days ago (UTC)."""
return (datetime.now(tz=timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
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 = days_ago(days)
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