Add Redis Caching

This commit is contained in:
Cal Corum 2025-08-27 22:49:37 -05:00
parent abf4435931
commit a540a3e7f3
6 changed files with 78 additions and 5 deletions

View File

@ -5,7 +5,7 @@ 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 oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors from ..dependencies import add_cache_headers, cache_result, oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors
logger = logging.getLogger('discord_app') logger = logging.getLogger('discord_app')
@ -49,6 +49,7 @@ class PlayerList(pydantic.BaseModel):
@router.get('') @router.get('')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=10*60)
async def get_players( async def get_players(
season: Optional[int], name: Optional[str] = None, team_id: list = Query(default=None), season: Optional[int], 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, pos: list = Query(default=None), strat_code: list = Query(default=None), is_injured: Optional[bool] = None,
@ -84,6 +85,8 @@ async def get_players(
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:
all_players = all_players.order_by(Player.id)
if csv: if csv:
player_list = [ player_list = [
@ -123,6 +126,7 @@ async def get_players(
@router.get('/{player_id}') @router.get('/{player_id}')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=10*60)
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:

View File

@ -9,7 +9,7 @@ from pydantic import BaseModel, validator
from ..db_engine import db, StratPlay, StratGame, Team, Player, Decision, model_to_dict, chunked, fn, SQL, \ from ..db_engine import db, StratPlay, StratGame, Team, Player, Decision, model_to_dict, chunked, fn, SQL, \
complex_data_to_csv complex_data_to_csv
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors from ..dependencies import add_cache_headers, cache_result, oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors
logger = logging.getLogger('discord_app') logger = logging.getLogger('discord_app')
@ -122,6 +122,8 @@ class PlayList(BaseModel):
@router.get('') @router.get('')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=10*60)
@cache_result(ttl=5*60, key_prefix='plays')
async def get_plays( async def get_plays(
game_id: list = Query(default=None), batter_id: list = Query(default=None), season: list = Query(default=None), game_id: list = Query(default=None), batter_id: list = Query(default=None), season: list = Query(default=None),
week: list = Query(default=None), has_defender: Optional[bool] = None, has_catcher: Optional[bool] = None, week: list = Query(default=None), has_defender: Optional[bool] = None, has_catcher: Optional[bool] = None,
@ -278,6 +280,8 @@ async def get_plays(
@router.get('/batting') @router.get('/batting')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=10*60)
@cache_result(ttl=5*60, key_prefix='plays-batting')
async def get_batting_totals( async def get_batting_totals(
season: list = Query(default=None), week: list = Query(default=None), season: list = Query(default=None), week: list = Query(default=None),
s_type: Literal['regular', 'post', 'total', None] = None, position: list = Query(default=None), s_type: Literal['regular', 'post', 'total', None] = None, position: list = Query(default=None),
@ -691,6 +695,8 @@ async def get_batting_totals(
@router.get('/pitching') @router.get('/pitching')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=10*60)
@cache_result(ttl=5*60, key_prefix='plays-batting')
async def get_pitching_totals( async def get_pitching_totals(
season: list = Query(default=None), week: list = Query(default=None), season: list = Query(default=None), week: list = Query(default=None),
s_type: Literal['regular', 'post', 'total', None] = None, player_id: list = Query(default=None), s_type: Literal['regular', 'post', 'total', None] = None, player_id: list = Query(default=None),
@ -942,6 +948,8 @@ async def get_pitching_totals(
@router.get('/fielding') @router.get('/fielding')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=10*60)
@cache_result(ttl=5*60, key_prefix='plays-fielding')
async def get_fielding_totals( async def get_fielding_totals(
season: list = Query(default=None), week: list = Query(default=None), season: list = Query(default=None), week: list = Query(default=None),
s_type: Literal['regular', 'post', 'total', None] = None, position: list = Query(default=None), s_type: Literal['regular', 'post', 'total', None] = None, position: list = Query(default=None),

View File

@ -5,7 +5,7 @@ import logging
import pydantic import pydantic
from ..db_engine import db, Team, Manager, Division, model_to_dict, chunked, fn, query_to_csv, Player from ..db_engine import db, Team, Manager, Division, model_to_dict, chunked, fn, query_to_csv, Player
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors from ..dependencies import add_cache_headers, cache_result, oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors
logger = logging.getLogger('discord_app') logger = logging.getLogger('discord_app')
@ -37,6 +37,7 @@ class TeamList(pydantic.BaseModel):
@router.get('') @router.get('')
@handle_db_errors @handle_db_errors
@cache_result(ttl=10*60, key_prefix='teams')
async def get_teams( async def get_teams(
season: Optional[int] = None, owner_id: list = Query(default=None), manager_id: list = Query(default=None), season: Optional[int] = None, owner_id: list = Query(default=None), manager_id: list = Query(default=None),
team_abbrev: list = Query(default=None), active_only: Optional[bool] = False, team_abbrev: list = Query(default=None), active_only: Optional[bool] = False,
@ -76,6 +77,8 @@ async def get_teams(
@router.get('/{team_id}') @router.get('/{team_id}')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=60*60)
@cache_result(ttl=30*60, key_prefix='team')
async def get_one_team(team_id: int): async def get_one_team(team_id: int):
this_team = Team.get_or_none(Team.id == team_id) this_team = Team.get_or_none(Team.id == team_id)
if this_team: if this_team:
@ -88,6 +91,8 @@ async def get_one_team(team_id: int):
@router.get('/{team_id}/roster/{which}', include_in_schema=PRIVATE_IN_SCHEMA) @router.get('/{team_id}/roster/{which}', include_in_schema=PRIVATE_IN_SCHEMA)
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=60*60)
@cache_result(ttl=30*60, key_prefix='team-roster')
async def get_team_roster(team_id: int, which: Literal['current', 'next'], sort: Optional[str] = None): async def get_team_roster(team_id: int, which: Literal['current', 'next'], sort: Optional[str] = None):
try: try:
this_team = Team.get_by_id(team_id) this_team = Team.get_by_id(team_id)

View File

@ -4,7 +4,7 @@ import logging
import pydantic import pydantic
from ..db_engine import SeasonBattingStats, SeasonPitchingStats, db, Manager, Team, Current, model_to_dict, fn, query_to_csv, StratPlay, StratGame from ..db_engine import SeasonBattingStats, SeasonPitchingStats, db, Manager, Team, Current, model_to_dict, fn, query_to_csv, StratPlay, StratGame
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors, update_season_batting_stats, update_season_pitching_stats from ..dependencies import add_cache_headers, cache_result, oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors, update_season_batting_stats, update_season_pitching_stats, get_cache_stats
logger = logging.getLogger('discord_app') logger = logging.getLogger('discord_app')
@ -15,6 +15,8 @@ router = APIRouter(
@router.get('/season-stats/batting') @router.get('/season-stats/batting')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=10*60)
@cache_result(ttl=5*60, key_prefix='season-batting')
async def get_season_batting_stats( async def get_season_batting_stats(
season: Optional[int] = None, season: Optional[int] = None,
team_id: Optional[int] = None, team_id: Optional[int] = None,
@ -109,6 +111,8 @@ async def refresh_season_batting_stats(
@router.get('/season-stats/pitching') @router.get('/season-stats/pitching')
@handle_db_errors @handle_db_errors
@add_cache_headers(max_age=10*60)
@cache_result(ttl=5*60, key_prefix='season-pitching')
async def get_season_pitching_stats( async def get_season_pitching_stats(
season: Optional[int] = None, season: Optional[int] = None,
team_id: Optional[int] = None, team_id: Optional[int] = None,
@ -207,3 +211,31 @@ async def refresh_season_pitching_stats(
except Exception as e: except Exception as e:
logger.error(f'Error refreshing season {season} pitching stats: {e}') logger.error(f'Error refreshing season {season} pitching stats: {e}')
raise HTTPException(status_code=500, detail=f'Refresh failed: {str(e)}') raise HTTPException(status_code=500, detail=f'Refresh failed: {str(e)}')
@router.get('/admin/cache', include_in_schema=PRIVATE_IN_SCHEMA)
@handle_db_errors
async def get_admin_cache_stats(
token: str = Depends(oauth2_scheme)
) -> dict:
"""
Get Redis cache statistics and status.
Private endpoint - requires authentication.
"""
if not valid_token(token):
logger.warning(f'get_admin_cache_stats - Bad Token: {token}')
raise HTTPException(status_code=401, detail='Unauthorized')
logger.info('Getting cache statistics')
try:
cache_stats = get_cache_stats()
logger.info(f'Cache stats retrieved: {cache_stats}')
return {
'status': 'success',
'cache_info': cache_stats
}
except Exception as e:
logger.error(f'Error getting cache stats: {e}')
raise HTTPException(status_code=500, detail=f'Failed to get cache stats: {str(e)}')

View File

@ -31,8 +31,12 @@ services:
- POSTGRES_DB=${SBA_DATABASE} - POSTGRES_DB=${SBA_DATABASE}
- POSTGRES_USER=${SBA_DB_USER} - POSTGRES_USER=${SBA_DB_USER}
- POSTGRES_PASSWORD=${SBA_DB_USER_PASSWORD} - POSTGRES_PASSWORD=${SBA_DB_USER_PASSWORD}
- REDIS_HOST=sba_redis
- REDIS_PORT=6379
- REDIS_DB=0
depends_on: depends_on:
- postgres - postgres
- redis
postgres: postgres:
image: postgres:17-alpine image: postgres:17-alpine
@ -55,6 +59,24 @@ services:
retries: 3 retries: 3
start_period: 30s start_period: 30s
redis:
image: redis:7-alpine
restart: unless-stopped
container_name: sba_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
environment:
- TZ=${TZ}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
command: redis-server --appendonly yes
adminer: adminer:
image: adminer:latest image: adminer:latest
restart: unless-stopped restart: unless-stopped
@ -94,3 +116,4 @@ services:
volumes: volumes:
postgres_data: postgres_data:
redis_data:

View File

@ -5,3 +5,4 @@ python-multipart
pandas pandas
psycopg2-binary>=2.9.0 psycopg2-binary>=2.9.0
requests requests
redis>=4.5.0