claude-configs/skills/major-domo/cli_stats.py
Cal Corum 6201b4c9af Major Domo CLI: modular refactor + 6 new command modules
Refactored monolithic cli.py into modular architecture:
- cli_common.py: shared state, console, output helpers
- cli_transactions.py: list + simulate (compliance checker)
- cli_injuries.py: injury listing with team/active filters
- cli_stats.py: batting/pitching leaderboards
- cli_results.py: game results
- cli_schedule.py: game schedules

Also: team get now shows salary_cap, SKILL.md fully updated
with CLI docs, flag ordering warning, and compliance workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 11:24:18 -06:00

279 lines
8.0 KiB
Python

#!/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)