refactor: fix code review issues across routes and services

- Extract shared route_utils.py (validate_symbol, safe decorator)
  removing duplication from 6 route files
- Extract shared obb_utils.py (to_list, extract_single, safe_last)
  removing duplication from calendar_service and market_service
- Fix _to_list dict mutation during iteration (use comprehension)
- Fix double vars() call and live __dict__ mutation risk
- Fix route ordering: /etf/search and /crypto/search now registered
  before /{symbol} path params to prevent shadowing
- Add date format validation (YYYY-MM-DD pattern) on calendar routes
- Use timezone-aware datetime.now(tz=timezone.utc) in all services
- Add explicit type annotation for asyncio.gather results
This commit is contained in:
Yaojia Wang
2026-03-09 10:56:21 +01:00
parent 507194397e
commit 003c1d6ffc
12 changed files with 271 additions and 428 deletions

View File

@@ -6,6 +6,8 @@ from typing import Any
from openbb import obb
from obb_utils import to_list
logger = logging.getLogger(__name__)
@@ -20,7 +22,7 @@ async def get_earnings_calendar(
if end_date:
kwargs["end_date"] = end_date
result = await asyncio.to_thread(obb.equity.calendar.earnings, **kwargs)
return _to_list(result)
return to_list(result)
except Exception:
logger.warning("Earnings calendar failed", exc_info=True)
return []
@@ -37,7 +39,7 @@ async def get_dividend_calendar(
if end_date:
kwargs["end_date"] = end_date
result = await asyncio.to_thread(obb.equity.calendar.dividend, **kwargs)
return _to_list(result)
return to_list(result)
except Exception:
logger.warning("Dividend calendar failed", exc_info=True)
return []
@@ -54,7 +56,7 @@ async def get_ipo_calendar(
if end_date:
kwargs["end_date"] = end_date
result = await asyncio.to_thread(obb.equity.calendar.ipo, **kwargs)
return _to_list(result)
return to_list(result)
except Exception:
logger.warning("IPO calendar failed", exc_info=True)
return []
@@ -71,7 +73,7 @@ async def get_splits_calendar(
if end_date:
kwargs["end_date"] = end_date
result = await asyncio.to_thread(obb.equity.calendar.splits, **kwargs)
return _to_list(result)
return to_list(result)
except Exception:
logger.warning("Splits calendar failed", exc_info=True)
return []
@@ -83,7 +85,7 @@ async def get_analyst_estimates(symbol: str) -> dict[str, Any]:
result = await asyncio.to_thread(
obb.equity.estimates.consensus, symbol, provider="yfinance"
)
items = _to_list(result)
items = to_list(result)
return {"symbol": symbol, "estimates": items}
except Exception:
logger.warning("Analyst estimates failed for %s", symbol, exc_info=True)
@@ -96,7 +98,7 @@ async def get_share_statistics(symbol: str) -> dict[str, Any]:
result = await asyncio.to_thread(
obb.equity.ownership.share_statistics, symbol, provider="yfinance"
)
items = _to_list(result)
items = to_list(result)
return items[0] if items else {}
except Exception:
logger.warning("Share statistics failed for %s", symbol, exc_info=True)
@@ -109,7 +111,7 @@ async def get_insider_trading(symbol: str) -> list[dict[str, Any]]:
result = await asyncio.to_thread(
obb.equity.ownership.insider_trading, symbol, provider="sec"
)
return _to_list(result)
return to_list(result)
except Exception:
logger.warning("SEC insider trading failed for %s", symbol, exc_info=True)
return []
@@ -121,7 +123,7 @@ async def get_institutional_holders(symbol: str) -> list[dict[str, Any]]:
result = await asyncio.to_thread(
obb.equity.ownership.form_13f, symbol, provider="sec"
)
return _to_list(result)
return to_list(result)
except Exception:
logger.warning("13F data failed for %s", symbol, exc_info=True)
return []
@@ -133,27 +135,7 @@ async def screen_stocks() -> list[dict[str, Any]]:
result = await asyncio.to_thread(
obb.equity.screener, provider="yfinance"
)
return _to_list(result)
return to_list(result)
except Exception:
logger.warning("Stock screener failed", exc_info=True)
return []
def _to_list(result: Any) -> list[dict[str, Any]]:
"""Convert OBBject result to list of dicts."""
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:
d = vars(item) if vars(item) else {}
for k, v in d.items():
if hasattr(v, "isoformat"):
d[k] = v.isoformat()
out.append(d)
return out