- Refactor major-domo skill: api_client.py, cli.py, and CLI modules (admin, common, injuries, results, schedule, transactions) with significant simplification (-275 lines net) - Update CLI_REFERENCE.md and SKILL.md docs for major-domo - Update create-scheduled-task SKILL.md - Update plugins blocklist.json and known_marketplaces.json - Add patterns/ directory to repo - Update CLAUDE.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
249 lines
7.6 KiB
Python
249 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Major Domo CLI - Admin Operations
|
|
|
|
Privileged operations: standings recalculation, stats refresh.
|
|
Each command performs the write operation then fetches fresh data to confirm.
|
|
"""
|
|
|
|
from typing import Annotated, Optional
|
|
|
|
import typer
|
|
|
|
from cli_common import (
|
|
console,
|
|
state,
|
|
output_json,
|
|
output_table,
|
|
handle_error,
|
|
get_season,
|
|
safe_nested,
|
|
)
|
|
from cli_stats import _format_rate_stat, _outs_to_ip
|
|
|
|
admin_app = typer.Typer(help="Admin operations (standings recalculate, stats refresh)")
|
|
|
|
|
|
def _show_standings(season: int):
|
|
"""Fetch and display standings after recalculation"""
|
|
standings_data = state.api.get_standings(season=season)
|
|
if not standings_data:
|
|
console.print(f"[yellow]No standings found for season {season}[/yellow]")
|
|
return
|
|
|
|
rows = []
|
|
for s in standings_data:
|
|
abbrev = safe_nested(s, "team", "abbrev")
|
|
name = safe_nested(s, "team", "lname")
|
|
wins = s.get("wins", 0)
|
|
losses = s.get("losses", 0)
|
|
total = wins + losses
|
|
pct = f".{int(wins / total * 1000):03d}" if total > 0 else ".000"
|
|
rd = s.get("run_diff", 0)
|
|
rd_str = f"+{rd}" if rd > 0 else str(rd)
|
|
rows.append([abbrev, name, wins, losses, pct, rd_str])
|
|
|
|
output_table(
|
|
f"Standings - Season {season} (Recalculated)",
|
|
["Team", "Name", "W", "L", "PCT", "RD"],
|
|
rows,
|
|
)
|
|
|
|
|
|
def _show_batting_preview(season: int, count: int):
|
|
"""Fetch and display top 5 batting leaders after refresh"""
|
|
stats = state.api.get_season_batting_stats(
|
|
season=season, sort_by="woba", sort_order="desc", limit=5, min_pa=50
|
|
)
|
|
if not stats:
|
|
return
|
|
|
|
rows = []
|
|
for rank, s in enumerate(stats, 1):
|
|
rows.append(
|
|
[
|
|
rank,
|
|
s.get("name", "N/A"),
|
|
s.get("player_team_abbrev", "N/A"),
|
|
s.get("pa", 0),
|
|
_format_rate_stat(s.get("avg")),
|
|
_format_rate_stat(s.get("obp")),
|
|
_format_rate_stat(s.get("slg")),
|
|
_format_rate_stat(s.get("woba")),
|
|
]
|
|
)
|
|
|
|
console.print()
|
|
output_table(
|
|
f"Top 5 by wOBA (min 50 PA) - {count} players refreshed",
|
|
["#", "Name", "Team", "PA", "AVG", "OBP", "SLG", "wOBA"],
|
|
rows,
|
|
)
|
|
|
|
|
|
def _show_pitching_preview(season: int, count: int):
|
|
"""Fetch and display top 5 pitching leaders after refresh"""
|
|
stats = state.api.get_season_pitching_stats(
|
|
season=season, sort_by="era", sort_order="asc", limit=5, min_outs=50
|
|
)
|
|
if not stats:
|
|
return
|
|
|
|
rows = []
|
|
for rank, s in enumerate(stats, 1):
|
|
rows.append(
|
|
[
|
|
rank,
|
|
s.get("name", "N/A"),
|
|
s.get("player_team_abbrev", "N/A"),
|
|
_outs_to_ip(s.get("outs", 0)),
|
|
_format_rate_stat(s.get("era"), decimals=2),
|
|
_format_rate_stat(s.get("whip"), decimals=2),
|
|
s.get("so", 0),
|
|
]
|
|
)
|
|
|
|
console.print()
|
|
output_table(
|
|
f"Top 5 by ERA (min 50 outs) - {count} players refreshed",
|
|
["#", "Name", "Team", "IP", "ERA", "WHIP", "SO"],
|
|
rows,
|
|
)
|
|
|
|
|
|
@admin_app.command("recalculate-standings")
|
|
def recalculate_standings(
|
|
season: Annotated[
|
|
Optional[int], typer.Option("--season", "-s", help="Season number")
|
|
] = None,
|
|
):
|
|
"""Recalculate standings from game results for a season"""
|
|
try:
|
|
season = get_season(season)
|
|
|
|
console.print(
|
|
f"[yellow]Recalculating standings for season {season}...[/yellow]"
|
|
)
|
|
result = state.api.recalculate_standings(season)
|
|
console.print(f"[green]Done:[/green] {result}")
|
|
|
|
if state.json_output:
|
|
standings_data = state.api.get_standings(season=season)
|
|
output_json(
|
|
{"message": result, "season": season, "standings": standings_data}
|
|
)
|
|
return
|
|
|
|
_show_standings(season)
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
@admin_app.command("refresh-batting")
|
|
def refresh_batting(
|
|
season: Annotated[
|
|
Optional[int], typer.Option("--season", "-s", help="Season number")
|
|
] = None,
|
|
):
|
|
"""Refresh season batting stats materialized view"""
|
|
try:
|
|
season = get_season(season)
|
|
|
|
console.print(
|
|
f"[yellow]Refreshing batting stats for season {season}...[/yellow]"
|
|
)
|
|
result = state.api.refresh_batting_stats(season)
|
|
players = result.get("players_updated", 0)
|
|
console.print(
|
|
f"[green]Done:[/green] {result.get('message', 'Batting stats refreshed')} "
|
|
f"({players} players updated)"
|
|
)
|
|
|
|
if state.json_output:
|
|
top_stats = state.api.get_season_batting_stats(
|
|
season=season, sort_by="woba", sort_order="desc", limit=5, min_pa=50
|
|
)
|
|
result["top_5_woba"] = top_stats
|
|
output_json(result)
|
|
return
|
|
|
|
_show_batting_preview(season, players)
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
@admin_app.command("refresh-pitching")
|
|
def refresh_pitching(
|
|
season: Annotated[
|
|
Optional[int], typer.Option("--season", "-s", help="Season number")
|
|
] = None,
|
|
):
|
|
"""Refresh season pitching stats materialized view"""
|
|
try:
|
|
season = get_season(season)
|
|
|
|
console.print(
|
|
f"[yellow]Refreshing pitching stats for season {season}...[/yellow]"
|
|
)
|
|
result = state.api.refresh_pitching_stats(season)
|
|
players = result.get("players_updated", 0)
|
|
console.print(
|
|
f"[green]Done:[/green] {result.get('message', 'Pitching stats refreshed')} "
|
|
f"({players} players updated)"
|
|
)
|
|
|
|
if state.json_output:
|
|
top_stats = state.api.get_season_pitching_stats(
|
|
season=season, sort_by="era", sort_order="asc", limit=5, min_outs=50
|
|
)
|
|
result["top_5_era"] = top_stats
|
|
output_json(result)
|
|
return
|
|
|
|
_show_pitching_preview(season, players)
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
@admin_app.command("refresh-stats")
|
|
def refresh_stats(
|
|
season: Annotated[
|
|
Optional[int], typer.Option("--season", "-s", help="Season number")
|
|
] = None,
|
|
):
|
|
"""Refresh both batting and pitching stats materialized views"""
|
|
try:
|
|
season = get_season(season)
|
|
|
|
console.print(f"[yellow]Refreshing all stats for season {season}...[/yellow]")
|
|
|
|
batting_result = state.api.refresh_batting_stats(season)
|
|
bat_count = batting_result.get("players_updated", 0)
|
|
console.print(f" Batting: {bat_count} players refreshed")
|
|
|
|
pitching_result = state.api.refresh_pitching_stats(season)
|
|
pit_count = pitching_result.get("players_updated", 0)
|
|
console.print(f" Pitching: {pit_count} players refreshed")
|
|
|
|
console.print(f"[green]Done![/green]")
|
|
|
|
if state.json_output:
|
|
top_batting = state.api.get_season_batting_stats(
|
|
season=season, sort_by="woba", sort_order="desc", limit=5, min_pa=50
|
|
)
|
|
top_pitching = state.api.get_season_pitching_stats(
|
|
season=season, sort_by="era", sort_order="asc", limit=5, min_outs=50
|
|
)
|
|
output_json(
|
|
{
|
|
"batting": {**batting_result, "top_5_woba": top_batting},
|
|
"pitching": {**pitching_result, "top_5_era": top_pitching},
|
|
}
|
|
)
|
|
return
|
|
|
|
_show_batting_preview(season, bat_count)
|
|
_show_pitching_preview(season, pit_count)
|
|
except Exception as e:
|
|
handle_error(e)
|