Closes #73 Adds POST /api/v2/evolution/cards/{card_id}/evaluate — force-recalculates a card's evolution state from career totals (SUM across all player_season_stats rows for the player-team pair). Changes: - app/services/evolution_evaluator.py: evaluate_card() function that aggregates career stats, delegates to formula engine for value/tier computation, updates evolution_card_state with no-regression guarantee - app/routers_v2/evolution.py: POST /cards/{card_id}/evaluate endpoint plus existing GET /tracks and GET /tracks/{id} endpoints (WP-06) - tests/test_evolution_evaluator.py: 15 unit tests covering tier assignment, advancement, partial progress, idempotency, fully evolved, no regression, multi-season aggregation, missing state error, and return shape - tests/__init__.py, tests/conftest.py: shared test infrastructure All 15 tests pass. Models and formula engine are lazily imported so this module is safely importable before WP-01/WP-05/WP-07/WP-09 merge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
71 lines
2.2 KiB
Python
71 lines
2.2 KiB
Python
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
|
|
|
|
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("/cards/{card_id}/evaluate")
|
|
async def evaluate_card(card_id: int, token: str = Depends(oauth2_scheme)):
|
|
"""Force-recalculate evolution state for a card from career stats.
|
|
|
|
Resolves card_id to (player_id, team_id), then recomputes the evolution
|
|
tier from all player_season_stats rows for that pair. Idempotent.
|
|
"""
|
|
if not valid_token(token):
|
|
logging.warning("Bad Token: [REDACTED]")
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
|
|
from ..db_engine import Card
|
|
from ..services.evolution_evaluator import evaluate_card as _evaluate
|
|
|
|
try:
|
|
card = Card.get_by_id(card_id)
|
|
except Exception:
|
|
raise HTTPException(status_code=404, detail=f"Card {card_id} not found")
|
|
|
|
try:
|
|
result = _evaluate(card.player_id, card.team_id)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc))
|
|
|
|
return result
|