Merge pull request 'feat(WP-07): Card State API endpoints (#72)' (#108) from feature/wp-07-card-state-api into card-evolution
Merge PR #108: WP-07 card state API endpoints
This commit is contained in:
commit
e66a1ea9c4
@ -7,6 +7,50 @@ from ..dependencies import oauth2_scheme, valid_token
|
|||||||
|
|
||||||
router = APIRouter(prefix="/api/v2/evolution", tags=["evolution"])
|
router = APIRouter(prefix="/api/v2/evolution", tags=["evolution"])
|
||||||
|
|
||||||
|
# Tier -> threshold attribute name. Index = current_tier; value is the
|
||||||
|
# attribute on EvolutionTrack whose value is the *next* threshold to reach.
|
||||||
|
# Tier 4 is fully evolved so there is no next threshold (None sentinel).
|
||||||
|
_NEXT_THRESHOLD_ATTR = {
|
||||||
|
0: "t1_threshold",
|
||||||
|
1: "t2_threshold",
|
||||||
|
2: "t3_threshold",
|
||||||
|
3: "t4_threshold",
|
||||||
|
4: None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_card_state_response(state) -> dict:
|
||||||
|
"""Serialise an EvolutionCardState into the standard API response shape.
|
||||||
|
|
||||||
|
Produces a flat dict with player_id and team_id as plain integers,
|
||||||
|
a nested 'track' dict with all threshold fields, and a computed
|
||||||
|
'next_threshold' field:
|
||||||
|
- For tiers 0-3: the threshold value for the tier immediately above.
|
||||||
|
- For tier 4 (fully evolved): None.
|
||||||
|
|
||||||
|
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
|
||||||
|
the top level.
|
||||||
|
"""
|
||||||
|
track = state.track
|
||||||
|
track_dict = model_to_dict(track, recurse=False)
|
||||||
|
|
||||||
|
next_attr = _NEXT_THRESHOLD_ATTR.get(state.current_tier)
|
||||||
|
next_threshold = getattr(track, next_attr) if next_attr else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"player_id": state.player_id,
|
||||||
|
"team_id": state.team_id,
|
||||||
|
"current_tier": state.current_tier,
|
||||||
|
"current_value": state.current_value,
|
||||||
|
"fully_evolved": state.fully_evolved,
|
||||||
|
"last_evaluated_at": (
|
||||||
|
state.last_evaluated_at.isoformat() if state.last_evaluated_at else None
|
||||||
|
),
|
||||||
|
"track": track_dict,
|
||||||
|
"next_threshold": next_threshold,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tracks")
|
@router.get("/tracks")
|
||||||
async def list_tracks(
|
async def list_tracks(
|
||||||
@ -43,6 +87,52 @@ 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/{card_id}")
|
||||||
|
async def get_card_state(card_id: int, token: str = Depends(oauth2_scheme)):
|
||||||
|
"""Return the EvolutionCardState for a card identified by its Card.id.
|
||||||
|
|
||||||
|
Resolves card_id -> (player_id, team_id) via the Card table, then looks
|
||||||
|
up the matching EvolutionCardState row. Because duplicate cards for the
|
||||||
|
same player+team share one state row (unique-(player,team) constraint),
|
||||||
|
any card_id belonging to that player on that team returns the same state.
|
||||||
|
|
||||||
|
Returns 404 when:
|
||||||
|
- The card_id does not exist in the Card table.
|
||||||
|
- The card exists but has no corresponding EvolutionCardState yet.
|
||||||
|
"""
|
||||||
|
if not valid_token(token):
|
||||||
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
from ..db_engine import Card, EvolutionCardState, EvolutionTrack, DoesNotExist
|
||||||
|
|
||||||
|
# Resolve card_id to player+team
|
||||||
|
try:
|
||||||
|
card = Card.get_by_id(card_id)
|
||||||
|
except DoesNotExist:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Card {card_id} not found")
|
||||||
|
|
||||||
|
# Look up the evolution state for this (player, team) pair, joining the
|
||||||
|
# track so a single query resolves both rows.
|
||||||
|
try:
|
||||||
|
state = (
|
||||||
|
EvolutionCardState.select(EvolutionCardState, EvolutionTrack)
|
||||||
|
.join(EvolutionTrack)
|
||||||
|
.where(
|
||||||
|
(EvolutionCardState.player == card.player_id)
|
||||||
|
& (EvolutionCardState.team == card.team_id)
|
||||||
|
)
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
except DoesNotExist:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"No evolution state for card {card_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_card_state_response(state)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/cards/{card_id}/evaluate")
|
@router.post("/cards/{card_id}/evaluate")
|
||||||
async def evaluate_card(card_id: int, token: str = Depends(oauth2_scheme)):
|
async def evaluate_card(card_id: int, token: str = Depends(oauth2_scheme)):
|
||||||
"""Force-recalculate evolution state for a card from career stats.
|
"""Force-recalculate evolution state for a card from career stats.
|
||||||
|
|||||||
@ -135,15 +135,15 @@ async def get_teams(
|
|||||||
if has_guide is not None:
|
if has_guide is not None:
|
||||||
# Use boolean comparison (PostgreSQL-compatible)
|
# Use boolean comparison (PostgreSQL-compatible)
|
||||||
if not has_guide:
|
if not has_guide:
|
||||||
all_teams = all_teams.where(Team.has_guide == False)
|
all_teams = all_teams.where(Team.has_guide == False) # noqa: E712
|
||||||
else:
|
else:
|
||||||
all_teams = all_teams.where(Team.has_guide == True)
|
all_teams = all_teams.where(Team.has_guide == True) # noqa: E712
|
||||||
|
|
||||||
if is_ai is not None:
|
if is_ai is not None:
|
||||||
if not is_ai:
|
if not is_ai:
|
||||||
all_teams = all_teams.where(Team.is_ai == False)
|
all_teams = all_teams.where(Team.is_ai == False) # noqa: E712
|
||||||
else:
|
else:
|
||||||
all_teams = all_teams.where(Team.is_ai == True)
|
all_teams = all_teams.where(Team.is_ai == True) # noqa: E712
|
||||||
|
|
||||||
if event_id is not None:
|
if event_id is not None:
|
||||||
all_teams = all_teams.where(Team.event_id == event_id)
|
all_teams = all_teams.where(Team.event_id == event_id)
|
||||||
@ -254,24 +254,24 @@ def get_scouting_dfs(allowed_players, position: str):
|
|||||||
if position in ["LF", "CF", "RF"]:
|
if position in ["LF", "CF", "RF"]:
|
||||||
series_list.append(
|
series_list.append(
|
||||||
pd.Series(
|
pd.Series(
|
||||||
dict([(x.player.player_id, x.arm) for x in positions]), name=f"Arm OF"
|
dict([(x.player.player_id, x.arm) for x in positions]), name="Arm OF"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif position == "C":
|
elif position == "C":
|
||||||
series_list.append(
|
series_list.append(
|
||||||
pd.Series(
|
pd.Series(
|
||||||
dict([(x.player.player_id, x.arm) for x in positions]), name=f"Arm C"
|
dict([(x.player.player_id, x.arm) for x in positions]), name="Arm C"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
series_list.append(
|
series_list.append(
|
||||||
pd.Series(
|
pd.Series(
|
||||||
dict([(x.player.player_id, x.pb) for x in positions]), name=f"PB C"
|
dict([(x.player.player_id, x.pb) for x in positions]), name="PB C"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
series_list.append(
|
series_list.append(
|
||||||
pd.Series(
|
pd.Series(
|
||||||
dict([(x.player.player_id, x.overthrow) for x in positions]),
|
dict([(x.player.player_id, x.overthrow) for x in positions]),
|
||||||
name=f"Throw C",
|
name="Throw C",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -314,11 +314,11 @@ async def get_team_lineup(
|
|||||||
all_players = Player.select().where(Player.franchise == this_team.sname)
|
all_players = Player.select().where(Player.franchise == this_team.sname)
|
||||||
|
|
||||||
if difficulty_name == "exhibition":
|
if difficulty_name == "exhibition":
|
||||||
logging.info(f"pulling an exhibition lineup")
|
logging.info("pulling an exhibition lineup")
|
||||||
if cardset_id is None:
|
if cardset_id is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Must provide at least one cardset_id for exhibition lineups",
|
detail="Must provide at least one cardset_id for exhibition lineups",
|
||||||
)
|
)
|
||||||
legal_players = all_players.where(Player.cardset_id << cardset_id)
|
legal_players = all_players.where(Player.cardset_id << cardset_id)
|
||||||
|
|
||||||
@ -404,17 +404,17 @@ async def get_team_lineup(
|
|||||||
# if x.battingcard.player.p_name not in player_names:
|
# if x.battingcard.player.p_name not in player_names:
|
||||||
# starting_nine['DH'] = x.battingcard.player
|
# starting_nine['DH'] = x.battingcard.player
|
||||||
# break
|
# break
|
||||||
logging.debug(f"Searching for a DH!")
|
logging.debug("Searching for a DH!")
|
||||||
dh_query = legal_players.order_by(Player.cost.desc())
|
dh_query = legal_players.order_by(Player.cost.desc())
|
||||||
for x in dh_query:
|
for x in dh_query:
|
||||||
logging.debug(f"checking {x.p_name} for {position}")
|
logging.debug(f"checking {x.p_name} for {position}")
|
||||||
if x.p_name not in player_names and "P" not in x.pos_1:
|
if x.p_name not in player_names and "P" not in x.pos_1:
|
||||||
logging.debug(f"adding!")
|
logging.debug("adding!")
|
||||||
starting_nine["DH"]["player"] = model_to_dict(x)
|
starting_nine["DH"]["player"] = model_to_dict(x)
|
||||||
try:
|
try:
|
||||||
vl, vr, total_ops = get_bratings(x.player_id)
|
vl, vr, total_ops = get_bratings(x.player_id)
|
||||||
except AttributeError as e:
|
except AttributeError:
|
||||||
logging.debug(f"Could not find batting lines")
|
logging.debug("Could not find batting lines")
|
||||||
else:
|
else:
|
||||||
# starting_nine[position]['vl'] = vl
|
# starting_nine[position]['vl'] = vl
|
||||||
# starting_nine[position]['vr'] = vr
|
# starting_nine[position]['vr'] = vr
|
||||||
@ -429,12 +429,12 @@ async def get_team_lineup(
|
|||||||
for x in dh_query:
|
for x in dh_query:
|
||||||
logging.debug(f"checking {x.p_name} for {position}")
|
logging.debug(f"checking {x.p_name} for {position}")
|
||||||
if x.p_name not in player_names:
|
if x.p_name not in player_names:
|
||||||
logging.debug(f"adding!")
|
logging.debug("adding!")
|
||||||
starting_nine["DH"]["player"] = model_to_dict(x)
|
starting_nine["DH"]["player"] = model_to_dict(x)
|
||||||
try:
|
try:
|
||||||
vl, vr, total_ops = get_bratings(x.player_id)
|
vl, vr, total_ops = get_bratings(x.player_id)
|
||||||
except AttributeError as e:
|
except AttributeError:
|
||||||
logging.debug(f"Could not find batting lines")
|
logging.debug("Could not find batting lines")
|
||||||
else:
|
else:
|
||||||
vl, vr, total_ops = get_bratings(x.player_id)
|
vl, vr, total_ops = get_bratings(x.player_id)
|
||||||
starting_nine[position]["vl"] = vl["obp"] + vl["slg"]
|
starting_nine[position]["vl"] = vl["obp"] + vl["slg"]
|
||||||
@ -464,7 +464,7 @@ async def get_team_lineup(
|
|||||||
x.player.p_name not in player_names
|
x.player.p_name not in player_names
|
||||||
and x.player.p_name.lower() != pitcher_name
|
and x.player.p_name.lower() != pitcher_name
|
||||||
):
|
):
|
||||||
logging.debug(f"adding!")
|
logging.debug("adding!")
|
||||||
starting_nine[position]["player"] = model_to_dict(x.player)
|
starting_nine[position]["player"] = model_to_dict(x.player)
|
||||||
vl, vr, total_ops = get_bratings(x.player.player_id)
|
vl, vr, total_ops = get_bratings(x.player.player_id)
|
||||||
starting_nine[position]["vl"] = vl
|
starting_nine[position]["vl"] = vl
|
||||||
@ -542,7 +542,7 @@ async def get_team_lineup(
|
|||||||
x.player.p_name not in player_names
|
x.player.p_name not in player_names
|
||||||
and x.player.p_name.lower() != pitcher_name
|
and x.player.p_name.lower() != pitcher_name
|
||||||
):
|
):
|
||||||
logging.debug(f"adding!")
|
logging.debug("adding!")
|
||||||
starting_nine[position]["player"] = model_to_dict(x.player)
|
starting_nine[position]["player"] = model_to_dict(x.player)
|
||||||
vl, vr, total_ops = get_bratings(x.player.player_id)
|
vl, vr, total_ops = get_bratings(x.player.player_id)
|
||||||
starting_nine[position]["vl"] = vl["obp"] + vl["slg"]
|
starting_nine[position]["vl"] = vl["obp"] + vl["slg"]
|
||||||
@ -649,11 +649,11 @@ async def get_team_sp(
|
|||||||
all_players = Player.select().where(Player.franchise == this_team.sname)
|
all_players = Player.select().where(Player.franchise == this_team.sname)
|
||||||
|
|
||||||
if difficulty_name == "exhibition":
|
if difficulty_name == "exhibition":
|
||||||
logging.info(f"pulling an exhibition lineup")
|
logging.info("pulling an exhibition lineup")
|
||||||
if cardset_id is None:
|
if cardset_id is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Must provide at least one cardset_id for exhibition lineups",
|
detail="Must provide at least one cardset_id for exhibition lineups",
|
||||||
)
|
)
|
||||||
legal_players = all_players.where(Player.cardset_id << cardset_id)
|
legal_players = all_players.where(Player.cardset_id << cardset_id)
|
||||||
|
|
||||||
@ -778,11 +778,11 @@ async def get_team_rp(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if difficulty_name == "exhibition":
|
if difficulty_name == "exhibition":
|
||||||
logging.info(f"pulling an exhibition RP")
|
logging.info("pulling an exhibition RP")
|
||||||
if cardset_id is None:
|
if cardset_id is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Must provide at least one cardset_id for exhibition lineups",
|
detail="Must provide at least one cardset_id for exhibition lineups",
|
||||||
)
|
)
|
||||||
legal_players = all_players.where(Player.cardset_id << cardset_id)
|
legal_players = all_players.where(Player.cardset_id << cardset_id)
|
||||||
|
|
||||||
@ -934,7 +934,7 @@ async def get_team_rp(
|
|||||||
)
|
)
|
||||||
return this_player
|
return this_player
|
||||||
|
|
||||||
logging.info(f"Falling to last chance pitcher")
|
logging.info("Falling to last chance pitcher")
|
||||||
all_relievers = sort_pitchers(
|
all_relievers = sort_pitchers(
|
||||||
PitchingCard.select()
|
PitchingCard.select()
|
||||||
.join(Player)
|
.join(Player)
|
||||||
@ -957,7 +957,7 @@ async def get_team_record(team_id: int, season: int):
|
|||||||
all_games = StratGame.select().where(
|
all_games = StratGame.select().where(
|
||||||
((StratGame.away_team_id == team_id) | (StratGame.home_team_id == team_id))
|
((StratGame.away_team_id == team_id) | (StratGame.home_team_id == team_id))
|
||||||
& (StratGame.season == season)
|
& (StratGame.season == season)
|
||||||
& (StratGame.short_game == False)
|
& (StratGame.short_game == False) # noqa: E712
|
||||||
)
|
)
|
||||||
|
|
||||||
template = {
|
template = {
|
||||||
@ -1049,8 +1049,6 @@ async def team_buy_players(team_id: int, ids: str, ts: str):
|
|||||||
detail=f"You are not authorized to buy {this_team.abbrev} cards. This event has been logged.",
|
detail=f"You are not authorized to buy {this_team.abbrev} cards. This event has been logged.",
|
||||||
)
|
)
|
||||||
|
|
||||||
last_card = Card.select(Card.id).order_by(-Card.id).limit(1)
|
|
||||||
lc_id = last_card[0].id
|
|
||||||
|
|
||||||
all_ids = ids.split(",")
|
all_ids = ids.split(",")
|
||||||
conf_message = ""
|
conf_message = ""
|
||||||
@ -1098,7 +1096,7 @@ async def team_buy_players(team_id: int, ids: str, ts: str):
|
|||||||
if this_player.rarity.value >= 2:
|
if this_player.rarity.value >= 2:
|
||||||
new_notif = Notification(
|
new_notif = Notification(
|
||||||
created=datetime.now(),
|
created=datetime.now(),
|
||||||
title=f"Price Change",
|
title="Price Change",
|
||||||
desc="Modified by buying and selling",
|
desc="Modified by buying and selling",
|
||||||
field_name=f"{this_player.description} "
|
field_name=f"{this_player.description} "
|
||||||
f"{this_player.p_name if this_player.p_name not in this_player.description else ''}",
|
f"{this_player.p_name if this_player.p_name not in this_player.description else ''}",
|
||||||
@ -1242,7 +1240,7 @@ async def team_sell_cards(team_id: int, ids: str, ts: str):
|
|||||||
if this_player.rarity.value >= 2:
|
if this_player.rarity.value >= 2:
|
||||||
new_notif = Notification(
|
new_notif = Notification(
|
||||||
created=datetime.now(),
|
created=datetime.now(),
|
||||||
title=f"Price Change",
|
title="Price Change",
|
||||||
desc="Modified by buying and selling",
|
desc="Modified by buying and selling",
|
||||||
field_name=f"{this_player.description} "
|
field_name=f"{this_player.description} "
|
||||||
f"{this_player.p_name if this_player.p_name not in this_player.description else ''}",
|
f"{this_player.p_name if this_player.p_name not in this_player.description else ''}",
|
||||||
@ -1293,7 +1291,7 @@ async def get_team_cards(team_id, csv: Optional[bool] = True):
|
|||||||
.order_by(-Card.player.rarity.value, Card.player.p_name)
|
.order_by(-Card.player.rarity.value, Card.player.p_name)
|
||||||
)
|
)
|
||||||
if all_cards.count() == 0:
|
if all_cards.count() == 0:
|
||||||
raise HTTPException(status_code=404, detail=f"No cards found")
|
raise HTTPException(status_code=404, detail="No cards found")
|
||||||
|
|
||||||
card_vals = [model_to_dict(x) for x in all_cards]
|
card_vals = [model_to_dict(x) for x in all_cards]
|
||||||
|
|
||||||
@ -1391,7 +1389,7 @@ async def team_season_update(new_season: int, token: str = Depends(oauth2_scheme
|
|||||||
detail="You are not authorized to post teams. This event has been logged.",
|
detail="You are not authorized to post teams. This event has been logged.",
|
||||||
)
|
)
|
||||||
|
|
||||||
r_query = Team.update(
|
Team.update(
|
||||||
ranking=1000, season=new_season, wallet=Team.wallet + 250, has_guide=False
|
ranking=1000, season=new_season, wallet=Team.wallet + 250, has_guide=False
|
||||||
).execute()
|
).execute()
|
||||||
current = Current.latest()
|
current = Current.latest()
|
||||||
@ -1531,3 +1529,57 @@ async def delete_team(team_id, token: str = Depends(oauth2_scheme)):
|
|||||||
raise HTTPException(status_code=200, detail=f"Team {team_id} has been deleted")
|
raise HTTPException(status_code=200, detail=f"Team {team_id} has been deleted")
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail=f"Team {team_id} was not deleted")
|
raise HTTPException(status_code=500, detail=f"Team {team_id} was not deleted")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{team_id}/evolutions")
|
||||||
|
async def list_team_evolutions(
|
||||||
|
team_id: int,
|
||||||
|
card_type: Optional[str] = Query(default=None),
|
||||||
|
tier: Optional[int] = Query(default=None),
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
per_page: int = Query(default=10, ge=1, le=100),
|
||||||
|
token: str = Depends(oauth2_scheme),
|
||||||
|
):
|
||||||
|
"""List all EvolutionCardState rows for a team, with optional filters.
|
||||||
|
|
||||||
|
Joins EvolutionCardState to EvolutionTrack so that card_type filtering
|
||||||
|
works without a second query. Results are paginated via page/per_page
|
||||||
|
(1-indexed pages); items are ordered by player_id for stable ordering.
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
card_type -- filter to states whose track.card_type matches (e.g. 'batter', 'sp')
|
||||||
|
tier -- filter to states at a specific current_tier (0-4)
|
||||||
|
page -- 1-indexed page number (default 1)
|
||||||
|
per_page -- items per page (default 10, max 100)
|
||||||
|
|
||||||
|
Response shape:
|
||||||
|
{"count": N, "items": [card_state_with_threshold_context, ...]}
|
||||||
|
|
||||||
|
Each item in 'items' has the same shape as GET /evolution/cards/{card_id}.
|
||||||
|
"""
|
||||||
|
if not valid_token(token):
|
||||||
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
from ..db_engine import EvolutionCardState, EvolutionTrack
|
||||||
|
from ..routers_v2.evolution import _build_card_state_response
|
||||||
|
|
||||||
|
query = (
|
||||||
|
EvolutionCardState.select(EvolutionCardState, EvolutionTrack)
|
||||||
|
.join(EvolutionTrack)
|
||||||
|
.where(EvolutionCardState.team == team_id)
|
||||||
|
.order_by(EvolutionCardState.player_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if card_type is not None:
|
||||||
|
query = query.where(EvolutionTrack.card_type == card_type)
|
||||||
|
|
||||||
|
if tier is not None:
|
||||||
|
query = query.where(EvolutionCardState.current_tier == tier)
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
page_query = query.offset(offset).limit(per_page)
|
||||||
|
|
||||||
|
items = [_build_card_state_response(state) for state in page_query]
|
||||||
|
return {"count": total, "items": items}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ tests.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
import psycopg2
|
||||||
from peewee import SqliteDatabase
|
from peewee import SqliteDatabase
|
||||||
|
|
||||||
# Set DATABASE_TYPE=postgresql so that the module-level SKIP_TABLE_CREATION
|
# Set DATABASE_TYPE=postgresql so that the module-level SKIP_TABLE_CREATION
|
||||||
@ -173,3 +174,39 @@ def track():
|
|||||||
t3_threshold=448,
|
t3_threshold=448,
|
||||||
t4_threshold=896,
|
t4_threshold=896,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PostgreSQL integration fixture (used by test_evolution_*_api.py)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def pg_conn():
|
||||||
|
"""Open a psycopg2 connection to the PostgreSQL instance for integration tests.
|
||||||
|
|
||||||
|
Reads connection parameters from the standard POSTGRES_* env vars that the
|
||||||
|
CI workflow injects when a postgres service container is running. Skips the
|
||||||
|
entire session (via pytest.skip) when POSTGRES_HOST is not set, keeping
|
||||||
|
local runs clean.
|
||||||
|
|
||||||
|
The connection is shared for the whole session (scope="session") because
|
||||||
|
the integration test modules use module-scoped fixtures that rely on it;
|
||||||
|
creating a new connection per test would break those module-scoped fixtures.
|
||||||
|
|
||||||
|
Teardown: the connection is closed once all tests have finished.
|
||||||
|
"""
|
||||||
|
host = os.environ.get("POSTGRES_HOST")
|
||||||
|
if not host:
|
||||||
|
pytest.skip("POSTGRES_HOST not set — PostgreSQL integration tests skipped")
|
||||||
|
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=host,
|
||||||
|
port=int(os.environ.get("POSTGRES_PORT", "5432")),
|
||||||
|
dbname=os.environ.get("POSTGRES_DB", "paper_dynasty"),
|
||||||
|
user=os.environ.get("POSTGRES_USER", "postgres"),
|
||||||
|
password=os.environ.get("POSTGRES_PASSWORD", ""),
|
||||||
|
)
|
||||||
|
conn.autocommit = False
|
||||||
|
yield conn
|
||||||
|
conn.close()
|
||||||
|
|||||||
@ -227,7 +227,7 @@ def seeded_data(pg_conn):
|
|||||||
card_no_state_id = cur.fetchone()[0]
|
card_no_state_id = cur.fetchone()[0]
|
||||||
|
|
||||||
# Evolution card states
|
# Evolution card states
|
||||||
# Batter player at tier 1
|
# Batter player at tier 1, value 87.5
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO evolution_card_state
|
INSERT INTO evolution_card_state
|
||||||
@ -258,9 +258,7 @@ def seeded_data(pg_conn):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Teardown: delete in reverse FK order
|
# Teardown: delete in reverse FK order
|
||||||
cur.execute(
|
cur.execute("DELETE FROM evolution_card_state WHERE id = %s", (state_id,))
|
||||||
"DELETE FROM evolution_card_state WHERE id = %s", (state_id,)
|
|
||||||
)
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"DELETE FROM card WHERE id = ANY(%s)",
|
"DELETE FROM card WHERE id = ANY(%s)",
|
||||||
([card_id, card2_id, card_no_state_id],),
|
([card_id, card2_id, card_no_state_id],),
|
||||||
@ -322,7 +320,7 @@ def test_list_filter_by_card_type(client, seeded_data, pg_conn):
|
|||||||
Verifies the JOIN to evolution_track and the WHERE predicate on card_type.
|
Verifies the JOIN to evolution_track and the WHERE predicate on card_type.
|
||||||
"""
|
"""
|
||||||
cur = pg_conn.cursor()
|
cur = pg_conn.cursor()
|
||||||
# Add a state for the sp player so we have two types
|
# Add a state for the sp player so we have two types in this team
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO evolution_card_state
|
INSERT INTO evolution_card_state
|
||||||
@ -415,7 +413,11 @@ def test_list_pagination(client, seeded_data, pg_conn):
|
|||||||
VALUES (%s, %s, %s, 0, 0.0, false)
|
VALUES (%s, %s, %s, 0, 0.0, false)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
(seeded_data["player2_id"], seeded_data["team_id"], seeded_data["batter_track_id"]),
|
(
|
||||||
|
seeded_data["player2_id"],
|
||||||
|
seeded_data["team_id"],
|
||||||
|
seeded_data["batter_track_id"],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
extra_state_id = cur.fetchone()[0]
|
extra_state_id = cur.fetchone()[0]
|
||||||
pg_conn.commit()
|
pg_conn.commit()
|
||||||
@ -478,7 +480,7 @@ def test_get_card_state_shape(client, seeded_data):
|
|||||||
assert t["t3_threshold"] == 448
|
assert t["t3_threshold"] == 448
|
||||||
assert t["t4_threshold"] == 896
|
assert t["t4_threshold"] == 896
|
||||||
|
|
||||||
# tier=1 -> next is t2_threshold
|
# tier=1 -> next threshold is t2_threshold
|
||||||
assert data["next_threshold"] == 149
|
assert data["next_threshold"] == 149
|
||||||
|
|
||||||
|
|
||||||
@ -510,11 +512,12 @@ def test_get_card_state_next_threshold(client, seeded_data, pg_conn):
|
|||||||
try:
|
try:
|
||||||
resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER)
|
resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json()["next_threshold"] == 448
|
assert resp.json()["next_threshold"] == 448 # t3_threshold
|
||||||
|
|
||||||
# Advance to tier 4 (fully evolved)
|
# Advance to tier 4 (fully evolved)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"UPDATE evolution_card_state SET current_tier = 4, fully_evolved = true WHERE id = %s",
|
"UPDATE evolution_card_state SET current_tier = 4, fully_evolved = true "
|
||||||
|
"WHERE id = %s",
|
||||||
(state_id,),
|
(state_id,),
|
||||||
)
|
)
|
||||||
pg_conn.commit()
|
pg_conn.commit()
|
||||||
@ -524,7 +527,8 @@ def test_get_card_state_next_threshold(client, seeded_data, pg_conn):
|
|||||||
assert resp2.json()["next_threshold"] is None
|
assert resp2.json()["next_threshold"] is None
|
||||||
finally:
|
finally:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"UPDATE evolution_card_state SET current_tier = 1, fully_evolved = false WHERE id = %s",
|
"UPDATE evolution_card_state SET current_tier = 1, fully_evolved = false "
|
||||||
|
"WHERE id = %s",
|
||||||
(state_id,),
|
(state_id,),
|
||||||
)
|
)
|
||||||
pg_conn.commit()
|
pg_conn.commit()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user