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.
46 lines
1.4 KiB
Python
46 lines
1.4 KiB
Python
"""Shared route utilities: symbol validation and error handling decorator."""
|
|
|
|
import functools
|
|
import logging
|
|
from collections.abc import Awaitable, Callable
|
|
from typing import ParamSpec, TypeVar
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from models import SYMBOL_PATTERN
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
P = ParamSpec("P")
|
|
R = TypeVar("R")
|
|
|
|
|
|
def validate_symbol(symbol: str) -> str:
|
|
"""Validate and normalize a stock symbol."""
|
|
if not SYMBOL_PATTERN.match(symbol):
|
|
raise HTTPException(status_code=400, detail="Invalid symbol format")
|
|
return symbol.upper()
|
|
|
|
|
|
def safe(fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
|
"""Decorator to catch upstream errors and return 502.
|
|
|
|
ValueError is caught separately and returned as 400 (bad request).
|
|
All other non-HTTP exceptions become 502 (upstream error).
|
|
"""
|
|
@functools.wraps(fn)
|
|
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
try:
|
|
return await fn(*args, **kwargs)
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
except Exception:
|
|
logger.exception("Upstream data error")
|
|
raise HTTPException(
|
|
status_code=502,
|
|
detail="Data provider error. Check server logs.",
|
|
)
|
|
return wrapper # type: ignore[return-value]
|