from fastapi import APIRouter, Depends, HTTPException, Query import logging from typing import Optional from ..db_engine import model_to_dict from ..dependencies import oauth2_scheme, valid_token logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v2/evolution", tags=["evolution"]) @router.get("/tracks") async def list_tracks( card_type: Optional[str] = Query(default=None), token: str = Depends(oauth2_scheme), ): if not valid_token(token): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") from ..db_engine import EvolutionTrack query = EvolutionTrack.select() if card_type is not None: query = query.where(EvolutionTrack.card_type == card_type) items = [model_to_dict(t, recurse=False) for t in query] return {"count": len(items), "items": items} @router.get("/tracks/{track_id}") async def get_track(track_id: int, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") from ..db_engine import EvolutionTrack try: track = EvolutionTrack.get_by_id(track_id) except Exception: raise HTTPException(status_code=404, detail=f"Track {track_id} not found") return model_to_dict(track, recurse=False) @router.post("/evaluate-game/{game_id}") async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)): """Evaluate evolution state for all players who appeared in a game. Finds all unique (player_id, team_id) pairs from the game's StratPlay rows, then for each pair that has an EvolutionCardState, re-computes the evolution tier by calling evaluate_card(). Pairs without a state row are silently skipped. Tier advancement detection: the tier before and after evaluate_card() are compared. If the tier increased, the player is added to the 'tier_ups' list in the response. Errors for individual players are logged but do not abort the batch; the endpoint always returns a summary even if some evaluations fail. Response: { "evaluated": N, "tier_ups": [ { "player_id": ..., "team_id": ..., "player_name": ..., "old_tier": ..., "new_tier": ..., "current_value": ..., "track_name": ... }, ... ] } """ if not valid_token(token): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") from ..db_engine import EvolutionCardState, EvolutionTrack, Player, StratPlay from ..services.evolution_evaluator import evaluate_card # Collect all unique (player_id, team_id) pairs who appeared in this game. # Both batters and pitchers are included. plays = list(StratPlay.select().where(StratPlay.game == game_id)) pairs: set[tuple[int, int]] = set() for play in plays: if play.batter_id is not None: pairs.add((play.batter_id, play.batter_team_id)) if play.pitcher_id is not None: pairs.add((play.pitcher_id, play.pitcher_team_id)) evaluated = 0 tier_ups = [] for player_id, team_id in pairs: # Check if an EvolutionCardState exists for this pair; skip if not. state = ( EvolutionCardState.select(EvolutionCardState, EvolutionTrack) .join(EvolutionTrack) .where( (EvolutionCardState.player == player_id) & (EvolutionCardState.team == team_id) ) .first() ) if state is None: continue old_tier = state.current_tier track_name = state.track.name try: result = evaluate_card(player_id, team_id) except Exception as exc: logger.error( "evaluate-game/%d: player=%d team=%d failed: %s", game_id, player_id, team_id, exc, exc_info=True, ) continue evaluated += 1 new_tier = result["current_tier"] if new_tier > old_tier: # Resolve player name for the tier-up notification. try: player_name = Player.get_by_id(player_id).p_name except Exception: player_name = f"player_{player_id}" tier_ups.append( { "player_id": player_id, "team_id": team_id, "player_name": player_name, "old_tier": old_tier, "new_tier": new_tier, "current_value": result["current_value"], "track_name": track_name, } ) logger.info( "evaluate-game/%d: evaluated=%d tier_ups=%d", game_id, evaluated, len(tier_ups), ) return {"evaluated": evaluated, "tier_ups": tier_ups}