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>
634 lines
13 KiB
Markdown
634 lines
13 KiB
Markdown
# 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
|