major-domo-v2/commands/gameplay/scorebug.py
Cal Corum 6f3339a42e perf: parallelize independent API calls (#90)
Closes #90

Replace sequential awaits with asyncio.gather() in all locations identified
in the issue:

- commands/gameplay/scorebug.py: parallel team lookups in publish_scorecard
  and scorebug commands; also fix missing await on async scorecard_tracker calls
- commands/league/submit_scorecard.py: parallel away/home team lookups
- tasks/live_scorebug_tracker.py: parallel team lookups inside per-scorecard
  loop (compounds across multiple active games); fix missing await on
  get_all_scorecards
- commands/injuries/management.py: parallel get_current_state() +
  search_players() in injury_roll, injury_set_new, and injury_clear
- services/trade_builder.py: parallel per-participant roster validation in
  validate_trade()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:48:10 +00:00

237 lines
9.1 KiB
Python

"""
Scorebug Commands
Implements commands for publishing and displaying live game scorebugs from Google Sheets scorecards.
"""
import asyncio
import discord
from discord.ext import commands
from discord import app_commands
from services.scorebug_service import ScorebugService
from services.team_service import team_service
from utils.logging import get_contextual_logger
from utils.decorators import logged_command
from utils.permissions import league_only
from utils.scorebug_helpers import create_scorebug_embed
from views.embeds import EmbedTemplate
from exceptions import SheetsException
from .scorecard_tracker import ScorecardTracker
class ScorebugCommands(commands.Cog):
"""Scorebug command handlers for live game tracking."""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f"{__name__}.ScorebugCommands")
self.scorebug_service = ScorebugService()
self.scorecard_tracker = ScorecardTracker()
self.logger.info("ScorebugCommands cog initialized")
@app_commands.command(
name="publish-scorecard",
description="Publish a Google Sheets scorecard to this channel for live tracking",
)
@app_commands.describe(
url="Full URL to the Google Sheets scorecard or just the sheet key"
)
@league_only()
@logged_command("/publish-scorecard")
async def publish_scorecard(self, interaction: discord.Interaction, url: str):
"""
Link a Google Sheets scorecard to the current channel for live scorebug tracking.
The scorecard will be monitored for live score updates which will be displayed
in the live scores channel and optionally in associated voice channels.
"""
await interaction.response.defer()
try:
# Validate access to the scorecard
await interaction.edit_original_response(
content="📋 Accessing scorecard..."
)
# Try to open the scorecard to validate it
scorecard = await self.scorebug_service.open_scorecard(url)
# Verify it has a Scorebug tab
try:
scorebug_data = await self.scorebug_service.read_scorebug_data(
url, full_length=False
)
except SheetsException:
embed = EmbedTemplate.error(
title="Invalid Scorecard",
description=(
"This doesn't appear to be a valid scorecard.\n\n"
"Make sure the sheet has a 'Scorebug' tab and is properly set up."
),
)
await interaction.edit_original_response(content=None, embed=embed)
return
# Get team data for display
away_team, home_team = await asyncio.gather(
(
team_service.get_team(scorebug_data.away_team_id)
if scorebug_data.away_team_id
else asyncio.sleep(0)
),
(
team_service.get_team(scorebug_data.home_team_id)
if scorebug_data.home_team_id
else asyncio.sleep(0)
),
)
# Format scorecard link
away_abbrev = away_team.abbrev if away_team else "AWAY"
home_abbrev = home_team.abbrev if home_team else "HOME"
scorecard_link = f"[{away_abbrev} @ {home_abbrev}]({url})"
# Store the scorecard in the tracker
await self.scorecard_tracker.publish_scorecard(
text_channel_id=interaction.channel_id, # type: ignore
sheet_url=url,
publisher_id=interaction.user.id,
)
# Create success embed
embed = EmbedTemplate.success(
title="Scorecard Published",
description=(
f"Your scorecard has been published to {interaction.channel.mention}\n\n" # type: ignore
f"**Sheet:** {scorecard.title}\n"
f"**Status:** Live tracking enabled\n"
f"**Scorecard:** {scorecard_link}\n\n"
f"Anyone can now run `/scorebug` in this channel to see the current score.\n"
f"The scorebug will also update in the live scores channel every 3 minutes."
),
)
embed.add_field(
name="Commands",
value=(
"`/scorebug` - Display full scorebug with details\n"
"`/scorebug full_length:False` - Display compact scorebug"
),
inline=False,
)
await interaction.edit_original_response(content=None, embed=embed)
except SheetsException as e:
embed = EmbedTemplate.error(
title="Cannot Access Scorecard",
description=(
f"{str(e)}\n\n"
f"**You provided:** `{url}`\n\n"
f"**Common issues:**\n"
f"• Sheet is not publicly accessible\n"
f"• Invalid sheet URL or key\n"
f"• Sheet doesn't exist\n\n"
f"Make sure your sheet is shared with 'Anyone with the link can view'."
),
)
await interaction.edit_original_response(content=None, embed=embed)
except Exception as e:
self.logger.error(f"Error publishing scorecard: {e}", exc_info=True)
embed = EmbedTemplate.error(
title="Publication Failed",
description=(
"❌ An unexpected error occurred while publishing the scorecard.\n\n"
"Please try again or contact support if the issue persists."
),
)
await interaction.edit_original_response(content=None, embed=embed)
@app_commands.command(
name="scorebug", description="Display the scorebug for the game in this channel"
)
@app_commands.describe(full_length="Include full game details (defaults to True)")
@league_only()
@logged_command("/scorebug")
async def scorebug(
self, interaction: discord.Interaction, full_length: bool = True
):
"""
Display the current scorebug from the scorecard published in this channel.
"""
await interaction.response.defer(ephemeral=True)
# Check if a scorecard is published in this channel
sheet_url = await self.scorecard_tracker.get_scorecard(interaction.channel_id) # type: ignore
if not sheet_url:
embed = EmbedTemplate.error(
title="No Scorecard Published",
description=(
"❌ No scorecard has been published in this channel.\n\n"
"Use `/publish-scorecard <url>` to publish a scorecard first."
),
)
await interaction.followup.send(embed=embed, ephemeral=True)
return
try:
# Read scorebug data
await interaction.edit_original_response(content="📊 Reading scorebug...")
scorebug_data = await self.scorebug_service.read_scorebug_data(
sheet_url, full_length=full_length
)
# Get team data
away_team, home_team = await asyncio.gather(
(
team_service.get_team(scorebug_data.away_team_id)
if scorebug_data.away_team_id
else asyncio.sleep(0)
),
(
team_service.get_team(scorebug_data.home_team_id)
if scorebug_data.home_team_id
else asyncio.sleep(0)
),
)
# Create scorebug embed using shared utility
embed = create_scorebug_embed(
scorebug_data, away_team, home_team, full_length
)
await interaction.edit_original_response(content=None, embed=embed)
# Update timestamp in tracker
await self.scorecard_tracker.update_timestamp(interaction.channel_id) # type: ignore
except SheetsException as e:
embed = EmbedTemplate.error(
title="Cannot Read Scorebug",
description=(
f"{str(e)}\n\n"
f"The scorecard may have been deleted or the sheet structure changed."
),
)
await interaction.edit_original_response(content=None, embed=embed)
except Exception as e:
self.logger.error(f"Error displaying scorebug: {e}", exc_info=True)
embed = EmbedTemplate.error(
title="Display Failed",
description=(
"❌ An error occurred while reading the scorebug.\n\n"
"Please try again or republish the scorecard."
),
)
await interaction.edit_original_response(content=None, embed=embed)
async def setup(bot: commands.Bot):
"""Load the scorebug commands cog."""
await bot.add_cog(ScorebugCommands(bot))