From 2e28d29ced05913b0635913ef0dff7d8bd96d7f0 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 18 Dec 2025 16:08:32 -0600 Subject: [PATCH] Add pd-cards CLI skeleton with Typer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pd_cards/__init__.py | 3 + pd_cards/cli.py | 47 +++ pd_cards/commands/__init__.py | 1 + pd_cards/commands/custom.py | 334 ++++++++++++++++++++++ pd_cards/commands/live_series.py | 51 ++++ pd_cards/commands/retrosheet.py | 107 +++++++ pd_cards/commands/scouting.py | 73 +++++ pd_cards/commands/upload.py | 91 ++++++ pd_cards/core/__init__.py | 1 + pd_cards/custom/__init__.py | 1 + pd_cards/custom/profiles/kalin_young.yaml | 88 ++++++ pyproject.toml | 56 ++++ 12 files changed, 853 insertions(+) create mode 100644 pd_cards/__init__.py create mode 100644 pd_cards/cli.py create mode 100644 pd_cards/commands/__init__.py create mode 100644 pd_cards/commands/custom.py create mode 100644 pd_cards/commands/live_series.py create mode 100644 pd_cards/commands/retrosheet.py create mode 100644 pd_cards/commands/scouting.py create mode 100644 pd_cards/commands/upload.py create mode 100644 pd_cards/core/__init__.py create mode 100644 pd_cards/custom/__init__.py create mode 100644 pd_cards/custom/profiles/kalin_young.yaml create mode 100644 pyproject.toml diff --git a/pd_cards/__init__.py b/pd_cards/__init__.py new file mode 100644 index 0000000..adf03dd --- /dev/null +++ b/pd_cards/__init__.py @@ -0,0 +1,3 @@ +"""Paper Dynasty Card Creation CLI.""" + +__version__ = "0.1.0" diff --git a/pd_cards/cli.py b/pd_cards/cli.py new file mode 100644 index 0000000..d498fe9 --- /dev/null +++ b/pd_cards/cli.py @@ -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() diff --git a/pd_cards/commands/__init__.py b/pd_cards/commands/__init__.py new file mode 100644 index 0000000..ce7b632 --- /dev/null +++ b/pd_cards/commands/__init__.py @@ -0,0 +1 @@ +"""CLI subcommands for pd-cards.""" diff --git a/pd_cards/commands/custom.py b/pd_cards/commands/custom.py new file mode 100644 index 0000000..c09f216 --- /dev/null +++ b/pd_cards/commands/custom.py @@ -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(' ', '_')}") diff --git a/pd_cards/commands/live_series.py b/pd_cards/commands/live_series.py new file mode 100644 index 0000000..ba99554 --- /dev/null +++ b/pd_cards/commands/live_series.py @@ -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]") diff --git a/pd_cards/commands/retrosheet.py b/pd_cards/commands/retrosheet.py new file mode 100644 index 0000000..0c37bac --- /dev/null +++ b/pd_cards/commands/retrosheet.py @@ -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) diff --git a/pd_cards/commands/scouting.py b/pd_cards/commands/scouting.py new file mode 100644 index 0000000..0312be8 --- /dev/null +++ b/pd_cards/commands/scouting.py @@ -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]") diff --git a/pd_cards/commands/upload.py b/pd_cards/commands/upload.py new file mode 100644 index 0000000..4f38552 --- /dev/null +++ b/pd_cards/commands/upload.py @@ -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") diff --git a/pd_cards/core/__init__.py b/pd_cards/core/__init__.py new file mode 100644 index 0000000..1c6cd48 --- /dev/null +++ b/pd_cards/core/__init__.py @@ -0,0 +1 @@ +"""Core card creation modules.""" diff --git a/pd_cards/custom/__init__.py b/pd_cards/custom/__init__.py new file mode 100644 index 0000000..9abca28 --- /dev/null +++ b/pd_cards/custom/__init__.py @@ -0,0 +1 @@ +"""Custom card creation system with YAML profiles.""" diff --git a/pd_cards/custom/profiles/kalin_young.yaml b/pd_cards/custom/profiles/kalin_young.yaml new file mode 100644 index 0000000..b60ece6 --- /dev/null +++ b/pd_cards/custom/profiles/kalin_young.yaml @@ -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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..79e6311 --- /dev/null +++ b/pyproject.toml @@ -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"]