Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@

::: mcsrranked.types.live.LiveMatch

::: mcsrranked.types.live.LiveMatchPlayer
::: mcsrranked.types.live.LivePlayerData

::: mcsrranked.types.live.LivePlayerTimeline

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "mcsrranked"
version = "0.1.1"
version = "0.2.0"
description = "Python SDK for the MCSR Ranked API"
readme = "README.md"
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions src/mcsrranked/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
LeaderboardUser,
LiveData,
LiveMatch,
LiveMatchPlayer,
LivePlayerData,
LivePlayerTimeline,
MatchInfo,
MatchRank,
Expand Down Expand Up @@ -141,7 +141,7 @@
# Models - live
"LiveData",
"LiveMatch",
"LiveMatchPlayer",
"LivePlayerData",
"LivePlayerTimeline",
"UserLiveMatch",
# Models - weekly_race
Expand Down
4 changes: 2 additions & 2 deletions src/mcsrranked/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from mcsrranked.types.live import (
LiveData,
LiveMatch,
LiveMatchPlayer,
LivePlayerData,
LivePlayerTimeline,
UserLiveMatch,
)
Expand Down Expand Up @@ -85,7 +85,7 @@
# live
"LiveData",
"LiveMatch",
"LiveMatchPlayer",
"LivePlayerData",
"LivePlayerTimeline",
"UserLiveMatch",
# weekly_race
Expand Down
12 changes: 1 addition & 11 deletions src/mcsrranked/types/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
__all__ = [
"LiveData",
"LiveMatch",
"LiveMatchPlayer",
"LivePlayerData",
"LivePlayerTimeline",
"UserLiveMatch",
]
Expand Down Expand Up @@ -55,16 +55,6 @@ class LiveMatch(BaseModel):
model_config = {"populate_by_name": True}


class LiveMatchPlayer(UserProfile):
"""Player in a live match with stream data."""

live_url: str | None = Field(
default=None, alias="liveUrl", description="Live stream URL"
)

model_config = {"populate_by_name": True}


class LiveData(BaseModel):
"""Live data response."""

Expand Down
12 changes: 12 additions & 0 deletions src/mcsrranked/types/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ class MatchInfo(BaseModel):
default_factory=list, description="Match spectators"
)
seed: MatchSeed | None = Field(default=None, description="Seed information")
seed_type: str | None = Field(
default=None, alias="seedType", description="Seed type"
)
bastion_type: str | None = Field(
default=None, alias="bastionType", description="Bastion type"
)
game_mode: str | None = Field(
default=None, alias="gameMode", description="Game mode"
)
bot_source: str | None = Field(
default=None, alias="botSource", description="Bot source if applicable"
)
result: MatchResult | None = Field(default=None, description="Match result")
forfeited: bool = Field(
default=False, description="Whether match had no completions"
Expand Down
23 changes: 14 additions & 9 deletions src/mcsrranked/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Any

from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, Field, computed_field, model_validator

from mcsrranked.types.shared import Achievement

Expand All @@ -28,25 +28,31 @@ class MatchTypeStats(BaseModel):
)
wins: int = Field(default=0, description="Total wins")
losses: int = Field(default=0, description="Total losses")
draws: int = Field(default=0, description="Total draws")
forfeits: int = Field(default=0, description="Total forfeits")
highest_winstreak: int = Field(
default=0, alias="highestWinstreak", description="Highest win streak achieved"
default=0, alias="highestWinStreak", description="Highest win streak achieved"
)
current_winstreak: int = Field(
default=0, alias="currentWinstreak", description="Current win streak"
default=0, alias="currentWinStreak", description="Current win streak"
)
playtime: int = Field(default=0, description="Total playtime in milliseconds")
completion_time: int = Field(
default=0,
alias="completionTime",
description="Total time spent on completions in milliseconds",
)
best_time: int | None = Field(
default=None,
alias="bestTime",
description="Best completion time in milliseconds",
)
best_time_id: int | None = Field(
default=None, alias="bestTimeId", description="Match ID of best time"
)
completions: int = Field(default=0, description="Total completions")

@computed_field(description="Total draws") # type: ignore[prop-decorator]
@property
def draws(self) -> int:
return self.played_matches - self.wins - self.losses

model_config = {"populate_by_name": True}


