# PD API Models Specification **Purpose**: Pydantic models that exactly match Paper Dynasty API responses **File**: `backend/app/models/api_models.py` (PD section) --- ## Overview PD player data comes from **3 separate API calls**: 1. `/api/v2/players/:player_id` - Basic player info 2. `/api/v2/battingcardratings/player/:player_id` - Batting outcome probabilities 3. `/api/v2/pitchingcardratings/player/:player_id` - Pitching outcome probabilities These models match the API responses **exactly** for type-safe deserialization. --- ## Base URL ```python PD_API_BASE_URL = "https://pd.manticorum.com/" ``` --- ## Model Hierarchy ``` PdPlayerApi (basic player data) ├─ cardset: PdCardsetApi ├─ rarity: PdRarityApi └─ mlbplayer: PdMlbPlayerApi PdBattingRatingsResponseApi (ratings response) └─ ratings: List[PdBattingRatingApi] └─ battingcard: PdBattingCardApi └─ player: PdPlayerApi (nested, same structure) PdPitchingRatingsResponseApi (ratings response) └─ ratings: List[PdPitchingRatingApi] └─ pitchingcard: PdPitchingCardApi └─ player: PdPlayerApi (nested, same structure) ``` --- ## Nested Models (Shared) ### PdCardsetApi **JSON Example**: ```json { "id": 20, "name": "1998 Season", "description": "Cards based on the 1998 MLB season", "event": null, "for_purchase": true, "total_cards": 1, "in_packs": true, "ranked_legal": true } ``` **Pydantic Model**: ```python class PdCardsetApi(BaseModel): """PD Cardset (nested in player response)""" id: int name: str description: str event: Optional[str] = None for_purchase: bool total_cards: int in_packs: bool ranked_legal: bool ``` ### PdRarityApi **JSON Example**: ```json { "id": 1, "value": 5, "name": "MVP", "color": "56f1fa" } ``` **Pydantic Model**: ```python class PdRarityApi(BaseModel): """PD Rarity tier (nested in player response)""" id: int value: int # 0-5 (Replacement to MVP) name: str # "Replacement", "Bench", "Starter", "All-Star", "Superstar", "MVP" color: str # Hex color without # ``` ### PdMlbPlayerApi **JSON Example**: ```json { "id": 4180, "first_name": "Matt", "last_name": "Karchner", "key_fangraphs": 1006697, "key_bbref": "karchma01", "key_retro": "karcm001", "key_mlbam": 116840, "offense_col": 2 } ``` **Pydantic Model**: ```python class PdMlbPlayerApi(BaseModel): """Real MLB player data (nested in player response)""" id: int first_name: str last_name: str key_fangraphs: int key_bbref: str key_retro: str key_mlbam: int offense_col: int # 1 or 2 (which offense column on card) ``` ### PdPaperdexEntryApi **JSON Example**: ```json { "id": 40108, "team": 69, "player": 11223, "created": 1742675422723 } ``` **Pydantic Model**: ```python class PdPaperdexEntryApi(BaseModel): """Single paperdex entry (ownership record)""" id: int team: int player: int created: int # Timestamp ``` ### PdPaperdexApi **JSON Example**: ```json { "count": 1, "paperdex": [ { "id": 40108, "team": 69, "player": 11223, "created": 1742675422723 } ] } ``` **Pydantic Model**: ```python class PdPaperdexApi(BaseModel): """Paperdex collection data (nested in player response)""" count: int paperdex: List[PdPaperdexEntryApi] ``` --- ## Player Model ### PdPlayerApi **API Endpoint**: `GET /api/v2/players/:player_id?csv=false` **JSON Example**: ```json { "player_id": 11223, "p_name": "Matt Karchner", "cost": 1266, "image": "https://pd.manticorum.com/api/v2/players/11223/pitchingcard?d=2025-4-14", "image2": null, "mlbclub": "Chicago White Sox", "franchise": "Chicago White Sox", "cardset": { "id": 20, "name": "1998 Season", "description": "Cards based on the 1998 MLB season", "event": null, "for_purchase": true, "total_cards": 1, "in_packs": true, "ranked_legal": true }, "set_num": 1006697, "rarity": { "id": 1, "value": 5, "name": "MVP", "color": "56f1fa" }, "pos_1": "RP", "pos_2": "CP", "pos_3": null, "pos_4": null, "pos_5": null, "pos_6": null, "pos_7": null, "pos_8": null, "headshot": "https://www.baseball-reference.com/req/202412180/images/headshots/5/506ce471_sabr.jpg", "vanity_card": null, "strat_code": null, "bbref_id": "karchma01", "fangr_id": "1006697", "description": "1998 Season", "quantity": 999, "mlbplayer": { "id": 4180, "first_name": "Matt", "last_name": "Karchner", "key_fangraphs": 1006697, "key_bbref": "karchma01", "key_retro": "karcm001", "key_mlbam": 116840, "offense_col": 2 }, "paperdex": { "count": 1, "paperdex": [ { "id": 40108, "team": 69, "player": 11223, "created": 1742675422723 } ] } } ``` **Pydantic Model**: ```python class PdPlayerApi(BaseModel): """PD Player API response""" player_id: int p_name: str cost: int image: str image2: Optional[str] = None mlbclub: str franchise: str cardset: PdCardsetApi set_num: int rarity: PdRarityApi # Positions (up to 8) pos_1: Optional[str] = None pos_2: Optional[str] = None pos_3: Optional[str] = None pos_4: Optional[str] = None pos_5: Optional[str] = None pos_6: Optional[str] = None pos_7: Optional[str] = None pos_8: Optional[str] = None headshot: Optional[str] = None vanity_card: Optional[str] = None strat_code: Optional[str] = None bbref_id: str fangr_id: str description: str quantity: int mlbplayer: PdMlbPlayerApi paperdex: PdPaperdexApi ``` --- ## Batting Card Models ### PdBattingCardApi **JSON Example** (nested in ratings response): ```json { "id": 4871, "player": { ... }, // Full PdPlayerApi structure "variant": 0, "steal_low": 8, "steal_high": 11, "steal_auto": true, "steal_jump": 0.25, "bunting": "C", "hit_and_run": "D", "running": 13, "offense_col": 1, "hand": "R" } ``` **Pydantic Model**: ```python class PdBattingCardApi(BaseModel): """PD Batting Card (nested in rating response)""" id: int player: PdPlayerApi variant: int steal_low: int steal_high: int steal_auto: bool steal_jump: float bunting: str # A, B, C, D, E grades hit_and_run: str # A, B, C, D, E grades running: int offense_col: int # 1 or 2 hand: str # R, L, S (switch) ``` ### PdBattingRatingApi **JSON Example** (single rating, vs L or vs R): ```json { "id": 9703, "battingcard": { ... }, // PdBattingCardApi "vs_hand": "L", "pull_rate": 0.29379, "center_rate": 0.41243, "slap_rate": 0.29378, "homerun": 0.0, "bp_homerun": 2.0, "triple": 1.4, "double_three": 0.0, "double_two": 5.1, "double_pull": 5.1, "single_two": 3.5, "single_one": 4.5, "single_center": 1.35, "bp_single": 5.0, "hbp": 2.0, "walk": 18.25, "strikeout": 9.75, "lineout": 9.0, "popout": 16.0, "flyout_a": 0.0, "flyout_bq": 1.65, "flyout_lf_b": 1.9, "flyout_rf_b": 2.0, "groundout_a": 7.0, "groundout_b": 10.5, "groundout_c": 2.0, "avg": 0.2263888888888889, "obp": 0.41388888888888886, "slg": 0.37453703703703706 } ``` **Pydantic Model**: ```python class PdBattingRatingApi(BaseModel): """Single batting rating (vs L or vs R pitcher)""" id: int battingcard: PdBattingCardApi vs_hand: str # "L" or "R" (vs LHP or RHP) # Hit distribution pull_rate: float center_rate: float slap_rate: float # Outcome probabilities (these are the critical fields!) homerun: float bp_homerun: float # Ballpark homerun triple: float double_three: float double_two: float double_pull: float single_two: float single_one: float single_center: float bp_single: float # Ballpark single hbp: float # Hit by pitch walk: float strikeout: float lineout: float popout: float flyout_a: float flyout_bq: float flyout_lf_b: float flyout_rf_b: float groundout_a: float groundout_b: float groundout_c: float # Summary stats avg: float obp: float slg: float ``` ### PdBattingRatingsResponseApi **API Endpoint**: `GET /api/v2/battingcardratings/player/:player_id?short_output=false` **JSON Example**: ```json { "count": 2, "ratings": [ { ... }, // vs L rating { ... } // vs R rating ] } ``` **Pydantic Model**: ```python class PdBattingRatingsResponseApi(BaseModel): """Full batting ratings API response""" count: int // Should always be 2 (vs L and vs R) ratings: List[PdBattingRatingApi] ``` --- ## Pitching Card Models ### PdPitchingCardApi **JSON Example** (nested in ratings response): ```json { "id": 4049, "player": { ... }, // Full PdPlayerApi structure "variant": 0, "balk": 0, "wild_pitch": 20, "hold": 9, "starter_rating": 1, "relief_rating": 2, "closer_rating": null, "batting": "#1WR-C", "offense_col": 1, "hand": "R" } ``` **Pydantic Model**: ```python class PdPitchingCardApi(BaseModel): """PD Pitching Card (nested in rating response)""" id: int player: PdPlayerApi variant: int balk: int wild_pitch: int # d20 range (e.g., 20 means on roll of 20) hold: int starter_rating: int # 1-5 relief_rating: int # 1-5 closer_rating: Optional[int] = None # 1-5 or null batting: str # Pitcher batting ability (e.g., "#1WR-C") offense_col: int # 1 or 2 hand: str # R, L ``` ### PdPitchingRatingApi **JSON Example** (single rating, vs L or vs R): ```json { "id": 8097, "pitchingcard": { ... }, // PdPitchingCardApi "vs_hand": "L", "homerun": 2.6, "bp_homerun": 6.0, "triple": 2.1, "double_three": 0.0, "double_two": 7.1, "double_cf": 0.0, "single_two": 1.0, "single_one": 1.0, "single_center": 0.0, "bp_single": 5.0, "hbp": 6.0, "walk": 17.6, "strikeout": 11.4, "flyout_lf_b": 0.0, "flyout_cf_b": 7.75, "flyout_rf_b": 3.6, "groundout_a": 1.75, "groundout_b": 6.1, "xcheck_p": 1.0, "xcheck_c": 3.0, "xcheck_1b": 2.0, "xcheck_2b": 6.0, "xcheck_3b": 3.0, "xcheck_ss": 7.0, "xcheck_lf": 2.0, "xcheck_cf": 3.0, "xcheck_rf": 2.0, "avg": 0.17870370370370367, "obp": 0.3972222222222222, "slg": 0.4388888888888889 } ``` **Pydantic Model**: ```python class PdPitchingRatingApi(BaseModel): """Single pitching rating (vs L or vs R batter)""" id: int pitchingcard: PdPitchingCardApi vs_hand: str # "L" or "R" (vs LHB or RHB) # Outcome probabilities (these are the critical fields!) homerun: float bp_homerun: float triple: float double_three: float double_two: float double_cf: float # Double to center field single_two: float single_one: float single_center: float bp_single: float hbp: float walk: float strikeout: float flyout_lf_b: float flyout_cf_b: float flyout_rf_b: float groundout_a: float groundout_b: float # X-check (fielding checks by position) xcheck_p: float xcheck_c: float xcheck_1b: float xcheck_2b: float xcheck_3b: float xcheck_ss: float xcheck_lf: float xcheck_cf: float xcheck_rf: float # Summary stats avg: float obp: float slg: float ``` ### PdPitchingRatingsResponseApi **API Endpoint**: `GET /api/v2/pitchingcardratings/player/:player_id?short_output=false` **JSON Example**: ```json { "count": 2, "ratings": [ { ... }, // vs L rating { ... } // vs R rating ] } ``` **Pydantic Model**: ```python class PdPitchingRatingsResponseApi(BaseModel): """Full pitching ratings API response""" count: int // Should always be 2 (vs L and vs R) ratings: List[PdPitchingRatingApi] ``` --- ## Usage Example ```python import httpx from app.models.api_models import ( PdPlayerApi, PdBattingRatingsResponseApi, PdPitchingRatingsResponseApi ) # Fetch and deserialize player async with httpx.AsyncClient() as client: # Player data response = await client.get( "https://pd.manticorum.com/api/v2/players/11223?csv=false" ) player = PdPlayerApi(**response.json()) # Batting ratings response = await client.get( "https://pd.manticorum.com/api/v2/battingcardratings/player/11223?short_output=false" ) batting_ratings = PdBattingRatingsResponseApi(**response.json()) # Pitching ratings response = await client.get( "https://pd.manticorum.com/api/v2/pitchingcardratings/player/11223?short_output=false" ) pitching_ratings = PdPitchingRatingsResponseApi(**response.json()) ``` --- ## Notes 1. **Ratings are per handedness**: Always 2 ratings (vs L and vs R) 2. **Outcome probabilities**: Floats represent percentage chance (e.g., 2.6 = 2.6%) 3. **All probabilities should sum to 100**: Use for validation in tests 4. **Player nested in cards**: Ratings responses contain full player data again 5. **Forward references**: May need `from __future__ import annotations` for circular refs --- **Next**: See [api-models-sba.md](./api-models-sba.md) for SBA API models