Merge branch 'main' into issue/140-feat-add-limit-pagination-to-notifications-endpoin
This commit is contained in:
commit
268b81aea4
@ -1210,7 +1210,7 @@ if not SKIP_TABLE_CREATION:
|
||||
db.create_tables([ScoutOpportunity, ScoutClaim], safe=True)
|
||||
|
||||
|
||||
class EvolutionTrack(BaseModel):
|
||||
class RefractorTrack(BaseModel):
|
||||
name = CharField(unique=True)
|
||||
card_type = CharField() # 'batter', 'sp', 'rp'
|
||||
formula = CharField() # e.g. "pa + tb * 2"
|
||||
@ -1221,13 +1221,13 @@ class EvolutionTrack(BaseModel):
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
table_name = "evolution_track"
|
||||
table_name = "refractor_track"
|
||||
|
||||
|
||||
class EvolutionCardState(BaseModel):
|
||||
class RefractorCardState(BaseModel):
|
||||
player = ForeignKeyField(Player)
|
||||
team = ForeignKeyField(Team)
|
||||
track = ForeignKeyField(EvolutionTrack)
|
||||
track = ForeignKeyField(RefractorTrack)
|
||||
current_tier = IntegerField(default=0) # 0-4
|
||||
current_value = FloatField(default=0.0)
|
||||
fully_evolved = BooleanField(default=False)
|
||||
@ -1235,19 +1235,19 @@ class EvolutionCardState(BaseModel):
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
table_name = "evolution_card_state"
|
||||
table_name = "refractor_card_state"
|
||||
|
||||
|
||||
evolution_card_state_index = ModelIndex(
|
||||
EvolutionCardState,
|
||||
(EvolutionCardState.player, EvolutionCardState.team),
|
||||
refractor_card_state_index = ModelIndex(
|
||||
RefractorCardState,
|
||||
(RefractorCardState.player, RefractorCardState.team),
|
||||
unique=True,
|
||||
)
|
||||
EvolutionCardState.add_index(evolution_card_state_index)
|
||||
RefractorCardState.add_index(refractor_card_state_index)
|
||||
|
||||
|
||||
class EvolutionTierBoost(BaseModel):
|
||||
track = ForeignKeyField(EvolutionTrack)
|
||||
class RefractorTierBoost(BaseModel):
|
||||
track = ForeignKeyField(RefractorTrack)
|
||||
tier = IntegerField() # 1-4
|
||||
boost_type = CharField() # e.g. 'rating', 'stat'
|
||||
boost_target = CharField() # e.g. 'contact_vl', 'power_vr'
|
||||
@ -1255,23 +1255,23 @@ class EvolutionTierBoost(BaseModel):
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
table_name = "evolution_tier_boost"
|
||||
table_name = "refractor_tier_boost"
|
||||
|
||||
|
||||
evolution_tier_boost_index = ModelIndex(
|
||||
EvolutionTierBoost,
|
||||
refractor_tier_boost_index = ModelIndex(
|
||||
RefractorTierBoost,
|
||||
(
|
||||
EvolutionTierBoost.track,
|
||||
EvolutionTierBoost.tier,
|
||||
EvolutionTierBoost.boost_type,
|
||||
EvolutionTierBoost.boost_target,
|
||||
RefractorTierBoost.track,
|
||||
RefractorTierBoost.tier,
|
||||
RefractorTierBoost.boost_type,
|
||||
RefractorTierBoost.boost_target,
|
||||
),
|
||||
unique=True,
|
||||
)
|
||||
EvolutionTierBoost.add_index(evolution_tier_boost_index)
|
||||
RefractorTierBoost.add_index(refractor_tier_boost_index)
|
||||
|
||||
|
||||
class EvolutionCosmetic(BaseModel):
|
||||
class RefractorCosmetic(BaseModel):
|
||||
name = CharField(unique=True)
|
||||
tier_required = IntegerField(default=0)
|
||||
cosmetic_type = CharField() # 'frame', 'badge', 'theme'
|
||||
@ -1280,12 +1280,12 @@ class EvolutionCosmetic(BaseModel):
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
table_name = "evolution_cosmetic"
|
||||
table_name = "refractor_cosmetic"
|
||||
|
||||
|
||||
if not SKIP_TABLE_CREATION:
|
||||
db.create_tables(
|
||||
[EvolutionTrack, EvolutionCardState, EvolutionTierBoost, EvolutionCosmetic],
|
||||
[RefractorTrack, RefractorCardState, RefractorTierBoost, RefractorCosmetic],
|
||||
safe=True,
|
||||
)
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ from .routers_v2 import ( # noqa: E402
|
||||
stratplays,
|
||||
scout_opportunities,
|
||||
scout_claims,
|
||||
evolution,
|
||||
refractor,
|
||||
season_stats,
|
||||
)
|
||||
|
||||
@ -107,7 +107,7 @@ app.include_router(stratplays.router)
|
||||
app.include_router(decisions.router)
|
||||
app.include_router(scout_opportunities.router)
|
||||
app.include_router(scout_claims.router)
|
||||
app.include_router(evolution.router)
|
||||
app.include_router(refractor.router)
|
||||
app.include_router(season_stats.router)
|
||||
|
||||
|
||||
|
||||
@ -8,16 +8,13 @@ from ..db_engine import Award, model_to_dict, DoesNotExist
|
||||
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/awards',
|
||||
tags=['awards']
|
||||
)
|
||||
router = APIRouter(prefix="/api/v2/awards", tags=["awards"])
|
||||
|
||||
|
||||
class AwardModel(pydantic.BaseModel):
|
||||
name: str
|
||||
season: int
|
||||
timing: str = 'In-Season'
|
||||
timing: str = "In-Season"
|
||||
card_id: Optional[int] = None
|
||||
team_id: Optional[int] = None
|
||||
image: Optional[str] = None
|
||||
@ -28,15 +25,21 @@ class AwardReturnList(pydantic.BaseModel):
|
||||
awards: list[AwardModel]
|
||||
|
||||
|
||||
@router.get('')
|
||||
@router.get("")
|
||||
async def get_awards(
|
||||
name: Optional[str] = None, season: Optional[int] = None, timing: Optional[str] = None,
|
||||
card_id: Optional[int] = None, team_id: Optional[int] = None, image: Optional[str] = None,
|
||||
csv: Optional[bool] = None):
|
||||
name: Optional[str] = None,
|
||||
season: Optional[int] = None,
|
||||
timing: Optional[str] = None,
|
||||
card_id: Optional[int] = None,
|
||||
team_id: Optional[int] = None,
|
||||
image: Optional[str] = None,
|
||||
csv: Optional[bool] = None,
|
||||
limit: int = 100,
|
||||
):
|
||||
all_awards = Award.select().order_by(Award.id)
|
||||
|
||||
if all_awards.count() == 0:
|
||||
raise HTTPException(status_code=404, detail=f'There are no awards to filter')
|
||||
raise HTTPException(status_code=404, detail="There are no awards to filter")
|
||||
|
||||
if name is not None:
|
||||
all_awards = all_awards.where(Award.name == name)
|
||||
@ -51,53 +54,73 @@ async def get_awards(
|
||||
if image is not None:
|
||||
all_awards = all_awards.where(Award.image == image)
|
||||
|
||||
limit = max(0, min(limit, 500))
|
||||
all_awards = all_awards.limit(limit)
|
||||
|
||||
if csv:
|
||||
data_list = [['id', 'name', 'season', 'timing', 'card', 'team', 'image']]
|
||||
data_list = [["id", "name", "season", "timing", "card", "team", "image"]]
|
||||
for line in all_awards:
|
||||
data_list.append([
|
||||
line.id, line.name, line.season, line.timing, line.card, line.team, line.image
|
||||
])
|
||||
data_list.append(
|
||||
[
|
||||
line.id,
|
||||
line.name,
|
||||
line.season,
|
||||
line.timing,
|
||||
line.card,
|
||||
line.team,
|
||||
line.image,
|
||||
]
|
||||
)
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = {'count': all_awards.count(), 'awards': []}
|
||||
return_val = {"count": all_awards.count(), "awards": []}
|
||||
for x in all_awards:
|
||||
return_val['awards'].append(model_to_dict(x))
|
||||
return_val["awards"].append(model_to_dict(x))
|
||||
|
||||
return return_val
|
||||
|
||||
|
||||
@router.get('/{award_id}')
|
||||
@router.get("/{award_id}")
|
||||
async def get_one_award(award_id, csv: Optional[bool] = None):
|
||||
try:
|
||||
this_award = Award.get_by_id(award_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No award found with id {award_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No award found with id {award_id}"
|
||||
)
|
||||
|
||||
if csv:
|
||||
data_list = [
|
||||
['id', 'name', 'season', 'timing', 'card', 'team', 'image'],
|
||||
[this_award.id, this_award.name, this_award.season, this_award.timing, this_award.card,
|
||||
this_award.team, this_award.image]
|
||||
["id", "name", "season", "timing", "card", "team", "image"],
|
||||
[
|
||||
this_award.id,
|
||||
this_award.name,
|
||||
this_award.season,
|
||||
this_award.timing,
|
||||
this_award.card,
|
||||
this_award.team,
|
||||
this_award.image,
|
||||
],
|
||||
]
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = model_to_dict(this_award)
|
||||
return return_val
|
||||
|
||||
|
||||
@router.post('', include_in_schema=PRIVATE_IN_SCHEMA)
|
||||
@router.post("", include_in_schema=PRIVATE_IN_SCHEMA)
|
||||
async def post_awards(award: AwardModel, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to post awards. This event has been logged.'
|
||||
detail="You are not authorized to post awards. This event has been logged.",
|
||||
)
|
||||
|
||||
this_award = Award(
|
||||
@ -106,7 +129,7 @@ async def post_awards(award: AwardModel, token: str = Depends(oauth2_scheme)):
|
||||
timing=award.season,
|
||||
card_id=award.card_id,
|
||||
team_id=award.team_id,
|
||||
image=award.image
|
||||
image=award.image,
|
||||
)
|
||||
|
||||
saved = this_award.save()
|
||||
@ -116,28 +139,30 @@ async def post_awards(award: AwardModel, token: str = Depends(oauth2_scheme)):
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that roster'
|
||||
detail="Well slap my ass and call me a teapot; I could not save that roster",
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{award_id}', include_in_schema=PRIVATE_IN_SCHEMA)
|
||||
@router.delete("/{award_id}", include_in_schema=PRIVATE_IN_SCHEMA)
|
||||
async def delete_award(award_id, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to delete awards. This event has been logged.'
|
||||
detail="You are not authorized to delete awards. This event has been logged.",
|
||||
)
|
||||
try:
|
||||
this_award = Award.get_by_id(award_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No award found with id {award_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No award found with id {award_id}"
|
||||
)
|
||||
|
||||
count = this_award.delete_instance()
|
||||
|
||||
if count == 1:
|
||||
raise HTTPException(status_code=200, detail=f'Award {award_id} has been deleted')
|
||||
raise HTTPException(
|
||||
status_code=200, detail=f"Award {award_id} has been deleted"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'Award {award_id} was not deleted')
|
||||
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"Award {award_id} was not deleted")
|
||||
|
||||
@ -66,7 +66,8 @@ class BatStatReturnList(pydantic.BaseModel):
|
||||
@router.get('', response_model=BatStatReturnList)
|
||||
async def get_batstats(
|
||||
card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None,
|
||||
season: int = None, week_start: int = None, week_end: int = None, created: int = None, csv: bool = None):
|
||||
season: int = None, week_start: int = None, week_end: int = None, created: int = None, csv: bool = None,
|
||||
limit: Optional[int] = 100):
|
||||
all_stats = BattingStat.select().join(Card).join(Player).order_by(BattingStat.id)
|
||||
|
||||
if season is not None:
|
||||
@ -98,6 +99,9 @@ async def get_batstats(
|
||||
# db.close()
|
||||
# raise HTTPException(status_code=404, detail=f'No batting stats found')
|
||||
|
||||
limit = max(0, min(limit, 500))
|
||||
all_stats = all_stats.limit(limit)
|
||||
|
||||
if csv:
|
||||
data_list = [['id', 'card_id', 'player_id', 'cardset', 'team', 'vs_team', 'pos', 'pa', 'ab', 'run', 'hit', 'rbi', 'double',
|
||||
'triple', 'hr', 'bb', 'so', 'hbp', 'sac', 'ibb', 'gidp', 'sb', 'cs', 'bphr', 'bpfo', 'bp1b',
|
||||
|
||||
@ -145,6 +145,7 @@ async def get_card_ratings(
|
||||
vs_hand: Literal["R", "L", "vR", "vL"] = None,
|
||||
short_output: bool = False,
|
||||
csv: bool = False,
|
||||
limit: int = 100,
|
||||
):
|
||||
this_team = Team.get_or_none(Team.id == team_id)
|
||||
logging.debug(f"Team: {this_team} / has_guide: {this_team.has_guide}")
|
||||
@ -178,6 +179,8 @@ async def get_card_ratings(
|
||||
)
|
||||
all_ratings = all_ratings.where(BattingCardRatings.battingcard << set_cards)
|
||||
|
||||
all_ratings = all_ratings.limit(max(0, min(limit, 500)))
|
||||
|
||||
if csv:
|
||||
# return_val = query_to_csv(all_ratings)
|
||||
return_vals = [model_to_dict(x) for x in all_ratings]
|
||||
@ -281,7 +284,7 @@ def get_scouting_dfs(cardset_id: list = None):
|
||||
)
|
||||
]
|
||||
),
|
||||
name=f"Arm OF",
|
||||
name="Arm OF",
|
||||
)
|
||||
)
|
||||
series_list.append(
|
||||
@ -292,7 +295,7 @@ def get_scouting_dfs(cardset_id: list = None):
|
||||
for x in positions.where(CardPosition.position == "C")
|
||||
]
|
||||
),
|
||||
name=f"Arm C",
|
||||
name="Arm C",
|
||||
)
|
||||
)
|
||||
series_list.append(
|
||||
@ -303,7 +306,7 @@ def get_scouting_dfs(cardset_id: list = None):
|
||||
for x in positions.where(CardPosition.position == "C")
|
||||
]
|
||||
),
|
||||
name=f"PB C",
|
||||
name="PB C",
|
||||
)
|
||||
)
|
||||
series_list.append(
|
||||
@ -314,7 +317,7 @@ def get_scouting_dfs(cardset_id: list = None):
|
||||
for x in positions.where(CardPosition.position == "C")
|
||||
]
|
||||
),
|
||||
name=f"Throw C",
|
||||
name="Throw C",
|
||||
)
|
||||
)
|
||||
logging.debug(f"series_list: {series_list}")
|
||||
@ -334,9 +337,9 @@ async def get_card_scouting(team_id: int, ts: str):
|
||||
"https://ko-fi.com/manticorum/shop"
|
||||
)
|
||||
|
||||
if os.path.isfile(f"storage/batting-ratings.csv"):
|
||||
if os.path.isfile("storage/batting-ratings.csv"):
|
||||
return FileResponse(
|
||||
path=f"storage/batting-ratings.csv",
|
||||
path="storage/batting-ratings.csv",
|
||||
media_type="text/csv",
|
||||
# headers=headers
|
||||
)
|
||||
@ -354,7 +357,7 @@ async def post_calc_scouting(token: str = Depends(oauth2_scheme)):
|
||||
status_code=401, detail="You are not authorized to calculate card ratings."
|
||||
)
|
||||
|
||||
logging.warning(f"Re-calculating batting ratings\n\n")
|
||||
logging.warning("Re-calculating batting ratings\n\n")
|
||||
|
||||
output = get_scouting_dfs()
|
||||
first = ["player_id", "player_name", "cardset_name", "rarity", "hand", "variant"]
|
||||
@ -370,9 +373,9 @@ async def post_calc_scouting(token: str = Depends(oauth2_scheme)):
|
||||
|
||||
@router.get("/basic")
|
||||
async def get_basic_scouting(cardset_id: list = Query(default=None)):
|
||||
if os.path.isfile(f"storage/batting-basic.csv"):
|
||||
if os.path.isfile("storage/batting-basic.csv"):
|
||||
return FileResponse(
|
||||
path=f"storage/batting-basic.csv",
|
||||
path="storage/batting-basic.csv",
|
||||
media_type="text/csv",
|
||||
# headers=headers
|
||||
)
|
||||
@ -390,7 +393,7 @@ async def post_calc_basic(token: str = Depends(oauth2_scheme)):
|
||||
status_code=401, detail="You are not authorized to calculate basic ratings."
|
||||
)
|
||||
|
||||
logging.warning(f"Re-calculating basic batting ratings\n\n")
|
||||
logging.warning("Re-calculating basic batting ratings\n\n")
|
||||
|
||||
raw_data = get_scouting_dfs()
|
||||
logging.debug(f"output: {raw_data}")
|
||||
@ -667,9 +670,11 @@ async def get_player_ratings(
|
||||
if variant is not None:
|
||||
all_cards = all_cards.where(BattingCard.variant << variant)
|
||||
|
||||
all_ratings = BattingCardRatings.select().where(
|
||||
BattingCardRatings.battingcard << all_cards
|
||||
).order_by(BattingCardRatings.id)
|
||||
all_ratings = (
|
||||
BattingCardRatings.select()
|
||||
.where(BattingCardRatings.battingcard << all_cards)
|
||||
.order_by(BattingCardRatings.id)
|
||||
)
|
||||
|
||||
return_val = {
|
||||
"count": all_ratings.count(),
|
||||
|
||||
@ -4,9 +4,19 @@ import logging
|
||||
import pydantic
|
||||
from pandas import DataFrame
|
||||
|
||||
from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex, CARDSETS, DoesNotExist
|
||||
from ..db_engine import (
|
||||
db,
|
||||
Card,
|
||||
model_to_dict,
|
||||
Team,
|
||||
Player,
|
||||
Pack,
|
||||
Paperdex,
|
||||
CARDSETS,
|
||||
DoesNotExist,
|
||||
)
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
from ..services.evolution_init import _determine_card_type, initialize_card_evolution
|
||||
from ..services.refractor_init import _determine_card_type, initialize_card_refractor
|
||||
|
||||
router = APIRouter(prefix="/api/v2/cards", tags=["cards"])
|
||||
|
||||
@ -47,19 +57,25 @@ async def get_cards(
|
||||
try:
|
||||
this_team = Team.get_by_id(team_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No team found with id {team_id}"
|
||||
)
|
||||
all_cards = all_cards.where(Card.team == this_team)
|
||||
if player_id is not None:
|
||||
try:
|
||||
this_player = Player.get_by_id(player_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No player found with id {player_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No player found with id {player_id}"
|
||||
)
|
||||
all_cards = all_cards.where(Card.player == this_player)
|
||||
if pack_id is not None:
|
||||
try:
|
||||
this_pack = Pack.get_by_id(pack_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No pack found with id {pack_id}"
|
||||
)
|
||||
all_cards = all_cards.where(Card.pack == this_pack)
|
||||
if value is not None:
|
||||
all_cards = all_cards.where(Card.value == value)
|
||||
@ -125,7 +141,6 @@ async def get_cards(
|
||||
dex_by_player.setdefault(row.player_id, []).append(row)
|
||||
return_val = {"count": len(card_list), "cards": []}
|
||||
for x in card_list:
|
||||
|
||||
this_record = model_to_dict(x)
|
||||
logging.debug(f"this_record: {this_record}")
|
||||
|
||||
@ -147,7 +162,7 @@ async def v1_cards_get_one(card_id, csv: Optional[bool] = False):
|
||||
try:
|
||||
this_card = Card.get_by_id(card_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No card found with id {card_id}')
|
||||
raise HTTPException(status_code=404, detail=f"No card found with id {card_id}")
|
||||
|
||||
if csv:
|
||||
data_list = [
|
||||
@ -207,15 +222,15 @@ async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)):
|
||||
cost_query.execute()
|
||||
# sheets.post_new_cards(SHEETS_AUTH, lc_id)
|
||||
|
||||
# WP-10: initialize evolution state for each new card (fire-and-forget)
|
||||
# WP-10: initialize refractor state for each new card (fire-and-forget)
|
||||
for x in cards.cards:
|
||||
try:
|
||||
this_player = Player.get_by_id(x.player_id)
|
||||
card_type = _determine_card_type(this_player)
|
||||
initialize_card_evolution(x.player_id, x.team_id, card_type)
|
||||
initialize_card_refractor(x.player_id, x.team_id, card_type)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"evolution hook: unexpected error for player_id=%s team_id=%s",
|
||||
"refractor hook: unexpected error for player_id=%s team_id=%s",
|
||||
x.player_id,
|
||||
x.team_id,
|
||||
)
|
||||
@ -319,8 +334,8 @@ async def v1_cards_wipe_team(team_id: int, token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
this_team = Team.get_by_id(team_id)
|
||||
except DoesNotExist:
|
||||
logging.error(f'/cards/wipe-team/{team_id} - could not find team')
|
||||
raise HTTPException(status_code=404, detail=f'Team {team_id} not found')
|
||||
logging.error(f"/cards/wipe-team/{team_id} - could not find team")
|
||||
raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
|
||||
|
||||
t_query = Card.update(team=None).where(Card.team == this_team).execute()
|
||||
return f"Wiped {t_query} cards"
|
||||
@ -348,7 +363,7 @@ async def v1_cards_patch(
|
||||
try:
|
||||
this_card = Card.get_by_id(card_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No card found with id {card_id}')
|
||||
raise HTTPException(status_code=404, detail=f"No card found with id {card_id}")
|
||||
|
||||
if player_id is not None:
|
||||
this_card.player_id = player_id
|
||||
@ -391,7 +406,7 @@ async def v1_cards_delete(card_id, token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
this_card = Card.get_by_id(card_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No cards found with id {card_id}')
|
||||
raise HTTPException(status_code=404, detail=f"No cards found with id {card_id}")
|
||||
|
||||
count = this_card.delete_instance()
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ class EventModel(pydantic.BaseModel):
|
||||
@router.get('')
|
||||
async def v1_events_get(
|
||||
name: Optional[str] = None, in_desc: Optional[str] = None, active: Optional[bool] = None,
|
||||
csv: Optional[bool] = None):
|
||||
csv: Optional[bool] = None, limit: Optional[int] = 100):
|
||||
all_events = Event.select().order_by(Event.id)
|
||||
|
||||
if name is not None:
|
||||
@ -39,6 +39,8 @@ async def v1_events_get(
|
||||
if active is not None:
|
||||
all_events = all_events.where(Event.active == active)
|
||||
|
||||
all_events = all_events.limit(max(0, min(limit, 500)))
|
||||
|
||||
if csv:
|
||||
data_list = [['id', 'name', 'short_desc', 'long_desc', 'url', 'thumbnail', 'active']]
|
||||
for line in all_events:
|
||||
|
||||
@ -8,10 +8,7 @@ from ..db_engine import GameRewards, model_to_dict, DoesNotExist
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/gamerewards',
|
||||
tags=['gamerewards']
|
||||
)
|
||||
router = APIRouter(prefix="/api/v2/gamerewards", tags=["gamerewards"])
|
||||
|
||||
|
||||
class GameRewardModel(pydantic.BaseModel):
|
||||
@ -21,10 +18,15 @@ class GameRewardModel(pydantic.BaseModel):
|
||||
money: Optional[int] = None
|
||||
|
||||
|
||||
@router.get('')
|
||||
@router.get("")
|
||||
async def v1_gamerewards_get(
|
||||
name: Optional[str] = None, pack_type_id: Optional[int] = None, player_id: Optional[int] = None,
|
||||
money: Optional[int] = None, csv: Optional[bool] = None):
|
||||
name: Optional[str] = None,
|
||||
pack_type_id: Optional[int] = None,
|
||||
player_id: Optional[int] = None,
|
||||
money: Optional[int] = None,
|
||||
csv: Optional[bool] = None,
|
||||
limit: int = 100,
|
||||
):
|
||||
all_rewards = GameRewards.select().order_by(GameRewards.id)
|
||||
|
||||
# if all_rewards.count() == 0:
|
||||
@ -40,61 +42,76 @@ async def v1_gamerewards_get(
|
||||
if money is not None:
|
||||
all_rewards = all_rewards.where(GameRewards.money == money)
|
||||
|
||||
limit = max(0, min(limit, 500))
|
||||
all_rewards = all_rewards.limit(limit)
|
||||
|
||||
if csv:
|
||||
data_list = [['id', 'pack_type_id', 'player_id', 'money']]
|
||||
data_list = [["id", "pack_type_id", "player_id", "money"]]
|
||||
for line in all_rewards:
|
||||
data_list.append([
|
||||
line.id, line.pack_type_id if line.pack_type else None, line.player_id if line.player else None,
|
||||
line.money
|
||||
])
|
||||
data_list.append(
|
||||
[
|
||||
line.id,
|
||||
line.pack_type_id if line.pack_type else None,
|
||||
line.player_id if line.player else None,
|
||||
line.money,
|
||||
]
|
||||
)
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = {'count': all_rewards.count(), 'gamerewards': []}
|
||||
return_val = {"count": all_rewards.count(), "gamerewards": []}
|
||||
for x in all_rewards:
|
||||
return_val['gamerewards'].append(model_to_dict(x))
|
||||
return_val["gamerewards"].append(model_to_dict(x))
|
||||
|
||||
return return_val
|
||||
|
||||
|
||||
@router.get('/{gameaward_id}')
|
||||
@router.get("/{gameaward_id}")
|
||||
async def v1_gamerewards_get_one(gamereward_id, csv: Optional[bool] = None):
|
||||
try:
|
||||
this_game_reward = GameRewards.get_by_id(gamereward_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No game reward found with id {gamereward_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No game reward found with id {gamereward_id}"
|
||||
)
|
||||
|
||||
if csv:
|
||||
data_list = [
|
||||
['id', 'pack_type_id', 'player_id', 'money'],
|
||||
[this_game_reward.id, this_game_reward.pack_type_id if this_game_reward.pack_type else None,
|
||||
this_game_reward.player_id if this_game_reward.player else None, this_game_reward.money]
|
||||
["id", "pack_type_id", "player_id", "money"],
|
||||
[
|
||||
this_game_reward.id,
|
||||
this_game_reward.pack_type_id if this_game_reward.pack_type else None,
|
||||
this_game_reward.player_id if this_game_reward.player else None,
|
||||
this_game_reward.money,
|
||||
],
|
||||
]
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = model_to_dict(this_game_reward)
|
||||
return return_val
|
||||
|
||||
|
||||
@router.post('')
|
||||
async def v1_gamerewards_post(game_reward: GameRewardModel, token: str = Depends(oauth2_scheme)):
|
||||
@router.post("")
|
||||
async def v1_gamerewards_post(
|
||||
game_reward: GameRewardModel, token: str = Depends(oauth2_scheme)
|
||||
):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to post game rewards. This event has been logged.'
|
||||
detail="You are not authorized to post game rewards. This event has been logged.",
|
||||
)
|
||||
|
||||
this_award = GameRewards(
|
||||
name=game_reward.name,
|
||||
pack_type_id=game_reward.pack_type_id,
|
||||
player_id=game_reward.player_id,
|
||||
money=game_reward.money
|
||||
money=game_reward.money,
|
||||
)
|
||||
|
||||
saved = this_award.save()
|
||||
@ -104,24 +121,31 @@ async def v1_gamerewards_post(game_reward: GameRewardModel, token: str = Depends
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that roster'
|
||||
detail="Well slap my ass and call me a teapot; I could not save that roster",
|
||||
)
|
||||
|
||||
|
||||
@router.patch('/{game_reward_id}')
|
||||
@router.patch("/{game_reward_id}")
|
||||
async def v1_gamerewards_patch(
|
||||
game_reward_id: int, name: Optional[str] = None, pack_type_id: Optional[int] = None,
|
||||
player_id: Optional[int] = None, money: Optional[int] = None, token: str = Depends(oauth2_scheme)):
|
||||
game_reward_id: int,
|
||||
name: Optional[str] = None,
|
||||
pack_type_id: Optional[int] = None,
|
||||
player_id: Optional[int] = None,
|
||||
money: Optional[int] = None,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to patch gamerewards. This event has been logged.'
|
||||
detail="You are not authorized to patch gamerewards. This event has been logged.",
|
||||
)
|
||||
try:
|
||||
this_game_reward = GameRewards.get_by_id(game_reward_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No game reward found with id {game_reward_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No game reward found with id {game_reward_id}"
|
||||
)
|
||||
|
||||
if name is not None:
|
||||
this_game_reward.name = name
|
||||
@ -147,27 +171,32 @@ async def v1_gamerewards_patch(
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that rarity'
|
||||
detail="Well slap my ass and call me a teapot; I could not save that rarity",
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{gamereward_id}')
|
||||
@router.delete("/{gamereward_id}")
|
||||
async def v1_gamerewards_delete(gamereward_id, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to delete awards. This event has been logged.'
|
||||
detail="You are not authorized to delete awards. This event has been logged.",
|
||||
)
|
||||
try:
|
||||
this_award = GameRewards.get_by_id(gamereward_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No award found with id {gamereward_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No award found with id {gamereward_id}"
|
||||
)
|
||||
|
||||
count = this_award.delete_instance()
|
||||
|
||||
if count == 1:
|
||||
raise HTTPException(status_code=200, detail=f'Game Reward {gamereward_id} has been deleted')
|
||||
raise HTTPException(
|
||||
status_code=200, detail=f"Game Reward {gamereward_id} has been deleted"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'Game Reward {gamereward_id} was not deleted')
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Game Reward {gamereward_id} was not deleted"
|
||||
)
|
||||
|
||||
@ -30,6 +30,7 @@ async def v1_gauntletreward_get(
|
||||
reward_id: list = Query(default=None),
|
||||
win_num: Optional[int] = None,
|
||||
loss_max: Optional[int] = None,
|
||||
limit: int = 100,
|
||||
):
|
||||
all_rewards = GauntletReward.select().order_by(GauntletReward.id)
|
||||
|
||||
@ -46,6 +47,9 @@ async def v1_gauntletreward_get(
|
||||
|
||||
all_rewards = all_rewards.order_by(-GauntletReward.loss_max, GauntletReward.win_num)
|
||||
|
||||
limit = max(0, min(limit, 500))
|
||||
all_rewards = all_rewards.limit(limit)
|
||||
|
||||
return_val = {"count": all_rewards.count(), "rewards": []}
|
||||
for x in all_rewards:
|
||||
return_val["rewards"].append(model_to_dict(x))
|
||||
|
||||
@ -8,10 +8,7 @@ from ..db_engine import GauntletRun, model_to_dict, DatabaseError, DoesNotExist
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/gauntletruns',
|
||||
tags=['notifs']
|
||||
)
|
||||
router = APIRouter(prefix="/api/v2/gauntletruns", tags=["notifs"])
|
||||
|
||||
|
||||
class GauntletRunModel(pydantic.BaseModel):
|
||||
@ -24,13 +21,25 @@ class GauntletRunModel(pydantic.BaseModel):
|
||||
ended: Optional[int] = None
|
||||
|
||||
|
||||
@router.get('')
|
||||
@router.get("")
|
||||
async def get_gauntletruns(
|
||||
team_id: list = Query(default=None), wins: Optional[int] = None, wins_min: Optional[int] = None,
|
||||
wins_max: Optional[int] = None, losses: Optional[int] = None, losses_min: Optional[int] = None,
|
||||
losses_max: Optional[int] = None, gsheet: Optional[str] = None, created_after: Optional[int] = None,
|
||||
created_before: Optional[int] = None, ended_after: Optional[int] = None, ended_before: Optional[int] = None,
|
||||
is_active: Optional[bool] = None, gauntlet_id: list = Query(default=None), season: list = Query(default=None)):
|
||||
team_id: list = Query(default=None),
|
||||
wins: Optional[int] = None,
|
||||
wins_min: Optional[int] = None,
|
||||
wins_max: Optional[int] = None,
|
||||
losses: Optional[int] = None,
|
||||
losses_min: Optional[int] = None,
|
||||
losses_max: Optional[int] = None,
|
||||
gsheet: Optional[str] = None,
|
||||
created_after: Optional[int] = None,
|
||||
created_before: Optional[int] = None,
|
||||
ended_after: Optional[int] = None,
|
||||
ended_before: Optional[int] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
gauntlet_id: list = Query(default=None),
|
||||
season: list = Query(default=None),
|
||||
limit: int = 100,
|
||||
):
|
||||
all_gauntlets = GauntletRun.select().order_by(GauntletRun.id)
|
||||
|
||||
if team_id is not None:
|
||||
@ -73,39 +82,48 @@ async def get_gauntletruns(
|
||||
if season is not None:
|
||||
all_gauntlets = all_gauntlets.where(GauntletRun.team.season << season)
|
||||
|
||||
return_val = {'count': all_gauntlets.count(), 'runs': []}
|
||||
for x in all_gauntlets:
|
||||
return_val['runs'].append(model_to_dict(x))
|
||||
limit = max(0, min(limit, 500))
|
||||
return_val = {"count": all_gauntlets.count(), "runs": []}
|
||||
for x in all_gauntlets.limit(limit):
|
||||
return_val["runs"].append(model_to_dict(x))
|
||||
|
||||
return return_val
|
||||
|
||||
|
||||
@router.get('/{gauntletrun_id}')
|
||||
@router.get("/{gauntletrun_id}")
|
||||
async def get_one_gauntletrun(gauntletrun_id):
|
||||
try:
|
||||
this_gauntlet = GauntletRun.get_by_id(gauntletrun_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No gauntlet found with id {gauntletrun_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No gauntlet found with id {gauntletrun_id}"
|
||||
)
|
||||
|
||||
return_val = model_to_dict(this_gauntlet)
|
||||
return return_val
|
||||
|
||||
|
||||
@router.patch('/{gauntletrun_id}')
|
||||
@router.patch("/{gauntletrun_id}")
|
||||
async def patch_gauntletrun(
|
||||
gauntletrun_id, team_id: Optional[int] = None, wins: Optional[int] = None, losses: Optional[int] = None,
|
||||
gsheet: Optional[str] = None, created: Optional[bool] = None, ended: Optional[bool] = None,
|
||||
token: str = Depends(oauth2_scheme)):
|
||||
gauntletrun_id,
|
||||
team_id: Optional[int] = None,
|
||||
wins: Optional[int] = None,
|
||||
losses: Optional[int] = None,
|
||||
gsheet: Optional[str] = None,
|
||||
created: Optional[bool] = None,
|
||||
ended: Optional[bool] = None,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to patch gauntlet runs. This event has been logged.'
|
||||
detail="You are not authorized to patch gauntlet runs. This event has been logged.",
|
||||
)
|
||||
|
||||
this_run = GauntletRun.get_or_none(GauntletRun.id == gauntletrun_id)
|
||||
if this_run is None:
|
||||
raise KeyError(f'Gauntlet Run ID {gauntletrun_id} not found')
|
||||
raise KeyError(f"Gauntlet Run ID {gauntletrun_id} not found")
|
||||
|
||||
if team_id is not None:
|
||||
this_run.team_id = team_id
|
||||
@ -130,41 +148,42 @@ async def patch_gauntletrun(
|
||||
r_curr = model_to_dict(this_run)
|
||||
return r_curr
|
||||
else:
|
||||
raise DatabaseError(f'Unable to patch gauntlet run {gauntletrun_id}')
|
||||
raise DatabaseError(f"Unable to patch gauntlet run {gauntletrun_id}")
|
||||
|
||||
|
||||
@router.post('')
|
||||
async def post_gauntletrun(gauntletrun: GauntletRunModel, token: str = Depends(oauth2_scheme)):
|
||||
@router.post("")
|
||||
async def post_gauntletrun(
|
||||
gauntletrun: GauntletRunModel, token: str = Depends(oauth2_scheme)
|
||||
):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to post gauntlets. This event has been logged.'
|
||||
detail="You are not authorized to post gauntlets. This event has been logged.",
|
||||
)
|
||||
|
||||
run_data = gauntletrun.dict()
|
||||
# Convert milliseconds timestamps to datetime for PostgreSQL
|
||||
if run_data.get('created'):
|
||||
run_data['created'] = datetime.fromtimestamp(run_data['created'] / 1000)
|
||||
if run_data.get("created"):
|
||||
run_data["created"] = datetime.fromtimestamp(run_data["created"] / 1000)
|
||||
else:
|
||||
run_data['created'] = datetime.now()
|
||||
if run_data.get('ended'):
|
||||
run_data['ended'] = datetime.fromtimestamp(run_data['ended'] / 1000)
|
||||
run_data["created"] = datetime.now()
|
||||
if run_data.get("ended"):
|
||||
run_data["ended"] = datetime.fromtimestamp(run_data["ended"] / 1000)
|
||||
else:
|
||||
run_data['ended'] = None
|
||||
run_data["ended"] = None
|
||||
this_run = GauntletRun(**run_data)
|
||||
|
||||
if this_run.save():
|
||||
r_run = model_to_dict(this_run)
|
||||
return r_run
|
||||
else:
|
||||
raise DatabaseError(f'Unable to post gauntlet run')
|
||||
raise DatabaseError("Unable to post gauntlet run")
|
||||
|
||||
|
||||
@router.delete('/{gauntletrun_id}')
|
||||
@router.delete("/{gauntletrun_id}")
|
||||
async def delete_gauntletrun(gauntletrun_id):
|
||||
if GauntletRun.delete_by_id(gauntletrun_id) == 1:
|
||||
return f'Deleted gauntlet run ID {gauntletrun_id}'
|
||||
|
||||
raise DatabaseError(f'Unable to delete gauntlet run {gauntletrun_id}')
|
||||
return f"Deleted gauntlet run ID {gauntletrun_id}"
|
||||
|
||||
raise DatabaseError(f"Unable to delete gauntlet run {gauntletrun_id}")
|
||||
|
||||
@ -73,6 +73,7 @@ async def get_players(
|
||||
key_mlbam: list = Query(default=None),
|
||||
offense_col: list = Query(default=None),
|
||||
csv: Optional[bool] = False,
|
||||
limit: int = 100,
|
||||
):
|
||||
all_players = MlbPlayer.select().order_by(MlbPlayer.id)
|
||||
|
||||
@ -101,6 +102,8 @@ async def get_players(
|
||||
if offense_col is not None:
|
||||
all_players = all_players.where(MlbPlayer.offense_col << offense_col)
|
||||
|
||||
all_players = all_players.limit(max(0, min(limit, 500)))
|
||||
|
||||
if csv:
|
||||
return_val = query_to_csv(all_players)
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
@ -222,7 +225,7 @@ async def post_one_player(player: PlayerModel, token: str = Depends(oauth2_schem
|
||||
| (MlbPlayer.key_bbref == player.key_bbref)
|
||||
)
|
||||
if dupes.count() > 0:
|
||||
logging.info(f"POST /mlbplayers/one - dupes found:")
|
||||
logging.info("POST /mlbplayers/one - dupes found:")
|
||||
for x in dupes:
|
||||
logging.info(f"{x}")
|
||||
raise HTTPException(
|
||||
|
||||
@ -143,6 +143,7 @@ async def get_card_ratings(
|
||||
short_output: bool = False,
|
||||
csv: bool = False,
|
||||
cardset_id: list = Query(default=None),
|
||||
limit: int = 100,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
):
|
||||
if not valid_token(token):
|
||||
@ -168,6 +169,8 @@ async def get_card_ratings(
|
||||
)
|
||||
all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard << set_cards)
|
||||
|
||||
all_ratings = all_ratings.limit(max(0, min(limit, 500)))
|
||||
|
||||
if csv:
|
||||
return_val = query_to_csv(all_ratings)
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
@ -231,10 +234,10 @@ def get_scouting_dfs(cardset_id: list = None):
|
||||
|
||||
series_list = [
|
||||
pd.Series(
|
||||
dict([(x.player.player_id, x.range) for x in positions]), name=f"Range P"
|
||||
dict([(x.player.player_id, x.range) for x in positions]), name="Range P"
|
||||
),
|
||||
pd.Series(
|
||||
dict([(x.player.player_id, x.error) for x in positions]), name=f"Error P"
|
||||
dict([(x.player.player_id, x.error) for x in positions]), name="Error P"
|
||||
),
|
||||
]
|
||||
logging.debug(f"series_list: {series_list}")
|
||||
@ -274,7 +277,7 @@ async def post_calc_scouting(token: str = Depends(oauth2_scheme)):
|
||||
status_code=401, detail="You are not authorized to calculate card ratings."
|
||||
)
|
||||
|
||||
logging.warning(f"Re-calculating pitching ratings\n\n")
|
||||
logging.warning("Re-calculating pitching ratings\n\n")
|
||||
|
||||
output = get_scouting_dfs()
|
||||
first = ["player_id", "player_name", "cardset_name", "rarity", "hand", "variant"]
|
||||
@ -310,7 +313,7 @@ async def post_calc_basic(token: str = Depends(oauth2_scheme)):
|
||||
status_code=401, detail="You are not authorized to calculate basic ratings."
|
||||
)
|
||||
|
||||
logging.warning(f"Re-calculating basic pitching ratings\n\n")
|
||||
logging.warning("Re-calculating basic pitching ratings\n\n")
|
||||
|
||||
raw_data = get_scouting_dfs()
|
||||
logging.debug(f"output: {raw_data}")
|
||||
|
||||
@ -5,14 +5,19 @@ import logging
|
||||
import pydantic
|
||||
from pandas import DataFrame
|
||||
|
||||
from ..db_engine import db, PitchingStat, model_to_dict, Card, Player, Current, DoesNotExist
|
||||
from ..db_engine import (
|
||||
db,
|
||||
PitchingStat,
|
||||
model_to_dict,
|
||||
Card,
|
||||
Player,
|
||||
Current,
|
||||
DoesNotExist,
|
||||
)
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/pitstats',
|
||||
tags=['pitstats']
|
||||
)
|
||||
router = APIRouter(prefix="/api/v2/pitstats", tags=["pitstats"])
|
||||
|
||||
|
||||
class PitStat(pydantic.BaseModel):
|
||||
@ -40,7 +45,7 @@ class PitStat(pydantic.BaseModel):
|
||||
bsv: Optional[int] = 0
|
||||
week: int
|
||||
season: int
|
||||
created: Optional[int] = int(datetime.timestamp(datetime.now())*1000)
|
||||
created: Optional[int] = int(datetime.timestamp(datetime.now()) * 1000)
|
||||
game_id: int
|
||||
|
||||
|
||||
@ -48,13 +53,23 @@ class PitchingStatModel(pydantic.BaseModel):
|
||||
stats: List[PitStat]
|
||||
|
||||
|
||||
@router.get('')
|
||||
@router.get("")
|
||||
async def get_pit_stats(
|
||||
card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None,
|
||||
season: int = None, week_start: int = None, week_end: int = None, created: int = None, gs: bool = None,
|
||||
csv: bool = None):
|
||||
card_id: int = None,
|
||||
player_id: int = None,
|
||||
team_id: int = None,
|
||||
vs_team_id: int = None,
|
||||
week: int = None,
|
||||
season: int = None,
|
||||
week_start: int = None,
|
||||
week_end: int = None,
|
||||
created: int = None,
|
||||
gs: bool = None,
|
||||
csv: bool = None,
|
||||
limit: Optional[int] = 100,
|
||||
):
|
||||
all_stats = PitchingStat.select().join(Card).join(Player).order_by(PitchingStat.id)
|
||||
logging.debug(f'pit query:\n\n{all_stats}')
|
||||
logging.debug(f"pit query:\n\n{all_stats}")
|
||||
|
||||
if season is not None:
|
||||
all_stats = all_stats.where(PitchingStat.season == season)
|
||||
@ -83,43 +98,99 @@ async def get_pit_stats(
|
||||
if gs is not None:
|
||||
all_stats = all_stats.where(PitchingStat.gs == 1 if gs else 0)
|
||||
|
||||
all_stats = all_stats.limit(max(0, min(limit, 500)))
|
||||
|
||||
# if all_stats.count() == 0:
|
||||
# db.close()
|
||||
# raise HTTPException(status_code=404, detail=f'No pitching stats found')
|
||||
|
||||
if csv:
|
||||
data_list = [['id', 'card_id', 'player_id', 'cardset', 'team', 'vs_team', 'ip', 'hit', 'run', 'erun', 'so', 'bb', 'hbp',
|
||||
'wp', 'balk', 'hr', 'ir', 'irs', 'gs', 'win', 'loss', 'hold', 'sv', 'bsv', 'week', 'season',
|
||||
'created', 'game_id', 'roster_num']]
|
||||
data_list = [
|
||||
[
|
||||
"id",
|
||||
"card_id",
|
||||
"player_id",
|
||||
"cardset",
|
||||
"team",
|
||||
"vs_team",
|
||||
"ip",
|
||||
"hit",
|
||||
"run",
|
||||
"erun",
|
||||
"so",
|
||||
"bb",
|
||||
"hbp",
|
||||
"wp",
|
||||
"balk",
|
||||
"hr",
|
||||
"ir",
|
||||
"irs",
|
||||
"gs",
|
||||
"win",
|
||||
"loss",
|
||||
"hold",
|
||||
"sv",
|
||||
"bsv",
|
||||
"week",
|
||||
"season",
|
||||
"created",
|
||||
"game_id",
|
||||
"roster_num",
|
||||
]
|
||||
]
|
||||
for line in all_stats:
|
||||
data_list.append(
|
||||
[
|
||||
line.id, line.card.id, line.card.player.player_id, line.card.player.cardset.name, line.team.abbrev,
|
||||
line.vs_team.abbrev, line.ip, line.hit,
|
||||
line.run, line.erun, line.so, line.bb, line.hbp, line.wp, line.balk, line.hr, line.ir, line.irs,
|
||||
line.gs, line.win, line.loss, line.hold, line.sv, line.bsv, line.week, line.season, line.created,
|
||||
line.game_id, line.roster_num
|
||||
line.id,
|
||||
line.card.id,
|
||||
line.card.player.player_id,
|
||||
line.card.player.cardset.name,
|
||||
line.team.abbrev,
|
||||
line.vs_team.abbrev,
|
||||
line.ip,
|
||||
line.hit,
|
||||
line.run,
|
||||
line.erun,
|
||||
line.so,
|
||||
line.bb,
|
||||
line.hbp,
|
||||
line.wp,
|
||||
line.balk,
|
||||
line.hr,
|
||||
line.ir,
|
||||
line.irs,
|
||||
line.gs,
|
||||
line.win,
|
||||
line.loss,
|
||||
line.hold,
|
||||
line.sv,
|
||||
line.bsv,
|
||||
line.week,
|
||||
line.season,
|
||||
line.created,
|
||||
line.game_id,
|
||||
line.roster_num,
|
||||
]
|
||||
)
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = {'count': all_stats.count(), 'stats': []}
|
||||
return_val = {"count": all_stats.count(), "stats": []}
|
||||
for x in all_stats:
|
||||
return_val['stats'].append(model_to_dict(x, recurse=False))
|
||||
return_val["stats"].append(model_to_dict(x, recurse=False))
|
||||
|
||||
return return_val
|
||||
|
||||
|
||||
@router.post('')
|
||||
@router.post("")
|
||||
async def post_pitstat(stats: PitchingStatModel, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to post stats. This event has been logged.'
|
||||
detail="You are not authorized to post stats. This event has been logged.",
|
||||
)
|
||||
|
||||
new_stats = []
|
||||
@ -149,33 +220,37 @@ async def post_pitstat(stats: PitchingStatModel, token: str = Depends(oauth2_sch
|
||||
bsv=x.bsv,
|
||||
week=x.week,
|
||||
season=x.season,
|
||||
created=datetime.fromtimestamp(x.created / 1000) if x.created else datetime.now(),
|
||||
game_id=x.game_id
|
||||
created=datetime.fromtimestamp(x.created / 1000)
|
||||
if x.created
|
||||
else datetime.now(),
|
||||
game_id=x.game_id,
|
||||
)
|
||||
new_stats.append(this_stat)
|
||||
|
||||
with db.atomic():
|
||||
PitchingStat.bulk_create(new_stats, batch_size=15)
|
||||
|
||||
raise HTTPException(status_code=200, detail=f'{len(new_stats)} pitching lines have been added')
|
||||
raise HTTPException(
|
||||
status_code=200, detail=f"{len(new_stats)} pitching lines have been added"
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{stat_id}')
|
||||
@router.delete("/{stat_id}")
|
||||
async def delete_pitstat(stat_id, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to delete stats. This event has been logged.'
|
||||
detail="You are not authorized to delete stats. This event has been logged.",
|
||||
)
|
||||
try:
|
||||
this_stat = PitchingStat.get_by_id(stat_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No stat found with id {stat_id}')
|
||||
raise HTTPException(status_code=404, detail=f"No stat found with id {stat_id}")
|
||||
|
||||
count = this_stat.delete_instance()
|
||||
|
||||
if count == 1:
|
||||
raise HTTPException(status_code=200, detail=f'Stat {stat_id} has been deleted')
|
||||
raise HTTPException(status_code=200, detail=f"Stat {stat_id} has been deleted")
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'Stat {stat_id} was not deleted')
|
||||
raise HTTPException(status_code=500, detail=f"Stat {stat_id} was not deleted")
|
||||
|
||||
@ -7,10 +7,10 @@ from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v2/evolution", tags=["evolution"])
|
||||
router = APIRouter(prefix="/api/v2/refractor", tags=["refractor"])
|
||||
|
||||
# Tier -> threshold attribute name. Index = current_tier; value is the
|
||||
# attribute on EvolutionTrack whose value is the *next* threshold to reach.
|
||||
# attribute on RefractorTrack 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",
|
||||
@ -22,7 +22,7 @@ _NEXT_THRESHOLD_ATTR = {
|
||||
|
||||
|
||||
def _build_card_state_response(state) -> dict:
|
||||
"""Serialise an EvolutionCardState 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,
|
||||
a nested 'track' dict with all threshold fields, and a computed
|
||||
@ -63,11 +63,11 @@ async def list_tracks(
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
from ..db_engine import EvolutionTrack
|
||||
from ..db_engine import RefractorTrack
|
||||
|
||||
query = EvolutionTrack.select()
|
||||
query = RefractorTrack.select()
|
||||
if card_type is not None:
|
||||
query = query.where(EvolutionTrack.card_type == card_type)
|
||||
query = query.where(RefractorTrack.card_type == card_type)
|
||||
|
||||
items = [model_to_dict(t, recurse=False) for t in query]
|
||||
return {"count": len(items), "items": items}
|
||||
@ -79,10 +79,10 @@ async def get_track(track_id: int, token: str = Depends(oauth2_scheme)):
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
from ..db_engine import EvolutionTrack
|
||||
from ..db_engine import RefractorTrack
|
||||
|
||||
try:
|
||||
track = EvolutionTrack.get_by_id(track_id)
|
||||
track = RefractorTrack.get_by_id(track_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail=f"Track {track_id} not found")
|
||||
|
||||
@ -91,22 +91,22 @@ async def get_track(track_id: int, token: str = Depends(oauth2_scheme)):
|
||||
|
||||
@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.
|
||||
"""Return the RefractorCardState 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
|
||||
up the matching RefractorCardState 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.
|
||||
- The card exists but has no corresponding RefractorCardState 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
|
||||
from ..db_engine import Card, RefractorCardState, RefractorTrack, DoesNotExist
|
||||
|
||||
# Resolve card_id to player+team
|
||||
try:
|
||||
@ -114,22 +114,22 @@ async def get_card_state(card_id: int, token: str = Depends(oauth2_scheme)):
|
||||
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
|
||||
# Look up the refractor state for this (player, team) pair, joining the
|
||||
# track so a single query resolves both rows.
|
||||
try:
|
||||
state = (
|
||||
EvolutionCardState.select(EvolutionCardState, EvolutionTrack)
|
||||
.join(EvolutionTrack)
|
||||
RefractorCardState.select(RefractorCardState, RefractorTrack)
|
||||
.join(RefractorTrack)
|
||||
.where(
|
||||
(EvolutionCardState.player == card.player_id)
|
||||
& (EvolutionCardState.team == card.team_id)
|
||||
(RefractorCardState.player == card.player_id)
|
||||
& (RefractorCardState.team == card.team_id)
|
||||
)
|
||||
.get()
|
||||
)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No evolution state for card {card_id}",
|
||||
detail=f"No refractor state for card {card_id}",
|
||||
)
|
||||
|
||||
return _build_card_state_response(state)
|
||||
@ -137,9 +137,9 @@ async def get_card_state(card_id: int, token: str = Depends(oauth2_scheme)):
|
||||
|
||||
@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.
|
||||
"""Force-recalculate refractor state for a card from career stats.
|
||||
|
||||
Resolves card_id to (player_id, team_id), then recomputes the evolution
|
||||
Resolves card_id to (player_id, team_id), then recomputes the refractor
|
||||
tier from all player_season_stats rows for that pair. Idempotent.
|
||||
"""
|
||||
if not valid_token(token):
|
||||
@ -147,7 +147,7 @@ async def evaluate_card(card_id: int, token: str = Depends(oauth2_scheme)):
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
from ..db_engine import Card
|
||||
from ..services.evolution_evaluator import evaluate_card as _evaluate
|
||||
from ..services.refractor_evaluator import evaluate_card as _evaluate
|
||||
|
||||
try:
|
||||
card = Card.get_by_id(card_id)
|
||||
@ -164,10 +164,10 @@ async def evaluate_card(card_id: int, token: str = Depends(oauth2_scheme)):
|
||||
|
||||
@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.
|
||||
"""Evaluate refractor 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
|
||||
then for each pair that has a RefractorCardState, re-computes the refractor
|
||||
tier. Pairs without a state row are silently skipped. Per-player errors are
|
||||
logged but do not abort the batch.
|
||||
"""
|
||||
@ -175,8 +175,8 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
||||
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
|
||||
from ..db_engine import RefractorCardState, RefractorTrack, Player, StratPlay
|
||||
from ..services.refractor_evaluator import evaluate_card
|
||||
|
||||
plays = list(StratPlay.select().where(StratPlay.game == game_id))
|
||||
|
||||
@ -192,9 +192,9 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
||||
|
||||
for player_id, team_id in pairs:
|
||||
try:
|
||||
state = EvolutionCardState.get_or_none(
|
||||
(EvolutionCardState.player_id == player_id)
|
||||
& (EvolutionCardState.team_id == team_id)
|
||||
state = RefractorCardState.get_or_none(
|
||||
(RefractorCardState.player_id == player_id)
|
||||
& (RefractorCardState.team_id == team_id)
|
||||
)
|
||||
if state is None:
|
||||
continue
|
||||
@ -225,7 +225,7 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
f"Evolution eval failed for player={player_id} team={team_id}: {exc}"
|
||||
f"Refractor eval failed for player={player_id} team={team_id}: {exc}"
|
||||
)
|
||||
|
||||
return {"evaluated": evaluated, "tier_ups": tier_ups}
|
||||
@ -8,10 +8,7 @@ from ..db_engine import Result, model_to_dict, Team, DataError, DoesNotExist
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/results',
|
||||
tags=['results']
|
||||
)
|
||||
router = APIRouter(prefix="/api/v2/results", tags=["results"])
|
||||
|
||||
|
||||
class ResultModel(pydantic.BaseModel):
|
||||
@ -31,15 +28,29 @@ class ResultModel(pydantic.BaseModel):
|
||||
game_type: str
|
||||
|
||||
|
||||
@router.get('')
|
||||
@router.get("")
|
||||
async def get_results(
|
||||
away_team_id: Optional[int] = None, home_team_id: Optional[int] = None, team_one_id: Optional[int] = None,
|
||||
team_two_id: Optional[int] = None, away_score_min: Optional[int] = None, away_score_max: Optional[int] = None,
|
||||
home_score_min: Optional[int] = None, home_score_max: Optional[int] = None, bothscore_min: Optional[int] = None,
|
||||
bothscore_max: Optional[int] = None, season: Optional[int] = None, week: Optional[int] = None,
|
||||
week_start: Optional[int] = None, week_end: Optional[int] = None, ranked: Optional[bool] = None,
|
||||
short_game: Optional[bool] = None, game_type: Optional[str] = None, vs_ai: Optional[bool] = None,
|
||||
csv: Optional[bool] = None):
|
||||
away_team_id: Optional[int] = None,
|
||||
home_team_id: Optional[int] = None,
|
||||
team_one_id: Optional[int] = None,
|
||||
team_two_id: Optional[int] = None,
|
||||
away_score_min: Optional[int] = None,
|
||||
away_score_max: Optional[int] = None,
|
||||
home_score_min: Optional[int] = None,
|
||||
home_score_max: Optional[int] = None,
|
||||
bothscore_min: Optional[int] = None,
|
||||
bothscore_max: Optional[int] = None,
|
||||
season: Optional[int] = None,
|
||||
week: Optional[int] = None,
|
||||
week_start: Optional[int] = None,
|
||||
week_end: Optional[int] = None,
|
||||
ranked: Optional[bool] = None,
|
||||
short_game: Optional[bool] = None,
|
||||
game_type: Optional[str] = None,
|
||||
vs_ai: Optional[bool] = None,
|
||||
csv: Optional[bool] = None,
|
||||
limit: int = 100,
|
||||
):
|
||||
all_results = Result.select()
|
||||
|
||||
# if all_results.count() == 0:
|
||||
@ -51,28 +62,40 @@ async def get_results(
|
||||
this_team = Team.get_by_id(away_team_id)
|
||||
all_results = all_results.where(Result.away_team == this_team)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No team found with id {away_team_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No team found with id {away_team_id}"
|
||||
)
|
||||
|
||||
if home_team_id is not None:
|
||||
try:
|
||||
this_team = Team.get_by_id(home_team_id)
|
||||
all_results = all_results.where(Result.home_team == this_team)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No team found with id {home_team_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No team found with id {home_team_id}"
|
||||
)
|
||||
|
||||
if team_one_id is not None:
|
||||
try:
|
||||
this_team = Team.get_by_id(team_one_id)
|
||||
all_results = all_results.where((Result.home_team == this_team) | (Result.away_team == this_team))
|
||||
all_results = all_results.where(
|
||||
(Result.home_team == this_team) | (Result.away_team == this_team)
|
||||
)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No team found with id {team_one_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No team found with id {team_one_id}"
|
||||
)
|
||||
|
||||
if team_two_id is not None:
|
||||
try:
|
||||
this_team = Team.get_by_id(team_two_id)
|
||||
all_results = all_results.where((Result.home_team == this_team) | (Result.away_team == this_team))
|
||||
all_results = all_results.where(
|
||||
(Result.home_team == this_team) | (Result.away_team == this_team)
|
||||
)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No team found with id {team_two_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No team found with id {team_two_id}"
|
||||
)
|
||||
|
||||
if away_score_min is not None:
|
||||
all_results = all_results.where(Result.away_score >= away_score_min)
|
||||
@ -87,10 +110,14 @@ async def get_results(
|
||||
all_results = all_results.where(Result.home_score <= home_score_max)
|
||||
|
||||
if bothscore_min is not None:
|
||||
all_results = all_results.where((Result.home_score >= bothscore_min) & (Result.away_score >= bothscore_min))
|
||||
all_results = all_results.where(
|
||||
(Result.home_score >= bothscore_min) & (Result.away_score >= bothscore_min)
|
||||
)
|
||||
|
||||
if bothscore_max is not None:
|
||||
all_results = all_results.where((Result.home_score <= bothscore_max) & (Result.away_score <= bothscore_max))
|
||||
all_results = all_results.where(
|
||||
(Result.home_score <= bothscore_max) & (Result.away_score <= bothscore_max)
|
||||
)
|
||||
|
||||
if season is not None:
|
||||
all_results = all_results.where(Result.season == season)
|
||||
@ -114,6 +141,8 @@ async def get_results(
|
||||
all_results = all_results.where(Result.game_type == game_type)
|
||||
|
||||
all_results = all_results.order_by(Result.id)
|
||||
limit = max(0, min(limit, 500))
|
||||
all_results = all_results.limit(limit)
|
||||
# Not functional
|
||||
# if vs_ai is not None:
|
||||
# AwayTeam = Team.alias()
|
||||
@ -134,60 +163,115 @@ async def get_results(
|
||||
# logging.info(f'Result Query:\n\n{all_results}')
|
||||
|
||||
if csv:
|
||||
data_list = [['id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_tv', 'home_tv',
|
||||
'game_type', 'season', 'week', 'short_game', 'ranked']]
|
||||
data_list = [
|
||||
[
|
||||
"id",
|
||||
"away_abbrev",
|
||||
"home_abbrev",
|
||||
"away_score",
|
||||
"home_score",
|
||||
"away_tv",
|
||||
"home_tv",
|
||||
"game_type",
|
||||
"season",
|
||||
"week",
|
||||
"short_game",
|
||||
"ranked",
|
||||
]
|
||||
]
|
||||
for line in all_results:
|
||||
data_list.append([
|
||||
line.id, line.away_team.abbrev, line.home_team.abbrev, line.away_score, line.home_score,
|
||||
line.away_team_value, line.home_team_value, line.game_type if line.game_type else 'minor-league',
|
||||
line.season, line.week, line.short_game, line.ranked
|
||||
])
|
||||
data_list.append(
|
||||
[
|
||||
line.id,
|
||||
line.away_team.abbrev,
|
||||
line.home_team.abbrev,
|
||||
line.away_score,
|
||||
line.home_score,
|
||||
line.away_team_value,
|
||||
line.home_team_value,
|
||||
line.game_type if line.game_type else "minor-league",
|
||||
line.season,
|
||||
line.week,
|
||||
line.short_game,
|
||||
line.ranked,
|
||||
]
|
||||
)
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = {'count': all_results.count(), 'results': []}
|
||||
return_val = {"count": all_results.count(), "results": []}
|
||||
for x in all_results:
|
||||
return_val['results'].append(model_to_dict(x))
|
||||
return_val["results"].append(model_to_dict(x))
|
||||
|
||||
return return_val
|
||||
|
||||
|
||||
@router.get('/{result_id}')
|
||||
@router.get("/{result_id}")
|
||||
async def get_one_results(result_id, csv: Optional[bool] = None):
|
||||
try:
|
||||
this_result = Result.get_by_id(result_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No result found with id {result_id}"
|
||||
)
|
||||
|
||||
if csv:
|
||||
data_list = [
|
||||
['id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_tv', 'home_tv', 'game_type',
|
||||
'season', 'week', 'game_type'],
|
||||
[this_result.id, this_result.away_team.abbrev, this_result.away_team.abbrev, this_result.away_score,
|
||||
this_result.home_score, this_result.away_team_value, this_result.home_team_value,
|
||||
this_result.game_type if this_result.game_type else 'minor-league',
|
||||
this_result.season, this_result.week, this_result.game_type]
|
||||
[
|
||||
"id",
|
||||
"away_abbrev",
|
||||
"home_abbrev",
|
||||
"away_score",
|
||||
"home_score",
|
||||
"away_tv",
|
||||
"home_tv",
|
||||
"game_type",
|
||||
"season",
|
||||
"week",
|
||||
"game_type",
|
||||
],
|
||||
[
|
||||
this_result.id,
|
||||
this_result.away_team.abbrev,
|
||||
this_result.away_team.abbrev,
|
||||
this_result.away_score,
|
||||
this_result.home_score,
|
||||
this_result.away_team_value,
|
||||
this_result.home_team_value,
|
||||
this_result.game_type if this_result.game_type else "minor-league",
|
||||
this_result.season,
|
||||
this_result.week,
|
||||
this_result.game_type,
|
||||
],
|
||||
]
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = model_to_dict(this_result)
|
||||
return return_val
|
||||
|
||||
|
||||
@router.get('/team/{team_id}')
|
||||
@router.get("/team/{team_id}")
|
||||
async def get_team_results(
|
||||
team_id: int, season: Optional[int] = None, week: Optional[int] = None, csv: Optional[bool] = False):
|
||||
all_results = Result.select().where((Result.away_team_id == team_id) | (Result.home_team_id == team_id)).order_by(Result.id)
|
||||
team_id: int,
|
||||
season: Optional[int] = None,
|
||||
week: Optional[int] = None,
|
||||
csv: Optional[bool] = False,
|
||||
):
|
||||
all_results = (
|
||||
Result.select()
|
||||
.where((Result.away_team_id == team_id) | (Result.home_team_id == team_id))
|
||||
.order_by(Result.id)
|
||||
)
|
||||
try:
|
||||
this_team = Team.get_by_id(team_id)
|
||||
except DoesNotExist as e:
|
||||
logging.error(f'Unknown team id {team_id} trying to pull team results')
|
||||
raise HTTPException(404, f'Team id {team_id} not found')
|
||||
except DoesNotExist:
|
||||
logging.error(f"Unknown team id {team_id} trying to pull team results")
|
||||
raise HTTPException(404, f"Team id {team_id} not found")
|
||||
|
||||
if season is not None:
|
||||
all_results = all_results.where(Result.season == season)
|
||||
@ -224,31 +308,38 @@ async def get_team_results(
|
||||
|
||||
if csv:
|
||||
data_list = [
|
||||
['team_id', 'ranked_wins', 'ranked_losses', 'casual_wins', 'casual_losses', 'team_ranking'],
|
||||
[team_id, r_wins, r_loss, c_wins, c_loss, this_team.ranking]
|
||||
[
|
||||
"team_id",
|
||||
"ranked_wins",
|
||||
"ranked_losses",
|
||||
"casual_wins",
|
||||
"casual_losses",
|
||||
"team_ranking",
|
||||
],
|
||||
[team_id, r_wins, r_loss, c_wins, c_loss, this_team.ranking],
|
||||
]
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = {
|
||||
'team': model_to_dict(this_team),
|
||||
'ranked_wins': r_wins,
|
||||
'ranked_losses': r_loss,
|
||||
'casual_wins': c_wins,
|
||||
'casual_losses': c_loss,
|
||||
"team": model_to_dict(this_team),
|
||||
"ranked_wins": r_wins,
|
||||
"ranked_losses": r_loss,
|
||||
"casual_wins": c_wins,
|
||||
"casual_losses": c_loss,
|
||||
}
|
||||
return return_val
|
||||
|
||||
|
||||
@router.post('')
|
||||
@router.post("")
|
||||
async def post_result(result: ResultModel, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to post results. This event has been logged.'
|
||||
detail="You are not authorized to post results. This event has been logged.",
|
||||
)
|
||||
|
||||
this_result = Result(**result.__dict__)
|
||||
@ -256,24 +347,28 @@ async def post_result(result: ResultModel, token: str = Depends(oauth2_scheme)):
|
||||
|
||||
if result.ranked:
|
||||
if not result.away_team_ranking:
|
||||
error = f'Ranked game did not include away team ({result.away_team_id}) ranking.'
|
||||
error = f"Ranked game did not include away team ({result.away_team_id}) ranking."
|
||||
logging.error(error)
|
||||
raise DataError(error)
|
||||
if not result.home_team_ranking:
|
||||
error = f'Ranked game did not include home team ({result.home_team_id}) ranking.'
|
||||
error = f"Ranked game did not include home team ({result.home_team_id}) ranking."
|
||||
logging.error(error)
|
||||
raise DataError(error)
|
||||
|
||||
k_value = 20 if result.short_game else 60
|
||||
ratio = (result.home_team_ranking - result.away_team_ranking) / 400
|
||||
exp_score = 1 / (1 + (10 ** ratio))
|
||||
exp_score = 1 / (1 + (10**ratio))
|
||||
away_win = True if result.away_score > result.home_score else False
|
||||
total_delta = k_value * exp_score
|
||||
high_delta = total_delta * exp_score if exp_score > .5 else total_delta * (1 - exp_score)
|
||||
high_delta = (
|
||||
total_delta * exp_score
|
||||
if exp_score > 0.5
|
||||
else total_delta * (1 - exp_score)
|
||||
)
|
||||
low_delta = total_delta - high_delta
|
||||
|
||||
# exp_score > .5 means away team is favorite
|
||||
if exp_score > .5 and away_win:
|
||||
if exp_score > 0.5 and away_win:
|
||||
final_delta = low_delta
|
||||
away_delta = low_delta * 3
|
||||
home_delta = -low_delta
|
||||
@ -281,7 +376,7 @@ async def post_result(result: ResultModel, token: str = Depends(oauth2_scheme)):
|
||||
final_delta = high_delta
|
||||
away_delta = high_delta * 3
|
||||
home_delta = -high_delta
|
||||
elif exp_score <= .5 and not away_win:
|
||||
elif exp_score <= 0.5 and not away_win:
|
||||
final_delta = low_delta
|
||||
away_delta = -low_delta
|
||||
home_delta = low_delta * 3
|
||||
@ -294,18 +389,20 @@ async def post_result(result: ResultModel, token: str = Depends(oauth2_scheme)):
|
||||
away_delta = 0
|
||||
home_delta = 0
|
||||
|
||||
logging.debug(f'/results ranking deltas\n\nk_value: {k_value} / ratio: {ratio} / '
|
||||
f'exp_score: {exp_score} / away_win: {away_win} / total_delta: {total_delta} / '
|
||||
f'high_delta: {high_delta} / low_delta: {low_delta} / final_delta: {final_delta} / ')
|
||||
logging.debug(
|
||||
f"/results ranking deltas\n\nk_value: {k_value} / ratio: {ratio} / "
|
||||
f"exp_score: {exp_score} / away_win: {away_win} / total_delta: {total_delta} / "
|
||||
f"high_delta: {high_delta} / low_delta: {low_delta} / final_delta: {final_delta} / "
|
||||
)
|
||||
|
||||
away_team = Team.get_by_id(result.away_team_id)
|
||||
away_team.ranking += away_delta
|
||||
away_team.save()
|
||||
logging.info(f'Just updated {away_team.abbrev} ranking to {away_team.ranking}')
|
||||
logging.info(f"Just updated {away_team.abbrev} ranking to {away_team.ranking}")
|
||||
home_team = Team.get_by_id(result.home_team_id)
|
||||
home_team.ranking += home_delta
|
||||
home_team.save()
|
||||
logging.info(f'Just updated {home_team.abbrev} ranking to {home_team.ranking}')
|
||||
logging.info(f"Just updated {home_team.abbrev} ranking to {home_team.ranking}")
|
||||
|
||||
if saved == 1:
|
||||
return_val = model_to_dict(this_result)
|
||||
@ -313,27 +410,38 @@ async def post_result(result: ResultModel, token: str = Depends(oauth2_scheme)):
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that roster'
|
||||
detail="Well slap my ass and call me a teapot; I could not save that roster",
|
||||
)
|
||||
|
||||
|
||||
@router.patch('/{result_id}')
|
||||
@router.patch("/{result_id}")
|
||||
async def patch_result(
|
||||
result_id, away_team_id: Optional[int] = None, home_team_id: Optional[int] = None,
|
||||
away_score: Optional[int] = None, home_score: Optional[int] = None, away_team_value: Optional[int] = None,
|
||||
home_team_value: Optional[int] = None, scorecard: Optional[str] = None, week: Optional[int] = None,
|
||||
season: Optional[int] = None, short_game: Optional[bool] = None, game_type: Optional[str] = None,
|
||||
token: str = Depends(oauth2_scheme)):
|
||||
result_id,
|
||||
away_team_id: Optional[int] = None,
|
||||
home_team_id: Optional[int] = None,
|
||||
away_score: Optional[int] = None,
|
||||
home_score: Optional[int] = None,
|
||||
away_team_value: Optional[int] = None,
|
||||
home_team_value: Optional[int] = None,
|
||||
scorecard: Optional[str] = None,
|
||||
week: Optional[int] = None,
|
||||
season: Optional[int] = None,
|
||||
short_game: Optional[bool] = None,
|
||||
game_type: Optional[str] = None,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to patch results. This event has been logged.'
|
||||
detail="You are not authorized to patch results. This event has been logged.",
|
||||
)
|
||||
try:
|
||||
this_result = Result.get_by_id(result_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No result found with id {result_id}"
|
||||
)
|
||||
|
||||
if away_team_id is not None:
|
||||
this_result.away_team_id = away_team_id
|
||||
@ -377,27 +485,32 @@ async def patch_result(
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that event'
|
||||
detail="Well slap my ass and call me a teapot; I could not save that event",
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{result_id}')
|
||||
@router.delete("/{result_id}")
|
||||
async def delete_result(result_id, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to post results. This event has been logged.'
|
||||
detail="You are not authorized to post results. This event has been logged.",
|
||||
)
|
||||
try:
|
||||
this_result = Result.get_by_id(result_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No result found with id {result_id}"
|
||||
)
|
||||
|
||||
count = this_result.delete_instance()
|
||||
|
||||
if count == 1:
|
||||
raise HTTPException(status_code=200, detail=f'Result {result_id} has been deleted')
|
||||
raise HTTPException(
|
||||
status_code=200, detail=f"Result {result_id} has been deleted"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'Result {result_id} was not deleted')
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Result {result_id} was not deleted"
|
||||
)
|
||||
|
||||
@ -9,10 +9,7 @@ from ..db_engine import Reward, model_to_dict, fn, DoesNotExist
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/rewards',
|
||||
tags=['rewards']
|
||||
)
|
||||
router = APIRouter(prefix="/api/v2/rewards", tags=["rewards"])
|
||||
|
||||
|
||||
class RewardModel(pydantic.BaseModel):
|
||||
@ -20,18 +17,25 @@ class RewardModel(pydantic.BaseModel):
|
||||
season: int
|
||||
week: int
|
||||
team_id: int
|
||||
created: Optional[int] = int(datetime.timestamp(datetime.now())*1000)
|
||||
created: Optional[int] = int(datetime.timestamp(datetime.now()) * 1000)
|
||||
|
||||
|
||||
@router.get('')
|
||||
@router.get("")
|
||||
async def get_rewards(
|
||||
name: Optional[str] = None, in_name: Optional[str] = None, team_id: Optional[int] = None,
|
||||
season: Optional[int] = None, week: Optional[int] = None, created_after: Optional[int] = None,
|
||||
flat: Optional[bool] = False, csv: Optional[bool] = None):
|
||||
name: Optional[str] = None,
|
||||
in_name: Optional[str] = None,
|
||||
team_id: Optional[int] = None,
|
||||
season: Optional[int] = None,
|
||||
week: Optional[int] = None,
|
||||
created_after: Optional[int] = None,
|
||||
flat: Optional[bool] = False,
|
||||
csv: Optional[bool] = None,
|
||||
limit: Optional[int] = 100,
|
||||
):
|
||||
all_rewards = Reward.select().order_by(Reward.id)
|
||||
|
||||
if all_rewards.count() == 0:
|
||||
raise HTTPException(status_code=404, detail=f'There are no rewards to filter')
|
||||
raise HTTPException(status_code=404, detail="There are no rewards to filter")
|
||||
|
||||
if name is not None:
|
||||
all_rewards = all_rewards.where(fn.Lower(Reward.name) == name.lower())
|
||||
@ -49,62 +53,71 @@ async def get_rewards(
|
||||
all_rewards = all_rewards.where(Reward.week == week)
|
||||
|
||||
if all_rewards.count() == 0:
|
||||
raise HTTPException(status_code=404, detail=f'No rewards found')
|
||||
raise HTTPException(status_code=404, detail="No rewards found")
|
||||
|
||||
limit = max(0, min(limit, 500))
|
||||
all_rewards = all_rewards.limit(limit)
|
||||
|
||||
if csv:
|
||||
data_list = [['id', 'name', 'team', 'daily', 'created']]
|
||||
data_list = [["id", "name", "team", "daily", "created"]]
|
||||
for line in all_rewards:
|
||||
data_list.append(
|
||||
[
|
||||
line.id, line.name, line.team.id, line.daily, line.created
|
||||
]
|
||||
[line.id, line.name, line.team.id, line.daily, line.created]
|
||||
)
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = {'count': all_rewards.count(), 'rewards': []}
|
||||
return_val = {"count": all_rewards.count(), "rewards": []}
|
||||
for x in all_rewards:
|
||||
return_val['rewards'].append(model_to_dict(x, recurse=not flat))
|
||||
return_val["rewards"].append(model_to_dict(x, recurse=not flat))
|
||||
|
||||
return return_val
|
||||
|
||||
|
||||
@router.get('/{reward_id}')
|
||||
@router.get("/{reward_id}")
|
||||
async def get_one_reward(reward_id, csv: Optional[bool] = False):
|
||||
try:
|
||||
this_reward = Reward.get_by_id(reward_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No reward found with id {reward_id}"
|
||||
)
|
||||
|
||||
if csv:
|
||||
data_list = [
|
||||
['id', 'name', 'card_count', 'description'],
|
||||
[this_reward.id, this_reward.name, this_reward.team.id, this_reward.daily, this_reward.created]
|
||||
["id", "name", "card_count", "description"],
|
||||
[
|
||||
this_reward.id,
|
||||
this_reward.name,
|
||||
this_reward.team.id,
|
||||
this_reward.daily,
|
||||
this_reward.created,
|
||||
],
|
||||
]
|
||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||
|
||||
return Response(content=return_val, media_type='text/csv')
|
||||
return Response(content=return_val, media_type="text/csv")
|
||||
|
||||
else:
|
||||
return_val = model_to_dict(this_reward)
|
||||
return return_val
|
||||
|
||||
|
||||
@router.post('')
|
||||
@router.post("")
|
||||
async def post_rewards(reward: RewardModel, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to post rewards. This event has been logged.'
|
||||
detail="You are not authorized to post rewards. This event has been logged.",
|
||||
)
|
||||
|
||||
reward_data = reward.dict()
|
||||
# Convert milliseconds timestamp to datetime for PostgreSQL
|
||||
if reward_data.get('created'):
|
||||
reward_data['created'] = datetime.fromtimestamp(reward_data['created'] / 1000)
|
||||
if reward_data.get("created"):
|
||||
reward_data["created"] = datetime.fromtimestamp(reward_data["created"] / 1000)
|
||||
this_reward = Reward(**reward_data)
|
||||
|
||||
saved = this_reward.save()
|
||||
@ -114,24 +127,30 @@ async def post_rewards(reward: RewardModel, token: str = Depends(oauth2_scheme))
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that cardset'
|
||||
detail="Well slap my ass and call me a teapot; I could not save that cardset",
|
||||
)
|
||||
|
||||
|
||||
@router.patch('/{reward_id}')
|
||||
@router.patch("/{reward_id}")
|
||||
async def patch_reward(
|
||||
reward_id, name: Optional[str] = None, team_id: Optional[int] = None, created: Optional[int] = None,
|
||||
token: str = Depends(oauth2_scheme)):
|
||||
reward_id,
|
||||
name: Optional[str] = None,
|
||||
team_id: Optional[int] = None,
|
||||
created: Optional[int] = None,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to patch rewards. This event has been logged.'
|
||||
detail="You are not authorized to patch rewards. This event has been logged.",
|
||||
)
|
||||
try:
|
||||
this_reward = Reward.get_by_id(reward_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No reward found with id {reward_id}"
|
||||
)
|
||||
|
||||
if name is not None:
|
||||
this_reward.name = name
|
||||
@ -147,28 +166,32 @@ async def patch_reward(
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that rarity'
|
||||
detail="Well slap my ass and call me a teapot; I could not save that rarity",
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{reward_id}')
|
||||
@router.delete("/{reward_id}")
|
||||
async def delete_reward(reward_id, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('Bad Token: [REDACTED]')
|
||||
logging.warning("Bad Token: [REDACTED]")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail='You are not authorized to delete rewards. This event has been logged.'
|
||||
detail="You are not authorized to delete rewards. This event has been logged.",
|
||||
)
|
||||
try:
|
||||
this_reward = Reward.get_by_id(reward_id)
|
||||
except DoesNotExist:
|
||||
raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No reward found with id {reward_id}"
|
||||
)
|
||||
|
||||
count = this_reward.delete_instance()
|
||||
|
||||
if count == 1:
|
||||
raise HTTPException(status_code=200, detail=f'Reward {reward_id} has been deleted')
|
||||
raise HTTPException(
|
||||
status_code=200, detail=f"Reward {reward_id} has been deleted"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'Reward {reward_id} was not deleted')
|
||||
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Reward {reward_id} was not deleted"
|
||||
)
|
||||
|
||||
@ -4,7 +4,7 @@ from typing import Optional
|
||||
import logging
|
||||
import pydantic
|
||||
|
||||
from ..db_engine import ScoutClaim, ScoutOpportunity, model_to_dict
|
||||
from ..db_engine import ScoutClaim, model_to_dict
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
router = APIRouter(prefix="/api/v2/scout_claims", tags=["scout_claims"])
|
||||
@ -18,7 +18,9 @@ class ScoutClaimModel(pydantic.BaseModel):
|
||||
|
||||
@router.get("")
|
||||
async def get_scout_claims(
|
||||
scout_opportunity_id: Optional[int] = None, claimed_by_team_id: Optional[int] = None
|
||||
scout_opportunity_id: Optional[int] = None,
|
||||
claimed_by_team_id: Optional[int] = None,
|
||||
limit: Optional[int] = 100,
|
||||
):
|
||||
|
||||
query = ScoutClaim.select().order_by(ScoutClaim.id)
|
||||
@ -28,6 +30,10 @@ async def get_scout_claims(
|
||||
if claimed_by_team_id is not None:
|
||||
query = query.where(ScoutClaim.claimed_by_team_id == claimed_by_team_id)
|
||||
|
||||
if limit is not None:
|
||||
limit = max(0, min(limit, 500))
|
||||
query = query.limit(limit)
|
||||
|
||||
results = [model_to_dict(x, recurse=False) for x in query]
|
||||
return {"count": len(results), "results": results}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -8,10 +8,7 @@ from ..db_engine import StratGame, model_to_dict, fn
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/games',
|
||||
tags=['games']
|
||||
)
|
||||
router = APIRouter(prefix="/api/v2/games", tags=["games"])
|
||||
|
||||
|
||||
class GameModel(pydantic.BaseModel):
|
||||
@ -35,13 +32,22 @@ class GameList(pydantic.BaseModel):
|
||||
games: List[GameModel]
|
||||
|
||||
|
||||
@router.get('')
|
||||
@router.get("")
|
||||
async def get_games(
|
||||
season: list = Query(default=None), forfeit: Optional[bool] = None, away_team_id: list = Query(default=None),
|
||||
home_team_id: list = Query(default=None), team1_id: list = Query(default=None),
|
||||
team2_id: list = Query(default=None), game_type: list = Query(default=None), ranked: Optional[bool] = None,
|
||||
short_game: Optional[bool] = None, csv: Optional[bool] = False, short_output: bool = False,
|
||||
gauntlet_id: Optional[int] = None):
|
||||
season: list = Query(default=None),
|
||||
forfeit: Optional[bool] = None,
|
||||
away_team_id: list = Query(default=None),
|
||||
home_team_id: list = Query(default=None),
|
||||
team1_id: list = Query(default=None),
|
||||
team2_id: list = Query(default=None),
|
||||
game_type: list = Query(default=None),
|
||||
ranked: Optional[bool] = None,
|
||||
short_game: Optional[bool] = None,
|
||||
csv: Optional[bool] = False,
|
||||
short_output: bool = False,
|
||||
gauntlet_id: Optional[int] = None,
|
||||
limit: int = 100,
|
||||
):
|
||||
all_games = StratGame.select().order_by(StratGame.id)
|
||||
|
||||
if season is not None:
|
||||
@ -68,49 +74,70 @@ async def get_games(
|
||||
if short_game is not None:
|
||||
all_games = all_games.where(StratGame.short_game == short_game)
|
||||
if gauntlet_id is not None:
|
||||
all_games = all_games.where(StratGame.game_type.contains(f'gauntlet-{gauntlet_id}'))
|
||||
all_games = all_games.where(
|
||||
StratGame.game_type.contains(f"gauntlet-{gauntlet_id}")
|
||||
)
|
||||
|
||||
all_games = all_games.limit(max(0, min(limit, 500)))
|
||||
|
||||
if csv:
|
||||
return_vals = [model_to_dict(x) for x in all_games]
|
||||
for x in return_vals:
|
||||
x['away_abbrev'] = x['away_team']['abbrev']
|
||||
x['home_abbrev'] = x['home_team']['abbrev']
|
||||
del x['away_team'], x['home_team']
|
||||
x["away_abbrev"] = x["away_team"]["abbrev"]
|
||||
x["home_abbrev"] = x["home_team"]["abbrev"]
|
||||
del x["away_team"], x["home_team"]
|
||||
|
||||
output = pd.DataFrame(return_vals)[[
|
||||
'id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_team_value', 'home_team_value',
|
||||
'game_type', 'season', 'week', 'short_game', 'ranked'
|
||||
]]
|
||||
output = pd.DataFrame(return_vals)[
|
||||
[
|
||||
"id",
|
||||
"away_abbrev",
|
||||
"home_abbrev",
|
||||
"away_score",
|
||||
"home_score",
|
||||
"away_team_value",
|
||||
"home_team_value",
|
||||
"game_type",
|
||||
"season",
|
||||
"week",
|
||||
"short_game",
|
||||
"ranked",
|
||||
]
|
||||
]
|
||||
|
||||
return Response(content=output.to_csv(index=False), media_type='text/csv')
|
||||
return Response(content=output.to_csv(index=False), media_type="text/csv")
|
||||
|
||||
return_val = {'count': all_games.count(), 'games': [
|
||||
model_to_dict(x, recurse=not short_output) for x in all_games
|
||||
]}
|
||||
return_val = {
|
||||
"count": all_games.count(),
|
||||
"games": [model_to_dict(x, recurse=not short_output) for x in all_games],
|
||||
}
|
||||
return return_val
|
||||
|
||||
|
||||
@router.get('/{game_id}')
|
||||
@router.get("/{game_id}")
|
||||
async def get_one_game(game_id: int):
|
||||
this_game = StratGame.get_or_none(StratGame.id == game_id)
|
||||
if not this_game:
|
||||
raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found')
|
||||
raise HTTPException(status_code=404, detail=f"StratGame ID {game_id} not found")
|
||||
|
||||
g_result = model_to_dict(this_game)
|
||||
return g_result
|
||||
|
||||
|
||||
@router.patch('/{game_id}')
|
||||
@router.patch("/{game_id}")
|
||||
async def patch_game(
|
||||
game_id: int, game_type: Optional[str] = None, away_score: Optional[int] = None,
|
||||
home_score: Optional[int] = None, token: str = Depends(oauth2_scheme)):
|
||||
game_id: int,
|
||||
game_type: Optional[str] = None,
|
||||
away_score: Optional[int] = None,
|
||||
home_score: Optional[int] = None,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
):
|
||||
if not valid_token(token):
|
||||
logging.warning('patch_game - Bad Token: [REDACTED]')
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
logging.warning("patch_game - Bad Token: [REDACTED]")
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
this_game = StratGame.get_or_none(StratGame.id == game_id)
|
||||
if not this_game:
|
||||
raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found')
|
||||
raise HTTPException(status_code=404, detail=f"StratGame ID {game_id} not found")
|
||||
|
||||
if away_score is not None:
|
||||
this_game.away_score = away_score
|
||||
@ -123,14 +150,14 @@ async def patch_game(
|
||||
g_result = model_to_dict(this_game)
|
||||
return g_result
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'Unable to patch game {game_id}')
|
||||
raise HTTPException(status_code=500, detail=f"Unable to patch game {game_id}")
|
||||
|
||||
|
||||
@router.post('')
|
||||
@router.post("")
|
||||
async def post_game(this_game: GameModel, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('post_games - Bad Token: [REDACTED]')
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
logging.warning("post_games - Bad Token: [REDACTED]")
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
this_game = StratGame(**this_game.dict())
|
||||
|
||||
@ -141,25 +168,25 @@ async def post_game(this_game: GameModel, token: str = Depends(oauth2_scheme)):
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that game'
|
||||
detail="Well slap my ass and call me a teapot; I could not save that game",
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{game_id}')
|
||||
@router.delete("/{game_id}")
|
||||
async def delete_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning('delete_game - Bad Token: [REDACTED]')
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
logging.warning("delete_game - Bad Token: [REDACTED]")
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
this_game = StratGame.get_or_none(StratGame.id == game_id)
|
||||
if not this_game:
|
||||
raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found')
|
||||
raise HTTPException(status_code=404, detail=f"StratGame ID {game_id} not found")
|
||||
|
||||
count = this_game.delete_instance()
|
||||
|
||||
if count == 1:
|
||||
return f'StratGame {game_id} has been deleted'
|
||||
return f"StratGame {game_id} has been deleted"
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'StratGame {game_id} could not be deleted')
|
||||
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"StratGame {game_id} could not be deleted"
|
||||
)
|
||||
|
||||
@ -1049,7 +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.",
|
||||
)
|
||||
|
||||
|
||||
all_ids = ids.split(",")
|
||||
conf_message = ""
|
||||
total_cost = 0
|
||||
@ -1531,8 +1530,8 @@ async def delete_team(team_id, token: str = Depends(oauth2_scheme)):
|
||||
raise HTTPException(status_code=500, detail=f"Team {team_id} was not deleted")
|
||||
|
||||
|
||||
@router.get("/{team_id}/evolutions")
|
||||
async def list_team_evolutions(
|
||||
@router.get("/{team_id}/refractors")
|
||||
async def list_team_refractors(
|
||||
team_id: int,
|
||||
card_type: Optional[str] = Query(default=None),
|
||||
tier: Optional[int] = Query(default=None),
|
||||
@ -1540,9 +1539,9 @@ async def list_team_evolutions(
|
||||
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.
|
||||
"""List all RefractorCardState rows for a team, with optional filters.
|
||||
|
||||
Joins EvolutionCardState to EvolutionTrack so that card_type filtering
|
||||
Joins RefractorCardState to RefractorTrack 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.
|
||||
|
||||
@ -1555,27 +1554,27 @@ async def list_team_evolutions(
|
||||
Response shape:
|
||||
{"count": N, "items": [card_state_with_threshold_context, ...]}
|
||||
|
||||
Each item in 'items' has the same shape as GET /evolution/cards/{card_id}.
|
||||
Each item in 'items' has the same shape as GET /refractor/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
|
||||
from ..db_engine import RefractorCardState, RefractorTrack
|
||||
from ..routers_v2.refractor import _build_card_state_response
|
||||
|
||||
query = (
|
||||
EvolutionCardState.select(EvolutionCardState, EvolutionTrack)
|
||||
.join(EvolutionTrack)
|
||||
.where(EvolutionCardState.team == team_id)
|
||||
.order_by(EvolutionCardState.player_id)
|
||||
RefractorCardState.select(RefractorCardState, RefractorTrack)
|
||||
.join(RefractorTrack)
|
||||
.where(RefractorCardState.team == team_id)
|
||||
.order_by(RefractorCardState.player_id)
|
||||
)
|
||||
|
||||
if card_type is not None:
|
||||
query = query.where(EvolutionTrack.card_type == card_type)
|
||||
query = query.where(RefractorTrack.card_type == card_type)
|
||||
|
||||
if tier is not None:
|
||||
query = query.where(EvolutionCardState.current_tier == tier)
|
||||
query = query.where(RefractorCardState.current_tier == tier)
|
||||
|
||||
total = query.count()
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
@ -1,36 +1,36 @@
|
||||
"""Seed script for EvolutionTrack records.
|
||||
"""Seed script for RefractorTrack records.
|
||||
|
||||
Loads track definitions from evolution_tracks.json and upserts them into the
|
||||
Loads track definitions from refractor_tracks.json and upserts them into the
|
||||
database using get_or_create keyed on name. Existing tracks have their
|
||||
thresholds and formula updated to match the JSON in case values have changed.
|
||||
|
||||
Can be run standalone:
|
||||
python -m app.seed.evolution_tracks
|
||||
python -m app.seed.refractor_tracks
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from app.db_engine import EvolutionTrack
|
||||
from app.db_engine import RefractorTrack
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_JSON_PATH = Path(__file__).parent / "evolution_tracks.json"
|
||||
_JSON_PATH = Path(__file__).parent / "refractor_tracks.json"
|
||||
|
||||
|
||||
def seed_evolution_tracks() -> list[EvolutionTrack]:
|
||||
"""Upsert evolution tracks from JSON seed data.
|
||||
def seed_refractor_tracks() -> list[RefractorTrack]:
|
||||
"""Upsert refractor tracks from JSON seed data.
|
||||
|
||||
Returns a list of EvolutionTrack instances that were created or updated.
|
||||
Returns a list of RefractorTrack instances that were created or updated.
|
||||
"""
|
||||
raw = _JSON_PATH.read_text(encoding="utf-8")
|
||||
track_defs = json.loads(raw)
|
||||
|
||||
results: list[EvolutionTrack] = []
|
||||
results: list[RefractorTrack] = []
|
||||
|
||||
for defn in track_defs:
|
||||
track, created = EvolutionTrack.get_or_create(
|
||||
track, created = RefractorTrack.get_or_create(
|
||||
name=defn["name"],
|
||||
defaults={
|
||||
"card_type": defn["card_type"],
|
||||
@ -61,6 +61,6 @@ def seed_evolution_tracks() -> list[EvolutionTrack]:
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger.info("Seeding evolution tracks...")
|
||||
tracks = seed_evolution_tracks()
|
||||
logger.info("Seeding refractor tracks...")
|
||||
tracks = seed_refractor_tracks()
|
||||
logger.info("Done. %d track(s) processed.", len(tracks))
|
||||
@ -1,6 +1,6 @@
|
||||
"""Formula engine for evolution value computation (WP-09).
|
||||
"""Formula engine for refractor value computation (WP-09).
|
||||
|
||||
Three pure functions that compute a numeric evolution value from career stats,
|
||||
Three pure functions that compute a numeric refractor value from career stats,
|
||||
plus helpers for formula dispatch and tier classification.
|
||||
|
||||
Stats attributes expected by each formula:
|
||||
@ -79,7 +79,7 @@ def compute_value_for_track(card_type: str, stats) -> float:
|
||||
|
||||
|
||||
def tier_from_value(value: float, track) -> int:
|
||||
"""Return the evolution tier (0-4) for a computed value against a track.
|
||||
"""Return the refractor tier (0-4) for a computed value against a track.
|
||||
|
||||
Tier boundaries are inclusive on the lower end:
|
||||
T0: value < t1
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Evolution evaluator service (WP-08).
|
||||
"""Refractor evaluator service (WP-08).
|
||||
|
||||
Force-recalculates a card's evolution state from career totals.
|
||||
Force-recalculates a card's refractor state from career totals.
|
||||
|
||||
evaluate_card() is the main entry point:
|
||||
1. Load career totals: SUM all BattingSeasonStats/PitchingSeasonStats rows for (player_id, team_id)
|
||||
@ -14,7 +14,7 @@ evaluate_card() is the main entry point:
|
||||
|
||||
Idempotent: calling multiple times with the same data produces the same result.
|
||||
|
||||
Depends on WP-05 (EvolutionCardState), WP-07 (BattingSeasonStats/PitchingSeasonStats),
|
||||
Depends on WP-05 (RefractorCardState), WP-07 (BattingSeasonStats/PitchingSeasonStats),
|
||||
and WP-09 (formula engine). Models and formula functions are imported lazily so
|
||||
this module can be imported before those PRs merge.
|
||||
"""
|
||||
@ -52,11 +52,11 @@ def evaluate_card(
|
||||
_compute_value_fn=None,
|
||||
_tier_from_value_fn=None,
|
||||
) -> dict:
|
||||
"""Force-recalculate a card's evolution tier from career stats.
|
||||
"""Force-recalculate a card's refractor tier from career stats.
|
||||
|
||||
Sums all BattingSeasonStats or PitchingSeasonStats rows (based on
|
||||
card_type) for (player_id, team_id) across all seasons, then delegates
|
||||
formula computation and tier classification to the formula engine. The result is written back to evolution_card_state and
|
||||
formula computation and tier classification to the formula engine. The result is written back to refractor_card_state and
|
||||
returned as a dict.
|
||||
|
||||
current_tier never decreases (no regression):
|
||||
@ -67,7 +67,7 @@ def evaluate_card(
|
||||
team_id: Team primary key.
|
||||
_stats_model: Override for BattingSeasonStats/PitchingSeasonStats
|
||||
(used in tests to inject a stub model with all stat fields).
|
||||
_state_model: Override for EvolutionCardState (used in tests to avoid
|
||||
_state_model: Override for RefractorCardState (used in tests to avoid
|
||||
importing from db_engine before WP-05 merges).
|
||||
_compute_value_fn: Override for formula_engine.compute_value_for_track
|
||||
(used in tests to avoid importing formula_engine before WP-09 merges).
|
||||
@ -79,10 +79,10 @@ def evaluate_card(
|
||||
last_evaluated_at (ISO-8601 string).
|
||||
|
||||
Raises:
|
||||
ValueError: If no evolution_card_state row exists for (player_id, team_id).
|
||||
ValueError: If no refractor_card_state row exists for (player_id, team_id).
|
||||
"""
|
||||
if _state_model is None:
|
||||
from app.db_engine import EvolutionCardState as _state_model # noqa: PLC0415
|
||||
from app.db_engine import RefractorCardState as _state_model # noqa: PLC0415
|
||||
|
||||
if _compute_value_fn is None or _tier_from_value_fn is None:
|
||||
from app.services.formula_engine import ( # noqa: PLC0415
|
||||
@ -101,7 +101,7 @@ def evaluate_card(
|
||||
)
|
||||
if card_state is None:
|
||||
raise ValueError(
|
||||
f"No evolution_card_state for player_id={player_id} team_id={team_id}"
|
||||
f"No refractor_card_state for player_id={player_id} team_id={team_id}"
|
||||
)
|
||||
|
||||
# 2. Load career totals from the appropriate season stats table
|
||||
@ -178,7 +178,7 @@ def evaluate_card(
|
||||
card_state.save()
|
||||
|
||||
logging.debug(
|
||||
"evolution_eval: player=%s team=%s value=%.2f tier=%s fully_evolved=%s",
|
||||
"refractor_eval: player=%s team=%s value=%.2f tier=%s fully_evolved=%s",
|
||||
player_id,
|
||||
team_id,
|
||||
value,
|
||||
@ -1,10 +1,10 @@
|
||||
"""
|
||||
WP-10: Pack opening hook — evolution_card_state initialization.
|
||||
WP-10: Pack opening hook — refractor_card_state initialization.
|
||||
|
||||
Public API
|
||||
----------
|
||||
initialize_card_evolution(player_id, team_id, card_type)
|
||||
Get-or-create an EvolutionCardState for the (player_id, team_id) pair.
|
||||
initialize_card_refractor(player_id, team_id, card_type)
|
||||
Get-or-create a RefractorCardState for the (player_id, team_id) pair.
|
||||
Returns the state instance on success, or None if initialization fails
|
||||
(missing track, integrity error, etc.). Never raises.
|
||||
|
||||
@ -16,23 +16,23 @@ Design notes
|
||||
------------
|
||||
- The function is intentionally fire-and-forget from the caller's perspective.
|
||||
All exceptions are caught and logged; pack opening is never blocked.
|
||||
- No EvolutionProgress rows are created here. Progress accumulation is a
|
||||
- No RefractorProgress rows are created here. Progress accumulation is a
|
||||
separate concern handled by the stats-update pipeline (WP-07/WP-08).
|
||||
- AI teams and Gauntlet teams skip Paperdex insertion (cards.py pattern);
|
||||
we do NOT replicate that exclusion here — all teams get an evolution state
|
||||
we do NOT replicate that exclusion here — all teams get a refractor state
|
||||
so that future rule changes don't require back-filling.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from app.db_engine import DoesNotExist, EvolutionCardState, EvolutionTrack
|
||||
from app.db_engine import DoesNotExist, RefractorCardState, RefractorTrack
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _determine_card_type(player) -> str:
|
||||
"""Map a player's primary position to an evolution card_type string.
|
||||
"""Map a player's primary position to a refractor card_type string.
|
||||
|
||||
Rules (from WP-10 spec):
|
||||
- pos_1 contains 'SP' -> 'sp'
|
||||
@ -53,34 +53,34 @@ def _determine_card_type(player) -> str:
|
||||
return "batter"
|
||||
|
||||
|
||||
def initialize_card_evolution(
|
||||
def initialize_card_refractor(
|
||||
player_id: int,
|
||||
team_id: int,
|
||||
card_type: str,
|
||||
) -> Optional[EvolutionCardState]:
|
||||
"""Get-or-create an EvolutionCardState for a newly acquired card.
|
||||
) -> Optional[RefractorCardState]:
|
||||
"""Get-or-create a RefractorCardState for a newly acquired card.
|
||||
|
||||
Called by the cards POST endpoint after each card is inserted. The
|
||||
function is idempotent: if a state row already exists for the
|
||||
(player_id, team_id) pair it is returned unchanged — existing
|
||||
evolution progress is never reset.
|
||||
refractor progress is never reset.
|
||||
|
||||
Args:
|
||||
player_id: Primary key of the Player row (Player.player_id).
|
||||
team_id: Primary key of the Team row (Team.id).
|
||||
card_type: One of 'batter', 'sp', 'rp'. Determines which
|
||||
EvolutionTrack is assigned to the new state.
|
||||
RefractorTrack is assigned to the new state.
|
||||
|
||||
Returns:
|
||||
The existing or newly created EvolutionCardState instance, or
|
||||
The existing or newly created RefractorCardState instance, or
|
||||
None if initialization could not complete (missing track seed
|
||||
data, unexpected DB error, etc.).
|
||||
"""
|
||||
try:
|
||||
track = EvolutionTrack.get(EvolutionTrack.card_type == card_type)
|
||||
track = RefractorTrack.get(RefractorTrack.card_type == card_type)
|
||||
except DoesNotExist:
|
||||
logger.warning(
|
||||
"evolution_init: no EvolutionTrack found for card_type=%r "
|
||||
"refractor_init: no RefractorTrack found for card_type=%r "
|
||||
"(player_id=%s, team_id=%s) — skipping state creation",
|
||||
card_type,
|
||||
player_id,
|
||||
@ -89,7 +89,7 @@ def initialize_card_evolution(
|
||||
return None
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"evolution_init: unexpected error fetching track "
|
||||
"refractor_init: unexpected error fetching track "
|
||||
"(card_type=%r, player_id=%s, team_id=%s)",
|
||||
card_type,
|
||||
player_id,
|
||||
@ -98,7 +98,7 @@ def initialize_card_evolution(
|
||||
return None
|
||||
|
||||
try:
|
||||
state, created = EvolutionCardState.get_or_create(
|
||||
state, created = RefractorCardState.get_or_create(
|
||||
player_id=player_id,
|
||||
team_id=team_id,
|
||||
defaults={
|
||||
@ -110,7 +110,7 @@ def initialize_card_evolution(
|
||||
)
|
||||
if created:
|
||||
logger.debug(
|
||||
"evolution_init: created EvolutionCardState id=%s "
|
||||
"refractor_init: created RefractorCardState id=%s "
|
||||
"(player_id=%s, team_id=%s, card_type=%r)",
|
||||
state.id,
|
||||
player_id,
|
||||
@ -119,7 +119,7 @@ def initialize_card_evolution(
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"evolution_init: state already exists id=%s "
|
||||
"refractor_init: state already exists id=%s "
|
||||
"(player_id=%s, team_id=%s) — no-op",
|
||||
state.id,
|
||||
player_id,
|
||||
@ -129,7 +129,7 @@ def initialize_card_evolution(
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"evolution_init: failed to get_or_create state "
|
||||
"refractor_init: failed to get_or_create state "
|
||||
"(player_id=%s, team_id=%s, card_type=%r)",
|
||||
player_id,
|
||||
team_id,
|
||||
19
migrations/2026-03-23_rename_evolution_to_refractor.sql
Normal file
19
migrations/2026-03-23_rename_evolution_to_refractor.sql
Normal file
@ -0,0 +1,19 @@
|
||||
-- Migration: Rename evolution tables to refractor tables
|
||||
-- Date: 2026-03-23
|
||||
--
|
||||
-- Renames all four evolution system tables to the refractor naming convention.
|
||||
-- This migration corresponds to the application-level rename from
|
||||
-- EvolutionTrack/EvolutionCardState/EvolutionTierBoost/EvolutionCosmetic
|
||||
-- to RefractorTrack/RefractorCardState/RefractorTierBoost/RefractorCosmetic.
|
||||
--
|
||||
-- The table renames are performed in order that respects foreign key
|
||||
-- dependencies (referenced tables first, then referencing tables).
|
||||
|
||||
ALTER TABLE evolution_track RENAME TO refractor_track;
|
||||
ALTER TABLE evolution_card_state RENAME TO refractor_card_state;
|
||||
ALTER TABLE evolution_tier_boost RENAME TO refractor_tier_boost;
|
||||
ALTER TABLE evolution_cosmetic RENAME TO refractor_cosmetic;
|
||||
|
||||
-- Rename indexes to match new table names
|
||||
ALTER INDEX IF EXISTS evolution_card_state_player_team_uniq RENAME TO refractor_card_state_player_team_uniq;
|
||||
ALTER INDEX IF EXISTS evolution_tier_boost_track_tier_type_target_uniq RENAME TO refractor_tier_boost_track_tier_type_target_uniq;
|
||||
@ -44,10 +44,10 @@ from app.db_engine import (
|
||||
BattingSeasonStats,
|
||||
PitchingSeasonStats,
|
||||
ProcessedGame,
|
||||
EvolutionTrack,
|
||||
EvolutionCardState,
|
||||
EvolutionTierBoost,
|
||||
EvolutionCosmetic,
|
||||
RefractorTrack,
|
||||
RefractorCardState,
|
||||
RefractorTierBoost,
|
||||
RefractorCosmetic,
|
||||
ScoutOpportunity,
|
||||
ScoutClaim,
|
||||
)
|
||||
@ -76,10 +76,10 @@ _TEST_MODELS = [
|
||||
ProcessedGame,
|
||||
ScoutOpportunity,
|
||||
ScoutClaim,
|
||||
EvolutionTrack,
|
||||
EvolutionCardState,
|
||||
EvolutionTierBoost,
|
||||
EvolutionCosmetic,
|
||||
RefractorTrack,
|
||||
RefractorCardState,
|
||||
RefractorTierBoost,
|
||||
RefractorCosmetic,
|
||||
]
|
||||
|
||||
|
||||
@ -164,8 +164,8 @@ def team():
|
||||
|
||||
@pytest.fixture
|
||||
def track():
|
||||
"""A minimal EvolutionTrack for batter cards."""
|
||||
return EvolutionTrack.create(
|
||||
"""A minimal RefractorTrack for batter cards."""
|
||||
return RefractorTrack.create(
|
||||
name="Batter Track",
|
||||
card_type="batter",
|
||||
formula="pa + tb * 2",
|
||||
@ -177,7 +177,7 @@ def track():
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PostgreSQL integration fixture (used by test_evolution_*_api.py)
|
||||
# PostgreSQL integration fixture (used by test_refractor_*_api.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
Unit tests only — no database required. Stats inputs are simple namespace
|
||||
objects whose attributes match what BattingSeasonStats/PitchingSeasonStats expose.
|
||||
|
||||
Tier thresholds used (from evolution_tracks.json seed data):
|
||||
Tier thresholds used (from refractor_tracks.json seed data):
|
||||
Batter: t1=37, t2=149, t3=448, t4=896
|
||||
SP: t1=10, t2=40, t3=120, t4=240
|
||||
RP: t1=3, t2=12, t3=35, t4=70
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Tests cover both post-game callback endpoints:
|
||||
POST /api/v2/season-stats/update-game/{game_id}
|
||||
POST /api/v2/evolution/evaluate-game/{game_id}
|
||||
POST /api/v2/refractor/evaluate-game/{game_id}
|
||||
|
||||
All tests run against a named shared-memory SQLite database so that Peewee
|
||||
model queries inside the route handlers (which execute in the TestClient's
|
||||
@ -34,7 +34,7 @@ Test matrix:
|
||||
test_evaluate_game_tier_ups_in_response
|
||||
Tier-up appears in tier_ups list with correct fields.
|
||||
test_evaluate_game_skips_players_without_state
|
||||
Players in game but without EvolutionCardState are silently skipped.
|
||||
Players in game but without RefractorCardState are silently skipped.
|
||||
test_auth_required_update_game
|
||||
Missing bearer token returns 401 on update-game.
|
||||
test_auth_required_evaluate_game
|
||||
@ -55,10 +55,10 @@ from peewee import SqliteDatabase
|
||||
|
||||
from app.db_engine import (
|
||||
Cardset,
|
||||
EvolutionCardState,
|
||||
EvolutionCosmetic,
|
||||
EvolutionTierBoost,
|
||||
EvolutionTrack,
|
||||
RefractorCardState,
|
||||
RefractorCosmetic,
|
||||
RefractorTierBoost,
|
||||
RefractorTrack,
|
||||
MlbPlayer,
|
||||
Pack,
|
||||
PackType,
|
||||
@ -111,10 +111,10 @@ _WP13_MODELS = [
|
||||
BattingSeasonStats,
|
||||
PitchingSeasonStats,
|
||||
ProcessedGame,
|
||||
EvolutionTrack,
|
||||
EvolutionCardState,
|
||||
EvolutionTierBoost,
|
||||
EvolutionCosmetic,
|
||||
RefractorTrack,
|
||||
RefractorCardState,
|
||||
RefractorTierBoost,
|
||||
RefractorCosmetic,
|
||||
]
|
||||
|
||||
# Patch the service-layer 'db' reference to use our shared test database so
|
||||
@ -165,7 +165,7 @@ def _build_test_app() -> FastAPI:
|
||||
connection even though it runs in a different thread from the fixture.
|
||||
"""
|
||||
from app.routers_v2.season_stats import router as ss_router
|
||||
from app.routers_v2.evolution import router as evo_router
|
||||
from app.routers_v2.refractor import router as evo_router
|
||||
|
||||
test_app = FastAPI()
|
||||
|
||||
@ -294,8 +294,8 @@ def _make_play(game, play_num, batter, batter_team, pitcher, pitcher_team, **sta
|
||||
|
||||
def _make_track(
|
||||
name: str = "WP13 Batter Track", card_type: str = "batter"
|
||||
) -> EvolutionTrack:
|
||||
track, _ = EvolutionTrack.get_or_create(
|
||||
) -> RefractorTrack:
|
||||
track, _ = RefractorTrack.get_or_create(
|
||||
name=name,
|
||||
defaults=dict(
|
||||
card_type=card_type,
|
||||
@ -311,8 +311,8 @@ def _make_track(
|
||||
|
||||
def _make_state(
|
||||
player, team, track, current_tier=0, current_value=0.0
|
||||
) -> EvolutionCardState:
|
||||
return EvolutionCardState.create(
|
||||
) -> RefractorCardState:
|
||||
return RefractorCardState.create(
|
||||
player=player,
|
||||
team=team,
|
||||
track=track,
|
||||
@ -428,14 +428,14 @@ def test_update_game_idempotent(client):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: POST /api/v2/evolution/evaluate-game/{game_id}
|
||||
# Tests: POST /api/v2/refractor/evaluate-game/{game_id}
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_evaluate_game_increases_current_value(client):
|
||||
"""After update-game, evaluate-game raises the card's current_value above 0.
|
||||
|
||||
What: Batter with an EvolutionCardState gets 3 hits (pa=3, hit=3) from a
|
||||
What: Batter with a RefractorCardState gets 3 hits (pa=3, hit=3) from a
|
||||
game. update-game writes those stats; evaluate-game then recomputes the
|
||||
value. current_value in the DB must be > 0 after the evaluate call.
|
||||
|
||||
@ -458,12 +458,12 @@ def test_evaluate_game_increases_current_value(client):
|
||||
|
||||
client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER)
|
||||
resp = client.post(
|
||||
f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
state = EvolutionCardState.get(
|
||||
(EvolutionCardState.player == batter) & (EvolutionCardState.team == team_a)
|
||||
state = RefractorCardState.get(
|
||||
(RefractorCardState.player == batter) & (RefractorCardState.team == team_a)
|
||||
)
|
||||
assert state.current_value > 0
|
||||
|
||||
@ -501,12 +501,12 @@ def test_evaluate_game_tier_advancement(client):
|
||||
|
||||
client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER)
|
||||
resp = client.post(
|
||||
f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
updated_state = EvolutionCardState.get(
|
||||
(EvolutionCardState.player == batter) & (EvolutionCardState.team == team_a)
|
||||
updated_state = RefractorCardState.get(
|
||||
(RefractorCardState.player == batter) & (RefractorCardState.team == team_a)
|
||||
)
|
||||
assert updated_state.current_tier >= 1
|
||||
|
||||
@ -535,15 +535,15 @@ def test_evaluate_game_no_tier_advancement(client):
|
||||
|
||||
client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER)
|
||||
resp = client.post(
|
||||
f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
|
||||
assert data["tier_ups"] == []
|
||||
|
||||
state = EvolutionCardState.get(
|
||||
(EvolutionCardState.player == batter) & (EvolutionCardState.team == team_a)
|
||||
state = RefractorCardState.get(
|
||||
(RefractorCardState.player == batter) & (RefractorCardState.team == team_a)
|
||||
)
|
||||
assert state.current_tier == 0
|
||||
|
||||
@ -577,7 +577,7 @@ def test_evaluate_game_tier_ups_in_response(client):
|
||||
|
||||
client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER)
|
||||
resp = client.post(
|
||||
f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
@ -596,7 +596,7 @@ def test_evaluate_game_tier_ups_in_response(client):
|
||||
|
||||
|
||||
def test_evaluate_game_skips_players_without_state(client):
|
||||
"""Players in a game without an EvolutionCardState are silently skipped.
|
||||
"""Players in a game without a RefractorCardState are silently skipped.
|
||||
|
||||
What: A game has two players: one with a card state and one without.
|
||||
After evaluate-game, evaluated should be 1 (only the player with state)
|
||||
@ -613,7 +613,7 @@ def test_evaluate_game_skips_players_without_state(client):
|
||||
game = _make_game(team_a, team_b)
|
||||
track = _make_track(name="WP13 Skip Track")
|
||||
|
||||
# Only batter_with_state gets an EvolutionCardState
|
||||
# Only batter_with_state gets a RefractorCardState
|
||||
_make_state(batter_with_state, team_a, track)
|
||||
|
||||
_make_play(game, 1, batter_with_state, team_a, pitcher, team_b, pa=1, ab=1, outs=1)
|
||||
@ -621,7 +621,7 @@ def test_evaluate_game_skips_players_without_state(client):
|
||||
|
||||
client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER)
|
||||
resp = client.post(
|
||||
f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
@ -663,5 +663,5 @@ def test_auth_required_evaluate_game(client):
|
||||
team_b = _make_team("WB2", gmid=20092)
|
||||
game = _make_game(team_a, team_b)
|
||||
|
||||
resp = client.post(f"/api/v2/evolution/evaluate-game/{game.id}")
|
||||
resp = client.post(f"/api/v2/refractor/evaluate-game/{game.id}")
|
||||
assert resp.status_code == 401
|
||||
@ -1,7 +1,7 @@
|
||||
"""Tests for the evolution evaluator service (WP-08).
|
||||
"""Tests for the refractor evaluator service (WP-08).
|
||||
|
||||
Unit tests verify tier assignment, advancement, partial progress, idempotency,
|
||||
full evolution, and no-regression behaviour without touching any database,
|
||||
full refractor tier, and no-regression behaviour without touching any database,
|
||||
using stub Peewee models bound to an in-memory SQLite database.
|
||||
|
||||
The formula engine (WP-09) and Peewee models (WP-05/WP-07) are not imported
|
||||
@ -33,7 +33,7 @@ from peewee import (
|
||||
SqliteDatabase,
|
||||
)
|
||||
|
||||
from app.services.evolution_evaluator import evaluate_card
|
||||
from app.services.refractor_evaluator import evaluate_card
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub models — mirror WP-01/WP-04/WP-07 schema without importing db_engine
|
||||
@ -43,7 +43,7 @@ _test_db = SqliteDatabase(":memory:")
|
||||
|
||||
|
||||
class TrackStub(Model):
|
||||
"""Minimal EvolutionTrack stub for evaluator tests."""
|
||||
"""Minimal RefractorTrack stub for evaluator tests."""
|
||||
|
||||
card_type = CharField(unique=True)
|
||||
t1_threshold = IntegerField()
|
||||
@ -53,11 +53,11 @@ class TrackStub(Model):
|
||||
|
||||
class Meta:
|
||||
database = _test_db
|
||||
table_name = "evolution_track"
|
||||
table_name = "refractor_track"
|
||||
|
||||
|
||||
class CardStateStub(Model):
|
||||
"""Minimal EvolutionCardState stub for evaluator tests."""
|
||||
"""Minimal RefractorCardState stub for evaluator tests."""
|
||||
|
||||
player_id = IntegerField()
|
||||
team_id = IntegerField()
|
||||
@ -69,7 +69,7 @@ class CardStateStub(Model):
|
||||
|
||||
class Meta:
|
||||
database = _test_db
|
||||
table_name = "evolution_card_state"
|
||||
table_name = "refractor_card_state"
|
||||
indexes = ((("player_id", "team_id"), True),)
|
||||
|
||||
|
||||
@ -331,7 +331,7 @@ class TestMissingState:
|
||||
def test_missing_state_raises(self, batter_track):
|
||||
"""evaluate_card raises ValueError when no state row exists."""
|
||||
# No card state created
|
||||
with pytest.raises(ValueError, match="No evolution_card_state"):
|
||||
with pytest.raises(ValueError, match="No refractor_card_state"):
|
||||
_eval(99, 99)
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
"""
|
||||
Tests for WP-10: evolution_card_state initialization on pack opening.
|
||||
Tests for WP-10: refractor_card_state initialization on pack opening.
|
||||
|
||||
Covers `app/services/evolution_init.py` — the `initialize_card_evolution`
|
||||
function that creates an EvolutionCardState row when a card is first acquired.
|
||||
Covers `app/services/refractor_init.py` — the `initialize_card_refractor`
|
||||
function that creates an RefractorCardState row when a card is first acquired.
|
||||
|
||||
Test strategy:
|
||||
- Unit tests for `_determine_card_type` cover all three branches (batter,
|
||||
@ -18,7 +18,7 @@ Why we test idempotency:
|
||||
|
||||
Why we test cross-player isolation:
|
||||
Two different players with the same team must each get their own
|
||||
EvolutionCardState row. A bug that checked only team_id would share state
|
||||
RefractorCardState row. A bug that checked only team_id would share state
|
||||
across players, so we assert that state.player_id matches.
|
||||
"""
|
||||
|
||||
@ -26,11 +26,11 @@ import pytest
|
||||
|
||||
from app.db_engine import (
|
||||
Cardset,
|
||||
EvolutionCardState,
|
||||
EvolutionTrack,
|
||||
RefractorCardState,
|
||||
RefractorTrack,
|
||||
Player,
|
||||
)
|
||||
from app.services.evolution_init import _determine_card_type, initialize_card_evolution
|
||||
from app.services.refractor_init import _determine_card_type, initialize_card_refractor
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -74,13 +74,13 @@ def _make_player(rarity, pos_1: str) -> Player:
|
||||
)
|
||||
|
||||
|
||||
def _make_track(card_type: str) -> EvolutionTrack:
|
||||
"""Create an EvolutionTrack for the given card_type.
|
||||
def _make_track(card_type: str) -> RefractorTrack:
|
||||
"""Create an RefractorTrack for the given card_type.
|
||||
|
||||
Thresholds are kept small and arbitrary; the unit under test only
|
||||
cares about card_type when selecting the track.
|
||||
"""
|
||||
return EvolutionTrack.create(
|
||||
return RefractorTrack.create(
|
||||
name=f"Track-{card_type}",
|
||||
card_type=card_type,
|
||||
formula="pa",
|
||||
@ -116,14 +116,14 @@ class TestDetermineCardType:
|
||||
"""pos_1 == 'RP' maps to card_type 'rp'.
|
||||
|
||||
Relief pitchers carry the 'RP' position flag and must follow a
|
||||
separate evolution track with lower thresholds.
|
||||
separate refractor track with lower thresholds.
|
||||
"""
|
||||
assert _determine_card_type(_FakePlayer("RP")) == "rp"
|
||||
|
||||
def test_closer_pitcher(self):
|
||||
"""pos_1 == 'CP' maps to card_type 'rp'.
|
||||
|
||||
Closers share the RP evolution track; the spec explicitly lists 'CP'
|
||||
Closers share the RP refractor track; the spec explicitly lists 'CP'
|
||||
as an rp-track position.
|
||||
"""
|
||||
assert _determine_card_type(_FakePlayer("CP")) == "rp"
|
||||
@ -154,12 +154,12 @@ class TestDetermineCardType:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration tests — initialize_card_evolution
|
||||
# Integration tests — initialize_card_refractor
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestInitializeCardEvolution:
|
||||
"""Integration tests for initialize_card_evolution against in-memory SQLite.
|
||||
"""Integration tests for initialize_card_refractor against in-memory SQLite.
|
||||
|
||||
Each test relies on the conftest autouse fixture to get a clean database.
|
||||
We create tracks for all three card types so the function can always find
|
||||
@ -168,9 +168,9 @@ class TestInitializeCardEvolution:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def seed_tracks(self):
|
||||
"""Create one EvolutionTrack per card_type before each test.
|
||||
"""Create one RefractorTrack per card_type before each test.
|
||||
|
||||
initialize_card_evolution does a DB lookup for a track matching the
|
||||
initialize_card_refractor does a DB lookup for a track matching the
|
||||
card_type. If no track exists the function must not crash (it should
|
||||
log and return None), but having tracks present lets us verify the
|
||||
happy path for all three types without repeating setup in every test.
|
||||
@ -180,7 +180,7 @@ class TestInitializeCardEvolution:
|
||||
self.rp_track = _make_track("rp")
|
||||
|
||||
def test_first_card_creates_state(self, rarity, team):
|
||||
"""First acquisition creates an EvolutionCardState with zeroed values.
|
||||
"""First acquisition creates an RefractorCardState with zeroed values.
|
||||
|
||||
Acceptance criteria from WP-10:
|
||||
- current_tier == 0
|
||||
@ -189,7 +189,7 @@ class TestInitializeCardEvolution:
|
||||
- track matches the player's card_type (batter here)
|
||||
"""
|
||||
player = _make_player(rarity, "2B")
|
||||
state = initialize_card_evolution(player.player_id, team.id, "batter")
|
||||
state = initialize_card_refractor(player.player_id, team.id, "batter")
|
||||
|
||||
assert state is not None
|
||||
assert state.player_id == player.player_id
|
||||
@ -208,7 +208,7 @@ class TestInitializeCardEvolution:
|
||||
"""
|
||||
player = _make_player(rarity, "SS")
|
||||
# First call creates the state
|
||||
state1 = initialize_card_evolution(player.player_id, team.id, "batter")
|
||||
state1 = initialize_card_refractor(player.player_id, team.id, "batter")
|
||||
assert state1 is not None
|
||||
|
||||
# Simulate partial evolution progress
|
||||
@ -217,22 +217,22 @@ class TestInitializeCardEvolution:
|
||||
state1.save()
|
||||
|
||||
# Second call (duplicate card) must not reset progress
|
||||
state2 = initialize_card_evolution(player.player_id, team.id, "batter")
|
||||
state2 = initialize_card_refractor(player.player_id, team.id, "batter")
|
||||
assert state2 is not None
|
||||
|
||||
# Exactly one row in the database
|
||||
count = (
|
||||
EvolutionCardState.select()
|
||||
RefractorCardState.select()
|
||||
.where(
|
||||
EvolutionCardState.player == player,
|
||||
EvolutionCardState.team == team,
|
||||
RefractorCardState.player == player,
|
||||
RefractorCardState.team == team,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
assert count == 1
|
||||
|
||||
# Progress was NOT reset
|
||||
refreshed = EvolutionCardState.get_by_id(state1.id)
|
||||
refreshed = RefractorCardState.get_by_id(state1.id)
|
||||
assert refreshed.current_tier == 2
|
||||
assert refreshed.current_value == 250.0
|
||||
|
||||
@ -246,8 +246,8 @@ class TestInitializeCardEvolution:
|
||||
player_a = _make_player(rarity, "LF")
|
||||
player_b = _make_player(rarity, "RF")
|
||||
|
||||
state_a = initialize_card_evolution(player_a.player_id, team.id, "batter")
|
||||
state_b = initialize_card_evolution(player_b.player_id, team.id, "batter")
|
||||
state_a = initialize_card_refractor(player_a.player_id, team.id, "batter")
|
||||
state_b = initialize_card_refractor(player_b.player_id, team.id, "batter")
|
||||
|
||||
assert state_a is not None
|
||||
assert state_b is not None
|
||||
@ -256,7 +256,7 @@ class TestInitializeCardEvolution:
|
||||
assert state_b.player_id == player_b.player_id
|
||||
|
||||
def test_sp_card_gets_sp_track(self, rarity, team):
|
||||
"""A starting pitcher is assigned the 'sp' EvolutionTrack.
|
||||
"""A starting pitcher is assigned the 'sp' RefractorTrack.
|
||||
|
||||
Track selection is driven by card_type, which in turn comes from
|
||||
pos_1. This test passes card_type='sp' explicitly (mirroring the
|
||||
@ -264,15 +264,15 @@ class TestInitializeCardEvolution:
|
||||
state links to the sp track, not the batter track.
|
||||
"""
|
||||
player = _make_player(rarity, "SP")
|
||||
state = initialize_card_evolution(player.player_id, team.id, "sp")
|
||||
state = initialize_card_refractor(player.player_id, team.id, "sp")
|
||||
|
||||
assert state is not None
|
||||
assert state.track_id == self.sp_track.id
|
||||
|
||||
def test_rp_card_gets_rp_track(self, rarity, team):
|
||||
"""A relief pitcher (RP or CP) is assigned the 'rp' EvolutionTrack."""
|
||||
"""A relief pitcher (RP or CP) is assigned the 'rp' RefractorTrack."""
|
||||
player = _make_player(rarity, "RP")
|
||||
state = initialize_card_evolution(player.player_id, team.id, "rp")
|
||||
state = initialize_card_refractor(player.player_id, team.id, "rp")
|
||||
|
||||
assert state is not None
|
||||
assert state.track_id == self.rp_track.id
|
||||
@ -291,7 +291,7 @@ class TestInitializeCardEvolution:
|
||||
# Delete the sp track to simulate missing seed data
|
||||
self.sp_track.delete_instance()
|
||||
|
||||
result = initialize_card_evolution(player.player_id, team.id, "sp")
|
||||
result = initialize_card_refractor(player.player_id, team.id, "sp")
|
||||
assert result is None
|
||||
|
||||
def test_card_type_from_pos1_batter(self, rarity, team):
|
||||
@ -302,7 +302,7 @@ class TestInitializeCardEvolution:
|
||||
"""
|
||||
player = _make_player(rarity, "3B")
|
||||
card_type = _determine_card_type(player)
|
||||
state = initialize_card_evolution(player.player_id, team.id, card_type)
|
||||
state = initialize_card_refractor(player.player_id, team.id, card_type)
|
||||
|
||||
assert state is not None
|
||||
assert state.track_id == self.batter_track.id
|
||||
@ -311,7 +311,7 @@ class TestInitializeCardEvolution:
|
||||
"""_determine_card_type is wired correctly for a starting pitcher."""
|
||||
player = _make_player(rarity, "SP")
|
||||
card_type = _determine_card_type(player)
|
||||
state = initialize_card_evolution(player.player_id, team.id, card_type)
|
||||
state = initialize_card_refractor(player.player_id, team.id, card_type)
|
||||
|
||||
assert state is not None
|
||||
assert state.track_id == self.sp_track.id
|
||||
@ -320,7 +320,7 @@ class TestInitializeCardEvolution:
|
||||
"""_determine_card_type correctly routes CP to the rp track."""
|
||||
player = _make_player(rarity, "CP")
|
||||
card_type = _determine_card_type(player)
|
||||
state = initialize_card_evolution(player.player_id, team.id, card_type)
|
||||
state = initialize_card_refractor(player.player_id, team.id, card_type)
|
||||
|
||||
assert state is not None
|
||||
assert state.track_id == self.rp_track.id
|
||||
@ -1,12 +1,12 @@
|
||||
"""
|
||||
Tests for evolution-related models and BattingSeasonStats.
|
||||
Tests for refractor-related models and BattingSeasonStats.
|
||||
|
||||
Covers WP-01 acceptance criteria:
|
||||
- EvolutionTrack: CRUD and unique-name constraint
|
||||
- EvolutionCardState: CRUD, defaults, unique-(player,team) constraint,
|
||||
and FK resolution back to EvolutionTrack
|
||||
- EvolutionTierBoost: CRUD and unique-(track, tier, boost_type, boost_target)
|
||||
- EvolutionCosmetic: CRUD and unique-name constraint
|
||||
- RefractorTrack: CRUD and unique-name constraint
|
||||
- RefractorCardState: CRUD, defaults, unique-(player,team) constraint,
|
||||
and FK resolution back to RefractorTrack
|
||||
- RefractorTierBoost: CRUD and unique-(track, tier, boost_type, boost_target)
|
||||
- RefractorCosmetic: CRUD and unique-name constraint
|
||||
- BattingSeasonStats: CRUD with defaults, unique-(player, team, season),
|
||||
and in-place stat accumulation
|
||||
|
||||
@ -21,21 +21,21 @@ from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from app.db_engine import (
|
||||
BattingSeasonStats,
|
||||
EvolutionCardState,
|
||||
EvolutionCosmetic,
|
||||
EvolutionTierBoost,
|
||||
EvolutionTrack,
|
||||
RefractorCardState,
|
||||
RefractorCosmetic,
|
||||
RefractorTierBoost,
|
||||
RefractorTrack,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EvolutionTrack
|
||||
# RefractorTrack
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEvolutionTrack:
|
||||
"""Tests for the EvolutionTrack model.
|
||||
class TestRefractorTrack:
|
||||
"""Tests for the RefractorTrack model.
|
||||
|
||||
EvolutionTrack defines a named progression path (formula +
|
||||
RefractorTrack defines a named progression path (formula +
|
||||
tier thresholds) for a card type. The name column carries a
|
||||
UNIQUE constraint so that accidental duplicates are caught at
|
||||
the database level.
|
||||
@ -60,12 +60,12 @@ class TestEvolutionTrack:
|
||||
def test_track_unique_name(self, track):
|
||||
"""Inserting a second track with the same name raises IntegrityError.
|
||||
|
||||
The UNIQUE constraint on EvolutionTrack.name must prevent two
|
||||
The UNIQUE constraint on RefractorTrack.name must prevent two
|
||||
tracks from sharing the same identifier, as the name is used as
|
||||
a human-readable key throughout the evolution system.
|
||||
"""
|
||||
with pytest.raises(IntegrityError):
|
||||
EvolutionTrack.create(
|
||||
RefractorTrack.create(
|
||||
name="Batter Track", # duplicate
|
||||
card_type="sp",
|
||||
formula="outs * 3",
|
||||
@ -77,15 +77,15 @@ class TestEvolutionTrack:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EvolutionCardState
|
||||
# RefractorCardState
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEvolutionCardState:
|
||||
"""Tests for EvolutionCardState, which tracks per-player evolution progress.
|
||||
class TestRefractorCardState:
|
||||
"""Tests for RefractorCardState, which tracks per-player refractor progress.
|
||||
|
||||
Each row represents one card (player) owned by one team, linked to a
|
||||
specific EvolutionTrack. The model records the current tier (0-4),
|
||||
specific RefractorTrack. The model records the current tier (0-4),
|
||||
accumulated progress value, and whether the card is fully evolved.
|
||||
"""
|
||||
|
||||
@ -98,9 +98,9 @@ class TestEvolutionCardState:
|
||||
fully_evolved → False (evolution is not complete at creation)
|
||||
last_evaluated_at → None (never evaluated yet)
|
||||
"""
|
||||
state = EvolutionCardState.create(player=player, team=team, track=track)
|
||||
state = RefractorCardState.create(player=player, team=team, track=track)
|
||||
|
||||
fetched = EvolutionCardState.get_by_id(state.id)
|
||||
fetched = RefractorCardState.get_by_id(state.id)
|
||||
assert fetched.player_id == player.player_id
|
||||
assert fetched.team_id == team.id
|
||||
assert fetched.track_id == track.id
|
||||
@ -113,34 +113,34 @@ class TestEvolutionCardState:
|
||||
"""A second card state for the same (player, team) pair raises IntegrityError.
|
||||
|
||||
The unique index on (player, team) enforces that each player card
|
||||
has at most one evolution state per team roster slot, preventing
|
||||
duplicate evolution progress rows for the same physical card.
|
||||
has at most one refractor state per team roster slot, preventing
|
||||
duplicate refractor progress rows for the same physical card.
|
||||
"""
|
||||
EvolutionCardState.create(player=player, team=team, track=track)
|
||||
RefractorCardState.create(player=player, team=team, track=track)
|
||||
with pytest.raises(IntegrityError):
|
||||
EvolutionCardState.create(player=player, team=team, track=track)
|
||||
RefractorCardState.create(player=player, team=team, track=track)
|
||||
|
||||
def test_card_state_fk_track(self, player, team, track):
|
||||
"""Accessing card_state.track returns the original EvolutionTrack instance.
|
||||
"""Accessing card_state.track returns the original RefractorTrack instance.
|
||||
|
||||
This confirms the FK is correctly wired and that Peewee resolves
|
||||
the relationship, returning an object with the same primary key and
|
||||
name as the track used during creation.
|
||||
"""
|
||||
state = EvolutionCardState.create(player=player, team=team, track=track)
|
||||
fetched = EvolutionCardState.get_by_id(state.id)
|
||||
state = RefractorCardState.create(player=player, team=team, track=track)
|
||||
fetched = RefractorCardState.get_by_id(state.id)
|
||||
resolved_track = fetched.track
|
||||
assert resolved_track.id == track.id
|
||||
assert resolved_track.name == "Batter Track"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EvolutionTierBoost
|
||||
# RefractorTierBoost
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEvolutionTierBoost:
|
||||
"""Tests for EvolutionTierBoost, the per-tier stat/rating bonus table.
|
||||
class TestRefractorTierBoost:
|
||||
"""Tests for RefractorTierBoost, the per-tier stat/rating bonus table.
|
||||
|
||||
Each row maps a (track, tier) combination to a single boost — the
|
||||
specific stat or rating column to buff and by how much. The four-
|
||||
@ -153,14 +153,14 @@ class TestEvolutionTierBoost:
|
||||
Verifies boost_type, boost_target, and boost_value are stored
|
||||
and retrieved without modification.
|
||||
"""
|
||||
boost = EvolutionTierBoost.create(
|
||||
boost = RefractorTierBoost.create(
|
||||
track=track,
|
||||
tier=1,
|
||||
boost_type="rating",
|
||||
boost_target="contact_vl",
|
||||
boost_value=1.5,
|
||||
)
|
||||
fetched = EvolutionTierBoost.get_by_id(boost.id)
|
||||
fetched = RefractorTierBoost.get_by_id(boost.id)
|
||||
assert fetched.track_id == track.id
|
||||
assert fetched.tier == 1
|
||||
assert fetched.boost_type == "rating"
|
||||
@ -174,7 +174,7 @@ class TestEvolutionTierBoost:
|
||||
(e.g. Tier-1 contact_vl rating) cannot be defined twice for the
|
||||
same track, which would create ambiguity during evolution evaluation.
|
||||
"""
|
||||
EvolutionTierBoost.create(
|
||||
RefractorTierBoost.create(
|
||||
track=track,
|
||||
tier=2,
|
||||
boost_type="rating",
|
||||
@ -182,7 +182,7 @@ class TestEvolutionTierBoost:
|
||||
boost_value=2.0,
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
EvolutionTierBoost.create(
|
||||
RefractorTierBoost.create(
|
||||
track=track,
|
||||
tier=2,
|
||||
boost_type="rating",
|
||||
@ -192,12 +192,12 @@ class TestEvolutionTierBoost:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EvolutionCosmetic
|
||||
# RefractorCosmetic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEvolutionCosmetic:
|
||||
"""Tests for EvolutionCosmetic, decorative unlocks tied to evolution tiers.
|
||||
class TestRefractorCosmetic:
|
||||
"""Tests for RefractorCosmetic, decorative unlocks tied to evolution tiers.
|
||||
|
||||
Cosmetics are purely visual rewards (frames, badges, themes) that a
|
||||
card unlocks when it reaches a required tier. The name column is
|
||||
@ -210,14 +210,14 @@ class TestEvolutionCosmetic:
|
||||
Verifies all columns including optional ones (css_class, asset_url)
|
||||
are stored and retrieved.
|
||||
"""
|
||||
cosmetic = EvolutionCosmetic.create(
|
||||
cosmetic = RefractorCosmetic.create(
|
||||
name="Gold Frame",
|
||||
tier_required=2,
|
||||
cosmetic_type="frame",
|
||||
css_class="evo-frame-gold",
|
||||
asset_url="https://cdn.example.com/frames/gold.png",
|
||||
)
|
||||
fetched = EvolutionCosmetic.get_by_id(cosmetic.id)
|
||||
fetched = RefractorCosmetic.get_by_id(cosmetic.id)
|
||||
assert fetched.name == "Gold Frame"
|
||||
assert fetched.tier_required == 2
|
||||
assert fetched.cosmetic_type == "frame"
|
||||
@ -227,16 +227,16 @@ class TestEvolutionCosmetic:
|
||||
def test_cosmetic_unique_name(self):
|
||||
"""Inserting a second cosmetic with the same name raises IntegrityError.
|
||||
|
||||
The UNIQUE constraint on EvolutionCosmetic.name prevents duplicate
|
||||
The UNIQUE constraint on RefractorCosmetic.name prevents duplicate
|
||||
cosmetic definitions that could cause ambiguous tier unlock lookups.
|
||||
"""
|
||||
EvolutionCosmetic.create(
|
||||
RefractorCosmetic.create(
|
||||
name="Silver Badge",
|
||||
tier_required=1,
|
||||
cosmetic_type="badge",
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
EvolutionCosmetic.create(
|
||||
RefractorCosmetic.create(
|
||||
name="Silver Badge", # duplicate
|
||||
tier_required=3,
|
||||
cosmetic_type="badge",
|
||||
@ -1,16 +1,16 @@
|
||||
"""
|
||||
Tests for app/seed/evolution_tracks.py — seed_evolution_tracks().
|
||||
Tests for app/seed/refractor_tracks.py — seed_refractor_tracks().
|
||||
|
||||
What: Verify that the JSON-driven seed function correctly creates, counts,
|
||||
and idempotently updates EvolutionTrack rows in the database.
|
||||
and idempotently updates RefractorTrack rows in the database.
|
||||
|
||||
Why: The seed is the single source of truth for track configuration. A
|
||||
regression here (duplicates, wrong thresholds, missing formula) would
|
||||
silently corrupt evolution scoring for every card in the system.
|
||||
silently corrupt refractor scoring for every card in the system.
|
||||
|
||||
Each test operates on a fresh in-memory SQLite database provided by the
|
||||
autouse `setup_test_db` fixture in conftest.py. The seed reads its data
|
||||
from `app/seed/evolution_tracks.json` on disk, so the tests also serve as
|
||||
from `app/seed/refractor_tracks.json` on disk, so the tests also serve as
|
||||
a light integration check between the JSON file and the Peewee model.
|
||||
"""
|
||||
|
||||
@ -19,11 +19,11 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.db_engine import EvolutionTrack
|
||||
from app.seed.evolution_tracks import seed_evolution_tracks
|
||||
from app.db_engine import RefractorTrack
|
||||
from app.seed.refractor_tracks import seed_refractor_tracks
|
||||
|
||||
# Path to the JSON fixture that the seed reads from at runtime
|
||||
_JSON_PATH = Path(__file__).parent.parent / "app" / "seed" / "evolution_tracks.json"
|
||||
_JSON_PATH = Path(__file__).parent.parent / "app" / "seed" / "refractor_tracks.json"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -37,48 +37,48 @@ def json_tracks():
|
||||
|
||||
|
||||
def test_seed_creates_three_tracks(json_tracks):
|
||||
"""After one seed call, exactly 3 EvolutionTrack rows must exist.
|
||||
"""After one seed call, exactly 3 RefractorTrack rows must exist.
|
||||
|
||||
Why: The JSON currently defines three card-type tracks (batter, sp, rp).
|
||||
If the count is wrong the system would either be missing tracks
|
||||
(evolution disabled for a card type) or have phantom extras.
|
||||
(refractor disabled for a card type) or have phantom extras.
|
||||
"""
|
||||
seed_evolution_tracks()
|
||||
assert EvolutionTrack.select().count() == 3
|
||||
seed_refractor_tracks()
|
||||
assert RefractorTrack.select().count() == 3
|
||||
|
||||
|
||||
def test_seed_correct_card_types(json_tracks):
|
||||
"""The set of card_type values persisted must match the JSON exactly.
|
||||
|
||||
Why: card_type is used as a discriminator throughout the evolution engine.
|
||||
Why: card_type is used as a discriminator throughout the refractor engine.
|
||||
An unexpected value (e.g. 'pitcher' instead of 'sp') would cause
|
||||
track-lookup misses and silently skip evolution scoring for that role.
|
||||
track-lookup misses and silently skip refractor scoring for that role.
|
||||
"""
|
||||
seed_evolution_tracks()
|
||||
seed_refractor_tracks()
|
||||
expected_types = {d["card_type"] for d in json_tracks}
|
||||
actual_types = {t.card_type for t in EvolutionTrack.select()}
|
||||
actual_types = {t.card_type for t in RefractorTrack.select()}
|
||||
assert actual_types == expected_types
|
||||
|
||||
|
||||
def test_seed_thresholds_ascending():
|
||||
"""For every track, t1 < t2 < t3 < t4.
|
||||
|
||||
Why: The evolution engine uses these thresholds to determine tier
|
||||
Why: The refractor engine uses these thresholds to determine tier
|
||||
boundaries. If they are not strictly ascending, tier comparisons
|
||||
would produce incorrect or undefined results (e.g. a player could
|
||||
simultaneously satisfy tier 3 and not satisfy tier 2).
|
||||
"""
|
||||
seed_evolution_tracks()
|
||||
for track in EvolutionTrack.select():
|
||||
assert (
|
||||
track.t1_threshold < track.t2_threshold
|
||||
), f"{track.name}: t1 ({track.t1_threshold}) >= t2 ({track.t2_threshold})"
|
||||
assert (
|
||||
track.t2_threshold < track.t3_threshold
|
||||
), f"{track.name}: t2 ({track.t2_threshold}) >= t3 ({track.t3_threshold})"
|
||||
assert (
|
||||
track.t3_threshold < track.t4_threshold
|
||||
), f"{track.name}: t3 ({track.t3_threshold}) >= t4 ({track.t4_threshold})"
|
||||
seed_refractor_tracks()
|
||||
for track in RefractorTrack.select():
|
||||
assert track.t1_threshold < track.t2_threshold, (
|
||||
f"{track.name}: t1 ({track.t1_threshold}) >= t2 ({track.t2_threshold})"
|
||||
)
|
||||
assert track.t2_threshold < track.t3_threshold, (
|
||||
f"{track.name}: t2 ({track.t2_threshold}) >= t3 ({track.t3_threshold})"
|
||||
)
|
||||
assert track.t3_threshold < track.t4_threshold, (
|
||||
f"{track.name}: t3 ({track.t3_threshold}) >= t4 ({track.t4_threshold})"
|
||||
)
|
||||
|
||||
|
||||
def test_seed_thresholds_positive():
|
||||
@ -86,10 +86,10 @@ def test_seed_thresholds_positive():
|
||||
|
||||
Why: A zero or negative threshold would mean a card starts the game
|
||||
already evolved (tier >= 1 at 0 accumulated stat points), which would
|
||||
bypass the entire progression system.
|
||||
bypass the entire refractor progression system.
|
||||
"""
|
||||
seed_evolution_tracks()
|
||||
for track in EvolutionTrack.select():
|
||||
seed_refractor_tracks()
|
||||
for track in RefractorTrack.select():
|
||||
assert track.t1_threshold > 0, f"{track.name}: t1_threshold is not positive"
|
||||
assert track.t2_threshold > 0, f"{track.name}: t2_threshold is not positive"
|
||||
assert track.t3_threshold > 0, f"{track.name}: t3_threshold is not positive"
|
||||
@ -99,29 +99,29 @@ def test_seed_thresholds_positive():
|
||||
def test_seed_formula_present():
|
||||
"""Every persisted track must have a non-empty formula string.
|
||||
|
||||
Why: The formula is evaluated at runtime to compute a player's evolution
|
||||
Why: The formula is evaluated at runtime to compute a player's refractor
|
||||
score. An empty formula would cause either a Python eval error or
|
||||
silently produce 0 for every player, halting all evolution progress.
|
||||
silently produce 0 for every player, halting all refractor progress.
|
||||
"""
|
||||
seed_evolution_tracks()
|
||||
for track in EvolutionTrack.select():
|
||||
assert (
|
||||
track.formula and track.formula.strip()
|
||||
), f"{track.name}: formula is empty or whitespace-only"
|
||||
seed_refractor_tracks()
|
||||
for track in RefractorTrack.select():
|
||||
assert track.formula and track.formula.strip(), (
|
||||
f"{track.name}: formula is empty or whitespace-only"
|
||||
)
|
||||
|
||||
|
||||
def test_seed_idempotent():
|
||||
"""Calling seed_evolution_tracks() twice must still yield exactly 3 rows.
|
||||
"""Calling seed_refractor_tracks() twice must still yield exactly 3 rows.
|
||||
|
||||
Why: The seed is designed to be safe to re-run (e.g. as part of a
|
||||
migration or CI bootstrap). If it inserts duplicates on a second call,
|
||||
the unique constraint on EvolutionTrack.name would raise an IntegrityError
|
||||
the unique constraint on RefractorTrack.name would raise an IntegrityError
|
||||
in PostgreSQL, and in SQLite it would silently create phantom rows that
|
||||
corrupt tier-lookup joins.
|
||||
"""
|
||||
seed_evolution_tracks()
|
||||
seed_evolution_tracks()
|
||||
assert EvolutionTrack.select().count() == 3
|
||||
seed_refractor_tracks()
|
||||
seed_refractor_tracks()
|
||||
assert RefractorTrack.select().count() == 3
|
||||
|
||||
|
||||
def test_seed_updates_on_rerun(json_tracks):
|
||||
@ -135,24 +135,24 @@ def test_seed_updates_on_rerun(json_tracks):
|
||||
build up silently and the production database would diverge from the
|
||||
checked-in JSON without any visible error.
|
||||
"""
|
||||
seed_evolution_tracks()
|
||||
seed_refractor_tracks()
|
||||
|
||||
# Pick the first track and corrupt its t1_threshold
|
||||
first_def = json_tracks[0]
|
||||
track = EvolutionTrack.get(EvolutionTrack.name == first_def["name"])
|
||||
track = RefractorTrack.get(RefractorTrack.name == first_def["name"])
|
||||
original_t1 = track.t1_threshold
|
||||
corrupted_value = original_t1 + 9999
|
||||
track.t1_threshold = corrupted_value
|
||||
track.save()
|
||||
|
||||
# Confirm the corruption took effect before re-seeding
|
||||
track_check = EvolutionTrack.get(EvolutionTrack.name == first_def["name"])
|
||||
track_check = RefractorTrack.get(RefractorTrack.name == first_def["name"])
|
||||
assert track_check.t1_threshold == corrupted_value
|
||||
|
||||
# Re-seed — should restore the JSON value
|
||||
seed_evolution_tracks()
|
||||
seed_refractor_tracks()
|
||||
|
||||
restored = EvolutionTrack.get(EvolutionTrack.name == first_def["name"])
|
||||
restored = RefractorTrack.get(RefractorTrack.name == first_def["name"])
|
||||
assert restored.t1_threshold == first_def["t1_threshold"], (
|
||||
f"Expected t1_threshold={first_def['t1_threshold']} after re-seed, "
|
||||
f"got {restored.t1_threshold}"
|
||||
@ -1,11 +1,11 @@
|
||||
"""Integration tests for the evolution card state API endpoints (WP-07).
|
||||
"""Integration tests for the refractor card state API endpoints (WP-07).
|
||||
|
||||
Tests cover:
|
||||
GET /api/v2/teams/{team_id}/evolutions
|
||||
GET /api/v2/evolution/cards/{card_id}
|
||||
GET /api/v2/teams/{team_id}/refractors
|
||||
GET /api/v2/refractor/cards/{card_id}
|
||||
|
||||
All tests require a live PostgreSQL connection (POSTGRES_HOST env var) and
|
||||
assume the evolution schema migration (WP-04) has already been applied.
|
||||
assume the refractor schema migration (WP-04) has already been applied.
|
||||
Tests auto-skip when POSTGRES_HOST is not set.
|
||||
|
||||
Test data is inserted via psycopg2 before each module fixture runs and
|
||||
@ -18,20 +18,20 @@ Object graph built by fixtures
|
||||
cardset_row -- a seeded cardset row
|
||||
player_row -- a seeded player row (FK: rarity, cardset)
|
||||
team_row -- a seeded team row
|
||||
track_row -- a seeded evolution_track row (batter)
|
||||
track_row -- a seeded refractor_track row (batter)
|
||||
card_row -- a seeded card row (FK: player, team, pack, pack_type, cardset)
|
||||
state_row -- a seeded evolution_card_state row (FK: player, team, track)
|
||||
state_row -- a seeded refractor_card_state row (FK: player, team, track)
|
||||
|
||||
Test matrix
|
||||
-----------
|
||||
test_list_team_evolutions -- baseline: returns count + items for a team
|
||||
test_list_team_refractors -- baseline: returns count + items for a team
|
||||
test_list_filter_by_card_type -- card_type query param filters by track.card_type
|
||||
test_list_filter_by_tier -- tier query param filters by current_tier
|
||||
test_list_pagination -- page/per_page params slice results correctly
|
||||
test_get_card_state_shape -- single card returns all required response fields
|
||||
test_get_card_state_next_threshold -- next_threshold is the threshold for tier above current
|
||||
test_get_card_id_resolves_player -- card_id joins Card -> Player/Team -> EvolutionCardState
|
||||
test_get_card_404_no_state -- card with no EvolutionCardState returns 404
|
||||
test_get_card_id_resolves_player -- card_id joins Card -> Player/Team -> RefractorCardState
|
||||
test_get_card_404_no_state -- card with no RefractorCardState returns 404
|
||||
test_duplicate_cards_share_state -- two cards same player+team return the same state row
|
||||
test_auth_required -- missing token returns 401 on both endpoints
|
||||
"""
|
||||
@ -63,7 +63,7 @@ def seeded_data(pg_conn):
|
||||
Insertion order respects FK dependencies:
|
||||
rarity -> cardset -> player
|
||||
pack_type (needs cardset) -> pack (needs team + pack_type) -> card
|
||||
evolution_track -> evolution_card_state
|
||||
refractor_track -> refractor_card_state
|
||||
"""
|
||||
cur = pg_conn.cursor()
|
||||
|
||||
@ -130,7 +130,7 @@ def seeded_data(pg_conn):
|
||||
# Evolution tracks
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO evolution_track (name, card_type, formula,
|
||||
INSERT INTO refractor_track (name, card_type, formula,
|
||||
t1_threshold, t2_threshold,
|
||||
t3_threshold, t4_threshold)
|
||||
VALUES ('WP07 Batter Track', 'batter', 'pa + tb * 2', 37, 149, 448, 896)
|
||||
@ -142,7 +142,7 @@ def seeded_data(pg_conn):
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO evolution_track (name, card_type, formula,
|
||||
INSERT INTO refractor_track (name, card_type, formula,
|
||||
t1_threshold, t2_threshold,
|
||||
t3_threshold, t4_threshold)
|
||||
VALUES ('WP07 SP Track', 'sp', 'ip + k', 10, 40, 120, 240)
|
||||
@ -230,7 +230,7 @@ def seeded_data(pg_conn):
|
||||
# Batter player at tier 1, value 87.5
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO evolution_card_state
|
||||
INSERT INTO refractor_card_state
|
||||
(player_id, team_id, track_id, current_tier, current_value,
|
||||
fully_evolved, last_evaluated_at)
|
||||
VALUES (%s, %s, %s, 1, 87.5, false, '2026-03-12T14:00:00Z')
|
||||
@ -258,7 +258,7 @@ def seeded_data(pg_conn):
|
||||
}
|
||||
|
||||
# Teardown: delete in reverse FK order
|
||||
cur.execute("DELETE FROM evolution_card_state WHERE id = %s", (state_id,))
|
||||
cur.execute("DELETE FROM refractor_card_state WHERE id = %s", (state_id,))
|
||||
cur.execute(
|
||||
"DELETE FROM card WHERE id = ANY(%s)",
|
||||
([card_id, card2_id, card_no_state_id],),
|
||||
@ -266,7 +266,7 @@ def seeded_data(pg_conn):
|
||||
cur.execute("DELETE FROM pack WHERE id = ANY(%s)", ([pack_id, pack2_id, pack3_id],))
|
||||
cur.execute("DELETE FROM pack_type WHERE id = %s", (pack_type_id,))
|
||||
cur.execute(
|
||||
"DELETE FROM evolution_track WHERE id = ANY(%s)",
|
||||
"DELETE FROM refractor_track WHERE id = ANY(%s)",
|
||||
([batter_track_id, sp_track_id],),
|
||||
)
|
||||
cur.execute(
|
||||
@ -288,19 +288,19 @@ def client():
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: GET /api/v2/teams/{team_id}/evolutions
|
||||
# Tests: GET /api/v2/teams/{team_id}/refractors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@_skip_no_pg
|
||||
def test_list_team_evolutions(client, seeded_data):
|
||||
"""GET /teams/{id}/evolutions returns count=1 and one item for the seeded state.
|
||||
def test_list_team_refractors(client, seeded_data):
|
||||
"""GET /teams/{id}/refractors returns count=1 and one item for the seeded state.
|
||||
|
||||
Verifies the basic list response shape: a dict with 'count' and 'items',
|
||||
and that the single item contains player_id, team_id, and current_tier.
|
||||
"""
|
||||
team_id = seeded_data["team_id"]
|
||||
resp = client.get(f"/api/v2/teams/{team_id}/evolutions", headers=AUTH_HEADER)
|
||||
resp = client.get(f"/api/v2/teams/{team_id}/refractors", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["count"] == 1
|
||||
@ -315,15 +315,15 @@ def test_list_team_evolutions(client, seeded_data):
|
||||
def test_list_filter_by_card_type(client, seeded_data, pg_conn):
|
||||
"""card_type filter includes states whose track.card_type matches and excludes others.
|
||||
|
||||
Seeds a second evolution_card_state for player2 (sp track) then queries
|
||||
Seeds a second refractor_card_state for player2 (sp track) then queries
|
||||
card_type=batter (returns 1) and card_type=sp (returns 1).
|
||||
Verifies the JOIN to evolution_track and the WHERE predicate on card_type.
|
||||
Verifies the JOIN to refractor_track and the WHERE predicate on card_type.
|
||||
"""
|
||||
cur = pg_conn.cursor()
|
||||
# Add a state for the sp player so we have two types in this team
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO evolution_card_state
|
||||
INSERT INTO refractor_card_state
|
||||
(player_id, team_id, track_id, current_tier, current_value, fully_evolved)
|
||||
VALUES (%s, %s, %s, 0, 0.0, false)
|
||||
RETURNING id
|
||||
@ -337,7 +337,7 @@ def test_list_filter_by_card_type(client, seeded_data, pg_conn):
|
||||
team_id = seeded_data["team_id"]
|
||||
|
||||
resp_batter = client.get(
|
||||
f"/api/v2/teams/{team_id}/evolutions?card_type=batter", headers=AUTH_HEADER
|
||||
f"/api/v2/teams/{team_id}/refractors?card_type=batter", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp_batter.status_code == 200
|
||||
batter_data = resp_batter.json()
|
||||
@ -345,14 +345,14 @@ def test_list_filter_by_card_type(client, seeded_data, pg_conn):
|
||||
assert batter_data["items"][0]["player_id"] == seeded_data["player_id"]
|
||||
|
||||
resp_sp = client.get(
|
||||
f"/api/v2/teams/{team_id}/evolutions?card_type=sp", headers=AUTH_HEADER
|
||||
f"/api/v2/teams/{team_id}/refractors?card_type=sp", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp_sp.status_code == 200
|
||||
sp_data = resp_sp.json()
|
||||
assert sp_data["count"] == 1
|
||||
assert sp_data["items"][0]["player_id"] == seeded_data["player2_id"]
|
||||
finally:
|
||||
cur.execute("DELETE FROM evolution_card_state WHERE id = %s", (sp_state_id,))
|
||||
cur.execute("DELETE FROM refractor_card_state WHERE id = %s", (sp_state_id,))
|
||||
pg_conn.commit()
|
||||
|
||||
|
||||
@ -368,7 +368,7 @@ def test_list_filter_by_tier(client, seeded_data, pg_conn):
|
||||
|
||||
# Advance to tier 2
|
||||
cur.execute(
|
||||
"UPDATE evolution_card_state SET current_tier = 2 WHERE id = %s",
|
||||
"UPDATE refractor_card_state SET current_tier = 2 WHERE id = %s",
|
||||
(seeded_data["state_id"],),
|
||||
)
|
||||
pg_conn.commit()
|
||||
@ -377,13 +377,13 @@ def test_list_filter_by_tier(client, seeded_data, pg_conn):
|
||||
team_id = seeded_data["team_id"]
|
||||
|
||||
resp_t1 = client.get(
|
||||
f"/api/v2/teams/{team_id}/evolutions?tier=1", headers=AUTH_HEADER
|
||||
f"/api/v2/teams/{team_id}/refractors?tier=1", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp_t1.status_code == 200
|
||||
assert resp_t1.json()["count"] == 0
|
||||
|
||||
resp_t2 = client.get(
|
||||
f"/api/v2/teams/{team_id}/evolutions?tier=2", headers=AUTH_HEADER
|
||||
f"/api/v2/teams/{team_id}/refractors?tier=2", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp_t2.status_code == 200
|
||||
t2_data = resp_t2.json()
|
||||
@ -391,7 +391,7 @@ def test_list_filter_by_tier(client, seeded_data, pg_conn):
|
||||
assert t2_data["items"][0]["current_tier"] == 2
|
||||
finally:
|
||||
cur.execute(
|
||||
"UPDATE evolution_card_state SET current_tier = 1 WHERE id = %s",
|
||||
"UPDATE refractor_card_state SET current_tier = 1 WHERE id = %s",
|
||||
(seeded_data["state_id"],),
|
||||
)
|
||||
pg_conn.commit()
|
||||
@ -408,7 +408,7 @@ def test_list_pagination(client, seeded_data, pg_conn):
|
||||
cur = pg_conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO evolution_card_state
|
||||
INSERT INTO refractor_card_state
|
||||
(player_id, team_id, track_id, current_tier, current_value, fully_evolved)
|
||||
VALUES (%s, %s, %s, 0, 0.0, false)
|
||||
RETURNING id
|
||||
@ -426,14 +426,14 @@ def test_list_pagination(client, seeded_data, pg_conn):
|
||||
team_id = seeded_data["team_id"]
|
||||
|
||||
resp1 = client.get(
|
||||
f"/api/v2/teams/{team_id}/evolutions?page=1&per_page=1", headers=AUTH_HEADER
|
||||
f"/api/v2/teams/{team_id}/refractors?page=1&per_page=1", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp1.status_code == 200
|
||||
data1 = resp1.json()
|
||||
assert len(data1["items"]) == 1
|
||||
|
||||
resp2 = client.get(
|
||||
f"/api/v2/teams/{team_id}/evolutions?page=2&per_page=1", headers=AUTH_HEADER
|
||||
f"/api/v2/teams/{team_id}/refractors?page=2&per_page=1", headers=AUTH_HEADER
|
||||
)
|
||||
assert resp2.status_code == 200
|
||||
data2 = resp2.json()
|
||||
@ -441,18 +441,18 @@ def test_list_pagination(client, seeded_data, pg_conn):
|
||||
|
||||
assert data1["items"][0]["player_id"] != data2["items"][0]["player_id"]
|
||||
finally:
|
||||
cur.execute("DELETE FROM evolution_card_state WHERE id = %s", (extra_state_id,))
|
||||
cur.execute("DELETE FROM refractor_card_state WHERE id = %s", (extra_state_id,))
|
||||
pg_conn.commit()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: GET /api/v2/evolution/cards/{card_id}
|
||||
# Tests: GET /api/v2/refractor/cards/{card_id}
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@_skip_no_pg
|
||||
def test_get_card_state_shape(client, seeded_data):
|
||||
"""GET /evolution/cards/{card_id} returns all required fields.
|
||||
"""GET /refractor/cards/{card_id} returns all required fields.
|
||||
|
||||
Verifies the full response envelope:
|
||||
player_id, team_id, current_tier, current_value, fully_evolved,
|
||||
@ -460,7 +460,7 @@ def test_get_card_state_shape(client, seeded_data):
|
||||
with id, name, card_type, formula, and t1-t4 thresholds.
|
||||
"""
|
||||
card_id = seeded_data["card_id"]
|
||||
resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER)
|
||||
resp = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
|
||||
@ -505,29 +505,29 @@ def test_get_card_state_next_threshold(client, seeded_data, pg_conn):
|
||||
|
||||
# Advance to tier 2
|
||||
cur.execute(
|
||||
"UPDATE evolution_card_state SET current_tier = 2 WHERE id = %s", (state_id,)
|
||||
"UPDATE refractor_card_state SET current_tier = 2 WHERE id = %s", (state_id,)
|
||||
)
|
||||
pg_conn.commit()
|
||||
|
||||
try:
|
||||
resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER)
|
||||
resp = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["next_threshold"] == 448 # t3_threshold
|
||||
|
||||
# Advance to tier 4 (fully evolved)
|
||||
cur.execute(
|
||||
"UPDATE evolution_card_state SET current_tier = 4, fully_evolved = true "
|
||||
"UPDATE refractor_card_state SET current_tier = 4, fully_evolved = true "
|
||||
"WHERE id = %s",
|
||||
(state_id,),
|
||||
)
|
||||
pg_conn.commit()
|
||||
|
||||
resp2 = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER)
|
||||
resp2 = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER)
|
||||
assert resp2.status_code == 200
|
||||
assert resp2.json()["next_threshold"] is None
|
||||
finally:
|
||||
cur.execute(
|
||||
"UPDATE evolution_card_state SET current_tier = 1, fully_evolved = false "
|
||||
"UPDATE refractor_card_state SET current_tier = 1, fully_evolved = false "
|
||||
"WHERE id = %s",
|
||||
(state_id,),
|
||||
)
|
||||
@ -538,11 +538,11 @@ def test_get_card_state_next_threshold(client, seeded_data, pg_conn):
|
||||
def test_get_card_id_resolves_player(client, seeded_data):
|
||||
"""card_id is resolved via the Card table to obtain (player_id, team_id).
|
||||
|
||||
The endpoint must JOIN Card -> Player + Team to find the EvolutionCardState.
|
||||
The endpoint must JOIN Card -> Player + Team to find the RefractorCardState.
|
||||
Verifies that card_id correctly maps to the right player's evolution state.
|
||||
"""
|
||||
card_id = seeded_data["card_id"]
|
||||
resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER)
|
||||
resp = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["player_id"] == seeded_data["player_id"]
|
||||
@ -551,20 +551,20 @@ def test_get_card_id_resolves_player(client, seeded_data):
|
||||
|
||||
@_skip_no_pg
|
||||
def test_get_card_404_no_state(client, seeded_data):
|
||||
"""GET /evolution/cards/{card_id} returns 404 when no EvolutionCardState exists.
|
||||
"""GET /refractor/cards/{card_id} returns 404 when no RefractorCardState exists.
|
||||
|
||||
card_no_state_id is a card row for player2 on the team, but no
|
||||
evolution_card_state row was created for player2. The endpoint must
|
||||
refractor_card_state row was created for player2. The endpoint must
|
||||
return 404, not 500 or an empty response.
|
||||
"""
|
||||
card_id = seeded_data["card_no_state_id"]
|
||||
resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER)
|
||||
resp = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@_skip_no_pg
|
||||
def test_duplicate_cards_share_state(client, seeded_data):
|
||||
"""Two Card rows for the same player+team share one EvolutionCardState.
|
||||
"""Two Card rows for the same player+team share one RefractorCardState.
|
||||
|
||||
card_id and card2_id both belong to player_id on team_id. Because the
|
||||
unique-(player,team) constraint means only one state row can exist, both
|
||||
@ -573,8 +573,8 @@ def test_duplicate_cards_share_state(client, seeded_data):
|
||||
card1_id = seeded_data["card_id"]
|
||||
card2_id = seeded_data["card2_id"]
|
||||
|
||||
resp1 = client.get(f"/api/v2/evolution/cards/{card1_id}", headers=AUTH_HEADER)
|
||||
resp2 = client.get(f"/api/v2/evolution/cards/{card2_id}", headers=AUTH_HEADER)
|
||||
resp1 = client.get(f"/api/v2/refractor/cards/{card1_id}", headers=AUTH_HEADER)
|
||||
resp2 = client.get(f"/api/v2/refractor/cards/{card2_id}", headers=AUTH_HEADER)
|
||||
|
||||
assert resp1.status_code == 200
|
||||
assert resp2.status_code == 200
|
||||
@ -596,14 +596,14 @@ def test_auth_required(client, seeded_data):
|
||||
"""Both endpoints return 401 when no Bearer token is provided.
|
||||
|
||||
Verifies that the valid_token dependency is enforced on:
|
||||
GET /api/v2/teams/{id}/evolutions
|
||||
GET /api/v2/evolution/cards/{id}
|
||||
GET /api/v2/teams/{id}/refractors
|
||||
GET /api/v2/refractor/cards/{id}
|
||||
"""
|
||||
team_id = seeded_data["team_id"]
|
||||
card_id = seeded_data["card_id"]
|
||||
|
||||
resp_list = client.get(f"/api/v2/teams/{team_id}/evolutions")
|
||||
resp_list = client.get(f"/api/v2/teams/{team_id}/refractors")
|
||||
assert resp_list.status_code == 401
|
||||
|
||||
resp_card = client.get(f"/api/v2/evolution/cards/{card_id}")
|
||||
resp_card = client.get(f"/api/v2/refractor/cards/{card_id}")
|
||||
assert resp_card.status_code == 401
|
||||
@ -1,11 +1,11 @@
|
||||
"""Integration tests for the evolution track catalog API endpoints (WP-06).
|
||||
"""Integration tests for the refractor track catalog API endpoints (WP-06).
|
||||
|
||||
Tests cover:
|
||||
GET /api/v2/evolution/tracks
|
||||
GET /api/v2/evolution/tracks/{track_id}
|
||||
GET /api/v2/refractor/tracks
|
||||
GET /api/v2/refractor/tracks/{track_id}
|
||||
|
||||
All tests require a live PostgreSQL connection (POSTGRES_HOST env var) and
|
||||
assume the evolution schema migration (WP-04) has already been applied.
|
||||
assume the refractor schema migration (WP-04) has already been applied.
|
||||
Tests auto-skip when POSTGRES_HOST is not set.
|
||||
|
||||
Test data is inserted via psycopg2 before the test module runs and deleted
|
||||
@ -45,7 +45,7 @@ def seeded_tracks(pg_conn):
|
||||
for name, card_type, formula, t1, t2, t3, t4 in _SEED_TRACKS:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO evolution_track
|
||||
INSERT INTO refractor_track
|
||||
(name, card_type, formula, t1_threshold, t2_threshold, t3_threshold, t4_threshold)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (card_type) DO UPDATE SET
|
||||
@ -62,7 +62,7 @@ def seeded_tracks(pg_conn):
|
||||
ids.append(cur.fetchone()[0])
|
||||
pg_conn.commit()
|
||||
yield ids
|
||||
cur.execute("DELETE FROM evolution_track WHERE id = ANY(%s)", (ids,))
|
||||
cur.execute("DELETE FROM refractor_track WHERE id = ANY(%s)", (ids,))
|
||||
pg_conn.commit()
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ def test_list_tracks_returns_count_3(client, seeded_tracks):
|
||||
After seeding batter/sp/rp, the table should have exactly those three
|
||||
rows (no other tracks are inserted by other test modules).
|
||||
"""
|
||||
resp = client.get("/api/v2/evolution/tracks", headers=AUTH_HEADER)
|
||||
resp = client.get("/api/v2/refractor/tracks", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["count"] == 3
|
||||
@ -92,7 +92,7 @@ def test_list_tracks_returns_count_3(client, seeded_tracks):
|
||||
@_skip_no_pg
|
||||
def test_filter_by_card_type(client, seeded_tracks):
|
||||
"""card_type=sp filter returns exactly 1 track with card_type 'sp'."""
|
||||
resp = client.get("/api/v2/evolution/tracks?card_type=sp", headers=AUTH_HEADER)
|
||||
resp = client.get("/api/v2/refractor/tracks?card_type=sp", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["count"] == 1
|
||||
@ -103,7 +103,7 @@ def test_filter_by_card_type(client, seeded_tracks):
|
||||
def test_get_single_track_with_thresholds(client, seeded_tracks):
|
||||
"""GET /tracks/{id} returns a track dict with formula and t1-t4 thresholds."""
|
||||
track_id = seeded_tracks[0] # batter
|
||||
resp = client.get(f"/api/v2/evolution/tracks/{track_id}", headers=AUTH_HEADER)
|
||||
resp = client.get(f"/api/v2/refractor/tracks/{track_id}", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["card_type"] == "batter"
|
||||
@ -117,16 +117,16 @@ def test_get_single_track_with_thresholds(client, seeded_tracks):
|
||||
@_skip_no_pg
|
||||
def test_404_for_nonexistent_track(client, seeded_tracks):
|
||||
"""GET /tracks/999999 returns 404 when the track does not exist."""
|
||||
resp = client.get("/api/v2/evolution/tracks/999999", headers=AUTH_HEADER)
|
||||
resp = client.get("/api/v2/refractor/tracks/999999", headers=AUTH_HEADER)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@_skip_no_pg
|
||||
def test_auth_required(client, seeded_tracks):
|
||||
"""Requests without a Bearer token return 401 for both endpoints."""
|
||||
resp_list = client.get("/api/v2/evolution/tracks")
|
||||
resp_list = client.get("/api/v2/refractor/tracks")
|
||||
assert resp_list.status_code == 401
|
||||
|
||||
track_id = seeded_tracks[0]
|
||||
resp_single = client.get(f"/api/v2/evolution/tracks/{track_id}")
|
||||
resp_single = client.get(f"/api/v2/refractor/tracks/{track_id}")
|
||||
assert resp_single.status_code == 401
|
||||
Loading…
Reference in New Issue
Block a user