diff --git a/pd_cards/commands/custom.py b/pd_cards/commands/custom.py index 01d0e26..86ed164 100644 --- a/pd_cards/commands/custom.py +++ b/pd_cards/commands/custom.py @@ -6,6 +6,7 @@ Supports both batters and pitchers with their respective schemas. """ import asyncio +from datetime import datetime from pathlib import Path from typing import Optional, Literal @@ -282,6 +283,7 @@ 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"), + create: bool = typer.Option(False, "--create", "-c", help="Create new player in database (requires cardset_id in profile)"), ): """Submit a custom character to the database.""" profile = load_profile(character) @@ -289,7 +291,8 @@ def submit( console.print() console.print("=" * 70) - console.print(f"[bold]SUBMITTING {profile['name']} ({player_type})[/bold]") + action = "CREATING" if create else "SUBMITTING" + console.print(f"[bold]{action} {profile['name']} ({player_type})[/bold]") console.print("=" * 70) if dry_run: @@ -307,20 +310,202 @@ def submit( console.print(f"[red]ERROR: {vs_hand} ratings total is {total:.2f}, must be 108.0[/red]") raise typer.Exit(1) + # Validate create requirements + if create: + if not profile.get('cardset_id'): + console.print("[red]ERROR: cardset_id required in profile for --create[/red]") + raise typer.Exit(1) + if profile.get('player_id'): + console.print("[red]ERROR: player_id already set - use submit without --create to update[/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 + from pd_cards.core.db import db_get, db_put, db_patch, db_post 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 + from db_calls import db_get, db_put, db_patch, db_post + + async def do_create(): + """Create a new player in the database.""" + # Parse name + name_parts = profile['name'].split() + name_first = name_parts[0] + name_last = ' '.join(name_parts[1:]) if len(name_parts) > 1 else name_parts[0] + + # Generate bbref_id for custom player (with timestamp to avoid conflicts) + timestamp = int(datetime.now().timestamp()) + bbref_id = f"custom_{name_last.lower().replace(' ', '')}{name_first[0].lower()}{timestamp}" + + cardset_id = profile['cardset_id'] + hand = profile.get('hand', 'R') + + console.print(f"Name: {name_first} {name_last}") + console.print(f"BBRef ID: {bbref_id}") + console.print(f"Cardset: {cardset_id}") + console.print() + + # Step 1: Create Player record (custom players don't need MLBPlayer) + console.print("Creating Player record...") + now = datetime.now() + release_date = f"{now.year}-{now.month}-{now.day}" + + # Get first position for pos_1 + positions = list(profile.get('positions', {}).keys()) + pos_1 = positions[0] if positions else 'DH' + + player_payload = { + 'p_name': f"{name_first} {name_last}", + 'bbref_id': bbref_id, + 'fangraphs_id': 0, + 'mlbam_id': 0, + 'retrosheet_id': '', + 'hand': hand, + 'mlb_team_id': 1, + 'franchise_id': 1, # Default franchise + 'cardset_id': cardset_id, + 'description': 'Custom', + 'is_custom': True, + # Required fields + 'cost': 100, # Default cost + 'image': '', # Will be patched after + 'mlbclub': 'Custom Ballplayers', + 'franchise': 'Custom Ballplayers', + 'set_num': 9999, + 'rarity_id': 5, # Common + 'pos_1': pos_1, + } + + new_player = await db_post('players', payload=player_payload) + player_id = new_player['player_id'] + console.print(f"[green] Created Player (ID: {player_id})[/green]") + + # Step 3: Patch Player with image URL + card_type = 'batting' if player_type == 'batter' else 'pitching' + image_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/{card_type}card?d={release_date}" + await db_patch('players', object_id=player_id, params=[('image', image_url)]) + console.print("[green] Updated Player with image URL[/green]") + + # Step 4: Create Card + if player_type == 'batter': + console.print("Creating BattingCard...") + baserunning = profile.get('baserunning', {}) + batting_card_payload = { + 'cards': [{ + 'player_id': player_id, + 'key_bbref': bbref_id, + 'key_fangraphs': 0, + 'key_mlbam': 0, + 'key_retro': '', + 'name_first': name_first, + 'name_last': name_last, + 'steal_low': baserunning.get('steal_low', 5), + 'steal_high': baserunning.get('steal_high', 12), + 'steal_auto': 1 if baserunning.get('steal_auto') else 0, + 'steal_jump': baserunning.get('steal_jump', 0.15), + 'hit_and_run': baserunning.get('hit_and_run', 'C'), + 'running': baserunning.get('running', 10), + 'bunting': baserunning.get('bunting', 'C'), + 'hand': hand, + }] + } + await db_put('battingcards', payload=batting_card_payload, timeout=10) + + # Get the card ID + bc_query = await db_get('battingcards', params=[('player_id', player_id)]) + card_id = bc_query['cards'][0]['id'] + card_id_field = 'battingcard_id' + ratings_endpoint = 'battingcardratings' + console.print(f"[green] Created BattingCard (ID: {card_id})[/green]") + else: + console.print("Creating PitchingCard...") + pitching = profile.get('pitching', {}) + pitching_card_payload = { + 'cards': [{ + 'player_id': player_id, + 'key_bbref': bbref_id, + 'key_fangraphs': 0, + 'key_mlbam': 0, + 'key_retro': '', + 'name_first': name_first, + 'name_last': name_last, + 'hand': hand, + 'starter_rating': pitching.get('starter_rating', 5), + 'relief_rating': pitching.get('relief_rating', 5), + 'closer_rating': pitching.get('closer_rating'), + }] + } + await db_put('pitchingcards', payload=pitching_card_payload, timeout=10) + + # Get the card ID + pc_query = await db_get('pitchingcards', params=[('player_id', player_id)]) + card_id = pc_query['cards'][0]['id'] + card_id_field = 'pitchingcard_id' + ratings_endpoint = 'pitchingcardratings' + console.print(f"[green] Created PitchingCard (ID: {card_id})[/green]") + + # Step 5: Create ratings + console.print("Creating ratings...") + ratings_list = [] + for vs_hand, ratings in profile['ratings'].items(): + hand_char = vs_hand[-1] # 'L' or 'R' + rating_data = { + card_id_field: card_id, + 'vs_hand': hand_char, + **ratings + } + ratings_list.append(rating_data) + + await db_put(ratings_endpoint, payload={'ratings': ratings_list}, timeout=10) + console.print("[green] Created ratings[/green]") + + # Step 6: Create positions + console.print("Creating positions...") + positions_list = [] + for pos, stats in profile.get('positions', {}).items(): + if isinstance(stats, dict): + positions_list.append({ + 'player_id': player_id, + 'position': pos, + **stats + }) + else: + positions_list.append({ + 'player_id': player_id, + 'position': pos, + }) + + if positions_list: + await db_put('cardpositions', payload={'positions': positions_list}, timeout=10) + console.print(f"[green] Created {len(positions_list)} position(s)[/green]") + + # Step 7: Update YAML profile with IDs + console.print("Updating profile with IDs...") + profile['player_id'] = player_id + if player_type == 'batter': + profile['batting_card_id'] = card_id + else: + profile['pitching_card_id'] = card_id + + profile_path = get_profile_path(character) + with open(profile_path, 'w') as f: + yaml.dump(profile, f, default_flow_style=False, sort_keys=False) + console.print(f"[green] Updated {profile_path.name}[/green]") + + console.print() + console.print(f"[bold green]Successfully created {profile['name']}![/bold green]") + console.print(f"Player ID: {player_id}") + console.print(f"Card ID: {card_id}") + console.print(f"URL: https://pd.manticorum.com/api/v2/players/{player_id}/{card_type}card?d={release_date}") async def do_submit(): + """Update an existing player in the database.""" player_id = profile.get('player_id') if player_type == 'pitcher': @@ -335,7 +520,7 @@ def submit( if not player_id or not card_id: id_field = 'pitching_card_id' if player_type == 'pitcher' else 'batting_card_id' console.print(f"[red]ERROR: Profile missing player_id or {id_field}[/red]") - console.print("Create the player first, then update the profile with IDs.") + console.print("Use --create to create a new player, or add IDs to the profile.") raise typer.Exit(1) # Update ratings @@ -382,7 +567,10 @@ def submit( 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()) + if create: + asyncio.run(do_create()) + else: + asyncio.run(do_submit()) @app.command() diff --git a/pd_cards/custom/profiles/adm_ball_traits.yaml b/pd_cards/custom/profiles/adm_ball_traits.yaml new file mode 100644 index 0000000..54dcb17 --- /dev/null +++ b/pd_cards/custom/profiles/adm_ball_traits.yaml @@ -0,0 +1,75 @@ +name: Adm Ball Traits +player_type: batter +hand: R +target_ops: 0.8 +cardset_id: 29 +player_id: 13361 +batting_card_id: 6244 +positions: + 3B: + range: 2 + error: 8 + SS: + range: 3 + error: 10 +baserunning: + steal_jump: 0.083333 + steal_high: 15 + steal_low: 9 + steal_auto: true + running: 13 + hit_and_run: B + bunting: B +ratings: + vs_L: + homerun: 0.0 + bp_homerun: 1.0 + triple: 2.75 + double_three: 0.0 + double_two: 2.0 + double_pull: 3.75 + single_two: 6.5 + single_one: 5.0 + single_center: 7.75 + bp_single: 5.0 + walk: 12.0 + hbp: 4.0 + strikeout: 16.0 + lineout: 4.0 + popout: 2.0 + flyout_a: 0.0 + flyout_bq: 2.0 + flyout_lf_b: 4.0 + flyout_rf_b: 4.0 + groundout_a: 9.0 + groundout_b: 11.0 + groundout_c: 6.25 + pull_rate: 0.2 + center_rate: 0.5 + slap_rate: 0.3 + vs_R: + homerun: 0.0 + bp_homerun: 1.0 + triple: 0.5 + double_three: 0.0 + double_two: 4.5 + double_pull: 4.5 + single_two: 5.0 + single_one: 8.0 + single_center: 5.5 + bp_single: 5.0 + walk: 11.0 + hbp: 1.0 + strikeout: 14.5 + lineout: 2.0 + popout: 2.0 + flyout_a: 0.0 + flyout_bq: 3.0 + flyout_lf_b: 5.0 + flyout_rf_b: 8.0 + groundout_a: 5.0 + groundout_b: 12.0 + groundout_c: 10.5 + pull_rate: 0.18 + center_rate: 0.42 + slap_rate: 0.4