Compare commits

..

10 Commits

Author SHA1 Message Date
Cal Corum
2c4ff01ff8 fix: batch Paperdex lookups to avoid N+1 queries (#17)
Replace per-player/card Paperdex.select().where() calls with a single
batched query grouped by player_id. Eliminates N+1 queries in:
- players list endpoint (get_players, with inc_dex flag)
- players by team endpoint
- cards list endpoint (also materializes query to avoid double count())

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 01:37:03 -06:00
cal
7295e77c96 Merge pull request 'fix: refactor Roster from 26 FK columns to RosterSlot junction table (#29)' (#58) from ai/paper-dynasty-database#29 into next-release
Some checks failed
Build Docker Image / build (push) Successful in 3m28s
Build Docker Image / build (pull_request) Has been cancelled
Reviewed-on: #58
2026-03-07 03:23:41 +00:00
cal
be02ba1e3f Merge pull request 'fix: remove broken live_update_batting stub endpoint (#10)' (#54) from ai/paper-dynasty-database#10 into next-release
Some checks are pending
Build Docker Image / build (push) Waiting to run
Reviewed-on: #54
2026-03-07 03:22:08 +00:00
cal
3ddb7028f3 Merge pull request 'fix: replace broad except Exception blocks with DoesNotExist (#15)' (#48) from ai/paper-dynasty-database#15 into next-release
Some checks are pending
Build Docker Image / build (push) Waiting to run
Reviewed-on: #48
2026-03-07 03:18:56 +00:00
cal
f1769966a0 Merge pull request 'fix: batch BattingCard/BattingCardRatings lookups in lineup builder (#18)' (#45) from ai/paper-dynasty-database#18 into next-release
Some checks failed
Build Docker Image / build (push) Has been cancelled
Reviewed-on: #45
2026-03-07 03:16:12 +00:00
Cal Corum
10983138a9 ci: Use docker-tags composite action for multi-channel release support
All checks were successful
Build Docker Image / build (push) Successful in 3m9s
Adds next-release branch trigger and replaces separate dev/production
build steps with the shared docker-tags action for tag resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:40:06 -06:00
Cal Corum
44b6222ad5 fix: refactor Roster from 26 FK columns to RosterSlot junction table (#29)
- Remove card_1..card_26 FK columns from Roster ORM model
- Add RosterSlot model with (roster, slot, card) and a unique index on (roster, slot)
- Activate get_cards() helper on Roster using the new junction table
- Register RosterSlot in create_tables for SQLite dev environments
- Add migrations/migrate_roster_junction_table.py to backfill existing data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 15:34:39 -06:00
Cal Corum
7b494faa99 fix: remove broken live_update_batting stub endpoint (#10)
The endpoint iterated over `files.vl_basic` (a string, not parsed CSV),
causing it to loop over individual characters. The body contained only
`pass` with TODO comments and no running stats logic. Removed the
endpoint entirely along with the dead commented-out csv helper code.
The `BattingFiles` model is retained as it is still used by
`live_update_pitching`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 15:34:06 -06:00
Cal Corum
0c042165b7 fix: replace broad except Exception blocks with DoesNotExist (#15)
Replace 71 broad `except Exception` blocks in 19 router files with the
specific `peewee.DoesNotExist` exception. GET endpoints that call
`Model.get_by_id()` now only catch the expected DoesNotExist error,
allowing real DB failures (connection errors, etc.) to propagate as
500s rather than being masked as 404s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 15:32:53 -06:00
Cal Corum
62b205bde2 fix: batch BattingCard/BattingCardRatings lookups in lineup builder (#18)
Replace per-player get_or_none() calls in get_bratings() with two bulk
SELECT queries before the position loop, keyed by player_id and card+hand.
This reduces DB round trips from O(3N) to O(2) for all lineup difficulties.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 15:31:13 -06:00
23 changed files with 245 additions and 234 deletions

View File

@ -3,6 +3,7 @@
# CI/CD pipeline for Paper Dynasty Database API:
# - Builds Docker images on every push/PR
# - Auto-generates CalVer version (YYYY.MM.BUILD) on main branch merges
# - Supports multi-channel releases: stable (main), rc (next-release), dev (PRs)
# - Pushes to Docker Hub and creates git tag on main
# - Sends Discord notifications on success/failure
@ -12,6 +13,7 @@ on:
push:
branches:
- main
- next-release
pull_request:
branches:
- main
@ -39,30 +41,20 @@ jobs:
id: calver
uses: cal/gitea-actions/calver@main
# Dev build: push with dev + dev-SHA tags (PR/feature branches)
- name: Build Docker image (dev)
if: github.ref != 'refs/heads/main'
uses: https://github.com/docker/build-push-action@v5
- name: Resolve Docker tags
id: tags
uses: cal/gitea-actions/docker-tags@main
with:
context: .
push: true
tags: |
manticorum67/paper-dynasty-database:dev
manticorum67/paper-dynasty-database:dev-${{ steps.calver.outputs.sha_short }}
cache-from: type=registry,ref=manticorum67/paper-dynasty-database:buildcache
cache-to: type=registry,ref=manticorum67/paper-dynasty-database:buildcache,mode=max
image: manticorum67/paper-dynasty-database
version: ${{ steps.calver.outputs.version }}
sha_short: ${{ steps.calver.outputs.sha_short }}
# Production build: push with latest + CalVer tags (main only)
- name: Build Docker image (production)
if: github.ref == 'refs/heads/main'
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5
with:
context: .
push: true
tags: |
manticorum67/paper-dynasty-database:latest
manticorum67/paper-dynasty-database:${{ steps.calver.outputs.version }}
manticorum67/paper-dynasty-database:${{ steps.calver.outputs.version_sha }}
tags: ${{ steps.tags.outputs.tags }}
cache-from: type=registry,ref=manticorum67/paper-dynasty-database:buildcache
cache-to: type=registry,ref=manticorum67/paper-dynasty-database:buildcache,mode=max
@ -77,38 +69,35 @@ jobs:
run: |
echo "## Docker Build Successful" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Channel:** \`${{ steps.tags.outputs.channel }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Image Tags:**" >> $GITHUB_STEP_SUMMARY
echo "- \`manticorum67/paper-dynasty-database:latest\`" >> $GITHUB_STEP_SUMMARY
echo "- \`manticorum67/paper-dynasty-database:${{ steps.calver.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- \`manticorum67/paper-dynasty-database:${{ steps.calver.outputs.version_sha }}\`" >> $GITHUB_STEP_SUMMARY
IFS=',' read -ra TAG_ARRAY <<< "${{ steps.tags.outputs.tags }}"
for tag in "${TAG_ARRAY[@]}"; do
echo "- \`${tag}\`" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Build Details:**" >> $GITHUB_STEP_SUMMARY
echo "- Branch: \`${{ steps.calver.outputs.branch }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Commit: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Timestamp: \`${{ steps.calver.outputs.timestamp }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "Pushed to Docker Hub!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Pull with: \`docker pull manticorum67/paper-dynasty-database:latest\`" >> $GITHUB_STEP_SUMMARY
else
echo "_PR build - image not pushed to Docker Hub_" >> $GITHUB_STEP_SUMMARY
fi
echo "Pull with: \`docker pull manticorum67/paper-dynasty-database:${{ steps.tags.outputs.primary_tag }}\`" >> $GITHUB_STEP_SUMMARY
- name: Discord Notification - Success
if: success() && github.ref == 'refs/heads/main'
if: success() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next-release')
uses: cal/gitea-actions/discord-notify@main
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
title: "Paper Dynasty Database"
status: success
version: ${{ steps.calver.outputs.version }}
image_tag: ${{ steps.calver.outputs.version_sha }}
image_tag: ${{ steps.tags.outputs.primary_tag }}
commit_sha: ${{ steps.calver.outputs.sha_short }}
timestamp: ${{ steps.calver.outputs.timestamp }}
- name: Discord Notification - Failure
if: failure() && github.ref == 'refs/heads/main'
if: failure() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next-release')
uses: cal/gitea-actions/discord-notify@main
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}

View File

@ -498,51 +498,34 @@ class Roster(BaseModel):
team = ForeignKeyField(Team)
name = CharField()
roster_num = IntegerField()
card_1 = ForeignKeyField(Card)
card_2 = ForeignKeyField(Card)
card_3 = ForeignKeyField(Card)
card_4 = ForeignKeyField(Card)
card_5 = ForeignKeyField(Card)
card_6 = ForeignKeyField(Card)
card_7 = ForeignKeyField(Card)
card_8 = ForeignKeyField(Card)
card_9 = ForeignKeyField(Card)
card_10 = ForeignKeyField(Card)
card_11 = ForeignKeyField(Card)
card_12 = ForeignKeyField(Card)
card_13 = ForeignKeyField(Card)
card_14 = ForeignKeyField(Card)
card_15 = ForeignKeyField(Card)
card_16 = ForeignKeyField(Card)
card_17 = ForeignKeyField(Card)
card_18 = ForeignKeyField(Card)
card_19 = ForeignKeyField(Card)
card_20 = ForeignKeyField(Card)
card_21 = ForeignKeyField(Card)
card_22 = ForeignKeyField(Card)
card_23 = ForeignKeyField(Card)
card_24 = ForeignKeyField(Card)
card_25 = ForeignKeyField(Card)
card_26 = ForeignKeyField(Card)
def __str__(self):
return f"{self.team} Roster"
# def get_cards(self, team):
# all_cards = Card.select().where(Card.roster == self)
# this_roster = []
# return [this_roster.card1, this_roster.card2, this_roster.card3, this_roster.card4, this_roster.card5,
# this_roster.card6, this_roster.card7, this_roster.card8, this_roster.card9, this_roster.card10,
# this_roster.card11, this_roster.card12, this_roster.card13, this_roster.card14, this_roster.card15,
# this_roster.card16, this_roster.card17, this_roster.card18, this_roster.card19, this_roster.card20,
# this_roster.card21, this_roster.card22, this_roster.card23, this_roster.card24, this_roster.card25,
# this_roster.card26]
def get_cards(self):
return (
Card.select()
.join(RosterSlot)
.where(RosterSlot.roster == self)
.order_by(RosterSlot.slot)
)
class Meta:
database = db
table_name = "roster"
class RosterSlot(BaseModel):
roster = ForeignKeyField(Roster, backref="slots")
slot = IntegerField()
card = ForeignKeyField(Card, backref="roster_slots")
class Meta:
database = db
table_name = "rosterslot"
indexes = ((("roster", "slot"), True),)
class Result(BaseModel):
away_team = ForeignKeyField(Team)
home_team = ForeignKeyField(Team)
@ -744,6 +727,7 @@ if not SKIP_TABLE_CREATION:
db.create_tables(
[
Roster,
RosterSlot,
BattingStat,
PitchingStat,
Result,

View File

@ -4,7 +4,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import Award, model_to_dict
from ..db_engine import Award, model_to_dict, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA
@ -73,7 +73,7 @@ async def get_awards(
async def get_one_award(award_id, csv: Optional[bool] = None):
try:
this_award = Award.get_by_id(award_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No award found with id {award_id}')
if csv:
@ -130,7 +130,7 @@ async def delete_award(award_id, token: str = Depends(oauth2_scheme)):
)
try:
this_award = Award.get_by_id(award_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No award found with id {award_id}')
count = this_award.delete_instance()

View File

@ -6,7 +6,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import db, BattingStat, model_to_dict, fn, Card, Player, Current
from ..db_engine import db, BattingStat, model_to_dict, fn, Card, Player, Current, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA
@ -236,7 +236,7 @@ async def delete_batstat(stat_id, token: str = Depends(oauth2_scheme)):
)
try:
this_stat = BattingStat.get_by_id(stat_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No stat found with id {stat_id}')
count = this_stat.delete_instance()

View File

@ -4,7 +4,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex, CARDSETS
from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex, CARDSETS, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
router = APIRouter(prefix="/api/v2/cards", tags=["cards"])
@ -45,26 +45,20 @@ async def get_cards(
if team_id is not None:
try:
this_team = Team.get_by_id(team_id)
except Exception:
raise HTTPException(
status_code=404, detail=f"No team found with 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 Exception:
raise HTTPException(
status_code=404, detail=f"No player found with 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 Exception:
raise HTTPException(
status_code=404, detail=f"No pack found with 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)
@ -151,8 +145,8 @@ async def get_cards(
async def v1_cards_get_one(card_id, csv: Optional[bool] = False):
try:
this_card = Card.get_by_id(card_id)
except Exception:
raise HTTPException(status_code=404, detail=f"No card found with id {card_id}")
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No card found with id {card_id}')
if csv:
data_list = [
@ -313,9 +307,9 @@ async def v1_cards_wipe_team(team_id: int, token: str = Depends(oauth2_scheme)):
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")
except DoesNotExist 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')
t_query = Card.update(team=None).where(Card.team == this_team).execute()
return f"Wiped {t_query} cards"
@ -342,8 +336,8 @@ async def v1_cards_patch(
)
try:
this_card = Card.get_by_id(card_id)
except Exception:
raise HTTPException(status_code=404, detail=f"No card found with 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
@ -385,8 +379,8 @@ async def v1_cards_delete(card_id, token: str = Depends(oauth2_scheme)):
)
try:
this_card = Card.get_by_id(card_id)
except Exception:
raise HTTPException(status_code=404, detail=f"No cards found with id {card_id}")
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No cards found with id {card_id}')
count = this_card.delete_instance()

View File

@ -4,7 +4,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import Cardset, model_to_dict, fn, Event
from ..db_engine import Cardset, model_to_dict, fn, Event, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -41,7 +41,7 @@ async def get_cardsets(
try:
this_event = Event.get_by_id(event_id)
all_cardsets = all_cardsets.where(Cardset.event == this_event)
except Exception as e:
except DoesNotExist as e:
logging.error(f'Failed to find event {event_id}: {e}')
raise HTTPException(status_code=404, detail=f'Event id {event_id} not found')
if in_packs is not None:
@ -104,7 +104,7 @@ async def search_cardsets(
try:
this_event = Event.get_by_id(event_id)
all_cardsets = all_cardsets.where(Cardset.event == this_event)
except Exception as e:
except DoesNotExist as e:
logging.error(f'Failed to find event {event_id}: {e}')
raise HTTPException(status_code=404, detail=f'Event id {event_id} not found')
@ -150,7 +150,7 @@ async def search_cardsets(
async def get_one_cardset(cardset_id, csv: Optional[bool] = False):
try:
this_cardset = Cardset.get_by_id(cardset_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}')
if csv:
@ -205,7 +205,7 @@ async def patch_cardsets(
)
try:
this_cardset = Cardset.get_by_id(cardset_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}')
if name is not None:
@ -241,7 +241,7 @@ async def delete_cardsets(cardset_id, token: str = Depends(oauth2_scheme)):
)
try:
this_cardset = Cardset.get_by_id(cardset_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}')
count = this_cardset.delete_instance()

View File

@ -4,7 +4,7 @@ from typing import Optional
import logging
import pydantic
from ..db_engine import Current, model_to_dict
from ..db_engine import Current, model_to_dict, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA
@ -45,7 +45,7 @@ async def get_current(season: Optional[int] = None, csv: Optional[bool] = False)
async def get_one_current(current_id, csv: Optional[bool] = False):
try:
current = Current.get_by_id(current_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No current found with id {current_id}')
if csv:
@ -102,7 +102,7 @@ async def patch_current(
)
try:
current = Current.get_by_id(current_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No current found with id {current_id}')
if season is not None:
@ -136,7 +136,7 @@ async def delete_current(current_id, token: str = Depends(oauth2_scheme)):
)
try:
this_curr = Current.get_by_id(current_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No current found with id {current_id}')
count = this_curr.delete_instance()

View File

@ -4,7 +4,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import Event, model_to_dict, fn
from ..db_engine import Event, model_to_dict, fn, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -63,7 +63,7 @@ async def v1_events_get(
async def v1_events_get_one(event_id, csv: Optional[bool] = False):
try:
this_event = Event.get_by_id(event_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
if csv:
@ -127,7 +127,7 @@ async def v1_events_patch(
)
try:
this_event = Event.get_by_id(event_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
if name is not None:
@ -163,7 +163,7 @@ async def v1_events_delete(event_id, token: str = Depends(oauth2_scheme)):
)
try:
this_event = Event.get_by_id(event_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
count = this_event.delete_instance()

View File

@ -4,7 +4,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import GameRewards, model_to_dict
from ..db_engine import GameRewards, model_to_dict, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -63,7 +63,7 @@ async def v1_gamerewards_get(
async def v1_gamerewards_get_one(gamereward_id, csv: Optional[bool] = None):
try:
this_game_reward = GameRewards.get_by_id(gamereward_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No game reward found with id {gamereward_id}')
if csv:
@ -120,7 +120,7 @@ async def v1_gamerewards_patch(
)
try:
this_game_reward = GameRewards.get_by_id(game_reward_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No game reward found with id {game_reward_id}')
if name is not None:
@ -161,7 +161,7 @@ async def v1_gamerewards_delete(gamereward_id, token: str = Depends(oauth2_schem
)
try:
this_award = GameRewards.get_by_id(gamereward_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No award found with id {gamereward_id}')
count = this_award.delete_instance()

View File

@ -3,7 +3,7 @@ from typing import Optional, List
import logging
import pydantic
from ..db_engine import db, GauntletReward, model_to_dict, DatabaseError
from ..db_engine import db, GauntletReward, model_to_dict, DatabaseError, DoesNotExist
from ..db_helpers import upsert_gauntlet_rewards
from ..dependencies import oauth2_scheme, valid_token
@ -57,7 +57,7 @@ async def v1_gauntletreward_get(
async def v1_gauntletreward_get_one(gauntletreward_id):
try:
this_reward = GauntletReward.get_by_id(gauntletreward_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404,
detail=f"No gauntlet reward found with id {gauntletreward_id}",

View File

@ -4,7 +4,7 @@ from typing import Optional
import logging
import pydantic
from ..db_engine import GauntletRun, model_to_dict, DatabaseError
from ..db_engine import GauntletRun, model_to_dict, DatabaseError, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -84,7 +84,7 @@ async def get_gauntletruns(
async def get_one_gauntletrun(gauntletrun_id):
try:
this_gauntlet = GauntletRun.get_by_id(gauntletrun_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No gauntlet found with id {gauntletrun_id}')
return_val = model_to_dict(this_gauntlet)

View File

@ -5,7 +5,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import Notification, model_to_dict, fn
from ..db_engine import Notification, model_to_dict, fn, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -74,7 +74,7 @@ async def get_notifs(
async def get_one_notif(notif_id, csv: Optional[bool] = None):
try:
this_notif = Notification.get_by_id(notif_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}')
if csv:
@ -135,7 +135,7 @@ async def patch_notif(
)
try:
this_notif = Notification.get_by_id(notif_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}')
if title is not None:
@ -173,7 +173,7 @@ async def delete_notif(notif_id, token: str = Depends(oauth2_scheme)):
)
try:
this_notif = Notification.get_by_id(notif_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}')
count = this_notif.delete_instance()

View File

@ -6,7 +6,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import db, Cardset, model_to_dict, Pack, Team, PackType
from ..db_engine import db, Cardset, model_to_dict, Pack, Team, PackType, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -41,20 +41,20 @@ async def get_packs(
if team_id is not None:
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
all_packs = all_packs.where(Pack.team == this_team)
if pack_type_id is not None:
try:
this_pack_type = PackType.get_by_id(pack_type_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No pack type found with id {pack_type_id}')
all_packs = all_packs.where(Pack.pack_type == this_pack_type)
if pack_team_id is not None:
try:
this_pack_team = Team.get_by_id(pack_team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No team found with id {pack_team_id}')
all_packs = all_packs.where(Pack.pack_team == this_pack_team)
elif exact_match:
@ -63,7 +63,7 @@ async def get_packs(
if pack_cardset_id is not None:
try:
this_pack_cardset = Cardset.get_by_id(pack_cardset_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No cardset found with id {pack_cardset_id}')
all_packs = all_packs.where(Pack.pack_cardset == this_pack_cardset)
elif exact_match:
@ -107,7 +107,7 @@ async def get_packs(
async def get_one_pack(pack_id: int, csv: Optional[bool] = False):
try:
this_pack = Pack.get_by_id(pack_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}')
if csv:
@ -191,7 +191,7 @@ async def patch_pack(
)
try:
this_pack = Pack.get_by_id(pack_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}')
if team_id is not None:
@ -234,7 +234,7 @@ async def delete_pack(pack_id, token: str = Depends(oauth2_scheme)):
)
try:
this_pack = Pack.get_by_id(pack_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No packs found with id {pack_id}')
count = this_pack.delete_instance()

View File

@ -4,7 +4,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import PackType, model_to_dict, fn
from ..db_engine import PackType, model_to_dict, fn, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -68,7 +68,7 @@ async def get_packtypes(
async def get_one_packtype(packtype_id, csv: Optional[bool] = False):
try:
this_packtype = PackType.get_by_id(packtype_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}')
if csv:
@ -129,7 +129,7 @@ async def patch_packtype(
)
try:
this_packtype = PackType.get_by_id(packtype_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}')
if name is not None:
@ -163,7 +163,7 @@ async def delete_packtype(packtype_id, token: str = Depends(oauth2_scheme)):
)
try:
this_packtype = PackType.get_by_id(packtype_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}')
count = this_packtype.delete_instance()

View File

@ -5,7 +5,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import Paperdex, model_to_dict, Player, Cardset, Team
from ..db_engine import Paperdex, model_to_dict, Player, Cardset, Team, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -75,7 +75,7 @@ async def get_paperdex(
async def get_one_paperdex(paperdex_id, csv: Optional[bool] = False):
try:
this_dex = Paperdex.get_by_id(paperdex_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
if csv:
@ -135,7 +135,7 @@ async def patch_paperdex(
)
try:
this_dex = Paperdex.get_by_id(paperdex_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
if team_id is not None:
@ -165,7 +165,7 @@ async def delete_paperdex(paperdex_id, token: str = Depends(oauth2_scheme)):
)
try:
this_dex = Paperdex.get_by_id(paperdex_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
count = this_dex.delete_instance()

View File

@ -5,7 +5,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import db, PitchingStat, model_to_dict, Card, Player, Current
from ..db_engine import db, PitchingStat, model_to_dict, Card, Player, Current, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -170,7 +170,7 @@ async def delete_pitstat(stat_id, token: str = Depends(oauth2_scheme)):
)
try:
this_stat = PitchingStat.get_by_id(stat_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No stat found with id {stat_id}')
count = this_stat.delete_instance()

View File

@ -12,7 +12,7 @@ from pandas import DataFrame
from playwright.async_api import async_playwright
from ..card_creation import get_batter_card_data, get_pitcher_card_data
from ..db_engine import (
from ..db_engine import (, DoesNotExist
db,
Player,
model_to_dict,
@ -26,6 +26,7 @@ from ..db_engine import (
PitchingCardRatings,
CardPosition,
MlbPlayer,
DoesNotExist,
)
from ..db_helpers import upsert_players
from ..dependencies import oauth2_scheme, valid_token
@ -594,7 +595,7 @@ async def search_players(
async def get_one_player(player_id: int, csv: Optional[bool] = False):
try:
this_player = Player.get_by_id(player_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404, detail=f"No player found with id {player_id}"
)
@ -677,7 +678,7 @@ async def get_batter_card(
):
try:
this_player = Player.get_by_id(player_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404, detail=f"No player found with id {player_id}"
)
@ -877,7 +878,7 @@ async def v1_players_patch(
try:
this_player = Player.get_by_id(player_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404, detail=f"No player found with id {player_id}"
)
@ -900,7 +901,7 @@ async def v1_players_patch(
if cardset_id is not None:
try:
this_cardset = Cardset.get_by_id(cardset_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404, detail=f"No cardset found with id {cardset_id}"
)
@ -908,7 +909,7 @@ async def v1_players_patch(
if rarity_id is not None:
try:
this_rarity = Rarity.get_by_id(rarity_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404, detail=f"No rarity found with id {rarity_id}"
)
@ -1133,7 +1134,7 @@ async def delete_player(player_id: int, token: str = Depends(oauth2_scheme)):
try:
this_player = Player.get_by_id(player_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404, detail=f"No player found with id {player_id}"
)

View File

@ -4,7 +4,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import Rarity, model_to_dict, fn
from ..db_engine import Rarity, model_to_dict, fn, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -64,7 +64,7 @@ async def get_rarities(value: Optional[int] = None, name: Optional[str] = None,
async def get_one_rarity(rarity_id, csv: Optional[bool] = False):
try:
this_rarity = Rarity.get_by_id(rarity_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}')
if csv:
@ -125,7 +125,7 @@ async def patch_rarity(
)
try:
this_rarity = Rarity.get_by_id(rarity_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}')
if value is not None:
@ -155,7 +155,7 @@ async def v1_rarities_delete(rarity_id, token: str = Depends(oauth2_scheme)):
)
try:
this_rarity = Rarity.get_by_id(rarity_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}')
count = this_rarity.delete_instance()

View File

@ -4,7 +4,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import Result, model_to_dict, Team, DataError
from ..db_engine import Result, model_to_dict, Team, DataError, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -50,28 +50,28 @@ async def get_results(
try:
this_team = Team.get_by_id(away_team_id)
all_results = all_results.where(Result.away_team == this_team)
except Exception:
except DoesNotExist:
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 Exception:
except DoesNotExist:
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))
except Exception:
except DoesNotExist:
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))
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No team found with id {team_two_id}')
if away_score_min is not None:
@ -158,7 +158,7 @@ async def get_results(
async def get_one_results(result_id, csv: Optional[bool] = None):
try:
this_result = Result.get_by_id(result_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
if csv:
@ -185,7 +185,7 @@ async def get_team_results(
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 Exception as e:
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')
@ -332,7 +332,7 @@ async def patch_result(
)
try:
this_result = Result.get_by_id(result_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
if away_team_id is not None:
@ -391,7 +391,7 @@ async def delete_result(result_id, token: str = Depends(oauth2_scheme)):
)
try:
this_result = Result.get_by_id(result_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
count = this_result.delete_instance()

View File

@ -5,7 +5,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import Reward, model_to_dict, fn
from ..db_engine import Reward, model_to_dict, fn, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
@ -75,7 +75,7 @@ async def get_rewards(
async def get_one_reward(reward_id, csv: Optional[bool] = False):
try:
this_reward = Reward.get_by_id(reward_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
if csv:
@ -130,7 +130,7 @@ async def patch_reward(
)
try:
this_reward = Reward.get_by_id(reward_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
if name is not None:
@ -161,7 +161,7 @@ async def delete_reward(reward_id, token: str = Depends(oauth2_scheme)):
)
try:
this_reward = Reward.get_by_id(reward_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
count = this_reward.delete_instance()

View File

@ -6,19 +6,15 @@ from ..db_engine import Player
from ..dependencies import oauth2_scheme, valid_token
from ..player_scouting import get_player_ids
router = APIRouter(
prefix='/api/v2/scouting',
tags=['scouting']
)
router = APIRouter(prefix="/api/v2/scouting", tags=["scouting"])
class BattingFiles(pydantic.BaseModel):
vl_basic: str = 'vl-basic.csv'
vl_rate: str = 'vl-rate.csv'
vr_basic: str = 'vr-basic.csv'
vr_rate: str = 'vr-rate.csv'
running: str = 'running.csv'
vl_basic: str = "vl-basic.csv"
vl_rate: str = "vl-rate.csv"
vr_basic: str = "vr-basic.csv"
vr_rate: str = "vr-rate.csv"
running: str = "running.csv"
# def csv_file_to_dataframe(filename: str) -> pd.DataFrame | None:
@ -28,63 +24,26 @@ class BattingFiles(pydantic.BaseModel):
# for row in reader:
@router.get('/playerkeys')
@router.get("/playerkeys")
async def get_player_keys(player_id: list = Query(default=None)):
all_keys = []
for x in player_id:
this_player = Player.get_or_none(Player.player_id == x)
if this_player is not None:
this_keys = get_player_ids(this_player.bbref_id, id_type='bbref')
this_keys = get_player_ids(this_player.bbref_id, id_type="bbref")
if this_keys is not None:
all_keys.append(this_keys)
return_val = {'count': len(all_keys), 'keys': [
dict(x) for x in all_keys
]}
return_val = {"count": len(all_keys), "keys": [dict(x) for x in all_keys]}
return return_val
@router.post('/live-update/batting')
def live_update_batting(files: BattingFiles, cardset_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 initiate live updates.'
)
data = {} # <fg id>: { 'vL': [combined vl stat data], 'vR': [combined vr stat data] }
for row in files.vl_basic:
if row['pa'] >= 20:
data[row['fgid']]['vL'] = row
for row in files.vl_rate:
if row['fgid'] in data.keys():
data[row['fgid']]['vL'].extend(row)
for row in files.vr_basic:
if row['pa'] >= 40 and row['fgid'] in data.keys():
data[row['fgid']]['vR'] = row
for row in files.vr_rate:
if row['fgid'] in data.keys():
data[row['fgid']]['vR'].extend(row)
for x in data.items():
pass
# Create BattingCardRating object for vL
# Create BattingCardRating object for vR
# Read running stats and create/update BattingCard object
return files.dict()
@router.post('/live-update/pitching')
@router.post("/live-update/pitching")
def live_update_pitching(files: BattingFiles, 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 initiate live updates.'
status_code=401, detail="You are not authorized to initiate live updates."
)
return files.dict()

View File

@ -8,7 +8,7 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import (
from ..db_engine import (, DoesNotExist
db,
Team,
model_to_dict,
@ -31,6 +31,7 @@ from ..db_engine import (
PitchingCardRatings,
StratGame,
LIVE_PROMO_CARDSET_ID,
DoesNotExist,
)
from ..dependencies import (
oauth2_scheme,
@ -170,7 +171,7 @@ async def get_teams(
async def get_one_team(team_id: int, inc_packs: bool = True, csv: Optional[bool] = False):
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f"No team found with id {team_id}")
p_query = Pack.select().where(
@ -354,17 +355,35 @@ async def get_team_lineup(
"DH": {"player": None, "vl": None, "vr": None, "ops": 0},
}
# Batch-fetch BattingCards and ratings for all candidate players to avoid
# per-player DB round trips inside the lineup construction loop below.
if backup_players is not None:
_batch_bcards = BattingCard.select().where(
(BattingCard.player << legal_players)
| (BattingCard.player << backup_players)
)
else:
_batch_bcards = BattingCard.select().where(BattingCard.player << legal_players)
_batting_cards_by_player = {bc.player_id: bc for bc in _batch_bcards}
_all_bratings = (
BattingCardRatings.select().where(
BattingCardRatings.battingcard << list(_batting_cards_by_player.values())
)
if _batting_cards_by_player
else []
)
_ratings_by_card_hand = {}
for _r in _all_bratings:
_ratings_by_card_hand.setdefault(_r.battingcard_id, {})[_r.vs_hand] = _r
def get_bratings(player_id):
this_bcard = BattingCard.get_or_none(BattingCard.player_id == player_id)
vl_ratings = BattingCardRatings.get_or_none(
BattingCardRatings.battingcard == this_bcard,
BattingCardRatings.vs_hand == "L",
this_bcard = _batting_cards_by_player.get(player_id)
card_ratings = (
_ratings_by_card_hand.get(this_bcard.id, {}) if this_bcard else {}
)
vl_ratings = card_ratings.get("L")
vr_ratings = card_ratings.get("R")
vl_ops = vl_ratings.obp + vl_ratings.slg
vr_ratings = BattingCardRatings.get_or_none(
BattingCardRatings.battingcard == this_bcard,
BattingCardRatings.vs_hand == "R",
)
vr_ops = vr_ratings.obp + vr_ratings.slg
return (
model_to_dict(vl_ratings),
@ -1017,7 +1036,7 @@ async def get_team_record(team_id: int, season: int):
async def team_buy_players(team_id: int, ids: str, ts: str):
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f"No team found with id {team_id}")
if ts != this_team.team_hash():
@ -1037,7 +1056,7 @@ async def team_buy_players(team_id: int, ids: str, ts: str):
if player_id != "":
try:
this_player = Player.get_by_id(player_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404,
detail=f"No player found with id {player_id} /// "
@ -1105,14 +1124,14 @@ async def team_buy_packs(
):
try:
this_packtype = PackType.get_by_id(packtype_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404, detail=f"No pack type found with id {packtype_id}"
)
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f"No team found with id {team_id}")
if ts != this_team.team_hash():
@ -1168,7 +1187,7 @@ async def team_buy_packs(
async def team_sell_cards(team_id: int, ids: str, ts: str):
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f"No team found with id {team_id}")
if ts != this_team.team_hash():
@ -1186,7 +1205,7 @@ async def team_sell_cards(team_id: int, ids: str, ts: str):
if card_id != "":
try:
this_card = Card.get_by_id(card_id)
except Exception:
except DoesNotExist:
raise HTTPException(
status_code=404, detail=f"No card found with id {card_id}"
)
@ -1254,7 +1273,7 @@ async def get_team_cards(team_id, csv: Optional[bool] = True):
"""
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f"No team found with id {team_id}")
if not csv:
@ -1394,7 +1413,7 @@ async def team_update_money(
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f"No team found with id {team_id}")
this_team.wallet += delta
@ -1438,7 +1457,7 @@ async def patch_team(
)
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f"No team found with id {team_id}")
if abbrev is not None:
@ -1500,7 +1519,7 @@ async def delete_team(team_id, token: str = Depends(oauth2_scheme)):
)
try:
this_team = Team.get_by_id(team_id)
except Exception:
except DoesNotExist:
raise HTTPException(status_code=404, detail=f"No team found with id {team_id}")
count = this_team.delete_instance()

View File

@ -0,0 +1,65 @@
"""
Migration: Replace 26 FK columns on Roster with RosterSlot junction table.
Creates the `rosterslot` table and migrates existing lineup data from the
card_1..card_26 columns. Safe to re-run (skips rosters already migrated).
Usage:
python migrations/migrate_roster_junction_table.py
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from app.db_engine import db, Roster, RosterSlot
SLOTS = 26
def migrate():
db.connect(reuse_if_open=True)
# Create the table if it doesn't exist yet
db.create_tables([RosterSlot], safe=True)
# Read raw rows from the old schema via plain SQL so we don't depend on
# the ORM model knowing about the legacy card_N columns.
cursor = db.execute_sql("SELECT * FROM roster")
columns = [desc[0] for desc in cursor.description]
migrated = 0
skipped = 0
with db.atomic():
for row in cursor.fetchall():
row_dict = dict(zip(columns, row))
roster_id = row_dict["id"]
already_migrated = (
RosterSlot.select().where(RosterSlot.roster == roster_id).exists()
)
if already_migrated:
skipped += 1
continue
slots_to_insert = []
for slot_num in range(1, SLOTS + 1):
col = f"card_{slot_num}_id"
card_id = row_dict.get(col)
if card_id is not None:
slots_to_insert.append(
{"roster": roster_id, "slot": slot_num, "card": card_id}
)
if slots_to_insert:
RosterSlot.insert_many(slots_to_insert).execute()
migrated += 1
print(f"Migration complete: {migrated} rosters migrated, {skipped} already done.")
db.close()
if __name__ == "__main__":
migrate()