Some checks failed
Build Docker Image / build (pull_request) Has been cancelled
CRITICAL BUG FIX - Root cause of Player.data AttributeError
Problem:
The PATCH endpoint was calling locals() AFTER creating data = {}, causing
locals_dict to capture 'data' and 'locals_dict' as variables. The loop then
added these to the data dict itself, resulting in:
data = {'team_id': 549, 'demotion_week': 7, 'data': {}, 'locals_dict': {...}}
When Peewee executed Player.update(**data), it tried to set Player.data = {},
but Player model has no 'data' field, causing AttributeError.
Solution:
1. Call locals() BEFORE creating data dict
2. Exclude 'locals_dict' from the filter (along with 'player_id', 'token')
This ensures only actual player field parameters are included in the update.
Version: 2.5.2 → 2.5.3
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
146 lines
4.4 KiB
Python
146 lines
4.4 KiB
Python
"""
|
|
Player Router - Refactored
|
|
Thin HTTP layer using PlayerService for business logic.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Query, Response, Depends
|
|
from typing import Optional, List
|
|
|
|
from ..dependencies import oauth2_scheme, cache_result, handle_db_errors
|
|
from ..services.base import BaseService
|
|
from ..services.player_service import PlayerService
|
|
|
|
router = APIRouter(prefix="/api/v3/players", tags=["players"])
|
|
|
|
|
|
@router.get("")
|
|
@handle_db_errors
|
|
@cache_result(ttl=30 * 60, key_prefix="players")
|
|
async def get_players(
|
|
season: Optional[int] = None,
|
|
name: Optional[str] = None,
|
|
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,
|
|
limit: Optional[int] = Query(default=None, ge=1, description="Maximum number of results to return"),
|
|
offset: Optional[int] = Query(default=None, ge=0, description="Number of results to skip for pagination"),
|
|
short_output: Optional[bool] = False,
|
|
csv: Optional[bool] = False,
|
|
):
|
|
"""Get players with filtering and sorting."""
|
|
result = PlayerService.get_players(
|
|
season=season,
|
|
team_id=team_id if team_id else None,
|
|
pos=pos if pos else None,
|
|
strat_code=strat_code if strat_code else None,
|
|
name=name,
|
|
is_injured=is_injured,
|
|
sort=sort,
|
|
limit=limit,
|
|
offset=offset,
|
|
short_output=short_output or False,
|
|
as_csv=csv or False
|
|
)
|
|
|
|
if csv:
|
|
return Response(content=result, media_type="text/csv")
|
|
return result
|
|
|
|
|
|
@router.get("/search")
|
|
@handle_db_errors
|
|
@cache_result(ttl=15 * 60, key_prefix="players-search")
|
|
async def search_players(
|
|
q: str = Query(..., description="Search query for player name"),
|
|
season: Optional[int] = Query(default=None, description="Season to search (0 for all)"),
|
|
limit: int = Query(default=10, ge=1, le=50),
|
|
short_output: bool = False,
|
|
):
|
|
"""Search players by name with fuzzy matching."""
|
|
return PlayerService.search_players(
|
|
query_str=q,
|
|
season=season,
|
|
limit=limit,
|
|
short_output=short_output
|
|
)
|
|
|
|
|
|
@router.get("/{player_id}")
|
|
@handle_db_errors
|
|
@cache_result(ttl=30 * 60, key_prefix="player")
|
|
async def get_one_player(
|
|
player_id: int,
|
|
short_output: Optional[bool] = False
|
|
):
|
|
"""Get a single player by ID."""
|
|
return PlayerService.get_player(player_id, short_output=short_output or False)
|
|
|
|
|
|
@router.put("/{player_id}")
|
|
async def put_player(
|
|
player_id: int,
|
|
new_player: dict,
|
|
token: str = Depends(oauth2_scheme)
|
|
):
|
|
"""Update a player (full replacement)."""
|
|
return PlayerService.update_player(player_id, new_player, token)
|
|
|
|
|
|
@router.patch("/{player_id}")
|
|
async def patch_player(
|
|
player_id: int,
|
|
token: str = Depends(oauth2_scheme),
|
|
name: Optional[str] = None,
|
|
wara: Optional[float] = None,
|
|
image: Optional[str] = None,
|
|
image2: Optional[str] = None,
|
|
team_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,
|
|
):
|
|
"""Patch a player (partial update)."""
|
|
# Build dict of provided fields
|
|
# IMPORTANT: Capture locals() BEFORE creating data dict to avoid including 'data' itself
|
|
locals_dict = locals()
|
|
data = {}
|
|
for key, value in locals_dict.items():
|
|
if key not in ('player_id', 'token', 'locals_dict') and value is not None:
|
|
data[key] = value
|
|
|
|
return PlayerService.patch_player(player_id, data, token)
|
|
|
|
|
|
@router.post("")
|
|
async def post_players(
|
|
p_list: dict,
|
|
token: str = Depends(oauth2_scheme)
|
|
):
|
|
"""Create multiple players."""
|
|
return PlayerService.create_players(p_list.get("players", []), token)
|
|
|
|
|
|
@router.delete("/{player_id}")
|
|
async def delete_player(
|
|
player_id: int,
|
|
token: str = Depends(oauth2_scheme)
|
|
):
|
|
"""Delete a player."""
|
|
return PlayerService.delete_player(player_id, token)
|