#!/usr/bin/env python3 """ Major Domo CLI - Statistics Operations This module provides batting and pitching statistics leaderboard commands. """ from typing import Annotated, Optional import typer from cli_common import ( console, state, output_json, output_table, handle_error, get_season, ) stats_app = typer.Typer(help="Season statistics") def _format_rate_stat(value: Optional[float], decimals: int = 3) -> str: """Format a rate stat like AVG/OBP/SLG with consistent decimal places""" if value is None: return "---" return f"{value:.{decimals}f}" def _outs_to_ip(outs: int) -> str: """Convert outs pitched to IP display format (e.g., 450 outs = '150.0')""" innings = outs // 3 partial = outs % 3 return f"{innings}.{partial}" @stats_app.command("batting") def stats_batting( sort: Annotated[ str, typer.Option( "--sort", "-S", help="Sort field (woba, avg, obp, slg, ops, homerun, rbi, run, etc.)", ), ] = "woba", min_pa: Annotated[ Optional[int], typer.Option("--min-pa", help="Minimum plate appearances (default: None)"), ] = None, team: Annotated[ Optional[str], typer.Option("--team", "-t", help="Filter by team abbreviation"), ] = None, limit: Annotated[ int, typer.Option("--limit", "-n", help="Maximum results to display") ] = 25, season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, order: Annotated[ str, typer.Option("--order", help="Sort order (asc or desc)"), ] = "desc", ): """Batting statistics leaderboard""" try: season = get_season(season) # Resolve team abbreviation to team ID if provided team_id = None team_abbrev = None if team: team_abbrev = team.upper() try: team_obj = state.api.get_team(abbrev=team_abbrev, season=season) team_id = team_obj["id"] except Exception: console.print( f"[red]Error:[/red] Team '{team_abbrev}' not found in season {season}" ) raise typer.Exit(1) # Fetch batting stats stats = state.api.get_season_batting_stats( season=season, team_id=team_id, min_pa=min_pa, sort_by=sort, sort_order=order, limit=limit, ) if state.json_output: output_json(stats) return if not stats: filter_parts = [] if team_abbrev: filter_parts.append(f"team {team_abbrev}") if min_pa: filter_parts.append(f"min {min_pa} PA") filter_str = " (" + ", ".join(filter_parts) + ")" if filter_parts else "" console.print( f"[yellow]No batting stats found for season {season}{filter_str}[/yellow]" ) return # Format table rows rows = [] for rank, stat in enumerate(stats, start=1): rows.append( [ rank, stat.get("name", "N/A"), stat.get("player_team_abbrev", "N/A"), stat.get("pa", 0), _format_rate_stat(stat.get("avg")), _format_rate_stat(stat.get("obp")), _format_rate_stat(stat.get("slg")), _format_rate_stat(stat.get("ops")), _format_rate_stat(stat.get("woba")), stat.get("homerun", 0), stat.get("rbi", 0), ] ) # Build title title = f"Batting Leaders - Season {season}" if team_abbrev: title += f" ({team_abbrev})" if min_pa: title += f" (Min {min_pa} PA)" title += f" - Sorted by {sort.upper()} ({order})" output_table( title, [ "#", "Name", "Team", "PA", "AVG", "OBP", "SLG", "OPS", "wOBA", "HR", "RBI", ], rows, ) except typer.Exit: raise except Exception as e: handle_error(e) @stats_app.command("pitching") def stats_pitching( sort: Annotated[ str, typer.Option( "--sort", "-S", help="Sort field (era, whip, so, win, saves, etc.)" ), ] = "era", min_outs: Annotated[ Optional[int], typer.Option("--min-outs", help="Minimum outs pitched (default: None)"), ] = None, team: Annotated[ Optional[str], typer.Option("--team", "-t", help="Filter by team abbreviation"), ] = None, limit: Annotated[ int, typer.Option("--limit", "-n", help="Maximum results to display") ] = 25, season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, order: Annotated[ str, typer.Option("--order", help="Sort order (asc or desc)"), ] = "asc", ): """Pitching statistics leaderboard""" try: season = get_season(season) # Resolve team abbreviation to team ID if provided team_id = None team_abbrev = None if team: team_abbrev = team.upper() try: team_obj = state.api.get_team(abbrev=team_abbrev, season=season) team_id = team_obj["id"] except Exception: console.print( f"[red]Error:[/red] Team '{team_abbrev}' not found in season {season}" ) raise typer.Exit(1) # Fetch pitching stats stats = state.api.get_season_pitching_stats( season=season, team_id=team_id, min_outs=min_outs, sort_by=sort, sort_order=order, limit=limit, ) if state.json_output: output_json(stats) return if not stats: filter_parts = [] if team_abbrev: filter_parts.append(f"team {team_abbrev}") if min_outs: filter_parts.append(f"min {min_outs} outs") filter_str = " (" + ", ".join(filter_parts) + ")" if filter_parts else "" console.print( f"[yellow]No pitching stats found for season {season}{filter_str}[/yellow]" ) return # Format table rows rows = [] for rank, stat in enumerate(stats, start=1): outs = stat.get("outs", 0) rows.append( [ rank, stat.get("name", "N/A"), stat.get("player_team_abbrev", "N/A"), _outs_to_ip(outs), _format_rate_stat(stat.get("era"), decimals=2), _format_rate_stat(stat.get("whip"), decimals=2), stat.get("win", 0), stat.get("loss", 0), stat.get("saves", 0), stat.get("so", 0), stat.get("bb", 0), ] ) # Build title title = f"Pitching Leaders - Season {season}" if team_abbrev: title += f" ({team_abbrev})" if min_outs: title += f" (Min {min_outs} outs)" title += f" - Sorted by {sort.upper()} ({order})" output_table( title, ["#", "Name", "Team", "IP", "ERA", "WHIP", "W", "L", "SV", "SO", "BB"], rows, ) except typer.Exit: raise except Exception as e: handle_error(e) # Make batting the default command when running 'majordomo stats' @stats_app.callback(invoke_without_command=True) def stats_default(ctx: typer.Context): """Default to batting command if no subcommand specified""" if ctx.invoked_subcommand is None: ctx.invoke(stats_batting)