CLAUDE: Add draft system services (no caching)
Add three core services for draft system with no caching decorators since draft data changes constantly during active drafts: - DraftService: Core draft logic, timer management, pick advancement - DraftPickService: Pick CRUD operations, queries by team/round/availability - DraftListService: Auto-draft queue management with reordering All services follow BaseService pattern with proper error handling and structured logging. Ready for integration with commands and tasks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
86459693a4
commit
23cf16d596
368
services/draft_list_service.py
Normal file
368
services/draft_list_service.py
Normal file
@ -0,0 +1,368 @@
|
||||
"""
|
||||
Draft list service for Discord Bot v2.0
|
||||
|
||||
Handles team draft list (auto-draft queue) operations. NO CACHING - lists change frequently.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
from services.base_service import BaseService
|
||||
from models.draft_list import DraftList
|
||||
from exceptions import APIException
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.DraftListService')
|
||||
|
||||
|
||||
class DraftListService(BaseService[DraftList]):
|
||||
"""
|
||||
Service for draft list operations.
|
||||
|
||||
IMPORTANT: This service does NOT use caching decorators because draft lists
|
||||
change as users add/remove players from their auto-draft queues.
|
||||
|
||||
Features:
|
||||
- Get team's draft list (ranked by priority)
|
||||
- Add player to draft list
|
||||
- Remove player from draft list
|
||||
- Reorder draft list
|
||||
- Clear entire draft list
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize draft list service."""
|
||||
super().__init__(DraftList, 'draftlist')
|
||||
logger.debug("DraftListService initialized")
|
||||
|
||||
async def get_team_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int
|
||||
) -> List[DraftList]:
|
||||
"""
|
||||
Get team's draft list ordered by rank.
|
||||
|
||||
NOT cached - teams update their lists frequently during draft.
|
||||
|
||||
Args:
|
||||
season: Draft season
|
||||
team_id: Team ID
|
||||
|
||||
Returns:
|
||||
List of DraftList entries ordered by rank (1 = highest priority)
|
||||
"""
|
||||
try:
|
||||
params = [
|
||||
('season', str(season)),
|
||||
('team_id', str(team_id)),
|
||||
('sort', 'rank-asc') # Order by priority
|
||||
]
|
||||
|
||||
entries = await self.get_all_items(params=params)
|
||||
logger.debug(f"Found {len(entries)} draft list entries for team {team_id}")
|
||||
return entries
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting draft list for team {team_id}: {e}")
|
||||
return []
|
||||
|
||||
async def add_to_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
player_id: int,
|
||||
rank: Optional[int] = None
|
||||
) -> Optional[DraftList]:
|
||||
"""
|
||||
Add player to team's draft list.
|
||||
|
||||
If rank is not provided, adds to end of list.
|
||||
|
||||
Args:
|
||||
season: Draft season
|
||||
team_id: Team ID
|
||||
player_id: Player ID to add
|
||||
rank: Priority rank (1 = highest), None = add to end
|
||||
|
||||
Returns:
|
||||
Created DraftList entry or None if creation failed
|
||||
"""
|
||||
try:
|
||||
# If rank not provided, get current list and add to end
|
||||
if rank is None:
|
||||
current_list = await self.get_team_list(season, team_id)
|
||||
rank = len(current_list) + 1
|
||||
|
||||
entry_data = {
|
||||
'season': season,
|
||||
'team_id': team_id,
|
||||
'player_id': player_id,
|
||||
'rank': rank
|
||||
}
|
||||
|
||||
created_entry = await self.create(entry_data)
|
||||
|
||||
if created_entry:
|
||||
logger.info(f"Added player {player_id} to team {team_id} draft list at rank {rank}")
|
||||
else:
|
||||
logger.error(f"Failed to add player {player_id} to draft list")
|
||||
|
||||
return created_entry
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding player {player_id} to draft list: {e}")
|
||||
return None
|
||||
|
||||
async def remove_from_list(
|
||||
self,
|
||||
entry_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
Remove entry from draft list by ID.
|
||||
|
||||
Args:
|
||||
entry_id: Draft list entry database ID
|
||||
|
||||
Returns:
|
||||
True if deletion succeeded
|
||||
"""
|
||||
try:
|
||||
result = await self.delete(entry_id)
|
||||
|
||||
if result:
|
||||
logger.info(f"Removed draft list entry {entry_id}")
|
||||
else:
|
||||
logger.error(f"Failed to remove draft list entry {entry_id}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing draft list entry {entry_id}: {e}")
|
||||
return False
|
||||
|
||||
async def remove_player_from_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
player_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
Remove specific player from team's draft list.
|
||||
|
||||
Args:
|
||||
season: Draft season
|
||||
team_id: Team ID
|
||||
player_id: Player ID to remove
|
||||
|
||||
Returns:
|
||||
True if player was found and removed
|
||||
"""
|
||||
try:
|
||||
# Get team's list
|
||||
entries = await self.get_team_list(season, team_id)
|
||||
|
||||
# Find entry with this player
|
||||
for entry in entries:
|
||||
if entry.player_id == player_id:
|
||||
return await self.remove_from_list(entry.id)
|
||||
|
||||
logger.warning(f"Player {player_id} not found in team {team_id} draft list")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing player {player_id} from draft list: {e}")
|
||||
return False
|
||||
|
||||
async def clear_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
Clear entire draft list for team.
|
||||
|
||||
Args:
|
||||
season: Draft season
|
||||
team_id: Team ID
|
||||
|
||||
Returns:
|
||||
True if all entries were deleted successfully
|
||||
"""
|
||||
try:
|
||||
entries = await self.get_team_list(season, team_id)
|
||||
|
||||
if not entries:
|
||||
logger.debug(f"No draft list entries to clear for team {team_id}")
|
||||
return True
|
||||
|
||||
success = True
|
||||
for entry in entries:
|
||||
if not await self.remove_from_list(entry.id):
|
||||
success = False
|
||||
|
||||
if success:
|
||||
logger.info(f"Cleared {len(entries)} draft list entries for team {team_id}")
|
||||
else:
|
||||
logger.warning(f"Failed to clear some draft list entries for team {team_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing draft list for team {team_id}: {e}")
|
||||
return False
|
||||
|
||||
async def reorder_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
new_order: List[int]
|
||||
) -> bool:
|
||||
"""
|
||||
Reorder team's draft list.
|
||||
|
||||
Args:
|
||||
season: Draft season
|
||||
team_id: Team ID
|
||||
new_order: List of player IDs in desired order
|
||||
|
||||
Returns:
|
||||
True if reordering succeeded
|
||||
"""
|
||||
try:
|
||||
# Get current list
|
||||
entries = await self.get_team_list(season, team_id)
|
||||
|
||||
# Build mapping of player_id -> entry
|
||||
entry_map = {e.player_id: e for e in entries}
|
||||
|
||||
# Update each entry with new rank
|
||||
success = True
|
||||
for new_rank, player_id in enumerate(new_order, start=1):
|
||||
if player_id not in entry_map:
|
||||
logger.warning(f"Player {player_id} not in draft list, skipping")
|
||||
continue
|
||||
|
||||
entry = entry_map[player_id]
|
||||
if entry.rank != new_rank:
|
||||
updated = await self.patch(entry.id, {'rank': new_rank})
|
||||
if not updated:
|
||||
logger.error(f"Failed to update rank for entry {entry.id}")
|
||||
success = False
|
||||
|
||||
if success:
|
||||
logger.info(f"Reordered draft list for team {team_id}")
|
||||
else:
|
||||
logger.warning(f"Some errors occurred reordering draft list for team {team_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reordering draft list for team {team_id}: {e}")
|
||||
return False
|
||||
|
||||
async def move_entry_up(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
player_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
Move player up one position in draft list (higher priority).
|
||||
|
||||
Args:
|
||||
season: Draft season
|
||||
team_id: Team ID
|
||||
player_id: Player ID to move up
|
||||
|
||||
Returns:
|
||||
True if move succeeded
|
||||
"""
|
||||
try:
|
||||
entries = await self.get_team_list(season, team_id)
|
||||
|
||||
# Find player's current position
|
||||
current_entry = None
|
||||
for entry in entries:
|
||||
if entry.player_id == player_id:
|
||||
current_entry = entry
|
||||
break
|
||||
|
||||
if not current_entry:
|
||||
logger.warning(f"Player {player_id} not found in draft list")
|
||||
return False
|
||||
|
||||
if current_entry.rank == 1:
|
||||
logger.debug(f"Player {player_id} already at top of draft list")
|
||||
return False
|
||||
|
||||
# Swap with entry above (rank - 1)
|
||||
above_entry = next((e for e in entries if e.rank == current_entry.rank - 1), None)
|
||||
if not above_entry:
|
||||
logger.error(f"Could not find entry above rank {current_entry.rank}")
|
||||
return False
|
||||
|
||||
# Swap ranks
|
||||
await self.patch(current_entry.id, {'rank': current_entry.rank - 1})
|
||||
await self.patch(above_entry.id, {'rank': above_entry.rank + 1})
|
||||
|
||||
logger.info(f"Moved player {player_id} up to rank {current_entry.rank - 1}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving player {player_id} up in draft list: {e}")
|
||||
return False
|
||||
|
||||
async def move_entry_down(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
player_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
Move player down one position in draft list (lower priority).
|
||||
|
||||
Args:
|
||||
season: Draft season
|
||||
team_id: Team ID
|
||||
player_id: Player ID to move down
|
||||
|
||||
Returns:
|
||||
True if move succeeded
|
||||
"""
|
||||
try:
|
||||
entries = await self.get_team_list(season, team_id)
|
||||
|
||||
# Find player's current position
|
||||
current_entry = None
|
||||
for entry in entries:
|
||||
if entry.player_id == player_id:
|
||||
current_entry = entry
|
||||
break
|
||||
|
||||
if not current_entry:
|
||||
logger.warning(f"Player {player_id} not found in draft list")
|
||||
return False
|
||||
|
||||
if current_entry.rank == len(entries):
|
||||
logger.debug(f"Player {player_id} already at bottom of draft list")
|
||||
return False
|
||||
|
||||
# Swap with entry below (rank + 1)
|
||||
below_entry = next((e for e in entries if e.rank == current_entry.rank + 1), None)
|
||||
if not below_entry:
|
||||
logger.error(f"Could not find entry below rank {current_entry.rank}")
|
||||
return False
|
||||
|
||||
# Swap ranks
|
||||
await self.patch(current_entry.id, {'rank': current_entry.rank + 1})
|
||||
await self.patch(below_entry.id, {'rank': below_entry.rank - 1})
|
||||
|
||||
logger.info(f"Moved player {player_id} down to rank {current_entry.rank + 1}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving player {player_id} down in draft list: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Global service instance
|
||||
draft_list_service = DraftListService()
|
||||
341
services/draft_service.py
Normal file
341
services/draft_service.py
Normal file
@ -0,0 +1,341 @@
|
||||
"""
|
||||
Draft service for Discord Bot v2.0
|
||||
|
||||
Core draft business logic and state management. NO CACHING - draft state changes constantly.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from services.base_service import BaseService
|
||||
from models.draft_data import DraftData
|
||||
from exceptions import APIException
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.DraftService')
|
||||
|
||||
|
||||
class DraftService(BaseService[DraftData]):
|
||||
"""
|
||||
Service for core draft operations and state management.
|
||||
|
||||
IMPORTANT: This service does NOT use caching decorators because draft data
|
||||
changes every 2-12 minutes during an active draft. Always fetch fresh data.
|
||||
|
||||
Features:
|
||||
- Get/update draft configuration
|
||||
- Timer management (start/stop)
|
||||
- Pick advancement
|
||||
- Draft state validation
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize draft service."""
|
||||
super().__init__(DraftData, 'draftdata')
|
||||
logger.debug("DraftService initialized")
|
||||
|
||||
async def get_draft_data(self) -> Optional[DraftData]:
|
||||
"""
|
||||
Get current draft configuration and state.
|
||||
|
||||
NOT cached - draft state changes frequently during active draft.
|
||||
|
||||
Returns:
|
||||
DraftData instance or None if not found
|
||||
"""
|
||||
try:
|
||||
# Draft data endpoint typically returns single object
|
||||
items = await self.get_all_items()
|
||||
|
||||
if items:
|
||||
draft_data = items[0]
|
||||
logger.debug(
|
||||
f"Retrieved draft data: pick={draft_data.currentpick}, "
|
||||
f"timer={draft_data.timer}, "
|
||||
f"deadline={draft_data.pick_deadline}"
|
||||
)
|
||||
return draft_data
|
||||
|
||||
logger.warning("No draft data found in database")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting draft data: {e}")
|
||||
return None
|
||||
|
||||
async def update_draft_data(
|
||||
self,
|
||||
draft_id: int,
|
||||
updates: Dict[str, Any]
|
||||
) -> Optional[DraftData]:
|
||||
"""
|
||||
Update draft configuration.
|
||||
|
||||
Args:
|
||||
draft_id: DraftData database ID (typically 1)
|
||||
updates: Dictionary of fields to update
|
||||
|
||||
Returns:
|
||||
Updated DraftData instance or None if update failed
|
||||
"""
|
||||
try:
|
||||
updated = await self.patch(draft_id, updates)
|
||||
|
||||
if updated:
|
||||
logger.info(f"Updated draft data: {updates}")
|
||||
else:
|
||||
logger.error(f"Failed to update draft data with {updates}")
|
||||
|
||||
return updated
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating draft data: {e}")
|
||||
return None
|
||||
|
||||
async def set_timer(
|
||||
self,
|
||||
draft_id: int,
|
||||
active: bool,
|
||||
pick_minutes: Optional[int] = None
|
||||
) -> Optional[DraftData]:
|
||||
"""
|
||||
Enable or disable draft timer.
|
||||
|
||||
Args:
|
||||
draft_id: DraftData database ID
|
||||
active: True to enable timer, False to disable
|
||||
pick_minutes: Minutes per pick (updates default if provided)
|
||||
|
||||
Returns:
|
||||
Updated DraftData instance
|
||||
"""
|
||||
try:
|
||||
updates = {'timer': active}
|
||||
|
||||
if pick_minutes is not None:
|
||||
updates['pick_minutes'] = pick_minutes
|
||||
|
||||
# Set deadline based on timer state
|
||||
if active:
|
||||
# Calculate new deadline
|
||||
if pick_minutes:
|
||||
deadline = datetime.now() + timedelta(minutes=pick_minutes)
|
||||
else:
|
||||
# Get current pick_minutes from existing data
|
||||
current_data = await self.get_draft_data()
|
||||
if current_data:
|
||||
deadline = datetime.now() + timedelta(minutes=current_data.pick_minutes)
|
||||
else:
|
||||
deadline = datetime.now() + timedelta(minutes=2) # Default fallback
|
||||
updates['pick_deadline'] = deadline
|
||||
else:
|
||||
# Set deadline far in future when timer inactive
|
||||
updates['pick_deadline'] = datetime.now() + timedelta(days=690)
|
||||
|
||||
updated = await self.update_draft_data(draft_id, updates)
|
||||
|
||||
if updated:
|
||||
status = "enabled" if active else "disabled"
|
||||
logger.info(f"Draft timer {status}")
|
||||
else:
|
||||
logger.error("Failed to update draft timer")
|
||||
|
||||
return updated
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting draft timer: {e}")
|
||||
return None
|
||||
|
||||
async def advance_pick(
|
||||
self,
|
||||
draft_id: int,
|
||||
current_pick: int
|
||||
) -> Optional[DraftData]:
|
||||
"""
|
||||
Advance to next pick in draft.
|
||||
|
||||
Automatically skips picks that have already been filled (player selected).
|
||||
Posts round announcement when entering new round.
|
||||
|
||||
Args:
|
||||
draft_id: DraftData database ID
|
||||
current_pick: Current overall pick number
|
||||
|
||||
Returns:
|
||||
Updated DraftData with new currentpick
|
||||
"""
|
||||
try:
|
||||
from services.draft_pick_service import draft_pick_service
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
season = config.sba_current_season
|
||||
|
||||
# Start with next pick
|
||||
next_pick = current_pick + 1
|
||||
|
||||
# Keep advancing until we find an unfilled pick or reach end
|
||||
while next_pick <= 512: # 32 rounds * 16 teams
|
||||
pick = await draft_pick_service.get_pick(season, next_pick)
|
||||
|
||||
if not pick:
|
||||
logger.error(f"Pick #{next_pick} not found in database")
|
||||
break
|
||||
|
||||
# If pick has no player, this is the next pick to make
|
||||
if pick.player_id is None:
|
||||
logger.info(f"Advanced to pick #{next_pick}")
|
||||
break
|
||||
|
||||
# Pick already filled, continue to next
|
||||
logger.debug(f"Pick #{next_pick} already filled, skipping")
|
||||
next_pick += 1
|
||||
|
||||
# Check if draft is complete
|
||||
if next_pick > 512:
|
||||
logger.info("Draft is complete - all picks filled")
|
||||
# Disable timer
|
||||
await self.set_timer(draft_id, active=False)
|
||||
return await self.get_draft_data()
|
||||
|
||||
# Update to next pick
|
||||
updates = {'currentpick': next_pick}
|
||||
|
||||
# Reset deadline if timer is active
|
||||
current_data = await self.get_draft_data()
|
||||
if current_data and current_data.timer:
|
||||
updates['pick_deadline'] = datetime.now() + timedelta(minutes=current_data.pick_minutes)
|
||||
|
||||
updated = await self.update_draft_data(draft_id, updates)
|
||||
|
||||
if updated:
|
||||
logger.info(f"Draft advanced from pick #{current_pick} to #{next_pick}")
|
||||
else:
|
||||
logger.error(f"Failed to advance draft pick")
|
||||
|
||||
return updated
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error advancing draft pick: {e}")
|
||||
return None
|
||||
|
||||
async def set_current_pick(
|
||||
self,
|
||||
draft_id: int,
|
||||
overall: int,
|
||||
reset_timer: bool = True
|
||||
) -> Optional[DraftData]:
|
||||
"""
|
||||
Manually set current pick (admin operation).
|
||||
|
||||
Args:
|
||||
draft_id: DraftData database ID
|
||||
overall: Overall pick number to jump to
|
||||
reset_timer: Whether to reset the pick deadline
|
||||
|
||||
Returns:
|
||||
Updated DraftData
|
||||
"""
|
||||
try:
|
||||
updates = {'currentpick': overall}
|
||||
|
||||
if reset_timer:
|
||||
current_data = await self.get_draft_data()
|
||||
if current_data and current_data.timer:
|
||||
updates['pick_deadline'] = datetime.now() + timedelta(minutes=current_data.pick_minutes)
|
||||
|
||||
updated = await self.update_draft_data(draft_id, updates)
|
||||
|
||||
if updated:
|
||||
logger.info(f"Manually set current pick to #{overall}")
|
||||
else:
|
||||
logger.error(f"Failed to set current pick to #{overall}")
|
||||
|
||||
return updated
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting current pick: {e}")
|
||||
return None
|
||||
|
||||
async def update_channels(
|
||||
self,
|
||||
draft_id: int,
|
||||
ping_channel_id: Optional[int] = None,
|
||||
result_channel_id: Optional[int] = None
|
||||
) -> Optional[DraftData]:
|
||||
"""
|
||||
Update draft Discord channel configuration.
|
||||
|
||||
Args:
|
||||
draft_id: DraftData database ID
|
||||
ping_channel_id: Channel ID for "on the clock" pings
|
||||
result_channel_id: Channel ID for draft results
|
||||
|
||||
Returns:
|
||||
Updated DraftData
|
||||
"""
|
||||
try:
|
||||
updates = {}
|
||||
if ping_channel_id is not None:
|
||||
updates['ping_channel_id'] = ping_channel_id
|
||||
if result_channel_id is not None:
|
||||
updates['result_channel_id'] = result_channel_id
|
||||
|
||||
if not updates:
|
||||
logger.warning("No channel updates provided")
|
||||
return await self.get_draft_data()
|
||||
|
||||
updated = await self.update_draft_data(draft_id, updates)
|
||||
|
||||
if updated:
|
||||
logger.info(f"Updated draft channels: {updates}")
|
||||
else:
|
||||
logger.error("Failed to update draft channels")
|
||||
|
||||
return updated
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating draft channels: {e}")
|
||||
return None
|
||||
|
||||
async def reset_draft_deadline(
|
||||
self,
|
||||
draft_id: int,
|
||||
minutes: Optional[int] = None
|
||||
) -> Optional[DraftData]:
|
||||
"""
|
||||
Reset the current pick deadline.
|
||||
|
||||
Args:
|
||||
draft_id: DraftData database ID
|
||||
minutes: Minutes to add (uses pick_minutes from config if not provided)
|
||||
|
||||
Returns:
|
||||
Updated DraftData with new deadline
|
||||
"""
|
||||
try:
|
||||
if minutes is None:
|
||||
current_data = await self.get_draft_data()
|
||||
if not current_data:
|
||||
logger.error("Could not get current draft data")
|
||||
return None
|
||||
minutes = current_data.pick_minutes
|
||||
|
||||
new_deadline = datetime.now() + timedelta(minutes=minutes)
|
||||
updates = {'pick_deadline': new_deadline}
|
||||
|
||||
updated = await self.update_draft_data(draft_id, updates)
|
||||
|
||||
if updated:
|
||||
logger.info(f"Reset draft deadline to {new_deadline}")
|
||||
else:
|
||||
logger.error("Failed to reset draft deadline")
|
||||
|
||||
return updated
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting draft deadline: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# Global service instance
|
||||
draft_service = DraftService()
|
||||
Loading…
Reference in New Issue
Block a user