Compare commits

...

10 Commits

Author SHA1 Message Date
Cal Corum
a6610d293d Bump version to 2.4.1
Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-28 16:07:27 -06:00
Cal Corum
ab23161500 Fix delete endpoint using wrong key for creator_id
Was accessing 'creator_id' but get_custom_command_by_id() returns 'creator_db_id'.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-28 16:07:05 -06:00
Cal Corum
b4029d0902 Bump version to 2.4.0 2026-01-23 14:26:19 -06:00
Cal Corum
6ff7ac1f62 Add all-season player search endpoint
- /api/v3/players/search now supports season=0 or omitting season to search ALL seasons
- Results ordered by most recent season first
- Added all_seasons field in response to indicate search mode
- Added numpy<2.0.0 constraint for server compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 14:26:16 -06:00
Cal Corum
99f501e748 Fix custom command creator POST validation (v2.3.1)
Changed CustomCommandCreatorModel.id from required `int` to `Optional[int] = None`
to allow POST requests to create new creators without specifying an ID (database
auto-generates it).

Bug: Users couldn't create custom commands with /new-cc - API returned 422 error
"Field required" for missing id field.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-13 16:31:47 -06:00
Cal Corum
d53b7259db Release v2.3.0 - Add draft pause support
Added `paused` parameter to DraftData PATCH endpoint for draft pause/resume functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 19:47:49 -06:00
Cal Corum
e6a325ac8f Add CACHE_ENABLED env var to toggle Redis caching (v2.2.1)
- Set CACHE_ENABLED=false to disable caching without stopping Redis
- Defaults to true (caching enabled)
- Useful for draft periods requiring real-time data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 07:59:54 -06:00
Cal Corum
254ce2ddc5 Add salary_cap column to Team model (v2.2.0)
- Add optional salary_cap (REAL/float) column to team table
- Create migration file for PostgreSQL schema change
- Update Peewee model with FloatField(null=True)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 07:28:16 -06:00
Cal Corum
ef67b716e7 Fix teams endpoint to return results sorted by ID ascending
Added default order_by(Team.id.asc()) to get_teams endpoint to ensure
consistent ordering of results.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 13:30:50 -06:00
Cal Corum
8f4f4aa321 Add VERSION file for docker build tracking
Initial version: 2.1.2

This file tracks the current version for Docker builds. When building
and pushing new versions, this file will be updated and the commit
will be tagged with the version number.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-15 09:24:44 -06:00
9 changed files with 255 additions and 114 deletions

View File

@ -72,6 +72,7 @@ app/
- **Models**: Pydantic models for request/response validation - **Models**: Pydantic models for request/response validation
- **Database Access**: Direct Peewee ORM queries with automatic connection pooling - **Database Access**: Direct Peewee ORM queries with automatic connection pooling
- **Response Format**: Consistent JSON with proper HTTP status codes - **Response Format**: Consistent JSON with proper HTTP status codes
- **POST Requests**: Pydantic models for POST (create) endpoints should use `Optional[int] = None` for `id` fields since the database auto-generates IDs
### Environment Variables ### Environment Variables
**Required**: **Required**:

1
VERSION Normal file
View File

@ -0,0 +1 @@
2.4.1

View File

@ -286,6 +286,7 @@ class Team(BaseModel):
dice_color = CharField(null=True) dice_color = CharField(null=True)
season = IntegerField() season = IntegerField()
auto_draft = BooleanField(null=True) auto_draft = BooleanField(null=True)
salary_cap = FloatField(null=True)
@staticmethod @staticmethod
def select_season(num): def select_season(num):

View File

