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:
Cal Corum 2025-12-18 16:08:32 -06:00
parent 1c39f7f8b3
commit 2e28d29ced
12 changed files with 853 additions and 0 deletions

3
pd_cards/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""Paper Dynasty Card Creation CLI."""
__version__ = "0.1.0"

47
pd_cards/cli.py Normal file
View 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()

View File

@ -0,0 +1 @@
"""CLI subcommands for pd-cards."""

334
pd_cards/commands/custom.py Normal file
View 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(' ', '_')}")

View 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]")

View 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)

View 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]")

View 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")

View File

@ -0,0 +1 @@
"""Core card creation modules."""

View File

@ -0,0 +1 @@
"""Custom card creation system with YAML profiles."""

View 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
View 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"]