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>
279 lines
8.0 KiB
Python
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)
|