"""Scores router — scoring engine endpoints.""" from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_db, require_access from app.schemas.common import APIEnvelope from app.schemas.score import ( CompositeBreakdownResponse, DimensionScoreResponse, RankingEntry, RankingResponse, ScoreBreakdownResponse, ScoreResponse, SubScoreResponse, WeightUpdateRequest, ) from app.services.scoring_service import get_rankings, get_score, update_weights def _map_breakdown(raw: dict | None) -> ScoreBreakdownResponse | None: """Convert a raw breakdown dict from the scoring service into a Pydantic model.""" if raw is None: return None return ScoreBreakdownResponse( sub_scores=[SubScoreResponse(**s) for s in raw.get("sub_scores", [])], formula=raw.get("formula", ""), unavailable=raw.get("unavailable", []), ) def _map_composite_breakdown(raw: dict | None) -> CompositeBreakdownResponse | None: """Convert a raw composite breakdown dict into a Pydantic model.""" if raw is None: return None return CompositeBreakdownResponse( weights=raw["weights"], available_dimensions=raw["available_dimensions"], missing_dimensions=raw["missing_dimensions"], renormalized_weights=raw["renormalized_weights"], formula=raw["formula"], ) router = APIRouter(tags=["scores"]) @router.get("/scores/{symbol}", response_model=APIEnvelope) async def read_score( symbol: str, _user=Depends(require_access), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Get composite + dimension scores for a symbol. Recomputes stale scores.""" result = await get_score(db, symbol) data = ScoreResponse( symbol=result["symbol"], composite_score=result["composite_score"], composite_stale=result["composite_stale"], weights=result["weights"], dimensions=[ DimensionScoreResponse( dimension=d["dimension"], score=d["score"], is_stale=d["is_stale"], computed_at=d.get("computed_at"), breakdown=_map_breakdown(d.get("breakdown")), ) for d in result["dimensions"] ], missing_dimensions=result["missing_dimensions"], computed_at=result["computed_at"], composite_breakdown=_map_composite_breakdown( result.get("composite_breakdown") ), ) return APIEnvelope(status="success", data=data.model_dump(mode="json")) @router.get("/rankings", response_model=APIEnvelope) async def read_rankings( _user=Depends(require_access), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Get all tickers ranked by composite score descending.""" result = await get_rankings(db) data = RankingResponse( rankings=[ RankingEntry( symbol=r["symbol"], composite_score=r["composite_score"], dimensions=[ DimensionScoreResponse(**d) for d in r["dimensions"] ], ) for r in result["rankings"] ], weights=result["weights"], ) return APIEnvelope(status="success", data=data.model_dump(mode="json")) @router.put("/scores/weights", response_model=APIEnvelope) async def update_score_weights( body: WeightUpdateRequest, _user=Depends(require_access), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Update dimension weights and recompute all composite scores.""" new_weights = await update_weights(db, body.weights) return APIEnvelope(status="success", data={"weights": new_weights})