@ -26,9 +26,14 @@ logger = logging.getLogger('discord_app')
REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost') REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.environ.get('REDIS_PORT', '6379')) REDIS_PORT = int(os.environ.get('REDIS_PORT', '6379'))
REDIS_DB = int(os.environ.get('REDIS_DB', '0')) REDIS_DB = int(os.environ.get('REDIS_DB', '0'))
CACHE_ENABLED = os.environ.get('CACHE_ENABLED', 'true').lower() == 'true'
# Initialize Redis client with connection error handling # Initialize Redis client with connection error handling
try: if not CACHE_ENABLED:
logger.info("Caching disabled via CACHE_ENABLED=false")
redis_client = None
else:
try:
redis_client = Redis( redis_client = Redis(
host=REDIS_HOST, host=REDIS_HOST,
port=REDIS_PORT, port=REDIS_PORT,
@ -40,7 +45,7 @@ try:
# Test connection # Test connection
redis_client.ping() redis_client.ping()
logger.info(f"Redis connected successfully at {REDIS_HOST}:{REDIS_PORT}") logger.info(f"Redis connected successfully at {REDIS_HOST}:{REDIS_PORT}")
except Exception as e: except Exception as e:
logger.warning(f"Redis connection failed: {e}. Caching will be disabled.") logger.warning(f"Redis connection failed: {e}. Caching will be disabled.")
redis_client = None redis_client = None

View File

@ -20,7 +20,7 @@ router = APIRouter(
# Pydantic Models for API # Pydantic Models for API
class CustomCommandCreatorModel(BaseModel): class CustomCommandCreatorModel(BaseModel):
id: int id: Optional[int] = None # Optional for POST (auto-generated), required on response
discord_id: int discord_id: int
username: str username: str
display_name: Optional[str] = None display_name: Optional[str] = None
@ -573,7 +573,7 @@ async def delete_custom_command_endpoint(
if not existing: if not existing:
raise HTTPException(status_code=404, detail=f"Custom command {command_id} not found") raise HTTPException(status_code=404, detail=f"Custom command {command_id} not found")
creator_id = existing['creator_id'] creator_id = existing['creator_db_id']
# Delete the command # Delete the command
delete_custom_command(command_id) delete_custom_command(command_id)

View File

@ -5,15 +5,20 @@ import pydantic
from pandas import DataFrame from pandas import DataFrame
from ..db_engine import db, Player, model_to_dict, chunked, fn, complex_data_to_csv from ..db_engine import db, Player, model_to_dict, chunked, fn, complex_data_to_csv
from ..dependencies import add_cache_headers, cache_result, oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors, invalidate_cache from ..dependencies import (
add_cache_headers,
logger = logging.getLogger('discord_app') cache_result,
oauth2_scheme,
router = APIRouter( valid_token,
prefix='/api/v3/players', PRIVATE_IN_SCHEMA,
tags=['players'] handle_db_errors,
invalidate_cache,
) )
logger = logging.getLogger("discord_app")
router = APIRouter(prefix="/api/v3/players", tags=["players"])
class PlayerModel(pydantic.BaseModel): class PlayerModel(pydantic.BaseModel):
name: str name: str
@ -47,14 +52,23 @@ class PlayerList(pydantic.BaseModel):
players: List[PlayerModel] players: List[PlayerModel]
@router.get('') @router.get("")
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=30*60) # 30 minutes - safe with cache invalidation on writes @add_cache_headers(
@cache_result(ttl=30*60, key_prefix='players') max_age=30 * 60
) # 30 minutes - safe with cache invalidation on writes
@cache_result(ttl=30 * 60, key_prefix="players")
async def get_players( async def get_players(
season: Optional[int], name: Optional[str] = None, team_id: list = Query(default=None), season: Optional[int],
pos: list = Query(default=None), strat_code: list = Query(default=None), is_injured: Optional[bool] = None, name: Optional[str] = None,
sort: Optional[str] = None, short_output: Optional[bool] = False, csv: Optional[bool] = False): team_id: list = Query(default=None),
pos: list = Query(default=None),
strat_code: list = Query(default=None),
is_injured: Optional[bool] = None,
sort: Optional[str] = None,
short_output: Optional[bool] = False,
csv: Optional[bool] = False,
):
all_players = Player.select_season(season) all_players = Player.select_season(season)
if team_id is not None: if team_id is not None:
@ -70,54 +84,110 @@ async def get_players(
if pos is not None: if pos is not None:
p_list = [x.upper() for x in pos] p_list = [x.upper() for x in pos]
all_players = all_players.where( all_players = all_players.where(
(Player.pos_1 << p_list) | (Player.pos_2 << p_list) | (Player.pos_3 << p_list) | (Player.pos_4 << p_list) | (Player.pos_1 << p_list)
(Player.pos_5 << p_list) | (Player.pos_6 << p_list) | (Player.pos_7 << p_list) | (Player.pos_8 << p_list) | (Player.pos_2 << p_list)
| (Player.pos_3 << p_list)
| (Player.pos_4 << p_list)
| (Player.pos_5 << p_list)
| (Player.pos_6 << p_list)
| (Player.pos_7 << p_list)
| (Player.pos_8 << p_list)
) )
if is_injured is not None: if is_injured is not None:
all_players = all_players.where(Player.il_return.is_null(False)) all_players = all_players.where(Player.il_return.is_null(False))
if sort is not None: if sort is not None:
if sort == 'cost-asc': if sort == "cost-asc":
all_players = all_players.order_by(Player.wara) all_players = all_players.order_by(Player.wara)
elif sort == 'cost-desc': elif sort == "cost-desc":
all_players = all_players.order_by(-Player.wara) all_players = all_players.order_by(-Player.wara)
elif sort == 'name-asc': elif sort == "name-asc":
all_players = all_players.order_by(Player.name) all_players = all_players.order_by(Player.name)
elif sort == 'name-desc': elif sort == "name-desc":
all_players = all_players.order_by(-Player.name) all_players = all_players.order_by(-Player.name)
else: else:
all_players = all_players.order_by(Player.id) all_players = all_players.order_by(Player.id)
if csv: if csv:
player_list = [ player_list = [
['name', 'wara', 'image', 'image2', 'team', 'season', 'pitcher_injury', 'pos_1', 'pos_2', 'pos_3', [
'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'last_game', 'last_game2', 'il_return', 'demotion_week', "name",
'headshot', 'vanity_card', 'strat_code', 'bbref_id', 'injury_rating', 'player_id', 'sbaref_id'] "wara",
"image",
"image2",
"team",
"season",
"pitcher_injury",
"pos_1",
"pos_2",
"pos_3",
"pos_4",
"pos_5",
"pos_6",
"pos_7",
"pos_8",
"last_game",
"last_game2",
"il_return",
"demotion_week",
"headshot",
"vanity_card",
"strat_code",
"bbref_id",
"injury_rating",
"player_id",
"sbaref_id",
]
] ]
for line in all_players: for line in all_players:
player_list.append( player_list.append(
[ [
line.name, line.wara, line.image, line.image2, line.team.abbrev, line.season, line.pitcher_injury, line.name,
line.pos_1, line.pos_2, line.pos_3, line.pos_4, line.pos_5, line.pos_6, line.pos_7, line.pos_8, line.wara,
line.last_game, line.last_game2, line.il_return, line.demotion_week, line.headshot, line.image,
line.vanity_card, line.strat_code.replace(",", "-_-") if line.strat_code is not None else "", line.image2,
line.bbref_id, line.injury_rating, line.id, line.sbaplayer line.team.abbrev,
line.season,
line.pitcher_injury,
line.pos_1,
line.pos_2,
line.pos_3,
line.pos_4,
line.pos_5,
line.pos_6,
line.pos_7,
line.pos_8,
line.last_game,
line.last_game2,
line.il_return,
line.demotion_week,
line.headshot,
line.vanity_card,
line.strat_code.replace(",", "-_-")
if line.strat_code is not None
else "",
line.bbref_id,
line.injury_rating,
line.id,
line.sbaplayer,
] ]
) )
return_players = { return_players = {
'count': all_players.count(), "count": all_players.count(),
'players': DataFrame(player_list).to_csv(header=False, index=False), "players": DataFrame(player_list).to_csv(header=False, index=False),
'csv': True "csv": True,
} }
db.close() db.close()
return Response(content=return_players['players'], media_type='text/csv') return Response(content=return_players["players"], media_type="text/csv")
else: else:
return_players = { return_players = {
'count': all_players.count(), "count": all_players.count(),
'players': [model_to_dict(x, recurse=not short_output) for x in all_players] "players": [
model_to_dict(x, recurse=not short_output) for x in all_players
],
} }
db.close() db.close()
# if csv: # if csv:
@ -125,27 +195,43 @@ async def get_players(
return return_players return return_players
@router.get('/search') @router.get("/search")
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=15*60) # 15 minutes - safe with cache invalidation on writes @add_cache_headers(
@cache_result(ttl=15*60, key_prefix='players-search') max_age=15 * 60
) # 15 minutes - safe with cache invalidation on writes
@cache_result(ttl=15 * 60, key_prefix="players-search")
async def search_players( async def search_players(
q: str = Query(..., description="Search query for player name"), q: str = Query(..., description="Search query for player name"),
season: Optional[int] = Query(default=None, description="Season to search in (defaults to current)"), season: Optional[int] = Query(
limit: int = Query(default=10, ge=1, le=50, description="Maximum number of results to return"), default=None,
short_output: bool = False): description="Season to search in. Use 0 or omit for all seasons, or specific season number.",
),
limit: int = Query(
default=10, ge=1, le=50, description="Maximum number of results to return"
),
short_output: bool = False,
):
""" """
Real-time fuzzy search for players by name. Real-time fuzzy search for players by name.
Returns players matching the query with exact matches prioritized over partial matches. Returns players matching the query with exact matches prioritized over partial matches.
"""
if season is None:
# Get current season from the database - using a simple approach
from ..db_engine import Current
current = Current.select().first()
season = current.season if current else 12 # fallback to season 12
# Get all players matching the name pattern (partial match supported by existing logic) Season parameter:
- Omit or use 0: Search across ALL seasons (most recent seasons prioritized)
- Specific number (1-13+): Search only that season
"""
search_all_seasons = season is None or season == 0
if search_all_seasons:
# Search across all seasons - no season filter
all_players = (
Player.select()
.where(fn.lower(Player.name).contains(q.lower()))
.order_by(-Player.season)
) # Most recent seasons first
else:
# Search specific season
all_players = Player.select_season(season).where( all_players = Player.select_season(season).where(
fn.lower(Player.name).contains(q.lower()) fn.lower(Player.name).contains(q.lower())
) )
@ -154,6 +240,7 @@ async def search_players(
players_list = list(all_players) players_list = list(all_players)
# Sort by relevance (exact matches first, then partial) # Sort by relevance (exact matches first, then partial)
# For all-season search, also prioritize by season (most recent first)
query_lower = q.lower() query_lower = q.lower()
exact_matches = [] exact_matches = []
partial_matches = [] partial_matches = []
@ -165,22 +252,32 @@ async def search_players(
elif query_lower in name_lower: elif query_lower in name_lower:
partial_matches.append(player) partial_matches.append(player)
# Sort exact and partial matches by season (most recent first) when searching all seasons
if search_all_seasons:
exact_matches.sort(key=lambda p: p.season, reverse=True)
partial_matches.sort(key=lambda p: p.season, reverse=True)
# Combine and limit results # Combine and limit results
results = exact_matches + partial_matches results = exact_matches + partial_matches
limited_results = results[:limit] limited_results = results[:limit]
db.close() db.close()
return { return {
'count': len(limited_results), "count": len(limited_results),
'total_matches': len(results), "total_matches": len(results),
'players': [model_to_dict(x, recurse=not short_output) for x in limited_results] "all_seasons": search_all_seasons,
"players": [
model_to_dict(x, recurse=not short_output) for x in limited_results
],
} }
@router.get('/{player_id}') @router.get("/{player_id}")
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=30*60) # 30 minutes - safe with cache invalidation on writes @add_cache_headers(
@cache_result(ttl=30*60, key_prefix='player') max_age=30 * 60
) # 30 minutes - safe with cache invalidation on writes
@cache_result(ttl=30 * 60, key_prefix="player")
async def get_one_player(player_id: int, short_output: Optional[bool] = False): async def get_one_player(player_id: int, short_output: Optional[bool] = False):
this_player = Player.get_or_none(Player.id == player_id) this_player = Player.get_or_none(Player.id == player_id)
if this_player: if this_player:
@ -191,17 +288,18 @@ async def get_one_player(player_id: int, short_output: Optional[bool] = False):
return r_player return r_player
@router.put('/{player_id}', include_in_schema=PRIVATE_IN_SCHEMA) @router.put("/{player_id}", include_in_schema=PRIVATE_IN_SCHEMA)
@handle_db_errors @handle_db_errors
async def put_player( async def put_player(
player_id: int, new_player: PlayerModel, token: str = Depends(oauth2_scheme)): player_id: int, new_player: PlayerModel, token: str = Depends(oauth2_scheme)
):
if not valid_token(token): if not valid_token(token):
logger.warning(f'patch_player - Bad Token: {token}') logger.warning(f"patch_player - Bad Token: {token}")
raise HTTPException(status_code=401, detail='Unauthorized') raise HTTPException(status_code=401, detail="Unauthorized")
if Player.get_or_none(Player.id == player_id) is None: if Player.get_or_none(Player.id == player_id) is None:
db.close() db.close()
raise HTTPException(status_code=404, detail=f'Player ID {player_id} not found') raise HTTPException(status_code=404, detail=f"Player ID {player_id} not found")
Player.update(**new_player.dict()).where(Player.id == player_id).execute() Player.update(**new_player.dict()).where(Player.id == player_id).execute()
r_player = model_to_dict(Player.get_by_id(player_id)) r_player = model_to_dict(Player.get_by_id(player_id))
@ -217,29 +315,46 @@ async def put_player(
return r_player return r_player
@router.patch('/{player_id}', include_in_schema=PRIVATE_IN_SCHEMA) @router.patch("/{player_id}", include_in_schema=PRIVATE_IN_SCHEMA)
@handle_db_errors @handle_db_errors
async def patch_player( async def patch_player(
player_id: int, token: str = Depends(oauth2_scheme), name: Optional[str] = None, player_id: int,
wara: Optional[float] = None, image: Optional[str] = None, image2: Optional[str] = None, token: str = Depends(oauth2_scheme),
team_id: Optional[int] = None, season: Optional[int] = None, pos_1: Optional[str] = None, name: Optional[str] = None,
pos_2: Optional[str] = None, pos_3: Optional[str] = None, pos_4: Optional[str] = None, wara: Optional[float] = None,
pos_5: Optional[str] = None, pos_6: Optional[str] = None, pos_7: Optional[str] = None, image: Optional[str] = None,
pos_8: Optional[str] = None, vanity_card: Optional[str] = None, headshot: Optional[str] = None, image2: Optional[str] = None,
il_return: Optional[str] = None, demotion_week: Optional[int] = None, strat_code: Optional[str] = None, team_id: Optional[int] = None,
bbref_id: Optional[str] = None, injury_rating: Optional[str] = None, sbaref_id: Optional[int] = None): season: Optional[int] = None,
pos_1: Optional[str] = None,
pos_2: Optional[str] = None,
pos_3: Optional[str] = None,
pos_4: Optional[str] = None,
pos_5: Optional[str] = None,
pos_6: Optional[str] = None,
pos_7: Optional[str] = None,
pos_8: Optional[str] = None,
vanity_card: Optional[str] = None,
headshot: Optional[str] = None,
il_return: Optional[str] = None,
demotion_week: Optional[int] = None,
strat_code: Optional[str] = None,
bbref_id: Optional[str] = None,
injury_rating: Optional[str] = None,
sbaref_id: Optional[int] = None,
):
if not valid_token(token): if not valid_token(token):
logger.warning(f'patch_player - Bad Token: {token}') logger.warning(f"patch_player - Bad Token: {token}")
raise HTTPException(status_code=401, detail='Unauthorized') raise HTTPException(status_code=401, detail="Unauthorized")
if Player.get_or_none(Player.id == player_id) is None: if Player.get_or_none(Player.id == player_id) is None:
db.close() db.close()
raise HTTPException(status_code=404, detail=f'Player ID {player_id} not found') raise HTTPException(status_code=404, detail=f"Player ID {player_id} not found")
this_player = Player.get_or_none(Player.id == player_id) this_player = Player.get_or_none(Player.id == player_id)
if this_player is None: if this_player is None:
db.close() db.close()
raise HTTPException(status_code=404, detail=f'Player ID {player_id} not found') raise HTTPException(status_code=404, detail=f"Player ID {player_id} not found")
if name is not None: if name is not None:
this_player.name = name this_player.name = name
@ -279,7 +394,9 @@ async def patch_player(
this_player.headshot = headshot this_player.headshot = headshot
if il_return is not None: if il_return is not None:
this_player.il_return = None if not il_return or il_return.lower() == 'none' else il_return this_player.il_return = (
None if not il_return or il_return.lower() == "none" else il_return
)
if demotion_week is not None: if demotion_week is not None:
this_player.demotion_week = demotion_week this_player.demotion_week = demotion_week
if strat_code is not None: if strat_code is not None:
@ -305,24 +422,28 @@ async def patch_player(
return r_player return r_player
else: else:
db.close() db.close()
raise HTTPException(status_code=500, detail=f'Unable to patch player {player_id}') raise HTTPException(
status_code=500, detail=f"Unable to patch player {player_id}"
)
@router.post('', include_in_schema=PRIVATE_IN_SCHEMA) @router.post("", include_in_schema=PRIVATE_IN_SCHEMA)
@handle_db_errors @handle_db_errors
async def post_players(p_list: PlayerList, token: str = Depends(oauth2_scheme)): async def post_players(p_list: PlayerList, token: str = Depends(oauth2_scheme)):
if not valid_token(token): if not valid_token(token):
logger.warning(f'post_players - Bad Token: {token}') logger.warning(f"post_players - Bad Token: {token}")
raise HTTPException(status_code=401, detail='Unauthorized') raise HTTPException(status_code=401, detail="Unauthorized")
new_players = [] new_players = []
for player in p_list.players: for player in p_list.players:
dupe = Player.get_or_none(Player.season == player.season, Player.name == player.name) dupe = Player.get_or_none(
Player.season == player.season, Player.name == player.name
)
if dupe: if dupe:
db.close() db.close()
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail=f'Player name {player.name} already in use in Season {player.season}' detail=f"Player name {player.name} already in use in Season {player.season}",
) )
new_players.append(player.dict()) new_players.append(player.dict())
@ -339,20 +460,20 @@ async def post_players(p_list: PlayerList, token: str = Depends(oauth2_scheme)):
# Invalidate team roster cache (new players added to teams) # Invalidate team roster cache (new players added to teams)
invalidate_cache("team-roster*") invalidate_cache("team-roster*")
return f'Inserted {len(new_players)} players' return f"Inserted {len(new_players)} players"
@router.delete('/{player_id}', include_in_schema=PRIVATE_IN_SCHEMA) @router.delete("/{player_id}", include_in_schema=PRIVATE_IN_SCHEMA)
@handle_db_errors @handle_db_errors
async def delete_player(player_id: int, token: str = Depends(oauth2_scheme)): async def delete_player(player_id: int, token: str = Depends(oauth2_scheme)):
if not valid_token(token): if not valid_token(token):
logger.warning(f'delete_player - Bad Token: {token}') logger.warning(f"delete_player - Bad Token: {token}")
raise HTTPException(status_code=401, detail='Unauthorized') raise HTTPException(status_code=401, detail="Unauthorized")
this_player = Player.get_or_none(Player.id == player_id) this_player = Player.get_or_none(Player.id == player_id)
if not this_player: if not this_player:
db.close() db.close()
raise HTTPException(status_code=404, detail=f'Player ID {player_id} not found') raise HTTPException(status_code=404, detail=f"Player ID {player_id} not found")
count = this_player.delete_instance() count = this_player.delete_instance()
db.close() db.close()
@ -365,6 +486,8 @@ async def delete_player(player_id: int, token: str = Depends(oauth2_scheme)):
# Invalidate team roster cache (player removed from team) # Invalidate team roster cache (player removed from team)
invalidate_cache("team-roster*") invalidate_cache("team-roster*")
return f'Player {player_id} has been deleted' return f"Player {player_id} has been deleted"
else: else:
raise HTTPException(status_code=500, detail=f'Player {player_id} could not be deleted') raise HTTPException(
status_code=500, detail=f"Player {player_id} could not be deleted"
)

View File

@ -43,9 +43,9 @@ async def get_teams(
team_abbrev: list = Query(default=None), active_only: Optional[bool] = False, team_abbrev: list = Query(default=None), active_only: Optional[bool] = False,
short_output: Optional[bool] = False, csv: Optional[bool] = False): short_output: Optional[bool] = False, csv: Optional[bool] = False):
if season is not None: if season is not None:
all_teams = Team.select_season(season) all_teams = Team.select_season(season).order_by(Team.id.asc())
else: else:
all_teams = Team.select() all_teams = Team.select().order_by(Team.id.asc())
if manager_id is not None: if manager_id is not None:
managers = Manager.select().where(Manager.id << manager_id) managers = Manager.select().where(Manager.id << manager_id)

View File

@ -0,0 +1,9 @@
-- Migration: Add salary_cap column to teams table
-- Date: 2025-12-09
-- Description: Adds optional salary_cap column to track team salary cap values
ALTER TABLE team
ADD COLUMN IF NOT EXISTS salary_cap REAL;
-- Note: REAL is PostgreSQL's single-precision floating-point type
-- Column is nullable (no NOT NULL constraint) so existing rows get NULL

View File

@ -2,6 +2,7 @@ fastapi
uvicorn uvicorn
peewee==3.13.3 peewee==3.13.3
python-multipart python-multipart
numpy<2.0.0
pandas pandas
psycopg2-binary>=2.9.0 psycopg2-binary>=2.9.0
requests requests