Implements full Google Sheets scorecard submission with: - Complete game data extraction (68 play fields, pitching decisions, box score) - Transaction rollback support at 3 states (plays/game/complete) - Duplicate game detection with confirmation dialog - Permission-based submission (GMs only) - Automated results posting to news channel - Automatic standings recalculation - Key plays display with WPA sorting New Components: - Play, Decision, Game models with full validation - SheetsService for Google Sheets integration - GameService, PlayService, DecisionService for data management - ConfirmationView for user confirmations - Discord helper utilities for channel operations Services Enhanced: - StandingsService: Added recalculate_standings() method - CustomCommandsService: Fixed creator endpoint path - Team/Player models: Added helper methods for display Configuration: - Added SHEETS_CREDENTIALS_PATH environment variable - Added SBA_NETWORK_NEWS_CHANNEL and role constants - Enabled pygsheets dependency Documentation: - Comprehensive README updates across all modules - Added command, service, model, and view documentation - Detailed workflow and error handling documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| base_service.py | ||
| chart_service.py | ||
| custom_commands_service.py | ||
| decision_service.py | ||
| game_service.py | ||
| help_commands_service.py | ||
| league_service.py | ||
| play_service.py | ||
| player_service.py | ||
| README.md | ||
| roster_service.py | ||
| schedule_service.py | ||
| sheets_service.py | ||
| standings_service.py | ||
| stats_service.py | ||
| team_service.py | ||
| trade_builder.py | ||
| transaction_builder.py | ||
| transaction_service.py | ||
Services Directory
The services directory contains the service layer for Discord Bot v2.0, providing clean abstractions for API interactions and business logic. All services inherit from BaseService and follow consistent patterns for data operations.
Architecture
Service Layer Pattern
Services act as the interface between Discord commands and the external API, providing:
- Data validation using Pydantic models
- Error handling with consistent exception patterns
- Caching support via Redis decorators
- Type safety with generic TypeVar support
- Logging integration with structured logging
Base Service (base_service.py)
The foundation for all services, providing:
- Generic CRUD operations (Create, Read, Update, Delete)
- API client management with connection pooling
- Response format handling for API responses
- Cache key generation and management
- Error handling with APIException wrapping
class BaseService(Generic[T]):
def __init__(self, model_class: Type[T], endpoint: str)
async def get_by_id(self, object_id: int) -> Optional[T]
async def get_all(self, params: Optional[List[tuple]] = None) -> Tuple[List[T], int]
async def create(self, model_data: Dict[str, Any]) -> Optional[T]
async def update(self, object_id: int, model_data: Dict[str, Any]) -> Optional[T]
async def patch(self, object_id: int, model_data: Dict[str, Any], use_query_params: bool = False) -> Optional[T]
async def delete(self, object_id: int) -> bool
PATCH vs PUT Operations:
update()uses HTTP PUT for full resource replacementpatch()uses HTTP PATCH for partial updatesuse_query_params=Truesends data as URL query parameters instead of JSON body
When to use use_query_params=True:
Some API endpoints (notably the player PATCH endpoint) expect data as query parameters instead of JSON body. Example:
# Standard PATCH with JSON body
await base_service.patch(object_id, {"field": "value"})
# → PATCH /api/v3/endpoint/{id} with JSON: {"field": "value"}
# PATCH with query parameters
await base_service.patch(object_id, {"field": "value"}, use_query_params=True)
# → PATCH /api/v3/endpoint/{id}?field=value
Service Files
Core Entity Services
player_service.py- Player data operations and search functionalityteam_service.py- Team information and roster managementleague_service.py- League-wide data and current season infostandings_service.py- Team standings and division rankingsschedule_service.py- Game scheduling and resultsstats_service.py- Player statistics (batting, pitching, fielding)roster_service.py- Team roster composition and position assignments
TeamService Key Methods
The TeamService provides team data operations with specific method names:
class TeamService(BaseService[Team]):
async def get_team(team_id: int) -> Optional[Team] # ✅ Correct method name
async def get_teams_by_owner(owner_id: int, season: Optional[int], roster_type: Optional[str]) -> List[Team]
async def get_team_by_abbrev(abbrev: str, season: Optional[int]) -> Optional[Team]
async def get_teams_by_season(season: int) -> List[Team]
async def get_team_roster(team_id: int, roster_type: str = 'current') -> Optional[Dict[str, Any]]
⚠️ Common Mistake (Fixed January 2025):
- Incorrect:
team_service.get_team_by_id(team_id)❌ (method does not exist) - Correct:
team_service.get_team(team_id)✅
This naming inconsistency was fixed in services/trade_builder.py line 201 and corresponding test mocks.
Transaction Services
transaction_service.py- Player transaction operations (trades, waivers, etc.)transaction_builder.py- Complex transaction building and validation
Game Submission Services (NEW - January 2025)
game_service.py- Game CRUD operations and scorecard submission supportplay_service.py- Play-by-play data management for game submissionsdecision_service.py- Pitching decision operations for game resultssheets_service.py- Google Sheets integration for scorecard reading
GameService Key Methods
class GameService(BaseService[Game]):
async def find_duplicate_game(season: int, week: int, game_num: int,
away_team_id: int, home_team_id: int) -> Optional[Game]
async def find_scheduled_game(season: int, week: int,
away_team_id: int, home_team_id: int) -> Optional[Game]
async def wipe_game_data(game_id: int) -> bool # Transaction rollback support
async def update_game_result(game_id: int, away_score: int, home_score: int,
away_manager_id: int, home_manager_id: int,
game_num: int, scorecard_url: str) -> Game
PlayService Key Methods
class PlayService:
async def create_plays_batch(plays: List[Dict[str, Any]]) -> bool
async def delete_plays_for_game(game_id: int) -> bool # Transaction rollback
async def get_top_plays_by_wpa(game_id: int, limit: int = 3) -> List[Play]
DecisionService Key Methods
class DecisionService:
async def create_decisions_batch(decisions: List[Dict[str, Any]]) -> bool
async def delete_decisions_for_game(game_id: int) -> bool # Transaction rollback
def find_winning_losing_pitchers(decisions_data: List[Dict[str, Any]])
-> Tuple[Optional[int], Optional[int], Optional[int], List[int], List[int]]
SheetsService Key Methods
class SheetsService:
async def open_scorecard(sheet_url: str) -> pygsheets.Spreadsheet
async def read_setup_data(scorecard: pygsheets.Spreadsheet) -> Dict[str, Any]
async def read_playtable_data(scorecard: pygsheets.Spreadsheet) -> List[Dict[str, Any]]
async def read_pitching_decisions(scorecard: pygsheets.Spreadsheet) -> List[Dict[str, Any]]
async def read_box_score(scorecard: pygsheets.Spreadsheet) -> Dict[str, List[int]]
Transaction Rollback Pattern: The game submission services implement a 3-state transaction rollback pattern:
- PLAYS_POSTED: Plays submitted → Rollback: Delete plays
- GAME_PATCHED: Game updated → Rollback: Wipe game + Delete plays
- COMPLETE: All data committed → No rollback needed
Usage Example:
# Create plays (state: PLAYS_POSTED)
await play_service.create_plays_batch(plays_data)
rollback_state = "PLAYS_POSTED"
try:
# Update game (state: GAME_PATCHED)
await game_service.update_game_result(game_id, ...)
rollback_state = "GAME_PATCHED"
# Create decisions (state: COMPLETE)
await decision_service.create_decisions_batch(decisions_data)
rollback_state = "COMPLETE"
except APIException as e:
# Rollback based on current state
if rollback_state == "GAME_PATCHED":
await game_service.wipe_game_data(game_id)
await play_service.delete_plays_for_game(game_id)
elif rollback_state == "PLAYS_POSTED":
await play_service.delete_plays_for_game(game_id)
Custom Features
custom_commands_service.py- User-created custom Discord commandshelp_commands_service.py- Admin-managed help system and documentation
Caching Integration
Services support optional Redis caching via decorators:
from utils.decorators import cached_api_call, cached_single_item
class PlayerService(BaseService[Player]):
@cached_api_call(ttl=600) # Cache for 10 minutes
async def get_players_by_team(self, team_id: int, season: int) -> List[Player]:
return await self.get_all_items(params=[('team_id', team_id), ('season', season)])
@cached_single_item(ttl=300) # Cache for 5 minutes
async def get_player_by_name(self, name: str) -> Optional[Player]:
players = await self.get_by_field('name', name)
return players[0] if players else None
Caching Features
- Graceful degradation - Works without Redis
- Automatic key generation based on method parameters
- TTL support with configurable expiration
- Cache invalidation patterns for data updates
Error Handling
All services use consistent error handling:
try:
result = await some_service.get_data()
return result
except APIException as e:
logger.error("API error occurred", error=e)
raise # Re-raise for command handlers
except Exception as e:
logger.error("Unexpected error", error=e)
raise APIException(f"Service operation failed: {e}")
Exception Types
APIException- API communication errorsValueError- Data validation errorsConnectionError- Network connectivity issues
Usage Patterns
Service Initialization
Services are typically initialized once and reused:
# In services/__init__.py
from .player_service import PlayerService
from models.player import Player
player_service = PlayerService(Player, 'players')
Command Integration
Services integrate with Discord commands via the @logged_command decorator:
@discord.app_commands.command(name="player")
@logged_command("/player")
async def player_info(self, interaction: discord.Interaction, name: str):
player = await player_service.get_player_by_name(name)
if not player:
await interaction.followup.send("Player not found")
return
embed = create_player_embed(player)
await interaction.followup.send(embed=embed)
API Response Format
Services handle the standard API response format:
{
"count": 150,
"players": [
{"id": 1, "name": "Player Name", ...},
{"id": 2, "name": "Another Player", ...}
]
}
The BaseService._extract_items_and_count_from_response() method automatically parses this format and returns typed model instances.
Development Guidelines
Adding New Services
- Inherit from BaseService with appropriate model type
- Define specific business methods beyond CRUD operations
- Add caching decorators for expensive operations
- Include comprehensive logging with structured context
- Handle edge cases and provide meaningful error messages
Service Method Patterns
- Query methods should return
List[T]orOptional[T] - Mutation methods should return the updated model or
None - Search methods should accept flexible parameters
- Bulk operations should handle batching efficiently
Testing Services
- Use
aioresponsesfor HTTP client mocking - Test both success and error scenarios
- Validate model parsing and transformation
- Verify caching behavior when Redis is available
Environment Integration
Services respect environment configuration:
DB_URL- Database API endpointAPI_TOKEN- Authentication tokenREDIS_URL- Optional caching backendLOG_LEVEL- Logging verbosity
Performance Considerations
Optimization Strategies
- Connection pooling via global API client
- Response caching for frequently accessed data
- Batch operations for bulk data processing
- Lazy loading for expensive computations
Monitoring
- All operations are logged with timing information
- Cache hit/miss ratios are tracked
- API error rates are monitored
- Service response times are measured
Transaction Builder Enhancements (January 2025)
Enhanced sWAR Calculations
The TransactionBuilder now includes comprehensive sWAR (sum of WARA) tracking for both current moves and pre-existing transactions:
class TransactionBuilder:
async def validate_transaction(self, next_week: Optional[int] = None) -> RosterValidationResult:
"""
Validate transaction with optional pre-existing transaction analysis.
Args:
next_week: Week to check for existing transactions (includes pre-existing analysis)
Returns:
RosterValidationResult with projected roster counts and sWAR values
"""
Pre-existing Transaction Support
When next_week is provided, the transaction builder:
- Fetches existing transactions for the specified week via API
- Calculates roster impact of scheduled moves using organizational team matching
- Tracks sWAR changes separately for Major League and Minor League rosters
- Provides contextual display for user transparency
Usage Examples
# Basic validation (current functionality)
validation = await builder.validate_transaction()
# Enhanced validation with pre-existing transactions
current_week = await league_service.get_current_week()
validation = await builder.validate_transaction(next_week=current_week + 1)
# Access enhanced data
print(f"Projected ML sWAR: {validation.major_league_swar}")
print(f"Pre-existing impact: {validation.pre_existing_transactions_note}")
Enhanced RosterValidationResult
New fields provide complete transaction context:
@dataclass
class RosterValidationResult:
# Existing fields...
major_league_swar: float = 0.0
minor_league_swar: float = 0.0
pre_existing_ml_swar_change: float = 0.0
pre_existing_mil_swar_change: float = 0.0
pre_existing_transaction_count: int = 0
@property
def major_league_swar_status(self) -> str:
"""Formatted sWAR display with emoji."""
@property
def pre_existing_transactions_note(self) -> str:
"""User-friendly note about pre-existing moves impact."""
Organizational Team Matching
Transaction processing now uses sophisticated team matching:
# Enhanced logic using Team.is_same_organization()
if transaction.oldteam.is_same_organization(self.team):
# Accurately determine which roster the player is leaving
from_roster_type = transaction.oldteam.roster_type()
if from_roster_type == RosterType.MAJOR_LEAGUE:
# Update ML roster and sWAR
elif from_roster_type == RosterType.MINOR_LEAGUE:
# Update MiL roster and sWAR
Key Improvements
- Accurate Roster Detection: Uses
Team.roster_type()instead of assumptions - Organization Awareness: Properly handles PORMIL, PORIL transactions for POR team
- Separate sWAR Tracking: ML and MiL sWAR changes tracked independently
- Performance Optimization: Pre-existing transactions loaded once and cached
- User Transparency: Clear display of how pre-existing moves affect calculations
Implementation Details
- Backwards Compatible: All existing functionality preserved
- Optional Enhancement:
next_weekparameter is optional - Error Handling: Graceful fallback if pre-existing transactions cannot be loaded
- Caching: Transaction and roster data cached to avoid repeated API calls
Next Steps for AI Agents:
- Review existing service implementations for patterns
- Check the corresponding model definitions in
/models - Understand the caching decorators in
/utils/decorators.py - Follow the error handling patterns established in
BaseService - Use structured logging with contextual information
- Consider pre-existing transaction impact when building new transaction features