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>
This commit is contained in:
parent
a4ae9774ad
commit
6201b4c9af
@ -491,10 +491,13 @@ This skill includes structured workflows for common tasks:
|
||||
**Team Analysis**:
|
||||
```
|
||||
"Compare team rosters"
|
||||
→ Get rosters for multiple teams and analyze
|
||||
→ cli.py team roster ABBREV (for each team)
|
||||
|
||||
"Show team's best players by WARA"
|
||||
→ api.list_players(season=12, team_id=X, sort='cost-desc')
|
||||
→ cli.py team roster ABBREV (sorted by WARA in output)
|
||||
|
||||
"Check if transactions are compliant"
|
||||
→ See "Transaction Compliance Check Workflow" in CLI section
|
||||
|
||||
"Analyze team transaction history"
|
||||
→ api.get_transactions(season=12, team_abbrev=X)
|
||||
@ -631,9 +634,14 @@ npm run type-check # Type checking only
|
||||
```
|
||||
major-domo/
|
||||
├── SKILL.md (this file)
|
||||
├── CLAUDE.md (detailed context - future)
|
||||
├── api_client.py (shared API client)
|
||||
├── cli.py (CLI application - alias: majordomo)
|
||||
├── cli.py (main CLI - mounts sub-apps, core commands)
|
||||
├── cli_common.py (shared state, console, output helpers)
|
||||
├── cli_transactions.py (transactions list + simulate)
|
||||
├── cli_injuries.py (injury listing)
|
||||
├── cli_stats.py (batting/pitching leaderboards)
|
||||
├── cli_results.py (game results)
|
||||
├── cli_schedule.py (game schedules)
|
||||
├── workflows/
|
||||
│ ├── bot-deployment.md
|
||||
│ ├── weekly-stats-update.md
|
||||
@ -715,30 +723,146 @@ majordomo team roster CAR # Team roster breakdown
|
||||
|
||||
# Standings
|
||||
majordomo standings [--division ALE] # League standings
|
||||
|
||||
# Transactions
|
||||
majordomo transactions --team CLS --week 9 # Team transactions for week
|
||||
majordomo transactions list --week-start 1 --week-end 4 # Week range
|
||||
majordomo transactions --player "Mike Trout" # Player-specific transactions
|
||||
majordomo transactions simulate CLS "Stott:CLSMiL,Walker:CLS,Castellanos:FA,Martin:CLS" # Check compliance
|
||||
|
||||
# Injuries
|
||||
majordomo injuries list --team CLS --active # Active injuries for team
|
||||
majordomo injuries list --sort return-asc # Sort by return date
|
||||
|
||||
# Statistics (Season Leaders)
|
||||
majordomo stats batting --sort woba --min-pa 100 --limit 25 # Batting leaders
|
||||
majordomo stats pitching --sort era --min-outs 100 --limit 25 # Pitching leaders
|
||||
majordomo stats batting --team CLS # Team batting stats
|
||||
|
||||
# Game Results
|
||||
majordomo results --team CLS --week 9 # Team results for week
|
||||
majordomo results list --week-start 1 --week-end 4 # Results for week range
|
||||
|
||||
# Game Schedule
|
||||
majordomo schedule --team CLS --week 10 # Team schedule for week
|
||||
majordomo schedule list --week-start 10 --week-end 12 # Schedule for week range
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
**⚠️ IMPORTANT**: `--env`, `--json`, `--verbose` are **top-level flags** that go BEFORE the subcommand.
|
||||
`--season` is a **subcommand flag** that goes AFTER the subcommand.
|
||||
|
||||
```bash
|
||||
# Top-level flags (BEFORE subcommand)
|
||||
--env prod|dev # Environment (default: prod)
|
||||
--json # Output as JSON
|
||||
--json # Output as JSON (shows all fields including salary_cap)
|
||||
--verbose / -v # Show API request details
|
||||
|
||||
# Subcommand flags (AFTER subcommand)
|
||||
--season / -s N # Specify season (defaults to current)
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
```bash
|
||||
python3 ~/.claude/skills/major-domo/cli.py --json team get CLS
|
||||
python3 ~/.claude/skills/major-domo/cli.py --env dev team roster CAN
|
||||
```
|
||||
|
||||
**Wrong** (will error):
|
||||
```bash
|
||||
python3 ~/.claude/skills/major-domo/cli.py team get CLS --json # ❌
|
||||
python3 ~/.claude/skills/major-domo/cli.py team roster CAN --env prod # ❌
|
||||
```
|
||||
|
||||
### Key Notes
|
||||
|
||||
- `team get` shows salary_cap in formatted output; use `--json` for all fields
|
||||
- `team roster` shows Active/Short IL/Long IL with WARA values for Active only
|
||||
- "Long IL" = MiL (minor leagues)
|
||||
- For individual player lookups, use `player get "Name"` — avoid bulk API queries (`/players?team_id=X`) which can timeout on large rosters
|
||||
- **Prefer CLI over direct API client** for all standard operations
|
||||
- `transactions simulate` validates compliance without making changes — check roster count, WARA limits, salary cap
|
||||
- `stats` commands support standard baseball stats sorting (woba, obp, slg, era, whip, fip, etc.)
|
||||
- `injuries list` shows return_date, weeks_out, and injury_type for all injured players
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# What we commonly do for roster moves:
|
||||
majordomo player move --batch "Gerrit Cole:CAN,Robert Suarez:CANMiL"
|
||||
# Roster moves
|
||||
python3 ~/.claude/skills/major-domo/cli.py player move --batch "Gerrit Cole:CAN,Robert Suarez:CANMiL"
|
||||
|
||||
# Check standings
|
||||
majordomo standings --division ALE
|
||||
python3 ~/.claude/skills/major-domo/cli.py standings --division ALE
|
||||
|
||||
# Get team roster
|
||||
majordomo team roster CAN
|
||||
python3 ~/.claude/skills/major-domo/cli.py team roster CAN
|
||||
|
||||
# Get team salary cap (need --json)
|
||||
python3 ~/.claude/skills/major-domo/cli.py --json team get CAN
|
||||
|
||||
# Check transaction compliance (dry run)
|
||||
python3 ~/.claude/skills/major-domo/cli.py transactions simulate CLS "Stott:CLSMiL,Walker:CLS,Castellanos:FA,Martin:CLS"
|
||||
|
||||
# View recent transactions
|
||||
python3 ~/.claude/skills/major-domo/cli.py transactions --team CLS --week 9
|
||||
python3 ~/.claude/skills/major-domo/cli.py transactions list --week-start 1 --week-end 4
|
||||
|
||||
# Check injuries
|
||||
python3 ~/.claude/skills/major-domo/cli.py injuries list --active
|
||||
python3 ~/.claude/skills/major-domo/cli.py injuries list --team CLS
|
||||
|
||||
# Season batting leaders
|
||||
python3 ~/.claude/skills/major-domo/cli.py stats batting --sort woba --min-pa 100 --limit 25
|
||||
python3 ~/.claude/skills/major-domo/cli.py stats batting --team CLS
|
||||
|
||||
# Season pitching leaders
|
||||
python3 ~/.claude/skills/major-domo/cli.py stats pitching --sort era --min-outs 100 --limit 25
|
||||
python3 ~/.claude/skills/major-domo/cli.py stats pitching --team CAN --sort k
|
||||
|
||||
# Game results and schedules
|
||||
python3 ~/.claude/skills/major-domo/cli.py results --team CLS --week 9
|
||||
python3 ~/.claude/skills/major-domo/cli.py schedule --team CLS --week 10
|
||||
python3 ~/.claude/skills/major-domo/cli.py schedule list --week-start 10 --week-end 12
|
||||
|
||||
# Practical workflows
|
||||
# 1. Check if proposed trades are compliant before executing
|
||||
python3 ~/.claude/skills/major-domo/cli.py transactions simulate CLS "Player1:CLS,Player2:CLSMiL"
|
||||
|
||||
# 2. Review team stats and identify weak positions
|
||||
python3 ~/.claude/skills/major-domo/cli.py team roster CLS
|
||||
python3 ~/.claude/skills/major-domo/cli.py stats batting --team CLS
|
||||
|
||||
# 3. Monitor weekly transactions across the league
|
||||
python3 ~/.claude/skills/major-domo/cli.py transactions list --week-start 8 --week-end 9
|
||||
|
||||
# 4. Check injury status before setting lineups
|
||||
python3 ~/.claude/skills/major-domo/cli.py injuries list --team CLS --active
|
||||
```
|
||||
|
||||
### Transaction Compliance Check Workflow
|
||||
|
||||
**Recommended approach** - Use the simulate command for automatic compliance validation:
|
||||
|
||||
```bash
|
||||
# Simulate proposed transactions (dry run - no changes made)
|
||||
python3 ~/.claude/skills/major-domo/cli.py transactions simulate CLS "Stott:CLSMiL,Walker:CLS,Castellanos:FA,Martin:CLS"
|
||||
```
|
||||
|
||||
The simulate command automatically validates:
|
||||
- Active roster count = 26
|
||||
- Total sWAR ≤ salary cap
|
||||
- All players exist and can be moved to target destinations
|
||||
- Returns detailed compliance report with before/after state
|
||||
|
||||
**Manual verification workflow** (if needed):
|
||||
|
||||
1. **Get current ML roster**: `cli.py team roster ABBREV` → note player count and WARA values
|
||||
2. **Get salary cap**: `cli.py --json team get ABBREV` → read `salary_cap` field
|
||||
3. **Look up incoming players**: `cli.py player get "Name"` for each player being added to ML
|
||||
4. **Calculate**: Current ML sWAR ± transaction changes ≤ salary_cap, and roster count = 26
|
||||
5. **Process moves**: `cli.py player move --batch "Name1:Team1,Name2:Team2"`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
Major Domo CLI - SBA League Management
|
||||
|
||||
A command-line interface for the Major Domo API, primarily for use with Claude Code.
|
||||
Modular architecture: each command group is a separate cli_*.py file.
|
||||
|
||||
Usage:
|
||||
majordomo status
|
||||
@ -13,24 +14,33 @@ Usage:
|
||||
majordomo team list
|
||||
majordomo team roster CAR
|
||||
majordomo standings --division ALE
|
||||
majordomo transactions --team CLS
|
||||
majordomo injuries --active
|
||||
majordomo stats batting --sort woba --min-pa 100
|
||||
|
||||
Environment:
|
||||
API_TOKEN: Required. Bearer token for API authentication.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Annotated, List, Optional
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
# Import the existing API client from same directory
|
||||
# Ensure skill directory is on path for imports
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from api_client import MajorDomoAPI
|
||||
from cli_common import (
|
||||
console,
|
||||
state,
|
||||
output_json,
|
||||
output_table,
|
||||
handle_error,
|
||||
get_season,
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# App Setup
|
||||
@ -47,66 +57,34 @@ team_app = typer.Typer(help="Team operations")
|
||||
app.add_typer(player_app, name="player")
|
||||
app.add_typer(team_app, name="team")
|
||||
|
||||
console = Console()
|
||||
# Import and mount sub-app modules
|
||||
from cli_transactions import transactions_app
|
||||
from cli_injuries import injuries_app
|
||||
from cli_stats import stats_app
|
||||
from cli_results import results_app
|
||||
from cli_schedule import schedule_app
|
||||
|
||||
|
||||
class State:
|
||||
"""Global state for API client and settings"""
|
||||
api: Optional[MajorDomoAPI] = None
|
||||
json_output: bool = False
|
||||
current_season: Optional[int] = None
|
||||
|
||||
|
||||
state = State()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Output Helpers
|
||||
# ============================================================================
|
||||
|
||||
def output_json(data):
|
||||
"""Output data as formatted JSON"""
|
||||
console.print_json(json.dumps(data, indent=2, default=str))
|
||||
|
||||
|
||||
def output_table(title: str, columns: List[str], rows: List[List], show_lines: bool = False):
|
||||
"""Output data as a rich table"""
|
||||
table = Table(title=title, show_header=True, header_style="bold cyan", show_lines=show_lines)
|
||||
for col in columns:
|
||||
table.add_column(col)
|
||||
for row in rows:
|
||||
table.add_row(*[str(cell) if cell is not None else "" for cell in row])
|
||||
console.print(table)
|
||||
|
||||
|
||||
def handle_error(e: Exception, context: str = ""):
|
||||
"""Graceful error handling with helpful messages"""
|
||||
error_str = str(e)
|
||||
if "401" in error_str:
|
||||
console.print("[red]Error:[/red] Unauthorized. Check your API_TOKEN.")
|
||||
elif "404" in error_str:
|
||||
console.print(f"[red]Error:[/red] Not found. {context}")
|
||||
elif "Connection" in error_str or "ConnectionError" in error_str:
|
||||
console.print("[red]Error:[/red] Cannot connect to API. Check network and --env setting.")
|
||||
else:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def get_season(season: Optional[int]) -> int:
|
||||
"""Get season, defaulting to current if not specified"""
|
||||
return season if season is not None else state.current_season
|
||||
app.add_typer(transactions_app, name="transactions")
|
||||
app.add_typer(injuries_app, name="injuries")
|
||||
app.add_typer(stats_app, name="stats")
|
||||
app.add_typer(results_app, name="results")
|
||||
app.add_typer(schedule_app, name="schedule")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Callback (Global Options)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main(
|
||||
env: Annotated[str, typer.Option("--env", help="Environment: prod or dev")] = "prod",
|
||||
env: Annotated[
|
||||
str, typer.Option("--env", help="Environment: prod or dev")
|
||||
] = "prod",
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
||||
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Verbose output")] = False,
|
||||
verbose: Annotated[
|
||||
bool, typer.Option("--verbose", "-v", help="Verbose output")
|
||||
] = False,
|
||||
):
|
||||
"""Major Domo SBA League Management CLI"""
|
||||
try:
|
||||
@ -114,7 +92,7 @@ def main(
|
||||
state.json_output = json_output
|
||||
# Cache current season
|
||||
current = state.api.get_current()
|
||||
state.current_season = current['season']
|
||||
state.current_season = current["season"]
|
||||
except ValueError as e:
|
||||
console.print(f"[red]Configuration Error:[/red] {e}")
|
||||
console.print("\nSet API_TOKEN environment variable:")
|
||||
@ -128,6 +106,7 @@ def main(
|
||||
# Status & Health Commands
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@app.command()
|
||||
def status():
|
||||
"""Show current season/week status"""
|
||||
@ -170,10 +149,13 @@ def health():
|
||||
# Player Commands
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@player_app.command("get")
|
||||
def player_get(
|
||||
name: Annotated[str, typer.Argument(help="Player name")],
|
||||
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Season number")] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
):
|
||||
"""Get player information by name"""
|
||||
try:
|
||||
@ -181,8 +163,10 @@ def player_get(
|
||||
player = state.api.get_player(name=name, season=season)
|
||||
|
||||
if not player:
|
||||
console.print(f"[yellow]Player '{name}' not found in season {season}[/yellow]")
|
||||
console.print("Try: majordomo player search \"partial name\"")
|
||||
console.print(
|
||||
f"[yellow]Player '{name}' not found in season {season}[/yellow]"
|
||||
)
|
||||
console.print('Try: majordomo player search "partial name"')
|
||||
raise typer.Exit(1)
|
||||
|
||||
if state.json_output:
|
||||
@ -190,11 +174,13 @@ def player_get(
|
||||
return
|
||||
|
||||
# Extract nested team info
|
||||
team = player.get('team', {})
|
||||
team_abbrev = team.get('abbrev', 'N/A') if isinstance(team, dict) else 'N/A'
|
||||
team = player.get("team", {})
|
||||
team_abbrev = team.get("abbrev", "N/A") if isinstance(team, dict) else "N/A"
|
||||
|
||||
# Collect positions
|
||||
positions = [player.get(f'pos_{i}') for i in range(1, 9) if player.get(f'pos_{i}')]
|
||||
positions = [
|
||||
player.get(f"pos_{i}") for i in range(1, 9) if player.get(f"pos_{i}")
|
||||
]
|
||||
|
||||
panel = Panel(
|
||||
f"[bold]ID:[/bold] {player['id']}\n"
|
||||
@ -217,7 +203,9 @@ def player_get(
|
||||
@player_app.command("search")
|
||||
def player_search(
|
||||
query: Annotated[str, typer.Argument(help="Search query (partial name)")],
|
||||
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Season number")] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
limit: Annotated[int, typer.Option("--limit", "-n", help="Max results")] = 10,
|
||||
):
|
||||
"""Search players by name (fuzzy match)"""
|
||||
@ -230,25 +218,29 @@ def player_search(
|
||||
return
|
||||
|
||||
if not players:
|
||||
console.print(f"[yellow]No players found matching '{query}' in season {season}[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]No players found matching '{query}' in season {season}[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
rows = []
|
||||
for p in players:
|
||||
team = p.get('team', {})
|
||||
team_abbrev = team.get('abbrev', 'N/A') if isinstance(team, dict) else 'N/A'
|
||||
rows.append([
|
||||
p['id'],
|
||||
p['name'],
|
||||
team_abbrev,
|
||||
p.get('pos_1', ''),
|
||||
f"{p.get('wara', 0):.2f}"
|
||||
])
|
||||
team = p.get("team", {})
|
||||
team_abbrev = team.get("abbrev", "N/A") if isinstance(team, dict) else "N/A"
|
||||
rows.append(
|
||||
[
|
||||
p["id"],
|
||||
p["name"],
|
||||
team_abbrev,
|
||||
p.get("pos_1", ""),
|
||||
f"{p.get('wara', 0):.2f}",
|
||||
]
|
||||
)
|
||||
|
||||
output_table(
|
||||
f"Search Results: '{query}' (Season {season})",
|
||||
["ID", "Name", "Team", "Pos", "WARA"],
|
||||
rows
|
||||
rows,
|
||||
)
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
@ -257,10 +249,19 @@ def player_search(
|
||||
@player_app.command("move")
|
||||
def player_move(
|
||||
name: Annotated[Optional[str], typer.Argument(help="Player name")] = None,
|
||||
team: Annotated[Optional[str], typer.Argument(help="Target team abbreviation")] = None,
|
||||
batch: Annotated[Optional[str], typer.Option("--batch", "-b", help="Batch moves: 'Name1:Team1,Name2:Team2'")] = None,
|
||||
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Season number")] = None,
|
||||
dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be done")] = False,
|
||||
team: Annotated[
|
||||
Optional[str], typer.Argument(help="Target team abbreviation")
|
||||
] = None,
|
||||
batch: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--batch", "-b", help="Batch moves: 'Name1:Team1,Name2:Team2'"),
|
||||
] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
dry_run: Annotated[
|
||||
bool, typer.Option("--dry-run", help="Show what would be done")
|
||||
] = False,
|
||||
):
|
||||
"""Move player(s) to a new team"""
|
||||
try:
|
||||
@ -282,10 +283,14 @@ def player_move(
|
||||
elif name and team:
|
||||
moves = [(name, team.upper())]
|
||||
else:
|
||||
console.print("[red]Error:[/red] Provide player name and team, or use --batch")
|
||||
console.print(
|
||||
"[red]Error:[/red] Provide player name and team, or use --batch"
|
||||
)
|
||||
console.print("\nUsage:")
|
||||
console.print(" majordomo player move \"Mike Trout\" CAR")
|
||||
console.print(" majordomo player move --batch \"Mike Trout:CAR,Aaron Judge:NYM\"")
|
||||
console.print(' majordomo player move "Mike Trout" CAR')
|
||||
console.print(
|
||||
' majordomo player move --batch "Mike Trout:CAR,Aaron Judge:NYM"'
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
results = []
|
||||
@ -294,35 +299,74 @@ def player_move(
|
||||
player = state.api.get_player(name=player_name, season=season)
|
||||
if not player:
|
||||
# Try search
|
||||
search_results = state.api.search_players(query=player_name, season=season, limit=1)
|
||||
search_results = state.api.search_players(
|
||||
query=player_name, season=season, limit=1
|
||||
)
|
||||
if search_results:
|
||||
player = search_results[0]
|
||||
else:
|
||||
results.append((player_name, team_abbrev, "[red]ERROR[/red]", "Player not found"))
|
||||
results.append(
|
||||
(
|
||||
player_name,
|
||||
team_abbrev,
|
||||
"[red]ERROR[/red]",
|
||||
"Player not found",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Get current team
|
||||
current_team = player.get('team', {})
|
||||
current_abbrev = current_team.get('abbrev', 'N/A') if isinstance(current_team, dict) else 'N/A'
|
||||
current_team = player.get("team", {})
|
||||
current_abbrev = (
|
||||
current_team.get("abbrev", "N/A")
|
||||
if isinstance(current_team, dict)
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
# Find target team
|
||||
try:
|
||||
target_team = state.api.get_team(abbrev=team_abbrev, season=season)
|
||||
except Exception:
|
||||
results.append((player_name, team_abbrev, "[red]ERROR[/red]", f"Team '{team_abbrev}' not found"))
|
||||
results.append(
|
||||
(
|
||||
player_name,
|
||||
team_abbrev,
|
||||
"[red]ERROR[/red]",
|
||||
f"Team '{team_abbrev}' not found",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
results.append((player['name'], team_abbrev, "[blue]DRY-RUN[/blue]", f"Would move from {current_abbrev}"))
|
||||
results.append(
|
||||
(
|
||||
player["name"],
|
||||
team_abbrev,
|
||||
"[blue]DRY-RUN[/blue]",
|
||||
f"Would move from {current_abbrev}",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Perform update
|
||||
state.api.update_player(player['id'], team_id=target_team['id'])
|
||||
results.append((player['name'], team_abbrev, "[green]SUCCESS[/green]", f"Moved from {current_abbrev}"))
|
||||
state.api.update_player(player["id"], team_id=target_team["id"])
|
||||
results.append(
|
||||
(
|
||||
player["name"],
|
||||
team_abbrev,
|
||||
"[green]SUCCESS[/green]",
|
||||
f"Moved from {current_abbrev}",
|
||||
)
|
||||
)
|
||||
|
||||
if state.json_output:
|
||||
json_results = [
|
||||
{"player": r[0], "team": r[1], "status": r[2].replace("[", "").replace("]", "").split("/")[0], "message": r[3]}
|
||||
{
|
||||
"player": r[0],
|
||||
"team": r[1],
|
||||
"status": r[2].replace("[", "").replace("]", "").split("/")[0],
|
||||
"message": r[3],
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
output_json(json_results)
|
||||
@ -343,10 +387,15 @@ def player_move(
|
||||
# Team Commands
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@team_app.command("list")
|
||||
def team_list(
|
||||
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Season number")] = None,
|
||||
active: Annotated[bool, typer.Option("--active", "-a", help="Exclude IL/MiL teams")] = False,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
active: Annotated[
|
||||
bool, typer.Option("--active", "-a", help="Exclude IL/MiL teams")
|
||||
] = False,
|
||||
):
|
||||
"""List all teams"""
|
||||
try:
|
||||
@ -363,16 +412,27 @@ def team_list(
|
||||
|
||||
rows = []
|
||||
for t in teams:
|
||||
manager = t.get('manager1', {})
|
||||
manager_name = manager.get('name', '') if isinstance(manager, dict) else ''
|
||||
division = t.get('division', {})
|
||||
div_abbrev = division.get('division_abbrev', '') if isinstance(division, dict) else ''
|
||||
rows.append([t['abbrev'], t.get('lname', t.get('sname', '')), div_abbrev, manager_name])
|
||||
manager = t.get("manager1", {})
|
||||
manager_name = manager.get("name", "") if isinstance(manager, dict) else ""
|
||||
division = t.get("division", {})
|
||||
div_abbrev = (
|
||||
division.get("division_abbrev", "")
|
||||
if isinstance(division, dict)
|
||||
else ""
|
||||
)
|
||||
rows.append(
|
||||
[
|
||||
t["abbrev"],
|
||||
t.get("lname", t.get("sname", "")),
|
||||
div_abbrev,
|
||||
manager_name,
|
||||
]
|
||||
)
|
||||
|
||||
output_table(
|
||||
f"Teams - Season {season}" + (" (Active Only)" if active else ""),
|
||||
["Abbrev", "Name", "Division", "Manager"],
|
||||
rows
|
||||
rows,
|
||||
)
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
@ -381,7 +441,9 @@ def team_list(
|
||||
@team_app.command("get")
|
||||
def team_get(
|
||||
abbrev: Annotated[str, typer.Argument(help="Team abbreviation")],
|
||||
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Season number")] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
):
|
||||
"""Get team information"""
|
||||
try:
|
||||
@ -392,10 +454,18 @@ def team_get(
|
||||
output_json(team)
|
||||
return
|
||||
|
||||
manager = team.get('manager1', {})
|
||||
manager_name = manager.get('name', 'N/A') if isinstance(manager, dict) else 'N/A'
|
||||
division = team.get('division', {})
|
||||
div_name = division.get('division_name', 'N/A') if isinstance(division, dict) else 'N/A'
|
||||
manager = team.get("manager1", {})
|
||||
manager_name = (
|
||||
manager.get("name", "N/A") if isinstance(manager, dict) else "N/A"
|
||||
)
|
||||
division = team.get("division", {})
|
||||
div_name = (
|
||||
division.get("division_name", "N/A")
|
||||
if isinstance(division, dict)
|
||||
else "N/A"
|
||||
)
|
||||
salary_cap = team.get("salary_cap")
|
||||
cap_str = f"{salary_cap:.1f}" if salary_cap is not None else "N/A"
|
||||
|
||||
panel = Panel(
|
||||
f"[bold]ID:[/bold] {team['id']}\n"
|
||||
@ -404,6 +474,7 @@ def team_get(
|
||||
f"[bold]Full Name:[/bold] {team.get('lname', 'N/A')}\n"
|
||||
f"[bold]Division:[/bold] {div_name}\n"
|
||||
f"[bold]Manager:[/bold] {manager_name}\n"
|
||||
f"[bold]Salary Cap:[/bold] {cap_str}\n"
|
||||
f"[bold]Stadium:[/bold] {team.get('stadium', 'N/A')}\n"
|
||||
f"[bold]Season:[/bold] {team.get('season', 'N/A')}",
|
||||
title=f"Team: {team.get('lname', abbrev)}",
|
||||
@ -417,40 +488,60 @@ def team_get(
|
||||
@team_app.command("roster")
|
||||
def team_roster(
|
||||
abbrev: Annotated[str, typer.Argument(help="Team abbreviation")],
|
||||
which: Annotated[str, typer.Option("--which", "-w", help="'current' or 'next'")] = "current",
|
||||
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Season number")] = None,
|
||||
which: Annotated[
|
||||
str, typer.Option("--which", "-w", help="'current' or 'next'")
|
||||
] = "current",
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
):
|
||||
"""Show team roster breakdown"""
|
||||
try:
|
||||
season = get_season(season)
|
||||
team = state.api.get_team(abbrev=abbrev.upper(), season=season)
|
||||
roster = state.api.get_team_roster(team_id=team['id'], which=which)
|
||||
roster = state.api.get_team_roster(team_id=team["id"], which=which)
|
||||
|
||||
if state.json_output:
|
||||
output_json(roster)
|
||||
return
|
||||
|
||||
console.print(f"\n[bold cyan]{team.get('lname', abbrev)} Roster ({which.title()})[/bold cyan]\n")
|
||||
console.print(
|
||||
f"\n[bold cyan]{team.get('lname', abbrev)} Roster ({which.title()})[/bold cyan]\n"
|
||||
)
|
||||
|
||||
# Active roster
|
||||
active = roster.get('active', {}).get('players', [])
|
||||
active = roster.get("active", {}).get("players", [])
|
||||
if active:
|
||||
active_rows = [[p['name'], p.get('pos_1', ''), f"{p.get('wara', 0):.2f}"] for p in active]
|
||||
output_table(f"Active ({len(active)})", ["Name", "Pos", "WARA"], active_rows)
|
||||
active_rows = [
|
||||
[p["name"], p.get("pos_1", ""), f"{p.get('wara', 0):.2f}"]
|
||||
for p in active
|
||||
]
|
||||
output_table(
|
||||
f"Active ({len(active)})", ["Name", "Pos", "WARA"], active_rows
|
||||
)
|
||||
|
||||
# Short IL
|
||||
short_il = roster.get('shortil', {}).get('players', [])
|
||||
short_il = roster.get("shortil", {}).get("players", [])
|
||||
if short_il:
|
||||
console.print()
|
||||
il_rows = [[p['name'], p.get('pos_1', ''), p.get('il_return', '')] for p in short_il]
|
||||
output_table(f"Short IL ({len(short_il)})", ["Name", "Pos", "Return"], il_rows)
|
||||
il_rows = [
|
||||
[p["name"], p.get("pos_1", ""), p.get("il_return", "")]
|
||||
for p in short_il
|
||||
]
|
||||
output_table(
|
||||
f"Short IL ({len(short_il)})", ["Name", "Pos", "Return"], il_rows
|
||||
)
|
||||
|
||||
# Long IL
|
||||
long_il = roster.get('longil', {}).get('players', [])
|
||||
long_il = roster.get("longil", {}).get("players", [])
|
||||
if long_il:
|
||||
console.print()
|
||||
lil_rows = [[p['name'], p.get('pos_1', ''), p.get('il_return', '')] for p in long_il]
|
||||
output_table(f"Long IL ({len(long_il)})", ["Name", "Pos", "Return"], lil_rows)
|
||||
lil_rows = [
|
||||
[p["name"], p.get("pos_1", ""), p.get("il_return", "")] for p in long_il
|
||||
]
|
||||
output_table(
|
||||
f"Long IL ({len(long_il)})", ["Name", "Pos", "Return"], lil_rows
|
||||
)
|
||||
|
||||
# Summary
|
||||
total = len(active) + len(short_il) + len(long_il)
|
||||
@ -464,11 +555,21 @@ def team_roster(
|
||||
# Standings Command
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@app.command()
|
||||
def standings(
|
||||
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Season number")] = None,
|
||||
division: Annotated[Optional[str], typer.Option("--division", "-d", help="Division (ALE, ALW, ALC, NLE, NLW, NLC)")] = None,
|
||||
league: Annotated[Optional[str], typer.Option("--league", "-l", help="League (AL or NL)")] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
division: Annotated[
|
||||
Optional[str],
|
||||
typer.Option(
|
||||
"--division", "-d", help="Division (ALE, ALW, ALC, NLE, NLW, NLC)"
|
||||
),
|
||||
] = None,
|
||||
league: Annotated[
|
||||
Optional[str], typer.Option("--league", "-l", help="League (AL or NL)")
|
||||
] = None,
|
||||
):
|
||||
"""Show league standings"""
|
||||
try:
|
||||
@ -476,7 +577,7 @@ def standings(
|
||||
standings_data = state.api.get_standings(
|
||||
season=season,
|
||||
division_abbrev=division.upper() if division else None,
|
||||
league_abbrev=league.upper() if league else None
|
||||
league_abbrev=league.upper() if league else None,
|
||||
)
|
||||
|
||||
if state.json_output:
|
||||
@ -489,14 +590,14 @@ def standings(
|
||||
|
||||
rows = []
|
||||
for s in standings_data:
|
||||
team = s.get('team', {})
|
||||
team_abbrev = team.get('abbrev', 'N/A') if isinstance(team, dict) else 'N/A'
|
||||
team_name = team.get('lname', 'N/A') if isinstance(team, dict) else 'N/A'
|
||||
wins = s.get('wins', 0)
|
||||
losses = s.get('losses', 0)
|
||||
team = s.get("team", {})
|
||||
team_abbrev = team.get("abbrev", "N/A") if isinstance(team, dict) else "N/A"
|
||||
team_name = team.get("lname", "N/A") if isinstance(team, dict) else "N/A"
|
||||
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 = s.get("run_diff", 0)
|
||||
rd_str = f"+{rd}" if rd > 0 else str(rd)
|
||||
rows.append([team_abbrev, team_name, wins, losses, pct, rd_str])
|
||||
|
||||
|
||||
72
skills/major-domo/cli_common.py
Normal file
72
skills/major-domo/cli_common.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""
|
||||
Shared utilities for Major Domo CLI modules.
|
||||
|
||||
All CLI sub-modules import from here for consistent output and state management.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
# Import the API client from same directory
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from api_client import MajorDomoAPI
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class State:
|
||||
"""Global state for API client and settings"""
|
||||
|
||||
api: Optional[MajorDomoAPI] = None
|
||||
json_output: bool = False
|
||||
current_season: Optional[int] = None
|
||||
|
||||
|
||||
state = State()
|
||||
|
||||
|
||||
def output_json(data):
|
||||
"""Output data as formatted JSON"""
|
||||
console.print_json(json.dumps(data, indent=2, default=str))
|
||||
|
||||
|
||||
def output_table(
|
||||
title: str, columns: List[str], rows: List[List], show_lines: bool = False
|
||||
):
|
||||
"""Output data as a rich table"""
|
||||
table = Table(
|
||||
title=title, show_header=True, header_style="bold cyan", show_lines=show_lines
|
||||
)
|
||||
for col in columns:
|
||||
table.add_column(col)
|
||||
for row in rows:
|
||||
table.add_row(*[str(cell) if cell is not None else "" for cell in row])
|
||||
console.print(table)
|
||||
|
||||
|
||||
def handle_error(e: Exception, context: str = ""):
|
||||
"""Graceful error handling with helpful messages"""
|
||||
import typer
|
||||
|
||||
error_str = str(e)
|
||||
if "401" in error_str:
|
||||
console.print("[red]Error:[/red] Unauthorized. Check your API_TOKEN.")
|
||||
elif "404" in error_str:
|
||||
console.print(f"[red]Error:[/red] Not found. {context}")
|
||||
elif "Connection" in error_str or "ConnectionError" in error_str:
|
||||
console.print(
|
||||
"[red]Error:[/red] Cannot connect to API. Check network and --env setting."
|
||||
)
|
||||
else:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def get_season(season: Optional[int]) -> int:
|
||||
"""Get season, defaulting to current if not specified"""
|
||||
return season if season is not None else state.current_season
|
||||
143
skills/major-domo/cli_injuries.py
Normal file
143
skills/major-domo/cli_injuries.py
Normal file
@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Major Domo CLI - Injury Operations
|
||||
|
||||
This module provides injury listing and filtering commands.
|
||||
"""
|
||||
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
|
||||
from cli_common import (
|
||||
console,
|
||||
state,
|
||||
output_json,
|
||||
output_table,
|
||||
handle_error,
|
||||
get_season,
|
||||
)
|
||||
|
||||
injuries_app = typer.Typer(help="Injury operations")
|
||||
|
||||
|
||||
@injuries_app.command("list")
|
||||
def injuries_list(
|
||||
team: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--team", "-t", help="Filter by team abbreviation"),
|
||||
] = None,
|
||||
active: Annotated[
|
||||
bool, typer.Option("--active", "-a", help="Only show active injuries")
|
||||
] = False,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
sort: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--sort", help="Sort order (start-asc, start-desc, return-asc, return-desc)"
|
||||
),
|
||||
] = "return-asc",
|
||||
):
|
||||
"""List injuries with optional filtering"""
|
||||
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)
|
||||
|
||||
# Call API with resolved parameters
|
||||
result = state.api.get(
|
||||
"injuries",
|
||||
season=season,
|
||||
team_id=team_id,
|
||||
is_active=active if active else None,
|
||||
sort=sort,
|
||||
)
|
||||
|
||||
injuries = result.get("injuries", [])
|
||||
|
||||
if state.json_output:
|
||||
output_json(injuries)
|
||||
return
|
||||
|
||||
if not injuries:
|
||||
filter_parts = []
|
||||
if team_abbrev:
|
||||
filter_parts.append(f"team {team_abbrev}")
|
||||
if active:
|
||||
filter_parts.append("active")
|
||||
filter_str = " (".join(filter_parts) + ")" if filter_parts else ""
|
||||
console.print(
|
||||
f"[yellow]No injuries found in season {season}{filter_str}[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Format table rows
|
||||
rows = []
|
||||
for injury in injuries:
|
||||
player = injury.get("player", {})
|
||||
player_name = player.get("name", "N/A")
|
||||
player_team = player.get("team", {})
|
||||
team_abbrev_display = (
|
||||
player_team.get("abbrev", "N/A")
|
||||
if isinstance(player_team, dict)
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
total_games = injury.get("total_games", 0)
|
||||
start_week = injury.get("start_week", 0)
|
||||
start_game = injury.get("start_game", 0)
|
||||
end_week = injury.get("end_week", 0)
|
||||
end_game = injury.get("end_game", 0)
|
||||
is_active = injury.get("is_active", False)
|
||||
|
||||
# Format start/end as wXXgY
|
||||
start_str = f"w{start_week:02d}g{start_game}" if start_week else "N/A"
|
||||
end_str = f"w{end_week:02d}g{end_game}" if end_week else "N/A"
|
||||
active_str = "Yes" if is_active else "No"
|
||||
|
||||
rows.append(
|
||||
[
|
||||
player_name,
|
||||
team_abbrev_display,
|
||||
total_games,
|
||||
start_str,
|
||||
end_str,
|
||||
active_str,
|
||||
]
|
||||
)
|
||||
|
||||
# Build title
|
||||
title = f"Injuries - Season {season}"
|
||||
if team_abbrev:
|
||||
title += f" ({team_abbrev})"
|
||||
if active:
|
||||
title += " (Active Only)"
|
||||
|
||||
output_table(title, ["Player", "Team", "Games", "Start", "End", "Active"], rows)
|
||||
|
||||
except typer.Exit:
|
||||
raise
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
|
||||
|
||||
# Make list the default command when running 'majordomo injuries'
|
||||
@injuries_app.callback(invoke_without_command=True)
|
||||
def injuries_default(ctx: typer.Context):
|
||||
"""Default to list command if no subcommand specified"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
ctx.invoke(injuries_list)
|
||||
243
skills/major-domo/cli_results.py
Normal file
243
skills/major-domo/cli_results.py
Normal file
@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Major Domo CLI - Game Results Operations
|
||||
|
||||
Commands for listing and filtering game results by team, week, and season.
|
||||
"""
|
||||
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from cli_common import (
|
||||
state,
|
||||
console,
|
||||
output_json,
|
||||
output_table,
|
||||
handle_error,
|
||||
get_season,
|
||||
)
|
||||
|
||||
results_app = typer.Typer(
|
||||
help="Game results operations",
|
||||
invoke_without_command=True,
|
||||
no_args_is_help=False,
|
||||
)
|
||||
|
||||
|
||||
@results_app.callback()
|
||||
def results_callback(
|
||||
ctx: typer.Context,
|
||||
team: Annotated[
|
||||
Optional[str], typer.Option("--team", "-t", help="Filter by team abbreviation")
|
||||
] = None,
|
||||
week: Annotated[
|
||||
Optional[int], typer.Option("--week", "-w", help="Filter by specific week")
|
||||
] = None,
|
||||
week_start: Annotated[
|
||||
Optional[int], typer.Option("--week-start", help="Start week for range")
|
||||
] = None,
|
||||
week_end: Annotated[
|
||||
Optional[int], typer.Option("--week-end", help="End week for range")
|
||||
] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
int, typer.Option("--limit", "-n", help="Max results to display")
|
||||
] = 50,
|
||||
):
|
||||
"""
|
||||
List game results with optional filters.
|
||||
|
||||
By default, shows recent game results for the current season.
|
||||
Use filters to narrow down results.
|
||||
|
||||
Examples:
|
||||
majordomo results --team CAR
|
||||
majordomo results --week 5
|
||||
majordomo results --week-start 1 --week-end 4
|
||||
"""
|
||||
# Only invoke if no subcommand was called
|
||||
if ctx.invoked_subcommand is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
season = get_season(season)
|
||||
|
||||
# Handle week vs week_start/week_end
|
||||
if week is not None:
|
||||
week_start = week
|
||||
week_end = week
|
||||
|
||||
# Use low-level API to access all parameters
|
||||
result = state.api.get(
|
||||
"results",
|
||||
season=season,
|
||||
team_abbrev=team,
|
||||
week_start=week_start,
|
||||
week_end=week_end,
|
||||
short_output=False,
|
||||
)
|
||||
results_list = result.get("results", [])
|
||||
|
||||
# Limit results
|
||||
results_list = results_list[:limit]
|
||||
|
||||
if state.json_output:
|
||||
output_json(results_list)
|
||||
return
|
||||
|
||||
if not results_list:
|
||||
console.print(
|
||||
f"[yellow]No results found for the given filters (Season {season})[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Build table rows
|
||||
rows = []
|
||||
for r in results_list:
|
||||
away_team = r.get("away_team", {})
|
||||
home_team = r.get("home_team", {})
|
||||
away_score = r.get("away_score", 0)
|
||||
home_score = r.get("home_score", 0)
|
||||
|
||||
away_abbrev = (
|
||||
away_team.get("abbrev", "N/A") if isinstance(away_team, dict) else "N/A"
|
||||
)
|
||||
home_abbrev = (
|
||||
home_team.get("abbrev", "N/A") if isinstance(home_team, dict) else "N/A"
|
||||
)
|
||||
|
||||
# Format score as "away_score-home_score"
|
||||
score_str = f"{away_score}-{home_score}"
|
||||
|
||||
rows.append(
|
||||
[
|
||||
r.get("week", ""),
|
||||
r.get("game_num", ""),
|
||||
away_abbrev,
|
||||
"@",
|
||||
home_abbrev,
|
||||
score_str,
|
||||
]
|
||||
)
|
||||
|
||||
# Build title with filters
|
||||
title_parts = [f"Game Results (Season {season})"]
|
||||
if team:
|
||||
title_parts.append(f"Team: {team}")
|
||||
if week:
|
||||
title_parts.append(f"Week {week}")
|
||||
elif week_start and week_end:
|
||||
title_parts.append(f"Weeks {week_start}-{week_end}")
|
||||
|
||||
title = " | ".join(title_parts)
|
||||
|
||||
output_table(title, ["Week", "Gm", "Away", "@", "Home", "Score"], rows)
|
||||
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
|
||||
|
||||
@results_app.command("list")
|
||||
def results_list(
|
||||
team: Annotated[
|
||||
Optional[str], typer.Option("--team", "-t", help="Filter by team abbreviation")
|
||||
] = None,
|
||||
week: Annotated[
|
||||
Optional[int], typer.Option("--week", "-w", help="Filter by specific week")
|
||||
] = None,
|
||||
week_start: Annotated[
|
||||
Optional[int], typer.Option("--week-start", help="Start week for range")
|
||||
] = None,
|
||||
week_end: Annotated[
|
||||
Optional[int], typer.Option("--week-end", help="End week for range")
|
||||
] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
int, typer.Option("--limit", "-n", help="Max results to display")
|
||||
] = 50,
|
||||
):
|
||||
"""
|
||||
List game results with optional filters (explicit command).
|
||||
|
||||
This is the same as calling 'majordomo results' without a subcommand.
|
||||
"""
|
||||
try:
|
||||
season = get_season(season)
|
||||
|
||||
# Handle week vs week_start/week_end
|
||||
if week is not None:
|
||||
week_start = week
|
||||
week_end = week
|
||||
|
||||
# Use low-level API to access all parameters
|
||||
result = state.api.get(
|
||||
"results",
|
||||
season=season,
|
||||
team_abbrev=team,
|
||||
week_start=week_start,
|
||||
week_end=week_end,
|
||||
short_output=False,
|
||||
)
|
||||
results_list = result.get("results", [])
|
||||
|
||||
# Limit results
|
||||
results_list = results_list[:limit]
|
||||
|
||||
if state.json_output:
|
||||
output_json(results_list)
|
||||
return
|
||||
|
||||
if not results_list:
|
||||
console.print(
|
||||
f"[yellow]No results found for the given filters (Season {season})[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Build table rows
|
||||
rows = []
|
||||
for r in results_list:
|
||||
away_team = r.get("away_team", {})
|
||||
home_team = r.get("home_team", {})
|
||||
away_score = r.get("away_score", 0)
|
||||
home_score = r.get("home_score", 0)
|
||||
|
||||
away_abbrev = (
|
||||
away_team.get("abbrev", "N/A") if isinstance(away_team, dict) else "N/A"
|
||||
)
|
||||
home_abbrev = (
|
||||
home_team.get("abbrev", "N/A") if isinstance(home_team, dict) else "N/A"
|
||||
)
|
||||
|
||||
# Format score as "away_score-home_score"
|
||||
score_str = f"{away_score}-{home_score}"
|
||||
|
||||
rows.append(
|
||||
[
|
||||
r.get("week", ""),
|
||||
r.get("game_num", ""),
|
||||
away_abbrev,
|
||||
"@",
|
||||
home_abbrev,
|
||||
score_str,
|
||||
]
|
||||
)
|
||||
|
||||
# Build title with filters
|
||||
title_parts = [f"Game Results (Season {season})"]
|
||||
if team:
|
||||
title_parts.append(f"Team: {team}")
|
||||
if week:
|
||||
title_parts.append(f"Week {week}")
|
||||
elif week_start and week_end:
|
||||
title_parts.append(f"Weeks {week_start}-{week_end}")
|
||||
|
||||
title = " | ".join(title_parts)
|
||||
|
||||
output_table(title, ["Week", "Gm", "Away", "@", "Home", "Score"], rows)
|
||||
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
231
skills/major-domo/cli_schedule.py
Normal file
231
skills/major-domo/cli_schedule.py
Normal file
@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Major Domo CLI - Schedule Operations
|
||||
|
||||
Commands for listing and filtering game schedules.
|
||||
"""
|
||||
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from cli_common import (
|
||||
state,
|
||||
console,
|
||||
output_json,
|
||||
output_table,
|
||||
handle_error,
|
||||
get_season,
|
||||
)
|
||||
|
||||
schedule_app = typer.Typer(
|
||||
help="Schedule operations",
|
||||
invoke_without_command=True,
|
||||
no_args_is_help=False,
|
||||
)
|
||||
|
||||
|
||||
@schedule_app.callback()
|
||||
def schedule_callback(
|
||||
ctx: typer.Context,
|
||||
team: Annotated[
|
||||
Optional[str], typer.Option("--team", "-t", help="Filter by team abbreviation")
|
||||
] = None,
|
||||
week: Annotated[
|
||||
Optional[int], typer.Option("--week", "-w", help="Filter by specific week")
|
||||
] = None,
|
||||
week_start: Annotated[
|
||||
Optional[int], typer.Option("--week-start", help="Start week for range")
|
||||
] = None,
|
||||
week_end: Annotated[
|
||||
Optional[int], typer.Option("--week-end", help="End week for range")
|
||||
] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
int, typer.Option("--limit", "-n", help="Max results to display")
|
||||
] = 50,
|
||||
):
|
||||
"""
|
||||
List game schedules with optional filters.
|
||||
|
||||
By default, shows recent schedules for the current season.
|
||||
Use filters to narrow down results.
|
||||
|
||||
Examples:
|
||||
majordomo schedule --team CAR
|
||||
majordomo schedule --week 5
|
||||
majordomo schedule --week-start 1 --week-end 4
|
||||
"""
|
||||
# Only invoke if no subcommand was called
|
||||
if ctx.invoked_subcommand is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
season = get_season(season)
|
||||
|
||||
# Handle week vs week_start/week_end
|
||||
if week is not None:
|
||||
week_start = week
|
||||
week_end = week
|
||||
|
||||
# Get schedules from API using low-level get method
|
||||
result = state.api.get(
|
||||
"schedules",
|
||||
season=season,
|
||||
team_abbrev=team,
|
||||
week_start=week_start,
|
||||
week_end=week_end,
|
||||
short_output=False,
|
||||
)
|
||||
schedules = result.get("schedules", [])
|
||||
|
||||
# Limit results
|
||||
schedules = schedules[:limit]
|
||||
|
||||
if state.json_output:
|
||||
output_json(schedules)
|
||||
return
|
||||
|
||||
if not schedules:
|
||||
console.print(
|
||||
f"[yellow]No schedules found for the given filters (Season {season})[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Build table rows
|
||||
rows = []
|
||||
for s in schedules:
|
||||
away_team = s.get("away_team", {})
|
||||
home_team = s.get("home_team", {})
|
||||
|
||||
away_abbrev = (
|
||||
away_team.get("abbrev", "N/A") if isinstance(away_team, dict) else "N/A"
|
||||
)
|
||||
home_abbrev = (
|
||||
home_team.get("abbrev", "N/A") if isinstance(home_team, dict) else "N/A"
|
||||
)
|
||||
|
||||
rows.append(
|
||||
[
|
||||
s.get("week", ""),
|
||||
s.get("game_num", ""),
|
||||
away_abbrev,
|
||||
"@",
|
||||
home_abbrev,
|
||||
]
|
||||
)
|
||||
|
||||
# Build title with filters
|
||||
title_parts = [f"Schedule (Season {season})"]
|
||||
if team:
|
||||
title_parts.append(f"Team: {team}")
|
||||
if week:
|
||||
title_parts.append(f"Week {week}")
|
||||
elif week_start and week_end:
|
||||
title_parts.append(f"Weeks {week_start}-{week_end}")
|
||||
|
||||
title = " | ".join(title_parts)
|
||||
|
||||
output_table(title, ["Week", "Gm", "Away", "@", "Home"], rows)
|
||||
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
|
||||
|
||||
@schedule_app.command("list")
|
||||
def schedule_list(
|
||||
team: Annotated[
|
||||
Optional[str], typer.Option("--team", "-t", help="Filter by team abbreviation")
|
||||
] = None,
|
||||
week: Annotated[
|
||||
Optional[int], typer.Option("--week", "-w", help="Filter by specific week")
|
||||
] = None,
|
||||
week_start: Annotated[
|
||||
Optional[int], typer.Option("--week-start", help="Start week for range")
|
||||
] = None,
|
||||
week_end: Annotated[
|
||||
Optional[int], typer.Option("--week-end", help="End week for range")
|
||||
] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
int, typer.Option("--limit", "-n", help="Max results to display")
|
||||
] = 50,
|
||||
):
|
||||
"""
|
||||
List game schedules with optional filters (explicit command).
|
||||
|
||||
This is the same as calling 'majordomo schedule' without a subcommand.
|
||||
"""
|
||||
try:
|
||||
season = get_season(season)
|
||||
|
||||
# Handle week vs week_start/week_end
|
||||
if week is not None:
|
||||
week_start = week
|
||||
week_end = week
|
||||
|
||||
# Get schedules from API using low-level get method
|
||||
result = state.api.get(
|
||||
"schedules",
|
||||
season=season,
|
||||
team_abbrev=team,
|
||||
week_start=week_start,
|
||||
week_end=week_end,
|
||||
short_output=False,
|
||||
)
|
||||
schedules = result.get("schedules", [])
|
||||
|
||||
# Limit results
|
||||
schedules = schedules[:limit]
|
||||
|
||||
if state.json_output:
|
||||
output_json(schedules)
|
||||
return
|
||||
|
||||
if not schedules:
|
||||
console.print(
|
||||
f"[yellow]No schedules found for the given filters (Season {season})[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Build table rows
|
||||
rows = []
|
||||
for s in schedules:
|
||||
away_team = s.get("away_team", {})
|
||||
home_team = s.get("home_team", {})
|
||||
|
||||
away_abbrev = (
|
||||
away_team.get("abbrev", "N/A") if isinstance(away_team, dict) else "N/A"
|
||||
)
|
||||
home_abbrev = (
|
||||
home_team.get("abbrev", "N/A") if isinstance(home_team, dict) else "N/A"
|
||||
)
|
||||
|
||||
rows.append(
|
||||
[
|
||||
s.get("week", ""),
|
||||
s.get("game_num", ""),
|
||||
away_abbrev,
|
||||
"@",
|
||||
home_abbrev,
|
||||
]
|
||||
)
|
||||
|
||||
# Build title with filters
|
||||
title_parts = [f"Schedule (Season {season})"]
|
||||
if team:
|
||||
title_parts.append(f"Team: {team}")
|
||||
if week:
|
||||
title_parts.append(f"Week {week}")
|
||||
elif week_start and week_end:
|
||||
title_parts.append(f"Weeks {week_start}-{week_end}")
|
||||
|
||||
title = " | ".join(title_parts)
|
||||
|
||||
output_table(title, ["Week", "Gm", "Away", "@", "Home"], rows)
|
||||
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
278
skills/major-domo/cli_stats.py
Normal file
278
skills/major-domo/cli_stats.py
Normal file
@ -0,0 +1,278 @@
|
||||
#!/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)
|
||||
467
skills/major-domo/cli_transactions.py
Normal file
467
skills/major-domo/cli_transactions.py
Normal file
@ -0,0 +1,467 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Major Domo CLI - Transaction Operations
|
||||
|
||||
Commands for listing and filtering player transactions (trades, waivers, etc.).
|
||||
"""
|
||||
|
||||
from typing import Annotated, List, Optional
|
||||
|
||||
import typer
|
||||
from rich.panel import Panel
|
||||
from cli_common import (
|
||||
state,
|
||||
console,
|
||||
output_json,
|
||||
output_table,
|
||||
handle_error,
|
||||
get_season,
|
||||
)
|
||||
|
||||
transactions_app = typer.Typer(
|
||||
help="Transaction operations",
|
||||
invoke_without_command=True,
|
||||
no_args_is_help=False,
|
||||
)
|
||||
|
||||
|
||||
@transactions_app.callback()
|
||||
def transactions_callback(
|
||||
ctx: typer.Context,
|
||||
team: Annotated[
|
||||
Optional[str], typer.Option("--team", "-t", help="Filter by team abbreviation")
|
||||
] = None,
|
||||
week: Annotated[
|
||||
Optional[int], typer.Option("--week", "-w", help="Filter by specific week")
|
||||
] = None,
|
||||
week_start: Annotated[
|
||||
Optional[int], typer.Option("--week-start", help="Start week for range")
|
||||
] = None,
|
||||
week_end: Annotated[
|
||||
Optional[int], typer.Option("--week-end", help="End week for range")
|
||||
] = None,
|
||||
player: Annotated[
|
||||
Optional[str], typer.Option("--player", "-p", help="Filter by player name")
|
||||
] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
int, typer.Option("--limit", "-n", help="Max results to display")
|
||||
] = 50,
|
||||
include_cancelled: Annotated[
|
||||
bool, typer.Option("--include-cancelled", help="Include cancelled transactions")
|
||||
] = False,
|
||||
):
|
||||
"""
|
||||
List transactions with optional filters.
|
||||
|
||||
By default, shows recent non-cancelled transactions for the current season.
|
||||
Use filters to narrow down results.
|
||||
|
||||
Examples:
|
||||
majordomo transactions --team CAR
|
||||
majordomo transactions --week 5
|
||||
majordomo transactions --week-start 1 --week-end 4
|
||||
majordomo transactions --player "Mike Trout"
|
||||
"""
|
||||
# Only invoke if no subcommand was called
|
||||
if ctx.invoked_subcommand is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
season = get_season(season)
|
||||
|
||||
# Handle week vs week_start/week_end
|
||||
if week is not None:
|
||||
week_start = week
|
||||
week_end = week
|
||||
|
||||
# Build filter parameters
|
||||
team_list = [team] if team else None
|
||||
player_list = [player] if player else None
|
||||
|
||||
# Get transactions from API
|
||||
transactions = state.api.get_transactions(
|
||||
season=season,
|
||||
team_abbrev=team_list,
|
||||
week_start=week_start,
|
||||
week_end=week_end,
|
||||
player_name=player_list,
|
||||
cancelled=True if include_cancelled else False,
|
||||
)
|
||||
|
||||
# Filter out cancelled transactions unless explicitly requested
|
||||
if not include_cancelled:
|
||||
transactions = [t for t in transactions if not t.get("cancelled", False)]
|
||||
|
||||
# Limit results
|
||||
transactions = transactions[:limit]
|
||||
|
||||
if state.json_output:
|
||||
output_json(transactions)
|
||||
return
|
||||
|
||||
if not transactions:
|
||||
console.print(
|
||||
f"[yellow]No transactions found for the given filters (Season {season})[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Build table rows
|
||||
rows = []
|
||||
for t in transactions:
|
||||
player_dict = t.get("player", {})
|
||||
oldteam_dict = t.get("oldteam", {})
|
||||
newteam_dict = t.get("newteam", {})
|
||||
|
||||
player_name = (
|
||||
player_dict.get("name", "N/A")
|
||||
if isinstance(player_dict, dict)
|
||||
else "N/A"
|
||||
)
|
||||
old_abbrev = (
|
||||
oldteam_dict.get("abbrev", "N/A")
|
||||
if isinstance(oldteam_dict, dict)
|
||||
else "N/A"
|
||||
)
|
||||
new_abbrev = (
|
||||
newteam_dict.get("abbrev", "N/A")
|
||||
if isinstance(newteam_dict, dict)
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
rows.append(
|
||||
[
|
||||
t.get("week", ""),
|
||||
player_name,
|
||||
old_abbrev,
|
||||
new_abbrev,
|
||||
t.get("moveid", ""),
|
||||
]
|
||||
)
|
||||
|
||||
# Build title with filters
|
||||
title_parts = [f"Transactions (Season {season})"]
|
||||
if team:
|
||||
title_parts.append(f"Team: {team}")
|
||||
if week:
|
||||
title_parts.append(f"Week {week}")
|
||||
elif week_start and week_end:
|
||||
title_parts.append(f"Weeks {week_start}-{week_end}")
|
||||
if player:
|
||||
title_parts.append(f"Player: {player}")
|
||||
|
||||
title = " | ".join(title_parts)
|
||||
|
||||
output_table(title, ["Week", "Player", "From", "To", "Move ID"], rows)
|
||||
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
|
||||
|
||||
@transactions_app.command("list")
|
||||
def transactions_list(
|
||||
team: Annotated[
|
||||
Optional[str], typer.Option("--team", "-t", help="Filter by team abbreviation")
|
||||
] = None,
|
||||
week: Annotated[
|
||||
Optional[int], typer.Option("--week", "-w", help="Filter by specific week")
|
||||
] = None,
|
||||
week_start: Annotated[
|
||||
Optional[int], typer.Option("--week-start", help="Start week for range")
|
||||
] = None,
|
||||
week_end: Annotated[
|
||||
Optional[int], typer.Option("--week-end", help="End week for range")
|
||||
] = None,
|
||||
player: Annotated[
|
||||
Optional[str], typer.Option("--player", "-p", help="Filter by player name")
|
||||
] = None,
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
int, typer.Option("--limit", "-n", help="Max results to display")
|
||||
] = 50,
|
||||
include_cancelled: Annotated[
|
||||
bool, typer.Option("--include-cancelled", help="Include cancelled transactions")
|
||||
] = False,
|
||||
):
|
||||
"""
|
||||
List transactions with optional filters (explicit command).
|
||||
|
||||
This is the same as calling 'majordomo transactions' without a subcommand.
|
||||
"""
|
||||
try:
|
||||
season = get_season(season)
|
||||
|
||||
# Handle week vs week_start/week_end
|
||||
if week is not None:
|
||||
week_start = week
|
||||
week_end = week
|
||||
|
||||
# Build filter parameters
|
||||
team_list = [team] if team else None
|
||||
player_list = [player] if player else None
|
||||
|
||||
# Get transactions from API
|
||||
transactions = state.api.get_transactions(
|
||||
season=season,
|
||||
team_abbrev=team_list,
|
||||
week_start=week_start,
|
||||
week_end=week_end,
|
||||
player_name=player_list,
|
||||
cancelled=True if include_cancelled else False,
|
||||
)
|
||||
|
||||
# Filter out cancelled transactions unless explicitly requested
|
||||
if not include_cancelled:
|
||||
transactions = [t for t in transactions if not t.get("cancelled", False)]
|
||||
|
||||
# Limit results
|
||||
transactions = transactions[:limit]
|
||||
|
||||
if state.json_output:
|
||||
output_json(transactions)
|
||||
return
|
||||
|
||||
if not transactions:
|
||||
console.print(
|
||||
f"[yellow]No transactions found for the given filters (Season {season})[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Build table rows
|
||||
rows = []
|
||||
for t in transactions:
|
||||
player_dict = t.get("player", {})
|
||||
oldteam_dict = t.get("oldteam", {})
|
||||
newteam_dict = t.get("newteam", {})
|
||||
|
||||
player_name = (
|
||||
player_dict.get("name", "N/A")
|
||||
if isinstance(player_dict, dict)
|
||||
else "N/A"
|
||||
)
|
||||
old_abbrev = (
|
||||
oldteam_dict.get("abbrev", "N/A")
|
||||
if isinstance(oldteam_dict, dict)
|
||||
else "N/A"
|
||||
)
|
||||
new_abbrev = (
|
||||
newteam_dict.get("abbrev", "N/A")
|
||||
if isinstance(newteam_dict, dict)
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
rows.append(
|
||||
[
|
||||
t.get("week", ""),
|
||||
player_name,
|
||||
old_abbrev,
|
||||
new_abbrev,
|
||||
t.get("moveid", ""),
|
||||
]
|
||||
)
|
||||
|
||||
# Build title with filters
|
||||
title_parts = [f"Transactions (Season {season})"]
|
||||
if team:
|
||||
title_parts.append(f"Team: {team}")
|
||||
if week:
|
||||
title_parts.append(f"Week {week}")
|
||||
elif week_start and week_end:
|
||||
title_parts.append(f"Weeks {week_start}-{week_end}")
|
||||
if player:
|
||||
title_parts.append(f"Player: {player}")
|
||||
|
||||
title = " | ".join(title_parts)
|
||||
|
||||
output_table(title, ["Week", "Player", "From", "To", "Move ID"], rows)
|
||||
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
|
||||
|
||||
@transactions_app.command("simulate")
|
||||
def transactions_simulate(
|
||||
team: Annotated[str, typer.Argument(help="Team abbreviation (e.g., CLS)")],
|
||||
moves: Annotated[
|
||||
str,
|
||||
typer.Argument(
|
||||
help="Moves as 'Player:Target,Player:Target' (e.g., 'Stott:CLSMiL,Walker:CLS')"
|
||||
),
|
||||
],
|
||||
season: Annotated[
|
||||
Optional[int], typer.Option("--season", "-s", help="Season number")
|
||||
] = None,
|
||||
):
|
||||
"""
|
||||
Simulate transactions and check compliance without making changes.
|
||||
|
||||
Validates that after all proposed moves:
|
||||
- Active (ML) roster has exactly 26 players
|
||||
- Total ML sWAR does not exceed team salary cap
|
||||
|
||||
Examples:
|
||||
transactions simulate CLS "Stott:CLSMiL,Walker:CLS,Castellanos:FA,Martin:CLS"
|
||||
transactions simulate DEN "Player1:DENMiL,Player2:DEN"
|
||||
"""
|
||||
try:
|
||||
season = get_season(season)
|
||||
team_upper = team.upper()
|
||||
|
||||
# Parse moves
|
||||
parsed_moves: List[tuple] = []
|
||||
for move_str in moves.split(","):
|
||||
move_str = move_str.strip()
|
||||
if not move_str:
|
||||
continue
|
||||
parts = move_str.split(":")
|
||||
if len(parts) != 2 or not parts[0].strip() or not parts[1].strip():
|
||||
console.print(f"[red]Invalid move format:[/red] '{move_str}'")
|
||||
console.print("Expected format: 'Player Name:TARGET'")
|
||||
raise typer.Exit(1)
|
||||
parsed_moves.append((parts[0].strip(), parts[1].strip().upper()))
|
||||
|
||||
if not parsed_moves:
|
||||
console.print("[red]No moves provided[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Get team info (includes salary_cap)
|
||||
team_data = state.api.get_team(abbrev=team_upper, season=season)
|
||||
salary_cap = team_data.get("salary_cap")
|
||||
|
||||
# Get current roster
|
||||
roster = state.api.get_team_roster(team_id=team_data["id"], which="current")
|
||||
active_players = roster.get("active", {}).get("players", [])
|
||||
|
||||
# Build current ML state: {player_name: wara}
|
||||
ml_roster = {}
|
||||
for p in active_players:
|
||||
ml_roster[p["name"]] = float(p.get("wara", 0) or 0)
|
||||
|
||||
current_count = len(ml_roster)
|
||||
current_swar = sum(ml_roster.values())
|
||||
|
||||
# Simulate each move
|
||||
move_details = []
|
||||
errors = []
|
||||
|
||||
for player_name, target in parsed_moves:
|
||||
# Look up the player
|
||||
player = state.api.get_player(name=player_name, season=season)
|
||||
if not player:
|
||||
search = state.api.search_players(
|
||||
query=player_name, season=season, limit=1
|
||||
)
|
||||
if search:
|
||||
player = search[0]
|
||||
else:
|
||||
errors.append(f"Player '{player_name}' not found")
|
||||
continue
|
||||
|
||||
p_name = player["name"]
|
||||
p_wara = float(player.get("wara", 0) or 0)
|
||||
p_team = (
|
||||
player.get("team", {}).get("abbrev", "?")
|
||||
if isinstance(player.get("team"), dict)
|
||||
else "?"
|
||||
)
|
||||
|
||||
# Determine if this move adds to or removes from ML
|
||||
target_is_ml = target == team_upper
|
||||
currently_on_ml = p_name in ml_roster
|
||||
|
||||
impact = ""
|
||||
|
||||
if currently_on_ml and not target_is_ml:
|
||||
# Removing from ML (demote, release, trade away)
|
||||
del ml_roster[p_name]
|
||||
impact = f"[red]-{p_wara:.2f}[/red]"
|
||||
elif not currently_on_ml and target_is_ml:
|
||||
# Adding to ML (promote, sign, trade in)
|
||||
ml_roster[p_name] = p_wara
|
||||
impact = f"[green]+{p_wara:.2f}[/green]"
|
||||
elif currently_on_ml and target_is_ml:
|
||||
impact = "[dim]no change (already ML)[/dim]"
|
||||
else:
|
||||
impact = "[dim]no ML impact[/dim]"
|
||||
|
||||
move_details.append(
|
||||
(p_name, f"{p_team} → {target}", f"{p_wara:.2f}", impact)
|
||||
)
|
||||
|
||||
# Calculate post-transaction state
|
||||
post_count = len(ml_roster)
|
||||
post_swar = sum(ml_roster.values())
|
||||
|
||||
# Output
|
||||
if state.json_output:
|
||||
output_json(
|
||||
{
|
||||
"team": team_upper,
|
||||
"salary_cap": salary_cap,
|
||||
"before": {
|
||||
"ml_count": current_count,
|
||||
"ml_swar": round(current_swar, 2),
|
||||
},
|
||||
"after": {
|
||||
"ml_count": post_count,
|
||||
"ml_swar": round(post_swar, 2),
|
||||
},
|
||||
"moves": [
|
||||
{"player": m[0], "move": m[1], "wara": m[2]}
|
||||
for m in move_details
|
||||
],
|
||||
"errors": errors,
|
||||
"roster_ok": post_count == 26,
|
||||
"cap_ok": post_swar <= salary_cap if salary_cap else True,
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
# Print errors if any
|
||||
for err in errors:
|
||||
console.print(f"[red]Error:[/red] {err}")
|
||||
|
||||
# Moves table
|
||||
output_table(
|
||||
f"Simulated Moves for {team_upper}",
|
||||
["Player", "Move", "WARA", "ML Impact"],
|
||||
move_details,
|
||||
)
|
||||
|
||||
console.print()
|
||||
|
||||
# Compliance summary
|
||||
roster_ok = post_count == 26
|
||||
cap_ok = post_swar <= salary_cap if salary_cap else True
|
||||
cap_space = salary_cap - post_swar if salary_cap else 0
|
||||
|
||||
roster_status = "[green]PASS[/green]" if roster_ok else "[red]FAIL[/red]"
|
||||
cap_status = "[green]PASS[/green]" if cap_ok else "[red]FAIL[/red]"
|
||||
|
||||
panel = Panel(
|
||||
f"[bold]ML Roster:[/bold] {current_count} → {post_count} {roster_status}\n"
|
||||
f"[bold]ML sWAR:[/bold] {current_swar:.2f} → {post_swar:.2f}\n"
|
||||
f"[bold]Salary Cap:[/bold] {salary_cap:.1f}\n"
|
||||
f"[bold]Cap Space:[/bold] {cap_space:.2f} {cap_status}",
|
||||
title="Compliance Check",
|
||||
border_style="green" if (roster_ok and cap_ok) else "red",
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
if not roster_ok:
|
||||
console.print(
|
||||
f"\n[red]Roster must be exactly 26. Currently {post_count}.[/red]"
|
||||
)
|
||||
if not cap_ok:
|
||||
console.print(
|
||||
f"\n[red]Over salary cap by {post_swar - salary_cap:.2f} sWAR.[/red]"
|
||||
)
|
||||
|
||||
except typer.Exit:
|
||||
raise
|
||||
except Exception as e:
|
||||
handle_error(e)
|
||||
Loading…
Reference in New Issue
Block a user