From e46a6e7493ab32e7d18cf78b1f61b8736c0a11f0 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 3 Mar 2026 13:36:26 -0600 Subject: [PATCH] fix: remove dead roster fields from CSV in v1_cards_get_one (#36) Removes non-existent roster1/roster2/roster3 columns from CSV output. Accessing this_card.roster1.name raised AttributeError at runtime for any GET /api/v2/cards/{card_id}?csv=true request. Co-Authored-By: Claude Sonnet 4.6 --- app/routers_v2/cards.py | 207 +++++++++++++++++++++++++--------------- 1 file changed, 129 insertions(+), 78 deletions(-) diff --git a/app/routers_v2/cards.py b/app/routers_v2/cards.py index a501021..3d8e706 100644 --- a/app/routers_v2/cards.py +++ b/app/routers_v2/cards.py @@ -8,15 +8,12 @@ from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex, C from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( - filename=LOG_DATA['filename'], - format=LOG_DATA['format'], - level=LOG_DATA['log_level'] + filename=LOG_DATA["filename"], + format=LOG_DATA["format"], + level=LOG_DATA["log_level"], ) -router = APIRouter( - prefix='/api/v2/cards', - tags=['cards'] -) +router = APIRouter(prefix="/api/v2/cards", tags=["cards"]) class CardPydantic(pydantic.BaseModel): @@ -31,12 +28,20 @@ class CardModel(pydantic.BaseModel): cards: List[CardPydantic] -@router.get('') +@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): + 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: @@ -48,21 +53,27 @@ async def get_cards( this_team = Team.get_by_id(team_id) except Exception: db.close() - 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 Exception: db.close() - 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 Exception: db.close() - 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) @@ -73,7 +84,7 @@ async def get_cards( 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': + if order_by.lower() == "new": all_cards = all_cards.order_by(-Card.id) else: all_cards = all_cards.order_by(Card.id) @@ -81,8 +92,10 @@ async def get_cards( 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(f'dupe check') + raise HTTPException( + status_code=400, detail="Dupe checking must include a team_id" + ) + logging.debug(f"dupe check") p_query = Card.select(Card.player).where(Card.team_id == team_id) seen = set() dupes = [] @@ -98,32 +111,44 @@ async def get_cards( # raise HTTPException(status_code=404, detail=f'No cards found') if csv: - data_list = [['id', 'player', 'cardset', 'rarity', 'team', 'pack', 'value']] #, 'variant']] + 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.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) db.close() - return Response(content=return_val, media_type='text/csv') + return Response(content=return_val, media_type="text/csv") else: - return_val = {'count': all_cards.count(), 'cards': []} + return_val = {"count": all_cards.count(), "cards": []} for x in all_cards: this_record = model_to_dict(x) - logging.debug(f'this_record: {this_record}') + logging.debug(f"this_record: {this_record}") this_dex = Paperdex.select().where(Paperdex.player == x) - this_record['player']['paperdex'] = {'count': this_dex.count(), 'paperdex': []} + this_record["player"]["paperdex"] = { + "count": this_dex.count(), + "paperdex": [], + } for y in this_dex: - this_record['player']['paperdex']['paperdex'].append(model_to_dict(y, recurse=False)) + this_record["player"]["paperdex"]["paperdex"].append( + model_to_dict(y, recurse=False) + ) - return_val['cards'].append(this_record) + return_val["cards"].append(this_record) # return_val['cards'].append(model_to_dict(x)) @@ -131,24 +156,29 @@ async def get_cards( return return_val -@router.get('/{card_id}') +@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 Exception: db.close() - 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 = [ - ['id', 'player', 'team', 'pack', 'value', 'roster1', 'roster2', 'roster3'], - [this_card.id, this_card.player, this_card.team.abbrev, this_card.pack, this_card.value, - this_card.roster1.name, this_card.roster2.name, this_card.roster3.name] + ["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) db.close() - 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_card) @@ -156,14 +186,14 @@ async def v1_cards_get_one(card_id, csv: Optional[bool] = False): return return_val -@router.post('') +@router.post("") async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)): if not valid_token(token): - logging.warning(f'Bad Token: {token}') + logging.warning(f"Bad Token: {token}") db.close() raise HTTPException( status_code=401, - detail='You are not authorized to post cards. This event has been logged.' + detail="You are not authorized to post cards. This event has been logged.", ) last_card = Card.select(Card.id).order_by(-Card.id).limit(1) lc_id = last_card[0].id @@ -172,7 +202,7 @@ async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)): 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: + if this_team.is_ai or "Gauntlet" in this_team.abbrev: inc_dex = False # new_dex = [] @@ -192,12 +222,16 @@ async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)): 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 = Player.update(cost=Player.cost + 1).where( + Player.player_id << player_ids + ) cost_query.execute() # sheets.post_new_cards(SHEETS_AUTH, lc_id) db.close() - raise HTTPException(status_code=200, detail=f'{len(new_cards)} cards have been added') + raise HTTPException( + status_code=200, detail=f"{len(new_cards)} cards have been added" + ) # @router.post('/ai-update') @@ -214,22 +248,28 @@ async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)): # raise HTTPException(status_code=200, detail=f'Just sent AI cards to sheets') -@router.post('/legal-check/{rarity_name}') +@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)): + rarity_name: str, + card_id: list = Query(default=None), + token: str = Depends(oauth2_scheme), +): if not valid_token(token): - logging.warning(f'Bad Token: {token}') + logging.warning(f"Bad Token: {token}") db.close() - raise HTTPException( - status_code=401, - detail='Unauthorized' - ) + raise HTTPException(status_code=401, detail="Unauthorized") if rarity_name not in CARDSETS.keys(): - return f'Rarity name {rarity_name} not a valid check' + 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('['): + 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): @@ -239,82 +279,93 @@ async def v1_cards_legal_check( 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.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}') + bad_cards.append(f"{x.player.description} {x.player.p_name}") - return {'count': len(bad_cards), 'bad_cards': bad_cards} + return {"count": len(bad_cards), "bad_cards": bad_cards} -@router.post('/post-update/{starting_id}') +@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(f'Bad Token: {token}') + logging.warning(f"Bad Token: {token}") db.close() raise HTTPException( status_code=401, - detail='You are not authorized to update card lists. This event has been logged.' + detail="You are not authorized to update card lists. This event has been logged.", ) # sheets.post_new_cards(SHEETS_AUTH, starting_id) db.close() - raise HTTPException(status_code=200, detail=f'Just sent cards to sheets starting at ID {starting_id}') + raise HTTPException( + status_code=200, + detail=f"Just sent cards to sheets starting at ID {starting_id}", + ) -@router.post('/post-delete') +@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(f'Bad Token: {token}') + logging.warning(f"Bad Token: {token}") db.close() raise HTTPException( status_code=401, - detail='You are not authorized to delete card lists. This event has been logged.' + 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)}') + 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}') +@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(f'Bad Token: {token}') + logging.warning(f"Bad Token: {token}") db.close() raise HTTPException( status_code=401, - detail='You are not authorized to wipe teams. This event has been logged.' + detail="You are not authorized to wipe teams. This event has been logged.", ) try: this_team = Team.get_by_id(team_id) except Exception as e: - 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() db.close() - return f'Wiped {t_query} cards' + return f"Wiped {t_query} cards" -@router.patch('/{card_id}') +@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)): + 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(f'Bad Token: {token}') + logging.warning(f"Bad Token: {token}") db.close() raise HTTPException( status_code=401, - detail='You are not authorized to patch cards. This event has been logged.' + detail="You are not authorized to patch cards. This event has been logged.", ) try: this_card = Card.get_by_id(card_id) except Exception: db.close() - 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 @@ -344,29 +395,29 @@ async def v1_cards_patch( db.close() 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('/{card_id}') +@router.delete("/{card_id}") async def v1_cards_delete(card_id, token: str = Depends(oauth2_scheme)): if not valid_token(token): - logging.warning(f'Bad Token: {token}') + logging.warning(f"Bad Token: {token}") db.close() raise HTTPException( status_code=401, - detail='You are not authorized to delete packs. This event has been logged.' + detail="You are not authorized to delete packs. This event has been logged.", ) try: this_card = Card.get_by_id(card_id) except Exception: db.close() - 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() db.close() if count == 1: - raise HTTPException(status_code=200, detail=f'Card {card_id} has been deleted') + 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') + raise HTTPException(status_code=500, detail=f"Card {card_id} was not deleted") -- 2.25.1