strat-gameplay-webapp/.claude/implementation/player-model-specs/api-models-pd.md
Cal Corum f9aa653c37 CLAUDE: Reorganize Week 6 documentation and separate player model specifications
Split player model architecture into dedicated documentation files for clarity
and maintainability. Added Phase 1 status tracking and comprehensive player
model specs covering API models, game models, mappers, and testing strategy.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 23:48:57 -05:00

13 KiB

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

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:

{
    "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:

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:

{
    "id": 1,
    "value": 5,
    "name": "MVP",
    "color": "56f1fa"
}

Pydantic Model:

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:

{
    "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:

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:

{
    "id": 40108,
    "team": 69,
    "player": 11223,
    "created": 1742675422723
}

Pydantic Model:

class PdPaperdexEntryApi(BaseModel):
    """Single paperdex entry (ownership record)"""
    id: int
    team: int
    player: int
    created: int  # Timestamp

PdPaperdexApi

JSON Example:

{
    "count": 1,
    "paperdex": [
        {
            "id": 40108,
            "team": 69,
            "player": 11223,
            "created": 1742675422723
        }
    ]
}

Pydantic Model:

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:

{
    "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:

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

{
    "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:

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

{
    "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:

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:

{
    "count": 2,
    "ratings": [
        { ... },  // vs L rating
        { ... }   // vs R rating
    ]
}

Pydantic Model:

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

{
    "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:

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

{
    "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:

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:

{
    "count": 2,
    "ratings": [
        { ... },  // vs L rating
        { ... }   // vs R rating
    ]
}

Pydantic Model:

class PdPitchingRatingsResponseApi(BaseModel):
    """Full pitching ratings API response"""
    count: int  // Should always be 2 (vs L and vs R)
    ratings: List[PdPitchingRatingApi]

Usage Example

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 for SBA API models