All checks were successful
Build Docker Image / build (pull_request) Successful in 1m16s
Closes #99 - Add module-level `_user_team_cache` dict with 60-second TTL so `get_user_major_league_team` is called at most once per minute per user instead of on every keystroke. - Reduce `search_players(limit=50)` to `limit=25` to match Discord's 25-choice display cap and avoid fetching unused results. - Add `TestGetCachedUserTeam` class covering cache hit, TTL expiry, and None caching; add `clear_user_team_cache` autouse fixture to prevent test interference via module-level state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
6.1 KiB
Python
195 lines
6.1 KiB
Python
"""
|
|
Autocomplete Utilities
|
|
|
|
Shared autocomplete functions for Discord slash commands.
|
|
"""
|
|
|
|
import time
|
|
from typing import Dict, List, Optional, Tuple
|
|
import discord
|
|
from discord import app_commands
|
|
|
|
from config import get_config
|
|
from models.team import RosterType, Team
|
|
from services.player_service import player_service
|
|
from services.team_service import team_service
|
|
from utils.team_utils import get_user_major_league_team
|
|
|
|
# Cache for user team lookups: user_id -> (team, cached_at)
|
|
_user_team_cache: Dict[int, Tuple[Optional[Team], float]] = {}
|
|
_USER_TEAM_CACHE_TTL = 60 # seconds
|
|
|
|
|
|
async def _get_cached_user_team(interaction: discord.Interaction) -> Optional[Team]:
|
|
"""Return the user's major league team, cached for 60 seconds per user."""
|
|
user_id = interaction.user.id
|
|
if user_id in _user_team_cache:
|
|
team, cached_at = _user_team_cache[user_id]
|
|
if time.time() - cached_at < _USER_TEAM_CACHE_TTL:
|
|
return team
|
|
team = await get_user_major_league_team(user_id)
|
|
_user_team_cache[user_id] = (team, time.time())
|
|
return team
|
|
|
|
|
|
async def player_autocomplete(
|
|
interaction: discord.Interaction, current: str
|
|
) -> List[app_commands.Choice[str]]:
|
|
"""
|
|
Autocomplete for player names with team context prioritization.
|
|
|
|
Prioritizes players from the user's team first, then shows other players.
|
|
|
|
Args:
|
|
interaction: Discord interaction object
|
|
current: Current input from user
|
|
|
|
Returns:
|
|
List of player name choices (user's team players first)
|
|
"""
|
|
if len(current) < 2:
|
|
return []
|
|
|
|
try:
|
|
# Get user's team for prioritization (cached per user, 60s TTL)
|
|
user_team = await _get_cached_user_team(interaction)
|
|
|
|
# Search for players using the search endpoint
|
|
players = await player_service.search_players(
|
|
current, limit=25, season=get_config().sba_season
|
|
)
|
|
|
|
# Separate players by team (user's team vs others)
|
|
user_team_players = []
|
|
other_players = []
|
|
|
|
for player in players:
|
|
# Check if player belongs to user's team (any roster section)
|
|
is_users_player = False
|
|
if user_team and hasattr(player, "team") and player.team:
|
|
# Check if player is from user's major league team or has same base team
|
|
if player.team.id == user_team.id or (
|
|
hasattr(player, "team_id") and player.team_id == user_team.id
|
|
):
|
|
is_users_player = True
|
|
|
|
if is_users_player:
|
|
user_team_players.append(player)
|
|
else:
|
|
other_players.append(player)
|
|
|
|
# Format choices with team context
|
|
choices = []
|
|
|
|
# Add user's team players first (prioritized)
|
|
for player in user_team_players[:15]: # Limit user team players
|
|
team_info = f"{player.primary_position}"
|
|
if hasattr(player, "team") and player.team:
|
|
team_info += f" - {player.team.abbrev}"
|
|
|
|
choice_name = f"{player.name} ({team_info})"
|
|
choices.append(app_commands.Choice(name=choice_name, value=player.name))
|
|
|
|
# Add other players (remaining slots)
|
|
remaining_slots = 25 - len(choices)
|
|
for player in other_players[:remaining_slots]:
|
|
team_info = f"{player.primary_position}"
|
|
if hasattr(player, "team") and player.team:
|
|
team_info += f" - {player.team.abbrev}"
|
|
|
|
choice_name = f"{player.name} ({team_info})"
|
|
choices.append(app_commands.Choice(name=choice_name, value=player.name))
|
|
|
|
return choices
|
|
|
|
except Exception:
|
|
# Silently fail on autocomplete errors to avoid disrupting user experience
|
|
return []
|
|
|
|
|
|
async def team_autocomplete(
|
|
interaction: discord.Interaction, current: str
|
|
) -> List[app_commands.Choice[str]]:
|
|
"""
|
|
Autocomplete for team abbreviations.
|
|
|
|
Args:
|
|
interaction: Discord interaction object
|
|
current: Current input from user
|
|
|
|
Returns:
|
|
List of team abbreviation choices
|
|
"""
|
|
if len(current) < 1:
|
|
return []
|
|
|
|
try:
|
|
# Get all teams for current season
|
|
teams = await team_service.get_teams_by_season(get_config().sba_season)
|
|
|
|
# Filter teams by current input and limit to 25
|
|
matching_teams = [
|
|
team
|
|
for team in teams
|
|
if current.lower() in team.abbrev.lower()
|
|
or current.lower() in team.sname.lower()
|
|
][:25]
|
|
|
|
choices = []
|
|
for team in matching_teams:
|
|
choice_name = f"{team.abbrev} - {team.sname}"
|
|
choices.append(app_commands.Choice(name=choice_name, value=team.abbrev))
|
|
|
|
return choices
|
|
|
|
except Exception:
|
|
# Silently fail on autocomplete errors
|
|
return []
|
|
|
|
|
|
async def major_league_team_autocomplete(
|
|
interaction: discord.Interaction, current: str
|
|
) -> List[app_commands.Choice[str]]:
|
|
"""
|
|
Autocomplete for Major League team abbreviations only.
|
|
|
|
Used for trade commands where only ML team owners should be able to initiate trades.
|
|
|
|
Args:
|
|
interaction: Discord interaction object
|
|
current: Current input from user
|
|
|
|
Returns:
|
|
List of Major League team abbreviation choices
|
|
"""
|
|
if len(current) < 1:
|
|
return []
|
|
|
|
try:
|
|
# Get all teams for current season
|
|
all_teams = await team_service.get_teams_by_season(get_config().sba_season)
|
|
|
|
# Filter to only Major League teams using the model's helper method
|
|
ml_teams = [
|
|
team for team in all_teams if team.roster_type() == RosterType.MAJOR_LEAGUE
|
|
]
|
|
|
|
# Filter teams by current input and limit to 25
|
|
matching_teams = [
|
|
team
|
|
for team in ml_teams
|
|
if current.lower() in team.abbrev.lower()
|
|
or current.lower() in team.sname.lower()
|
|
][:25]
|
|
|
|
choices = []
|
|
for team in matching_teams:
|
|
choice_name = f"{team.abbrev} - {team.sname}"
|
|
choices.append(app_commands.Choice(name=choice_name, value=team.abbrev))
|
|
|
|
return choices
|
|
|
|
except Exception:
|
|
# Silently fail on autocomplete errors
|
|
return []
|