fix: invalidate cache after PlayerService write operations (#32)
All checks were successful
Build Docker Image / build (pull_request) Successful in 2m10s

Add finally blocks to update_player, patch_player, create_players, and
delete_player in PlayerService to call invalidate_related_cache() using
the existing cache_patterns. Matches the pattern already used in
TeamService.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-03-05 13:03:53 -06:00
parent ddf5f77da4
commit ea7c62c93f

View File

@ -39,7 +39,7 @@ class PlayerService(BaseService):
cache_patterns = ["players*", "players-search*", "player*", "team-roster*"]
# Deprecated fields to exclude from player responses
EXCLUDED_FIELDS = ['pitcher_injury']
EXCLUDED_FIELDS = ["pitcher_injury"]
# Class-level repository for dependency injection
_injected_repo: Optional[AbstractPlayerRepository] = None
@ -154,7 +154,7 @@ class PlayerService(BaseService):
return {
"count": len(players_data),
"total": total_count,
"players": players_data
"players": players_data,
}
except Exception as e:
@ -204,9 +204,9 @@ class PlayerService(BaseService):
p_list = [x.upper() for x in pos]
# Expand generic "P" to match all pitcher positions
pitcher_positions = ['SP', 'RP', 'CP']
if 'P' in p_list:
p_list.remove('P')
pitcher_positions = ["SP", "RP", "CP"]
if "P" in p_list:
p_list.remove("P")
p_list.extend(pitcher_positions)
pos_conditions = (
@ -245,9 +245,9 @@ class PlayerService(BaseService):
p_list = [p.upper() for p in pos]
# Expand generic "P" to match all pitcher positions
pitcher_positions = ['SP', 'RP', 'CP']
if 'P' in p_list:
p_list.remove('P')
pitcher_positions = ["SP", "RP", "CP"]
if "P" in p_list:
p_list.remove("P")
p_list.extend(pitcher_positions)
player_pos = [
@ -385,19 +385,23 @@ class PlayerService(BaseService):
# This filters at the database level instead of loading all players
if search_all_seasons:
# Search all seasons, order by season DESC (newest first)
query = (Player.select()
.where(fn.Lower(Player.name).contains(query_lower))
.order_by(Player.season.desc(), Player.name)
.limit(limit * 2)) # Get extra for exact match sorting
query = (
Player.select()
.where(fn.Lower(Player.name).contains(query_lower))
.order_by(Player.season.desc(), Player.name)
.limit(limit * 2)
) # Get extra for exact match sorting
else:
# Search specific season
query = (Player.select()
.where(
(Player.season == season) &
(fn.Lower(Player.name).contains(query_lower))
)
.order_by(Player.name)
.limit(limit * 2)) # Get extra for exact match sorting
query = (
Player.select()
.where(
(Player.season == season)
& (fn.Lower(Player.name).contains(query_lower))
)
.order_by(Player.name)
.limit(limit * 2)
) # Get extra for exact match sorting
# Execute query and convert limited results to dicts
players = list(query)
@ -468,19 +472,29 @@ class PlayerService(BaseService):
# Use backrefs=False to avoid circular reference issues
player_dict = model_to_dict(player, recurse=recurse, backrefs=False)
# Filter out excluded fields
return {k: v for k, v in player_dict.items() if k not in cls.EXCLUDED_FIELDS}
return {
k: v for k, v in player_dict.items() if k not in cls.EXCLUDED_FIELDS
}
except (ImportError, AttributeError, TypeError) as e:
# Log the error and fall back to non-recursive serialization
logger.warning(f"Error in recursive player serialization: {e}, falling back to non-recursive")
logger.warning(
f"Error in recursive player serialization: {e}, falling back to non-recursive"
)
try:
# Fallback to non-recursive serialization
player_dict = model_to_dict(player, recurse=False)
return {k: v for k, v in player_dict.items() if k not in cls.EXCLUDED_FIELDS}
return {
k: v for k, v in player_dict.items() if k not in cls.EXCLUDED_FIELDS
}
except Exception as fallback_error:
# Final fallback to basic dict conversion
logger.error(f"Error in non-recursive serialization: {fallback_error}, using basic dict")
logger.error(
f"Error in non-recursive serialization: {fallback_error}, using basic dict"
)
player_dict = dict(player)
return {k: v for k, v in player_dict.items() if k not in cls.EXCLUDED_FIELDS}
return {
k: v for k, v in player_dict.items() if k not in cls.EXCLUDED_FIELDS
}
@classmethod
def update_player(
@ -508,6 +522,8 @@ class PlayerService(BaseService):
raise HTTPException(
status_code=500, detail=f"Error updating player {player_id}: {str(e)}"
)
finally:
temp_service.invalidate_related_cache(cls.cache_patterns)
@classmethod
def patch_player(
@ -535,6 +551,8 @@ class PlayerService(BaseService):
raise HTTPException(
status_code=500, detail=f"Error patching player {player_id}: {str(e)}"
)
finally:
temp_service.invalidate_related_cache(cls.cache_patterns)
@classmethod
def create_players(
@ -567,6 +585,8 @@ class PlayerService(BaseService):
raise HTTPException(
status_code=500, detail=f"Error creating players: {str(e)}"
)
finally:
temp_service.invalidate_related_cache(cls.cache_patterns)
@classmethod
def delete_player(cls, player_id: int, token: str) -> Dict[str, str]:
@ -590,6 +610,8 @@ class PlayerService(BaseService):
raise HTTPException(
status_code=500, detail=f"Error deleting player {player_id}: {str(e)}"
)
finally:
temp_service.invalidate_related_cache(cls.cache_patterns)
@classmethod
def _format_player_csv(cls, players: List[Dict]) -> str:
@ -603,12 +625,12 @@ class PlayerService(BaseService):
flat_player = player.copy()
# Flatten team object to just abbreviation
if isinstance(flat_player.get('team'), dict):
flat_player['team'] = flat_player['team'].get('abbrev', '')
if isinstance(flat_player.get("team"), dict):
flat_player["team"] = flat_player["team"].get("abbrev", "")
# Flatten sbaplayer object to just ID
if isinstance(flat_player.get('sbaplayer'), dict):
flat_player['sbaplayer'] = flat_player['sbaplayer'].get('id', '')
if isinstance(flat_player.get("sbaplayer"), dict):
flat_player["sbaplayer"] = flat_player["sbaplayer"].get("id", "")
flattened_players.append(flat_player)