major-domo-database/app/routers_v3/players.py
Cal Corum 2c9000ef4b fix: Remove browser cache headers to prevent stale roster data
Users were seeing stale roster data on the website even after updates
because browsers cached responses for 30 minutes. Direct API calls
showed correct data, confirming this was a client-side caching issue.

Changes:
- Remove @add_cache_headers decorators from all player endpoints
- Keep @cache_result (Redis server-side caching) for performance
- Server cache still gets invalidated on write operations

Benefits:
- Users always see fresh data (within Redis TTL of 30 minutes max)
- Server cache invalidation now effective for end users
- Minimal performance impact (~10ms Redis lookup vs 0ms browser cache)
- Redis already provides 80-90% of caching benefit

Trade-off:
- Browsers now make request to server on every page load
- Server handles more requests but Redis makes them fast
- For fantasy sports, fresh data > marginal performance gain

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 01:21:50 -06:00

141 lines
4.0 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,
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,
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
data = {}
locals_dict = locals()
for key, value in locals_dict.items():
if key not in ('player_id', 'token') 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)