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>
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:
/api/v2/players/:player_id- Basic player info/api/v2/battingcardratings/player/:player_id- Batting outcome probabilities/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
- Ratings are per handedness: Always 2 ratings (vs L and vs R)
- Outcome probabilities: Floats represent percentage chance (e.g., 2.6 = 2.6%)
- All probabilities should sum to 100: Use for validation in tests
- Player nested in cards: Ratings responses contain full player data again
- Forward references: May need
from __future__ import annotationsfor circular refs
Next: See api-models-sba.md for SBA API models