From a540a3e7f3c0c829cbc6adb3ae2b80183e0f67b8 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 27 Aug 2025 22:49:37 -0500 Subject: [PATCH] Add Redis Caching --- app/routers_v3/players.py | 6 +++++- app/routers_v3/stratplay.py | 10 +++++++++- app/routers_v3/teams.py | 7 ++++++- app/routers_v3/views.py | 34 +++++++++++++++++++++++++++++++++- docker-compose.yml | 25 ++++++++++++++++++++++++- requirements.txt | 1 + 6 files changed, 78 insertions(+), 5 deletions(-) diff --git a/app/routers_v3/players.py b/app/routers_v3/players.py index a733df3..bf7c2f5 100644 --- a/app/routers_v3/players.py +++ b/app/routers_v3/players.py @@ -5,7 +5,7 @@ import pydantic from pandas import DataFrame 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') @@ -49,6 +49,7 @@ class PlayerList(pydantic.BaseModel): @router.get('') @handle_db_errors +@add_cache_headers(max_age=10*60) async def get_players( 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, @@ -84,6 +85,8 @@ async def get_players( all_players = all_players.order_by(Player.name) elif sort == 'name-desc': all_players = all_players.order_by(-Player.name) + else: + all_players = all_players.order_by(Player.id) if csv: player_list = [ @@ -123,6 +126,7 @@ async def get_players( @router.get('/{player_id}') @handle_db_errors +@add_cache_headers(max_age=10*60) async def get_one_player(player_id: int, short_output: Optional[bool] = False): this_player = Player.get_or_none(Player.id == player_id) if this_player: diff --git a/app/routers_v3/stratplay.py b/app/routers_v3/stratplay.py index 8b751ba..11eb2cd 100644 --- a/app/routers_v3/stratplay.py +++ b/app/routers_v3/stratplay.py @@ -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, \ 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') @@ -122,6 +122,8 @@ class PlayList(BaseModel): @router.get('') @handle_db_errors +@add_cache_headers(max_age=10*60) +@cache_result(ttl=5*60, key_prefix='plays') async def get_plays( 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, @@ -278,6 +280,8 @@ async def get_plays( @router.get('/batting') @handle_db_errors +@add_cache_headers(max_age=10*60) +@cache_result(ttl=5*60, key_prefix='plays-batting') async def get_batting_totals( season: list = Query(default=None), week: 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') @handle_db_errors +@add_cache_headers(max_age=10*60) +@cache_result(ttl=5*60, key_prefix='plays-batting') async def get_pitching_totals( season: list = Query(default=None), week: 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') @handle_db_errors +@add_cache_headers(max_age=10*60) +@cache_result(ttl=5*60, key_prefix='plays-fielding') async def get_fielding_totals( season: list = Query(default=None), week: list = Query(default=None), s_type: Literal['regular', 'post', 'total', None] = None, position: list = Query(default=None), diff --git a/app/routers_v3/teams.py b/app/routers_v3/teams.py index 98a9421..f373099 100644 --- a/app/routers_v3/teams.py +++ b/app/routers_v3/teams.py @@ -5,7 +5,7 @@ import logging import pydantic 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') @@ -37,6 +37,7 @@ class TeamList(pydantic.BaseModel): @router.get('') @handle_db_errors +@cache_result(ttl=10*60, key_prefix='teams') async def get_teams( 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, @@ -76,6 +77,8 @@ async def get_teams( @router.get('/{team_id}') @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): this_team = Team.get_or_none(Team.id == team_id) 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) @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): try: this_team = Team.get_by_id(team_id) diff --git a/app/routers_v3/views.py b/app/routers_v3/views.py index f10d895..ba77a0d 100644 --- a/app/routers_v3/views.py +++ b/app/routers_v3/views.py @@ -4,7 +4,7 @@ import logging import pydantic 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') @@ -15,6 +15,8 @@ router = APIRouter( @router.get('/season-stats/batting') @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( season: Optional[int] = None, team_id: Optional[int] = None, @@ -109,6 +111,8 @@ async def refresh_season_batting_stats( @router.get('/season-stats/pitching') @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( season: Optional[int] = None, team_id: Optional[int] = None, @@ -207,3 +211,31 @@ async def refresh_season_pitching_stats( except Exception as e: logger.error(f'Error refreshing season {season} pitching stats: {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)}') diff --git a/docker-compose.yml b/docker-compose.yml index 1909268..d935aaa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,8 +31,12 @@ services: - POSTGRES_DB=${SBA_DATABASE} - POSTGRES_USER=${SBA_DB_USER} - POSTGRES_PASSWORD=${SBA_DB_USER_PASSWORD} + - REDIS_HOST=sba_redis + - REDIS_PORT=6379 + - REDIS_DB=0 depends_on: - postgres + - redis postgres: image: postgres:17-alpine @@ -55,6 +59,24 @@ services: retries: 3 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: image: adminer:latest restart: unless-stopped @@ -93,4 +115,5 @@ services: - default volumes: - postgres_data: \ No newline at end of file + postgres_data: + redis_data: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 784e9d1..1e32705 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ python-multipart pandas psycopg2-binary>=2.9.0 requests +redis>=4.5.0