Expand Down Expand Up @@ -74,12 +80,11 @@ def _pivot_stats(data: dict[str, Any]) -> dict[str, Any]:
# API key -> model field name
"wins": "wins",
"loses": "losses", # Note: API uses 'loses' not 'losses'
"draws": "draws",
"forfeits": "forfeits",
"completions": "completions",
"playtime": "playtime",
"completionTime": "completion_time",
"bestTime": "best_time",
"bestTimeId": "best_time_id",
"playedMatches": "played_matches",
"currentWinStreak": "current_winstreak",
"highestWinStreak": "highest_winstreak",
Expand Down
1 change: 1 addition & 0 deletions src/mcsrranked/types/weekly_race.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class WeeklyRaceSeed(BaseModel):
nether: str | None = Field(default=None, description="Nether seed")
the_end: str | None = Field(default=None, alias="theEnd", description="End seed")
rng: str | None = Field(default=None, description="RNG seed")
flags: list[str] | None = Field(default=None, description="Seed flags")

model_config = {"populate_by_name": True}

Expand Down
37 changes: 35 additions & 2 deletions tests/test_field_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
LiveMatch,
LivePlayerData,
LivePlayerTimeline,
UserLiveMatch,
)
from mcsrranked.types.match import (
Completion,
Expand Down Expand Up @@ -111,13 +112,13 @@ def test_match_type_stats_all_fields(self, user_fixture):
assert isinstance(mts.played_matches, int)
assert isinstance(mts.wins, int)
assert isinstance(mts.losses, int)
assert isinstance(mts.draws, int)
assert isinstance(mts.draws, int) # computed field
assert isinstance(mts.forfeits, int)
assert isinstance(mts.highest_winstreak, int)
assert isinstance(mts.current_winstreak, int)
assert isinstance(mts.playtime, int)
assert isinstance(mts.completion_time, int)
assert mts.best_time is None or isinstance(mts.best_time, int)
assert mts.best_time_id is None or isinstance(mts.best_time_id, int)
assert isinstance(mts.completions, int)

def test_season_result_all_fields(self, user_fixture):
Expand Down Expand Up @@ -224,6 +225,10 @@ def test_match_info_all_fields(self, match_detail_fixture):
assert isinstance(match.players, list)
assert isinstance(match.spectators, list)
assert match.seed is None or isinstance(match.seed, MatchSeed)
assert match.seed_type is None or isinstance(match.seed_type, str)
assert match.bastion_type is None or isinstance(match.bastion_type, str)
assert match.game_mode is None or isinstance(match.game_mode, str)
assert match.bot_source is None or isinstance(match.bot_source, str)
assert match.result is None or isinstance(match.result, MatchResult)
assert isinstance(match.forfeited, bool)
assert isinstance(match.decayed, bool)
Expand Down Expand Up @@ -467,6 +472,33 @@ def test_live_player_timeline_all_fields(self, live_fixture):
# If no timeline found, that's OK - it's nullable


class TestUserLiveMatchFieldCoverage:
"""Test UserLiveMatch fields."""

def test_user_live_match_structure(self):
"""Test UserLiveMatch can be constructed with all fields."""
# User live endpoint data is not in fixtures, so test the model directly
live_match = UserLiveMatch(
last_id=123,
type=2,
status="running",
time=180000,
players=[],
spectators=[],
timelines=[],
completions=[],
)

assert live_match.last_id is None or isinstance(live_match.last_id, int)
assert isinstance(live_match.type, int)
assert isinstance(live_match.status, str)
assert isinstance(live_match.time, int)
assert isinstance(live_match.players, list)
assert isinstance(live_match.spectators, list)
assert isinstance(live_match.timelines, list)
assert isinstance(live_match.completions, list)


class TestWeeklyRaceFieldCoverage:
"""Test every field in WeeklyRace types."""

Expand All @@ -488,6 +520,7 @@ def test_weekly_race_seed_all_fields(self, weekly_race_fixture):
assert race.seed.nether is None or isinstance(race.seed.nether, str)
assert race.seed.the_end is None or isinstance(race.seed.the_end, str)
assert race.seed.rng is None or isinstance(race.seed.rng, str)
assert race.seed.flags is None or isinstance(race.seed.flags, list)

def test_race_leaderboard_entry_all_fields(self, weekly_race_fixture):
"""Verify all RaceLeaderboardEntry fields."""
Expand Down
Loading