Add pd-cards CLI skeleton with Typer
Introduces new pd-cards CLI tool for all card creation workflows: - custom: manage fictional character cards via YAML profiles - live-series: live season card updates (stub) - retrosheet: historical data processing (stub) - scouting: scouting report generation (stub) - upload: S3 card image upload (stub) Key features: - Typer-based CLI with auto-generated help and shell completion - YAML profiles for custom characters (replaces per-character Python scripts) - Preview, submit, new, and list commands for custom cards - First character migrated: Kalin Young Install with: uv pip install -e . Run with: pd-cards --help 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1c39f7f8b3
commit
2e28d29ced
3
pd_cards/__init__.py
Normal file
3
pd_cards/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Paper Dynasty Card Creation CLI."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
47
pd_cards/cli.py
Normal file
47
pd_cards/cli.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""
|
||||||
|
Paper Dynasty Card Creation CLI.
|
||||||
|
|
||||||
|
Main entry point for all card creation workflows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from pd_cards.commands import custom, live_series, retrosheet, scouting, upload
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="pd-cards",
|
||||||
|
help="Paper Dynasty card creation CLI - create player cards from statistics",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
# Register subcommands
|
||||||
|
app.add_typer(custom.app, name="custom", help="Custom character card management")
|
||||||
|
app.add_typer(live_series.app, name="live-series", help="Live season card updates")
|
||||||
|
app.add_typer(retrosheet.app, name="retrosheet", help="Historical Retrosheet data processing")
|
||||||
|
app.add_typer(scouting.app, name="scouting", help="Scouting report generation")
|
||||||
|
app.add_typer(upload.app, name="upload", help="Card image upload to S3")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def version():
|
||||||
|
"""Show pd-cards version."""
|
||||||
|
from pd_cards import __version__
|
||||||
|
console.print(f"pd-cards version {__version__}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback()
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Paper Dynasty Card Creation CLI.
|
||||||
|
|
||||||
|
Create player cards from FanGraphs, Baseball Reference, and Retrosheet data,
|
||||||
|
or design custom fictional characters using archetypes.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
1
pd_cards/commands/__init__.py
Normal file
1
pd_cards/commands/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""CLI subcommands for pd-cards."""
|
||||||
334
pd_cards/commands/custom.py
Normal file
334
pd_cards/commands/custom.py
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
"""
|
||||||
|
Custom character card management.
|
||||||
|
|
||||||
|
Commands for creating and managing fictional player cards using YAML profiles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
import yaml
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
app = typer.Typer(no_args_is_help=True)
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
# Profile directory relative to this file
|
||||||
|
PROFILES_DIR = Path(__file__).parent.parent / "custom" / "profiles"
|
||||||
|
|
||||||
|
|
||||||
|
def get_profile_path(name: str) -> Path:
|
||||||
|
"""Get the path to a character profile YAML file."""
|
||||||
|
# Normalize name: lowercase, replace spaces with underscores
|
||||||
|
normalized = name.lower().replace(" ", "_").replace("-", "_")
|
||||||
|
return PROFILES_DIR / f"{normalized}.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
def load_profile(name: str) -> dict:
|
||||||
|
"""Load a character profile from YAML."""
|
||||||
|
profile_path = get_profile_path(name)
|
||||||
|
if not profile_path.exists():
|
||||||
|
console.print(f"[red]Profile not found:[/red] {profile_path}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
with open(profile_path) as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def calc_ops(ratings: dict) -> tuple[float, float, float]:
|
||||||
|
"""Calculate AVG, OBP, SLG, OPS from ratings dict."""
|
||||||
|
avg = (
|
||||||
|
ratings['homerun'] + ratings['bp_homerun'] / 2 + ratings['triple'] +
|
||||||
|
ratings['double_three'] + ratings['double_two'] + ratings['double_pull'] +
|
||||||
|
ratings['single_two'] + ratings['single_one'] + ratings['single_center'] +
|
||||||
|
ratings['bp_single'] / 2
|
||||||
|
) / 108
|
||||||
|
obp = avg + (ratings['hbp'] + ratings['walk']) / 108
|
||||||
|
slg = (
|
||||||
|
ratings['homerun'] * 4 + ratings['bp_homerun'] * 2 + ratings['triple'] * 3 +
|
||||||
|
(ratings['double_three'] + ratings['double_two'] + ratings['double_pull']) * 2 +
|
||||||
|
ratings['single_two'] + ratings['single_one'] + ratings['single_center'] +
|
||||||
|
ratings['bp_single'] / 2
|
||||||
|
) / 108
|
||||||
|
return avg, obp, slg
|
||||||
|
|
||||||
|
|
||||||
|
def verify_total(ratings: dict) -> float:
|
||||||
|
"""Verify ratings sum to 108."""
|
||||||
|
return sum([
|
||||||
|
ratings['homerun'], ratings['bp_homerun'], ratings['triple'],
|
||||||
|
ratings['double_three'], ratings['double_two'], ratings['double_pull'],
|
||||||
|
ratings['single_two'], ratings['single_one'], ratings['single_center'], ratings['bp_single'],
|
||||||
|
ratings['walk'], ratings['hbp'], ratings['strikeout'],
|
||||||
|
ratings['lineout'], ratings['popout'],
|
||||||
|
ratings['flyout_a'], ratings['flyout_bq'], ratings['flyout_lf_b'], ratings['flyout_rf_b'],
|
||||||
|
ratings['groundout_a'], ratings['groundout_b'], ratings['groundout_c']
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("list")
|
||||||
|
def list_profiles():
|
||||||
|
"""List all available character profiles."""
|
||||||
|
if not PROFILES_DIR.exists():
|
||||||
|
console.print("[yellow]No profiles directory found.[/yellow]")
|
||||||
|
console.print(f"Create profiles in: {PROFILES_DIR}")
|
||||||
|
return
|
||||||
|
|
||||||
|
profiles = list(PROFILES_DIR.glob("*.yaml"))
|
||||||
|
if not profiles:
|
||||||
|
console.print("[yellow]No character profiles found.[/yellow]")
|
||||||
|
console.print(f"Create YAML profiles in: {PROFILES_DIR}")
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(title="Custom Character Profiles")
|
||||||
|
table.add_column("Name", style="cyan")
|
||||||
|
table.add_column("Target OPS", justify="right")
|
||||||
|
table.add_column("Positions")
|
||||||
|
table.add_column("Player ID", justify="right")
|
||||||
|
|
||||||
|
for profile_path in sorted(profiles):
|
||||||
|
try:
|
||||||
|
with open(profile_path) as f:
|
||||||
|
profile = yaml.safe_load(f)
|
||||||
|
name = profile.get('name', profile_path.stem)
|
||||||
|
target_ops = profile.get('target_ops', '-')
|
||||||
|
positions = ', '.join(profile.get('positions', {}).keys()) if isinstance(profile.get('positions'), dict) else '-'
|
||||||
|
player_id = str(profile.get('player_id', '-'))
|
||||||
|
table.add_row(name, str(target_ops), positions, player_id)
|
||||||
|
except Exception as e:
|
||||||
|
table.add_row(profile_path.stem, f"[red]Error: {e}[/red]", "", "")
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def preview(
|
||||||
|
character: str = typer.Argument(..., help="Character profile name (e.g., kalin_young)"),
|
||||||
|
):
|
||||||
|
"""Preview a custom character's calculated ratings."""
|
||||||
|
profile = load_profile(character)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]{profile['name']}[/bold] - CUSTOM PLAYER PREVIEW")
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
console.print(f"Hand: {profile.get('hand', 'R')}")
|
||||||
|
console.print(f"Target OPS: {profile.get('target_ops', 'N/A')}")
|
||||||
|
console.print(f"Player ID: {profile.get('player_id', 'Not created')}")
|
||||||
|
console.print(f"Cardset ID: {profile.get('cardset_id', 'N/A')}")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
# Display positions
|
||||||
|
if 'positions' in profile:
|
||||||
|
console.print("[bold]Defensive Positions:[/bold]")
|
||||||
|
for pos, stats in profile['positions'].items():
|
||||||
|
console.print(f" {pos}: Range {stats.get('range', '-')} / Error {stats.get('error', '-')} / Arm {stats.get('arm', '-')}")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
# Display ratings
|
||||||
|
if 'ratings' in profile:
|
||||||
|
console.print("[bold]Batting Ratings:[/bold]")
|
||||||
|
for vs_hand in ['vs_L', 'vs_R']:
|
||||||
|
if vs_hand in profile['ratings']:
|
||||||
|
ratings = profile['ratings'][vs_hand]
|
||||||
|
avg, obp, slg = calc_ops(ratings)
|
||||||
|
ops = obp + slg
|
||||||
|
total = verify_total(ratings)
|
||||||
|
|
||||||
|
console.print(f"\n VS {vs_hand[-1]}HP:")
|
||||||
|
console.print(f" AVG: {avg:.3f} OBP: {obp:.3f} SLG: {slg:.3f} OPS: {ops:.3f}")
|
||||||
|
console.print(f" Total Chances: {total:.2f} (must be 108.0)")
|
||||||
|
|
||||||
|
if abs(total - 108.0) > 0.01:
|
||||||
|
console.print(f" [red]WARNING: Total is not 108![/red]")
|
||||||
|
|
||||||
|
# Calculate combined OPS
|
||||||
|
if 'vs_L' in profile['ratings'] and 'vs_R' in profile['ratings']:
|
||||||
|
_, _, slg_l = calc_ops(profile['ratings']['vs_L'])
|
||||||
|
_, obp_l, _ = calc_ops(profile['ratings']['vs_L'])
|
||||||
|
_, _, slg_r = calc_ops(profile['ratings']['vs_R'])
|
||||||
|
_, obp_r, _ = calc_ops(profile['ratings']['vs_R'])
|
||||||
|
ops_l = obp_l + slg_l
|
||||||
|
ops_r = obp_r + slg_r
|
||||||
|
total_ops = (ops_l + ops_r + min(ops_l, ops_r)) / 3
|
||||||
|
console.print(f"\n [bold]Combined OPS: {total_ops:.3f}[/bold]")
|
||||||
|
|
||||||
|
# Display baserunning
|
||||||
|
if 'baserunning' in profile:
|
||||||
|
br = profile['baserunning']
|
||||||
|
console.print("\n[bold]Baserunning:[/bold]")
|
||||||
|
console.print(f" Steal Range: {br.get('steal_low', '-')}-{br.get('steal_high', '-')}")
|
||||||
|
console.print(f" Steal Jump: {br.get('steal_jump', '-')}")
|
||||||
|
console.print(f" Running: {br.get('running', '-')}")
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Preview only - not submitted to database[/yellow]")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def submit(
|
||||||
|
character: str = typer.Argument(..., help="Character profile name"),
|
||||||
|
dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Preview changes without saving"),
|
||||||
|
skip_s3: bool = typer.Option(False, "--skip-s3", help="Skip S3 upload after submit"),
|
||||||
|
):
|
||||||
|
"""Submit a custom character to the database."""
|
||||||
|
profile = load_profile(character)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]SUBMITTING {profile['name']}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
console.print("[yellow]DRY RUN - no changes will be made[/yellow]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
# Verify ratings
|
||||||
|
if 'ratings' in profile:
|
||||||
|
for vs_hand in ['vs_L', 'vs_R']:
|
||||||
|
if vs_hand in profile['ratings']:
|
||||||
|
total = verify_total(profile['ratings'][vs_hand])
|
||||||
|
if abs(total - 108.0) > 0.01:
|
||||||
|
console.print(f"[red]ERROR: {vs_hand} ratings total is {total:.2f}, must be 108.0[/red]")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
console.print("[green]Validation passed - ready to submit[/green]")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Import database functions
|
||||||
|
try:
|
||||||
|
from pd_cards.core.db import db_get, db_put, db_patch
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to old location during migration
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
from db_calls import db_get, db_put, db_patch
|
||||||
|
|
||||||
|
async def do_submit():
|
||||||
|
player_id = profile.get('player_id')
|
||||||
|
batting_card_id = profile.get('batting_card_id')
|
||||||
|
|
||||||
|
if not player_id or not batting_card_id:
|
||||||
|
console.print("[red]ERROR: Profile missing player_id or batting_card_id[/red]")
|
||||||
|
console.print("Create the player first, then update the profile with IDs.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# Update ratings
|
||||||
|
if 'ratings' in profile:
|
||||||
|
console.print("Updating ratings...")
|
||||||
|
ratings_list = []
|
||||||
|
for vs_hand, ratings in profile['ratings'].items():
|
||||||
|
hand = vs_hand[-1] # 'L' or 'R'
|
||||||
|
rating_data = {
|
||||||
|
'battingcard_id': batting_card_id,
|
||||||
|
'vs_hand': hand,
|
||||||
|
**ratings
|
||||||
|
}
|
||||||
|
ratings_list.append(rating_data)
|
||||||
|
|
||||||
|
await db_put('battingcardratings', payload={'ratings': ratings_list}, timeout=10)
|
||||||
|
console.print("[green] Ratings updated[/green]")
|
||||||
|
|
||||||
|
# Update positions
|
||||||
|
if 'positions' in profile:
|
||||||
|
console.print("Updating positions...")
|
||||||
|
positions_list = []
|
||||||
|
for pos, stats in profile['positions'].items():
|
||||||
|
positions_list.append({
|
||||||
|
'player_id': player_id,
|
||||||
|
'position': pos,
|
||||||
|
**stats
|
||||||
|
})
|
||||||
|
|
||||||
|
await db_put('cardpositions', payload={'positions': positions_list}, timeout=10)
|
||||||
|
console.print("[green] Positions updated[/green]")
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print(f"[green]Successfully updated {profile['name']}![/green]")
|
||||||
|
|
||||||
|
if not skip_s3:
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]S3 upload not yet implemented in CLI[/yellow]")
|
||||||
|
console.print("Run manually: python check_cards_and_upload.py")
|
||||||
|
|
||||||
|
asyncio.run(do_submit())
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def new(
|
||||||
|
name: str = typer.Option(..., "--name", "-n", help="Character name"),
|
||||||
|
hand: str = typer.Option("R", "--hand", "-h", help="Batting hand (L/R/S)"),
|
||||||
|
target_ops: float = typer.Option(0.750, "--target-ops", help="Target OPS"),
|
||||||
|
):
|
||||||
|
"""Create a new character profile template."""
|
||||||
|
profile_path = get_profile_path(name)
|
||||||
|
|
||||||
|
if profile_path.exists():
|
||||||
|
console.print(f"[red]Profile already exists:[/red] {profile_path}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# Create template
|
||||||
|
template = {
|
||||||
|
'name': name,
|
||||||
|
'hand': hand.upper(),
|
||||||
|
'target_ops': target_ops,
|
||||||
|
'cardset_id': None, # Set after creation
|
||||||
|
'player_id': None, # Set after creation
|
||||||
|
'batting_card_id': None, # Set after creation
|
||||||
|
|
||||||
|
'positions': {
|
||||||
|
'RF': {'range': 3, 'error': 7, 'arm': 0},
|
||||||
|
},
|
||||||
|
|
||||||
|
'baserunning': {
|
||||||
|
'steal_jump': 0.15,
|
||||||
|
'steal_high': 12,
|
||||||
|
'steal_low': 5,
|
||||||
|
'steal_auto': 0,
|
||||||
|
'running': 10,
|
||||||
|
'hit_and_run': 'C',
|
||||||
|
'bunting': 'C',
|
||||||
|
},
|
||||||
|
|
||||||
|
'ratings': {
|
||||||
|
'vs_L': {
|
||||||
|
'homerun': 2.0, 'bp_homerun': 1.0, 'triple': 0.5,
|
||||||
|
'double_three': 0.0, 'double_two': 4.0, 'double_pull': 2.0,
|
||||||
|
'single_two': 5.0, 'single_one': 5.0, 'single_center': 5.0, 'bp_single': 3.0,
|
||||||
|
'walk': 10.0, 'hbp': 1.0,
|
||||||
|
'strikeout': 20.0, 'lineout': 12.0, 'popout': 2.0,
|
||||||
|
'flyout_a': 2.0, 'flyout_bq': 2.0, 'flyout_lf_b': 4.0, 'flyout_rf_b': 4.0,
|
||||||
|
'groundout_a': 5.0, 'groundout_b': 10.0, 'groundout_c': 8.5,
|
||||||
|
'pull_rate': 0.30, 'center_rate': 0.40, 'slap_rate': 0.30,
|
||||||
|
},
|
||||||
|
'vs_R': {
|
||||||
|
'homerun': 2.0, 'bp_homerun': 1.0, 'triple': 0.5,
|
||||||
|
'double_three': 0.0, 'double_two': 4.0, 'double_pull': 2.0,
|
||||||
|
'single_two': 5.0, 'single_one': 5.0, 'single_center': 5.0, 'bp_single': 3.0,
|
||||||
|
'walk': 10.0, 'hbp': 1.0,
|
||||||
|
'strikeout': 20.0, 'lineout': 12.0, 'popout': 2.0,
|
||||||
|
'flyout_a': 2.0, 'flyout_bq': 2.0, 'flyout_lf_b': 4.0, 'flyout_rf_b': 4.0,
|
||||||
|
'groundout_a': 5.0, 'groundout_b': 10.0, 'groundout_c': 8.5,
|
||||||
|
'pull_rate': 0.30, 'center_rate': 0.40, 'slap_rate': 0.30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
PROFILES_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(profile_path, 'w') as f:
|
||||||
|
yaml.dump(template, f, default_flow_style=False, sort_keys=False)
|
||||||
|
|
||||||
|
console.print(f"[green]Created profile:[/green] {profile_path}")
|
||||||
|
console.print()
|
||||||
|
console.print("Next steps:")
|
||||||
|
console.print(f" 1. Edit the profile: {profile_path}")
|
||||||
|
console.print(f" 2. Preview: pd-cards custom preview {name.lower().replace(' ', '_')}")
|
||||||
|
console.print(f" 3. Submit: pd-cards custom submit {name.lower().replace(' ', '_')}")
|
||||||
51
pd_cards/commands/live_series.py
Normal file
51
pd_cards/commands/live_series.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Live series card update commands.
|
||||||
|
|
||||||
|
Commands for generating cards from current season FanGraphs/Baseball Reference data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
app = typer.Typer(no_args_is_help=True)
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def update(
|
||||||
|
cardset: str = typer.Option(..., "--cardset", "-c", help="Target cardset name (e.g., '2025 Live')"),
|
||||||
|
season: int = typer.Option(None, "--season", "-s", help="Season year (defaults to current)"),
|
||||||
|
games_played: int = typer.Option(None, "--games", "-g", help="Number of games played (for prorating)"),
|
||||||
|
ignore_limits: bool = typer.Option(False, "--ignore-limits", help="Ignore minimum PA/TBF requirements"),
|
||||||
|
dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Preview without saving to database"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update live series cards from FanGraphs/Baseball Reference data.
|
||||||
|
|
||||||
|
Reads CSV files from data-input/ and generates batting/pitching cards.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]LIVE SERIES UPDATE - {cardset}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
console.print("[yellow]DRY RUN - no changes will be made[/yellow]")
|
||||||
|
|
||||||
|
# TODO: Migrate logic from live_series_update.py
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(" python live_series_update.py")
|
||||||
|
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def status(
|
||||||
|
cardset: str = typer.Option(None, "--cardset", "-c", help="Filter by cardset name"),
|
||||||
|
):
|
||||||
|
"""Show status of live series cardsets."""
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented[/yellow]")
|
||||||
107
pd_cards/commands/retrosheet.py
Normal file
107
pd_cards/commands/retrosheet.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
Retrosheet historical data processing commands.
|
||||||
|
|
||||||
|
Commands for generating cards from historical Retrosheet play-by-play data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
app = typer.Typer(no_args_is_help=True)
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def process(
|
||||||
|
year: int = typer.Argument(..., help="Season year to process (e.g., 2005)"),
|
||||||
|
cardset_id: int = typer.Option(..., "--cardset-id", "-c", help="Target cardset ID"),
|
||||||
|
description: str = typer.Option("Live", "--description", "-d", help="Player description (e.g., 'Live', 'June PotM')"),
|
||||||
|
start_date: Optional[str] = typer.Option(None, "--start", help="Start date YYYYMMDD (defaults to season start)"),
|
||||||
|
end_date: Optional[str] = typer.Option(None, "--end", help="End date YYYYMMDD (defaults to season end)"),
|
||||||
|
events_file: Optional[Path] = typer.Option(None, "--events", "-e", help="Retrosheet events CSV file"),
|
||||||
|
dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Preview without saving to database"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Process Retrosheet data and create player cards.
|
||||||
|
|
||||||
|
Generates batting and pitching cards from historical play-by-play data.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]RETROSHEET PROCESSING - {year}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
console.print(f"Cardset ID: {cardset_id}")
|
||||||
|
console.print(f"Description: {description}")
|
||||||
|
if start_date:
|
||||||
|
console.print(f"Start Date: {start_date}")
|
||||||
|
if end_date:
|
||||||
|
console.print(f"End Date: {end_date}")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
console.print("[yellow]DRY RUN - no changes will be made[/yellow]")
|
||||||
|
|
||||||
|
# TODO: Migrate logic from retrosheet_data.py
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(" python retrosheet_data.py")
|
||||||
|
console.print()
|
||||||
|
console.print("[dim]Configure settings in retrosheet_data.py before running[/dim]")
|
||||||
|
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def arms(
|
||||||
|
year: int = typer.Argument(..., help="Season year"),
|
||||||
|
events_file: Path = typer.Option(..., "--events", "-e", help="Retrosheet events CSV file"),
|
||||||
|
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output CSV file"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generate outfield arm ratings from Retrosheet data.
|
||||||
|
|
||||||
|
Analyzes play-by-play events to calculate OF arm strength ratings.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]OUTFIELD ARM RATINGS - {year}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
if output is None:
|
||||||
|
output = Path(f"data-output/retrosheet_arm_ratings_{year}.csv")
|
||||||
|
|
||||||
|
console.print(f"Events file: {events_file}")
|
||||||
|
console.print(f"Output: {output}")
|
||||||
|
|
||||||
|
# TODO: Migrate logic from generate_arm_ratings_csv.py
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(f" python generate_arm_ratings_csv.py --year {year} --events {events_file}")
|
||||||
|
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def validate(
|
||||||
|
cardset_id: int = typer.Argument(..., help="Cardset ID to validate"),
|
||||||
|
api_url: str = typer.Option("https://pd.manticorum.com/api", "--api", help="API URL"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Validate positions for a cardset.
|
||||||
|
|
||||||
|
Checks for anomalous DH counts and missing outfield positions.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]POSITION VALIDATION - Cardset {cardset_id}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
# TODO: Migrate logic from scripts/check_positions.sh
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(f" ./scripts/check_positions.sh {cardset_id} {api_url}")
|
||||||
|
|
||||||
|
raise typer.Exit(0)
|
||||||
73
pd_cards/commands/scouting.py
Normal file
73
pd_cards/commands/scouting.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
Scouting report generation commands.
|
||||||
|
|
||||||
|
Commands for generating scouting reports and ratings comparisons.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
app = typer.Typer(no_args_is_help=True)
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def batters(
|
||||||
|
cardset: str = typer.Option(..., "--cardset", "-c", help="Cardset name"),
|
||||||
|
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output directory"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generate batting scouting reports.
|
||||||
|
|
||||||
|
Creates CSV files with batting ratings and comparisons.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]BATTING SCOUTING REPORT - {cardset}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
# TODO: Migrate logic from scouting_batters.py
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(" python scouting_batters.py")
|
||||||
|
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def pitchers(
|
||||||
|
cardset: str = typer.Option(..., "--cardset", "-c", help="Cardset name"),
|
||||||
|
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output directory"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generate pitching scouting reports.
|
||||||
|
|
||||||
|
Creates CSV files with pitching ratings and comparisons.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]PITCHING SCOUTING REPORT - {cardset}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
# TODO: Migrate logic from scouting_pitchers.py
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(" python scouting_pitchers.py")
|
||||||
|
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def upload(
|
||||||
|
cardset: str = typer.Option(..., "--cardset", "-c", help="Cardset name"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Upload scouting reports to database.
|
||||||
|
|
||||||
|
Uploads generated scouting CSV data to Paper Dynasty API.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented[/yellow]")
|
||||||
91
pd_cards/commands/upload.py
Normal file
91
pd_cards/commands/upload.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
Card image upload commands.
|
||||||
|
|
||||||
|
Commands for uploading card images to AWS S3.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
app = typer.Typer(no_args_is_help=True)
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def s3(
|
||||||
|
cardset: str = typer.Option(..., "--cardset", "-c", help="Cardset name to upload"),
|
||||||
|
start_id: Optional[int] = typer.Option(None, "--start-id", help="Player ID to start from (for resuming)"),
|
||||||
|
limit: Optional[int] = typer.Option(None, "--limit", "-l", help="Limit number of cards to process"),
|
||||||
|
html: bool = typer.Option(False, "--html", help="Upload HTML preview cards instead of PNG"),
|
||||||
|
dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Preview without uploading"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Upload card images to AWS S3.
|
||||||
|
|
||||||
|
Fetches card images from Paper Dynasty API and uploads to S3 bucket.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]S3 UPLOAD - {cardset}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
console.print(f"Cardset: {cardset}")
|
||||||
|
if start_id:
|
||||||
|
console.print(f"Starting from player ID: {start_id}")
|
||||||
|
if limit:
|
||||||
|
console.print(f"Limit: {limit} cards")
|
||||||
|
if html:
|
||||||
|
console.print("Mode: HTML preview cards")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
console.print("[yellow]DRY RUN - no uploads will be made[/yellow]")
|
||||||
|
|
||||||
|
# TODO: Migrate logic from check_cards_and_upload.py
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(" python check_cards_and_upload.py")
|
||||||
|
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def migrate(
|
||||||
|
cardset: str = typer.Option(..., "--cardset", "-c", help="Cardset name"),
|
||||||
|
dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Preview without uploading"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Migrate all cards for a cardset to S3.
|
||||||
|
|
||||||
|
Bulk upload for initial cardset migration.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("=" * 70)
|
||||||
|
console.print(f"[bold]S3 MIGRATION - {cardset}[/bold]")
|
||||||
|
console.print("=" * 70)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
console.print("[yellow]DRY RUN - no uploads will be made[/yellow]")
|
||||||
|
|
||||||
|
# TODO: Migrate logic from migrate_all_cards_to_s3.py
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(" python migrate_all_cards_to_s3.py")
|
||||||
|
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def refresh(
|
||||||
|
cardset: str = typer.Option(..., "--cardset", "-c", help="Cardset name"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Refresh card images for a cardset.
|
||||||
|
|
||||||
|
Re-generates and re-uploads card images.
|
||||||
|
"""
|
||||||
|
console.print()
|
||||||
|
console.print("[yellow]Not yet implemented - run legacy script:[/yellow]")
|
||||||
|
console.print(" python refresh_cards.py")
|
||||||
1
pd_cards/core/__init__.py
Normal file
1
pd_cards/core/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Core card creation modules."""
|
||||||
1
pd_cards/custom/__init__.py
Normal file
1
pd_cards/custom/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Custom card creation system with YAML profiles."""
|
||||||
88
pd_cards/custom/profiles/kalin_young.yaml
Normal file
88
pd_cards/custom/profiles/kalin_young.yaml
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# Kalin Young - Custom Player
|
||||||
|
# Target OPS: 0.880
|
||||||
|
# Method: Keep XBH identical to current, boost OPS via walks/singles equally
|
||||||
|
# Pull rate: vL=33%, vR=25%
|
||||||
|
|
||||||
|
name: Kalin Young
|
||||||
|
hand: R
|
||||||
|
target_ops: 0.880
|
||||||
|
cardset_id: 29
|
||||||
|
player_id: 13009
|
||||||
|
batting_card_id: 6072
|
||||||
|
|
||||||
|
positions:
|
||||||
|
RF:
|
||||||
|
range: 2
|
||||||
|
error: 6
|
||||||
|
arm: 0
|
||||||
|
fielding_pct: null
|
||||||
|
LF:
|
||||||
|
range: 4
|
||||||
|
error: 6
|
||||||
|
arm: 0
|
||||||
|
fielding_pct: null
|
||||||
|
|
||||||
|
baserunning:
|
||||||
|
steal_jump: 0.22222
|
||||||
|
steal_high: 15
|
||||||
|
steal_low: 7
|
||||||
|
steal_auto: 0
|
||||||
|
running: 13
|
||||||
|
hit_and_run: C
|
||||||
|
bunting: C
|
||||||
|
|
||||||
|
ratings:
|
||||||
|
# Updated ratings from submit_kalin_young.py
|
||||||
|
vs_L:
|
||||||
|
homerun: 2.05
|
||||||
|
bp_homerun: 2.00
|
||||||
|
triple: 0.00
|
||||||
|
double_three: 0.00
|
||||||
|
double_two: 6.00
|
||||||
|
double_pull: 2.35
|
||||||
|
single_two: 5.05
|
||||||
|
single_one: 5.10
|
||||||
|
single_center: 5.80 # +1.55 from original
|
||||||
|
bp_single: 5.00
|
||||||
|
walk: 16.05 # +1.55 from original
|
||||||
|
hbp: 1.00
|
||||||
|
strikeout: 15.40 # -3.10 from original
|
||||||
|
lineout: 15.00
|
||||||
|
popout: 0.00
|
||||||
|
flyout_a: 0.00
|
||||||
|
flyout_bq: 0.75
|
||||||
|
flyout_lf_b: 3.65
|
||||||
|
flyout_rf_b: 3.95
|
||||||
|
groundout_a: 3.95
|
||||||
|
groundout_b: 10.00
|
||||||
|
groundout_c: 4.90
|
||||||
|
pull_rate: 0.33
|
||||||
|
center_rate: 0.37
|
||||||
|
slap_rate: 0.30
|
||||||
|
|
||||||
|
vs_R:
|
||||||
|
homerun: 2.10
|
||||||
|
bp_homerun: 1.00
|
||||||
|
triple: 1.25
|
||||||
|
double_three: 0.00
|
||||||
|
double_two: 6.35
|
||||||
|
double_pull: 1.80
|
||||||
|
single_two: 5.40
|
||||||
|
single_one: 4.95
|
||||||
|
single_center: 6.05 # +1.55 from original
|
||||||
|
bp_single: 5.00
|
||||||
|
walk: 14.55 # +1.55 from original
|
||||||
|
hbp: 2.00
|
||||||
|
strikeout: 17.90 # -3.10 from original
|
||||||
|
lineout: 12.00
|
||||||
|
popout: 1.00
|
||||||
|
flyout_a: 0.00
|
||||||
|
flyout_bq: 0.50
|
||||||
|
flyout_lf_b: 4.10
|
||||||
|
flyout_rf_b: 4.00
|
||||||
|
groundout_a: 4.00
|
||||||
|
groundout_b: 5.00
|
||||||
|
groundout_c: 9.05
|
||||||
|
pull_rate: 0.25
|
||||||
|
center_rate: 0.40
|
||||||
|
slap_rate: 0.35
|
||||||
56
pyproject.toml
Normal file
56
pyproject.toml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
[project]
|
||||||
|
name = "pd-cards"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Paper Dynasty card creation CLI"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
# CLI
|
||||||
|
"typer[all]>=0.12.0",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"rich>=13.0",
|
||||||
|
|
||||||
|
# Data processing
|
||||||
|
"pandas>=2.2.0",
|
||||||
|
"numpy>=2.0.0",
|
||||||
|
"polars>=1.0.0",
|
||||||
|
|
||||||
|
# Web/API
|
||||||
|
"aiohttp>=3.10.0",
|
||||||
|
"requests>=2.32.0",
|
||||||
|
|
||||||
|
# Database
|
||||||
|
"peewee>=3.17.0",
|
||||||
|
|
||||||
|
# Baseball data
|
||||||
|
"pybaseball>=2.2.7",
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
"pydantic>=2.9.0",
|
||||||
|
|
||||||
|
# AWS
|
||||||
|
"boto3>=1.35.0",
|
||||||
|
|
||||||
|
# Scraping
|
||||||
|
"beautifulsoup4>=4.12.0",
|
||||||
|
"lxml>=5.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"pytest-asyncio>=0.24.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
pd-cards = "pd_cards.cli:app"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["pd_cards"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
Loading…
Reference in New Issue
Block a user