paper-dynasty-database/app/routers_v2/cards.py
Cal Corum b7dec3f231 refactor: rename evolution system to refractor
Complete rename of the card progression system from "Evolution" to
"Refractor" across all code, routes, models, services, seeds, and tests.

- Route prefix: /api/v2/evolution → /api/v2/refractor
- Model classes: EvolutionTrack → RefractorTrack, etc.
- 12 files renamed, 8 files content-edited
- New migration to rename DB tables
- 117 tests pass, no logic changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:31:55 -05:00

417 lines
13 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Response, Query
from typing import Optional, List
import logging
import pydantic
from pandas import DataFrame
from ..db_engine import (
db,
Card,
model_to_dict,
Team,
Player,
Pack,
Paperdex,
CARDSETS,
DoesNotExist,
)
from ..dependencies import oauth2_scheme, valid_token
from ..services.refractor_init import _determine_card_type, initialize_card_evolution
router = APIRouter(prefix="/api/v2/cards", tags=["cards"])
class CardPydantic(pydantic.BaseModel):
player_id: int
team_id: int
pack_id: int
value: Optional[int] = 0
variant: Optional[int] = 0
class CardModel(pydantic.BaseModel):
cards: List[CardPydantic]
@router.get("")
async def get_cards(
player_id: Optional[int] = None,
team_id: Optional[int] = None,
pack_id: Optional[int] = None,
value: Optional[int] = None,
min_value: Optional[int] = None,
max_value: Optional[int] = None,
variant: Optional[int] = None,
order_by: Optional[str] = None,
limit: Optional[int] = None,
dupes: Optional[bool] = None,
csv: Optional[bool] = None,
):
all_cards = Card.select()
# if all_cards.count() == 0:
# db.close()
# raise HTTPException(status_code=404, detail=f'There are no cards to filter')
if team_id is not None:
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}"
)
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}"
)
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}"
)
all_cards = all_cards.where(Card.pack == this_pack)
if value is not None:
all_cards = all_cards.where(Card.value == value)
# if variant is not None:
# all_cards = all_cards.where(Card.variant == variant)
if min_value is not None:
all_cards = all_cards.where(Card.value >= min_value)
if max_value is not None:
all_cards = all_cards.where(Card.value <= max_value)
if order_by is not None:
if order_by.lower() == "new":
all_cards = all_cards.order_by(-Card.id)
else:
all_cards = all_cards.order_by(Card.id)
if limit is not None:
all_cards = all_cards.limit(limit)
if dupes:
if team_id is None:
raise HTTPException(
status_code=400, detail="Dupe checking must include a team_id"
)
logging.debug("dupe check")
p_query = Card.select(Card.player).where(Card.team_id == team_id)
seen = set()
dupes = []
for x in p_query:
if x.player.player_id in seen:
dupes.append(x.player_id)
else:
seen.add(x.player_id)
all_cards = all_cards.where(Card.player_id << dupes)
# if all_cards.count() == 0:
# db.close()
# raise HTTPException(status_code=404, detail=f'No cards found')
if csv:
data_list = [
["id", "player", "cardset", "rarity", "team", "pack", "value"]
] # , 'variant']]
for line in all_cards:
data_list.append(
[
line.id,
line.player.p_name,
line.player.cardset,
line.player.rarity,
line.team.abbrev,
line.pack,
line.value, # line.variant
]
)
return_val = DataFrame(data_list).to_csv(header=False, index=False)
return Response(content=return_val, media_type="text/csv")
else:
card_list = list(all_cards)
player_ids = [c.player_id for c in card_list if c.player_id is not None]
dex_by_player = {}
if player_ids:
for row in Paperdex.select().where(Paperdex.player_id << player_ids):
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}")
entries = dex_by_player.get(x.player_id, [])
this_record["player"]["paperdex"] = {
"count": len(entries),
"paperdex": [model_to_dict(y, recurse=False) for y in entries],
}
return_val["cards"].append(this_record)
# return_val['cards'].append(model_to_dict(x))
return return_val
@router.get("/{card_id}")
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}")
if csv:
data_list = [
["id", "player", "team", "pack", "value"],
[
this_card.id,
this_card.player,
this_card.team.abbrev,
this_card.pack,
this_card.value,
],
]
return_val = DataFrame(data_list).to_csv(header=False, index=False)
return Response(content=return_val, media_type="text/csv")
else:
return_val = model_to_dict(this_card)
return return_val
@router.post("")
async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)):
if not valid_token(token):
logging.warning("Bad Token: [REDACTED]")
raise HTTPException(
status_code=401,
detail="You are not authorized to post cards. This event has been logged.",
)
new_cards = []
player_ids = []
inc_dex = True
this_team = Team.get_by_id(cards.cards[0].team_id)
if this_team.is_ai or "Gauntlet" in this_team.abbrev:
inc_dex = False
# new_dex = []
# now = int(datetime.timestamp(datetime.now()) * 1000)
for x in cards.cards:
this_card = Card(
player_id=x.player_id,
team_id=x.team_id,
pack_id=x.pack_id,
value=x.value,
# variant=x.variant
)
if inc_dex:
Paperdex.get_or_create(team_id=x.team_id, player_id=x.player_id)
player_ids.append(x.player_id)
new_cards.append(this_card)
with db.atomic():
Card.bulk_create(new_cards, batch_size=15)
cost_query = Player.update(cost=Player.cost + 1).where(
Player.player_id << player_ids
)
cost_query.execute()
# sheets.post_new_cards(SHEETS_AUTH, lc_id)
# WP-10: initialize evolution 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)
except Exception:
logging.exception(
"refractor hook: unexpected error for player_id=%s team_id=%s",
x.player_id,
x.team_id,
)
raise HTTPException(
status_code=200, detail=f"{len(new_cards)} cards have been added"
)
# @router.post('/ai-update')
# async def v1_cards_ai_update(token: str = Depends(oauth2_scheme)):
# if not valid_token(token):
# logging.warning('Bad Token: [REDACTED]')
# db.close()
# raise HTTPException(
# status_code=401,
# detail='You are not authorized to update AI cards. This event has been logged.'
# )
#
# sheets.send_ai_cards(SHEETS_AUTH)
# raise HTTPException(status_code=200, detail=f'Just sent AI cards to sheets')
@router.post("/legal-check/{rarity_name}")
async def v1_cards_legal_check(
rarity_name: str,
card_id: list = Query(default=None),
token: str = Depends(oauth2_scheme),
):
if not valid_token(token):
logging.warning("Bad Token: [REDACTED]")
raise HTTPException(status_code=401, detail="Unauthorized")
if rarity_name not in CARDSETS.keys():
return f"Rarity name {rarity_name} not a valid check"
# Handle case where card_id is passed as a stringified list
if (
card_id
and len(card_id) == 1
and isinstance(card_id[0], str)
and card_id[0].startswith("[")
):
import ast
try:
card_id = [int(x) for x in ast.literal_eval(card_id[0])]
except (ValueError, SyntaxError):
pass
bad_cards = []
all_cards = Card.select().where(Card.id << card_id)
for x in all_cards:
if x.player.cardset_id not in CARDSETS[rarity_name]["human"]:
if x.player.p_name in x.player.description:
bad_cards.append(x.player.description)
else:
bad_cards.append(f"{x.player.description} {x.player.p_name}")
return {"count": len(bad_cards), "bad_cards": bad_cards}
@router.post("/post-update/{starting_id}")
async def v1_cards_post_update(starting_id: int, token: str = Depends(oauth2_scheme)):
if not valid_token(token):
logging.warning("Bad Token: [REDACTED]")
raise HTTPException(
status_code=401,
detail="You are not authorized to update card lists. This event has been logged.",
)
# sheets.post_new_cards(SHEETS_AUTH, starting_id)
raise HTTPException(
status_code=200,
detail=f"Just sent cards to sheets starting at ID {starting_id}",
)
@router.post("/post-delete")
async def v1_cards_post_delete(del_ids: str, token: str = Depends(oauth2_scheme)):
if not valid_token(token):
logging.warning("Bad Token: [REDACTED]")
raise HTTPException(
status_code=401,
detail="You are not authorized to delete card lists. This event has been logged.",
)
logging.info(f"del_ids: {del_ids} / type: {type(del_ids)}")
# sheets.post_deletion(SHEETS_AUTH, del_ids.split(','))
@router.post("/wipe-team/{team_id}")
async def v1_cards_wipe_team(team_id: int, token: str = Depends(oauth2_scheme)):
if not valid_token(token):
logging.warning("Bad Token: [REDACTED]")
raise HTTPException(
status_code=401,
detail="You are not authorized to wipe teams. This event has been logged.",
)
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")
t_query = Card.update(team=None).where(Card.team == this_team).execute()
return f"Wiped {t_query} cards"
@router.patch("/{card_id}")
async def v1_cards_patch(
card_id,
player_id: Optional[int] = None,
team_id: Optional[int] = None,
pack_id: Optional[int] = None,
value: Optional[int] = None,
variant: Optional[int] = None,
roster1_id: Optional[int] = None,
roster2_id: Optional[int] = None,
roster3_id: Optional[int] = None,
token: str = Depends(oauth2_scheme),
):
if not valid_token(token):
logging.warning("Bad Token: [REDACTED]")
raise HTTPException(
status_code=401,
detail="You are not authorized to patch cards. This event has been logged.",
)
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}")
if player_id is not None:
this_card.player_id = player_id
if team_id is not None:
if team_id == 0:
this_card.team_id = None
else:
this_card.team_id = team_id
if pack_id is not None:
this_card.pack_id = pack_id
if value is not None:
this_card.value = value
# if variant is not None:
# this_card.variant = variant
if roster1_id is not None:
this_card.roster1_id = roster1_id
if roster2_id is not None:
this_card.roster2_id = roster2_id
if roster3_id is not None:
this_card.roster3_id = roster3_id
if this_card.save() == 1:
return_val = model_to_dict(this_card)
return return_val
else:
raise HTTPException(
status_code=418,
detail="Well slap my ass and call me a teapot; I could not save that rarity",
)
@router.delete("/{card_id}")
async def v1_cards_delete(card_id, token: str = Depends(oauth2_scheme)):
if not valid_token(token):
logging.warning("Bad Token: [REDACTED]")
raise HTTPException(
status_code=401,
detail="You are not authorized to delete packs. This event has been logged.",
)
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}")
count = this_card.delete_instance()
if count == 1:
raise HTTPException(status_code=200, detail=f"Card {card_id} has been deleted")
else:
raise HTTPException(status_code=500, detail=f"Card {card_id} was not deleted")