Merge pull request 'feat: add GET /api/v2/refractor/cards list endpoint (#172)' (#173) from issue/172-feat-add-get-api-v2-refractor-cards-list-endpoint into main
This commit is contained in:
commit
eefd4afa37
@ -1245,6 +1245,13 @@ refractor_card_state_index = ModelIndex(
|
|||||||
)
|
)
|
||||||
RefractorCardState.add_index(refractor_card_state_index)
|
RefractorCardState.add_index(refractor_card_state_index)
|
||||||
|
|
||||||
|
refractor_card_state_team_index = ModelIndex(
|
||||||
|
RefractorCardState,
|
||||||
|
(RefractorCardState.team,),
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
RefractorCardState.add_index(refractor_card_state_team_index)
|
||||||
|
|
||||||
|
|
||||||
class RefractorTierBoost(BaseModel):
|
class RefractorTierBoost(BaseModel):
|
||||||
track = ForeignKeyField(RefractorTrack)
|
track = ForeignKeyField(RefractorTrack)
|
||||||
|
|||||||
@ -21,14 +21,15 @@ _NEXT_THRESHOLD_ATTR = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _build_card_state_response(state) -> dict:
|
def _build_card_state_response(state, player_name=None) -> dict:
|
||||||
"""Serialise a RefractorCardState into the standard API response shape.
|
"""Serialise a RefractorCardState into the standard API response shape.
|
||||||
|
|
||||||
Produces a flat dict with player_id and team_id as plain integers,
|
Produces a flat dict with player_id and team_id as plain integers,
|
||||||
a nested 'track' dict with all threshold fields, and a computed
|
a nested 'track' dict with all threshold fields, and computed fields:
|
||||||
'next_threshold' field:
|
- 'next_threshold': threshold for the tier immediately above (None when fully evolved).
|
||||||
- For tiers 0-3: the threshold value for the tier immediately above.
|
- 'progress_pct': current_value / next_threshold * 100, rounded to 1 decimal
|
||||||
- For tier 4 (fully evolved): None.
|
(None when fully evolved or next_threshold is zero).
|
||||||
|
- 'player_name': included when passed (e.g. from a list join); omitted otherwise.
|
||||||
|
|
||||||
Uses model_to_dict(recurse=False) internally so FK fields are returned
|
Uses model_to_dict(recurse=False) internally so FK fields are returned
|
||||||
as IDs rather than nested objects, then promotes the needed IDs up to
|
as IDs rather than nested objects, then promotes the needed IDs up to
|
||||||
@ -40,7 +41,11 @@ def _build_card_state_response(state) -> dict:
|
|||||||
next_attr = _NEXT_THRESHOLD_ATTR.get(state.current_tier)
|
next_attr = _NEXT_THRESHOLD_ATTR.get(state.current_tier)
|
||||||
next_threshold = getattr(track, next_attr) if next_attr else None
|
next_threshold = getattr(track, next_attr) if next_attr else None
|
||||||
|
|
||||||
return {
|
progress_pct = None
|
||||||
|
if next_threshold is not None and next_threshold > 0:
|
||||||
|
progress_pct = round((state.current_value / next_threshold) * 100, 1)
|
||||||
|
|
||||||
|
result = {
|
||||||
"player_id": state.player_id,
|
"player_id": state.player_id,
|
||||||
"team_id": state.team_id,
|
"team_id": state.team_id,
|
||||||
"current_tier": state.current_tier,
|
"current_tier": state.current_tier,
|
||||||
@ -51,8 +56,14 @@ def _build_card_state_response(state) -> dict:
|
|||||||
),
|
),
|
||||||
"track": track_dict,
|
"track": track_dict,
|
||||||
"next_threshold": next_threshold,
|
"next_threshold": next_threshold,
|
||||||
|
"progress_pct": progress_pct,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if player_name is not None:
|
||||||
|
result["player_name"] = player_name
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tracks")
|
@router.get("/tracks")
|
||||||
async def list_tracks(
|
async def list_tracks(
|
||||||
@ -89,6 +100,118 @@ async def get_track(track_id: int, token: str = Depends(oauth2_scheme)):
|
|||||||
return model_to_dict(track, recurse=False)
|
return model_to_dict(track, recurse=False)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/cards")
|
||||||
|
async def list_card_states(
|
||||||
|
team_id: int = Query(...),
|
||||||
|
card_type: Optional[str] = Query(default=None),
|
||||||
|
tier: Optional[int] = Query(default=None, ge=0, le=4),
|
||||||
|
season: Optional[int] = Query(default=None),
|
||||||
|
progress: Optional[str] = Query(default=None),
|
||||||
|
limit: int = Query(default=10, ge=1, le=100),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
token: str = Depends(oauth2_scheme),
|
||||||
|
):
|
||||||
|
"""List RefractorCardState rows for a team, with optional filters and pagination.
|
||||||
|
|
||||||
|
Required:
|
||||||
|
team_id -- filter to this team's cards; returns empty list if team has no states
|
||||||
|
|
||||||
|
Optional filters:
|
||||||
|
card_type -- one of 'batter', 'sp', 'rp'; filters by RefractorTrack.card_type
|
||||||
|
tier -- filter by current_tier (0-4)
|
||||||
|
season -- filter to players who have batting or pitching season stats in that
|
||||||
|
season (EXISTS subquery against batting/pitching_season_stats)
|
||||||
|
progress -- 'close' = only cards within 80% of their next tier threshold;
|
||||||
|
fully evolved cards are always excluded from this filter
|
||||||
|
|
||||||
|
Pagination:
|
||||||
|
limit -- page size (1-100, default 10)
|
||||||
|
offset -- items to skip (default 0)
|
||||||
|
|
||||||
|
Response: {"count": N, "items": [...]}
|
||||||
|
count is the total matching rows before limit/offset.
|
||||||
|
Each item includes player_name and progress_pct in addition to the
|
||||||
|
standard single-card response fields.
|
||||||
|
|
||||||
|
Sort order: current_tier DESC, current_value DESC.
|
||||||
|
"""
|
||||||
|
if not valid_token(token):
|
||||||
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
from ..db_engine import (
|
||||||
|
RefractorCardState,
|
||||||
|
RefractorTrack,
|
||||||
|
Player,
|
||||||
|
BattingSeasonStats,
|
||||||
|
PitchingSeasonStats,
|
||||||
|
fn,
|
||||||
|
Case,
|
||||||
|
JOIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
RefractorCardState.select(RefractorCardState, RefractorTrack, Player)
|
||||||
|
.join(RefractorTrack)
|
||||||
|
.switch(RefractorCardState)
|
||||||
|
.join(
|
||||||
|
Player, JOIN.LEFT_OUTER, on=(RefractorCardState.player == Player.player_id)
|
||||||
|
)
|
||||||
|
.where(RefractorCardState.team == team_id)
|
||||||
|
.order_by(
|
||||||
|
RefractorCardState.current_tier.desc(),
|
||||||
|
RefractorCardState.current_value.desc(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if card_type is not None:
|
||||||
|
query = query.where(RefractorTrack.card_type == card_type)
|
||||||
|
|
||||||
|
if tier is not None:
|
||||||
|
query = query.where(RefractorCardState.current_tier == tier)
|
||||||
|
|
||||||
|
if season is not None:
|
||||||
|
batter_exists = BattingSeasonStats.select().where(
|
||||||
|
(BattingSeasonStats.player == RefractorCardState.player)
|
||||||
|
& (BattingSeasonStats.team == RefractorCardState.team)
|
||||||
|
& (BattingSeasonStats.season == season)
|
||||||
|
)
|
||||||
|
pitcher_exists = PitchingSeasonStats.select().where(
|
||||||
|
(PitchingSeasonStats.player == RefractorCardState.player)
|
||||||
|
& (PitchingSeasonStats.team == RefractorCardState.team)
|
||||||
|
& (PitchingSeasonStats.season == season)
|
||||||
|
)
|
||||||
|
query = query.where(fn.EXISTS(batter_exists) | fn.EXISTS(pitcher_exists))
|
||||||
|
|
||||||
|
if progress == "close":
|
||||||
|
next_threshold_expr = Case(
|
||||||
|
RefractorCardState.current_tier,
|
||||||
|
(
|
||||||
|
(0, RefractorTrack.t1_threshold),
|
||||||
|
(1, RefractorTrack.t2_threshold),
|
||||||
|
(2, RefractorTrack.t3_threshold),
|
||||||
|
(3, RefractorTrack.t4_threshold),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
query = query.where(
|
||||||
|
(RefractorCardState.fully_evolved == False) # noqa: E712
|
||||||
|
& (RefractorCardState.current_value >= next_threshold_expr * 0.8)
|
||||||
|
)
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
items = []
|
||||||
|
for state in query.offset(offset).limit(limit):
|
||||||
|
player_name = None
|
||||||
|
try:
|
||||||
|
player_name = state.player.p_name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
items.append(_build_card_state_response(state, player_name=player_name))
|
||||||
|
|
||||||
|
return {"count": total, "items": items}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/cards/{card_id}")
|
@router.get("/cards/{card_id}")
|
||||||
async def get_card_state(card_id: int, token: str = Depends(oauth2_scheme)):
|
async def get_card_state(card_id: int, token: str = Depends(oauth2_scheme)):
|
||||||
"""Return the RefractorCardState for a card identified by its Card.id.
|
"""Return the RefractorCardState for a card identified by its Card.id.
|
||||||
@ -175,7 +298,7 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
|||||||
logging.warning("Bad Token: [REDACTED]")
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
from ..db_engine import RefractorCardState, RefractorTrack, Player, StratPlay
|
from ..db_engine import RefractorCardState, Player, StratPlay
|
||||||
from ..services.refractor_evaluator import evaluate_card
|
from ..services.refractor_evaluator import evaluate_card
|
||||||
|
|
||||||
plays = list(StratPlay.select().where(StratPlay.game == game_id))
|
plays = list(StratPlay.select().where(StratPlay.game == game_id))
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
-- Migration: Add team_id index to refractor_card_state
|
||||||
|
-- Date: 2026-03-25
|
||||||
|
--
|
||||||
|
-- Adds a non-unique index on refractor_card_state.team_id to support the new
|
||||||
|
-- GET /api/v2/refractor/cards list endpoint, which filters by team as its
|
||||||
|
-- primary discriminator and is called on every /refractor status bot command.
|
||||||
|
--
|
||||||
|
-- The existing unique index is on (player_id, team_id) with player leading,
|
||||||
|
-- so team-only queries cannot use it efficiently.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_refractor_card_state_team
|
||||||
|
ON refractor_card_state (team_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- Rollback:
|
||||||
|
-- DROP INDEX IF EXISTS idx_refractor_card_state_team;
|
||||||
Loading…
Reference in New Issue
Block a user