Compare commits

..

1 Commits

Author SHA1 Message Date
Cal Corum
8e984d1d07 perf: eliminate redundant API calls in trade views (#94)
Closes #94

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 13:33:03 -05:00
4 changed files with 24 additions and 29 deletions

View File

@ -61,7 +61,7 @@ class ScorecardTracker:
except Exception as e:
logger.error(f"Failed to save scorecard data: {e}")
async def publish_scorecard(
def publish_scorecard(
self, text_channel_id: int, sheet_url: str, publisher_id: int
) -> None:
"""
@ -82,7 +82,7 @@ class ScorecardTracker:
self.save_data()
logger.info(f"Published scorecard to channel {text_channel_id}: {sheet_url}")
async def unpublish_scorecard(self, text_channel_id: int) -> bool:
def unpublish_scorecard(self, text_channel_id: int) -> bool:
"""
Remove scorecard from a text channel.
@ -103,7 +103,7 @@ class ScorecardTracker:
return False
async def get_scorecard(self, text_channel_id: int) -> Optional[str]:
def get_scorecard(self, text_channel_id: int) -> Optional[str]:
"""
Get scorecard URL for a text channel.
@ -118,7 +118,7 @@ class ScorecardTracker:
scorecard_data = scorecards.get(str(text_channel_id))
return scorecard_data["sheet_url"] if scorecard_data else None
async def get_all_scorecards(self) -> List[Tuple[int, str]]:
def get_all_scorecards(self) -> List[Tuple[int, str]]:
"""
Get all published scorecards.
@ -132,7 +132,7 @@ class ScorecardTracker:
for channel_id, data in scorecards.items()
]
async def update_timestamp(self, text_channel_id: int) -> None:
def update_timestamp(self, text_channel_id: int) -> None:
"""
Update the last_updated timestamp for a scorecard.
@ -146,7 +146,7 @@ class ScorecardTracker:
scorecards[channel_key]["last_updated"] = datetime.now(UTC).isoformat()
self.save_data()
async def cleanup_stale_entries(self, valid_channel_ids: List[int]) -> int:
def cleanup_stale_entries(self, valid_channel_ids: List[int]) -> int:
"""
Remove tracking entries for text channels that no longer exist.

View File

@ -128,7 +128,7 @@ class VoiceChannelCleanupService:
if channel_data and channel_data.get("text_channel_id"):
try:
text_channel_id_int = int(channel_data["text_channel_id"])
was_unpublished = await self.scorecard_tracker.unpublish_scorecard(
was_unpublished = self.scorecard_tracker.unpublish_scorecard(
text_channel_id_int
)
if was_unpublished:
@ -218,10 +218,8 @@ class VoiceChannelCleanupService:
if text_channel_id:
try:
text_channel_id_int = int(text_channel_id)
was_unpublished = (
await self.scorecard_tracker.unpublish_scorecard(
text_channel_id_int
)
was_unpublished = self.scorecard_tracker.unpublish_scorecard(
text_channel_id_int
)
if was_unpublished:
self.logger.info(
@ -246,10 +244,8 @@ class VoiceChannelCleanupService:
if text_channel_id:
try:
text_channel_id_int = int(text_channel_id)
was_unpublished = (
await self.scorecard_tracker.unpublish_scorecard(
text_channel_id_int
)
was_unpublished = self.scorecard_tracker.unpublish_scorecard(
text_channel_id_int
)
if was_unpublished:
self.logger.info(
@ -334,7 +330,7 @@ class VoiceChannelCleanupService:
if text_channel_id:
try:
text_channel_id_int = int(text_channel_id)
was_unpublished = await self.scorecard_tracker.unpublish_scorecard(
was_unpublished = self.scorecard_tracker.unpublish_scorecard(
text_channel_id_int
)
if was_unpublished:
@ -362,7 +358,7 @@ class VoiceChannelCleanupService:
if text_channel_id:
try:
text_channel_id_int = int(text_channel_id)
was_unpublished = await self.scorecard_tracker.unpublish_scorecard(
was_unpublished = self.scorecard_tracker.unpublish_scorecard(
text_channel_id_int
)
if was_unpublished:

View File

@ -24,8 +24,7 @@ from utils.scorebug_helpers import create_scorebug_embed, create_team_progress_b
class TestScorecardTrackerFreshReads:
"""Tests that ScorecardTracker reads fresh data from disk (fix for #40)."""
@pytest.mark.asyncio
async def test_get_all_scorecards_reads_fresh_data(self, tmp_path):
def test_get_all_scorecards_reads_fresh_data(self, tmp_path):
"""get_all_scorecards() should pick up scorecards written by another process.
Simulates the background task having a stale tracker instance while
@ -35,7 +34,7 @@ class TestScorecardTrackerFreshReads:
data_file.write_text(json.dumps({"scorecards": {}}))
tracker = ScorecardTracker(data_file=str(data_file))
assert await tracker.get_all_scorecards() == []
assert tracker.get_all_scorecards() == []
# Another process writes a scorecard to the same file
new_data = {
@ -52,18 +51,17 @@ class TestScorecardTrackerFreshReads:
data_file.write_text(json.dumps(new_data))
# Should see the new scorecard without restart
result = await tracker.get_all_scorecards()
result = tracker.get_all_scorecards()
assert len(result) == 1
assert result[0] == (111, "https://docs.google.com/spreadsheets/d/abc123")
@pytest.mark.asyncio
async def test_get_scorecard_reads_fresh_data(self, tmp_path):
def test_get_scorecard_reads_fresh_data(self, tmp_path):
"""get_scorecard() should pick up a scorecard written by another process."""
data_file = tmp_path / "scorecards.json"
data_file.write_text(json.dumps({"scorecards": {}}))
tracker = ScorecardTracker(data_file=str(data_file))
assert await tracker.get_scorecard(222) is None
assert tracker.get_scorecard(222) is None
# Another process writes a scorecard
new_data = {
@ -81,7 +79,7 @@ class TestScorecardTrackerFreshReads:
# Should see the new scorecard
assert (
await tracker.get_scorecard(222)
tracker.get_scorecard(222)
== "https://docs.google.com/spreadsheets/d/xyz789"
)

View File

@ -328,6 +328,7 @@ class TradeAcceptanceView(discord.ui.View):
def __init__(self, builder: TradeBuilder):
super().__init__(timeout=3600.0) # 1 hour timeout
self.builder = builder
self._checked_team: Optional[Team] = None
async def _get_user_team(self, interaction: discord.Interaction) -> Optional[Team]:
"""Get the team owned by the interacting user."""
@ -353,6 +354,7 @@ class TradeAcceptanceView(discord.ui.View):
)
return False
self._checked_team = user_team
return True
async def on_timeout(self) -> None:
@ -366,7 +368,7 @@ class TradeAcceptanceView(discord.ui.View):
self, interaction: discord.Interaction, button: discord.ui.Button
):
"""Handle accept button click."""
user_team = await self._get_user_team(interaction)
user_team = self._checked_team
if not user_team:
return
@ -401,7 +403,7 @@ class TradeAcceptanceView(discord.ui.View):
self, interaction: discord.Interaction, button: discord.ui.Button
):
"""Handle reject button click - moves trade back to DRAFT."""
user_team = await self._get_user_team(interaction)
user_team = self._checked_team
if not user_team:
return
@ -708,10 +710,10 @@ async def create_trade_embed(builder: TradeBuilder) -> discord.Embed:
Returns:
Discord embed with current trade state
"""
validation = await builder.validate_trade()
if builder.is_empty:
color = EmbedColors.SECONDARY
else:
validation = await builder.validate_trade()
color = EmbedColors.SUCCESS if validation.is_legal else EmbedColors.WARNING
embed = EmbedTemplate.create_base_embed(
@ -766,7 +768,6 @@ async def create_trade_embed(builder: TradeBuilder) -> discord.Embed:
inline=False,
)
validation = await builder.validate_trade()
if validation.is_legal:
status_text = "Trade appears legal"
else: