diff --git a/docs/api/client.md b/docs/api/client.md index 3515cab..706271a 100644 --- a/docs/api/client.md +++ b/docs/api/client.md @@ -1,108 +1,34 @@ # Client Reference -## MCSRRanked - -The synchronous client for the MCSR Ranked API. - -```python -from mcsrranked import MCSRRanked - -client = MCSRRanked( - api_key=None, # API key for expanded rate limits - private_key=None, # Private key for live data - base_url=None, # Custom API base URL - timeout=30.0, # Request timeout in seconds - max_retries=2, # Maximum retry attempts -) -``` - -### Constructor Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `api_key` | `str \| None` | `None` | API key for expanded rate limits. Falls back to `MCSRRANKED_API_KEY` env var. | -| `private_key` | `str \| None` | `None` | Private key for live data. Falls back to `MCSRRANKED_PRIVATE_KEY` env var. | -| `base_url` | `str \| None` | `"https://api.mcsrranked.com"` | API base URL. | -| `timeout` | `float \| None` | `30.0` | Request timeout in seconds. | -| `max_retries` | `int \| None` | `2` | Maximum number of retries. | - -### Resources - -| Resource | Description | -|----------|-------------| -| `client.users` | User profiles, matches, versus stats | -| `client.matches` | Match listings and details | -| `client.leaderboards` | Elo, phase, and record leaderboards | -| `client.live` | Online players and live streams | -| `client.weekly_races` | Weekly race data | - -### Methods - -#### `with_options(**kwargs)` - -Create a new client with modified options: - -```python -new_client = client.with_options(timeout=60.0) -``` - -#### `close()` - -Close the HTTP client: - -```python -client.close() -``` - -### Context Manager - -```python -with MCSRRanked() as client: - user = client.users.get("Feinberg") -# Client automatically closed -``` +## Synchronous Client + +::: mcsrranked.MCSRRanked + options: + members: + - __init__ + - users + - matches + - leaderboards + - live + - weekly_races + - with_options + - close --- -## AsyncMCSRRanked - -The asynchronous client for the MCSR Ranked API. - -```python -from mcsrranked import AsyncMCSRRanked - -client = AsyncMCSRRanked( - api_key=None, - private_key=None, - base_url=None, - timeout=30.0, - max_retries=2, -) -``` - -### Async Context Manager - -```python -async with AsyncMCSRRanked() as client: - user = await client.users.get("Feinberg") -# Client automatically closed -``` - -### Async Methods - -All resource methods are async: - -```python -user = await client.users.get("Feinberg") -matches = await client.matches.list() -leaderboard = await client.leaderboards.elo() -``` - -### Async Close - -```python -await client.close() -``` +## Asynchronous Client + +::: mcsrranked.AsyncMCSRRanked + options: + members: + - __init__ + - users + - matches + - leaderboards + - live + - weekly_races + - with_options + - close --- diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md index b298a14..272ca3b 100644 --- a/docs/api/exceptions.md +++ b/docs/api/exceptions.md @@ -10,151 +10,83 @@ MCSRRankedError │ │ ├── AuthenticationError │ │ ├── NotFoundError │ │ └── RateLimitError -│ ├── APIConnectionError -│ └── APITimeoutError +│ └── APIConnectionError +│ └── APITimeoutError ``` --- ## Base Exceptions -### MCSRRankedError +::: mcsrranked.MCSRRankedError + options: + show_docstring_attributes: true -Base exception for all SDK errors. - -```python -from mcsrranked import MCSRRankedError - -try: - user = mcsrranked.users.get("someone") -except MCSRRankedError as e: - print(f"SDK error: {e}") -``` - -### APIError - -Base exception for API-related errors. - -| Attribute | Type | Description | -|-----------|------|-------------| -| `message` | `str` | Error message | +::: mcsrranked.APIError + options: + show_docstring_attributes: true --- ## HTTP Status Exceptions -### APIStatusError - -Exception for HTTP error responses. +::: mcsrranked.APIStatusError + options: + show_docstring_attributes: true -| Attribute | Type | Description | -|-----------|------|-------------| -| `message` | `str` | Error message | -| `status_code` | `int` | HTTP status code | -| `response` | `httpx.Response` | Raw response object | -| `body` | `object \| None` | Response body | +::: mcsrranked.BadRequestError -### BadRequestError +::: mcsrranked.AuthenticationError -Raised for HTTP 400 responses (invalid parameters). +::: mcsrranked.NotFoundError -```python -from mcsrranked import BadRequestError +::: mcsrranked.RateLimitError -try: - matches = mcsrranked.matches.list(count=1000) -except BadRequestError as e: - print(f"Status: {e.status_code}") # 400 - print(f"Message: {e.message}") -``` +--- -### AuthenticationError +## Connection Exceptions -Raised for HTTP 401 responses (invalid credentials). +::: mcsrranked.APIConnectionError -```python -from mcsrranked import AuthenticationError +::: mcsrranked.APITimeoutError -try: - live = mcsrranked.users.live("uuid") -except AuthenticationError as e: - print(f"Status: {e.status_code}") # 401 -``` +--- -### NotFoundError +## Usage Examples -Raised for HTTP 404 responses (resource not found). +### Catching all SDK errors ```python -from mcsrranked import NotFoundError +from mcsrranked import MCSRRankedError try: - user = mcsrranked.users.get("nonexistent") -except NotFoundError as e: - print(f"Status: {e.status_code}") # 404 + user = mcsrranked.users.get("someone") +except MCSRRankedError as e: + print(f"SDK error: {e}") ``` -### RateLimitError - -Raised for HTTP 429 responses (too many requests). +### Catching specific errors ```python -from mcsrranked import RateLimitError +from mcsrranked import NotFoundError, RateLimitError try: - # Too many requests - for i in range(1000): - mcsrranked.users.get("Feinberg") + user = mcsrranked.users.get("nonexistent") +except NotFoundError as e: + print(f"User not found: {e.status_code}") except RateLimitError as e: - print(f"Status: {e.status_code}") # 429 -``` - ---- - -## Connection Exceptions - -### APIConnectionError - -Raised for network-related errors. - -```python -from mcsrranked import APIConnectionError - -try: - user = mcsrranked.users.get("Feinberg") -except APIConnectionError as e: - print(f"Connection failed: {e.message}") + print(f"Rate limited: {e.status_code}") ``` -### APITimeoutError - -Raised when a request times out. Inherits from `APIConnectionError`. +### Accessing error details ```python -from mcsrranked import APITimeoutError, MCSRRanked - -client = MCSRRanked(timeout=1.0) +from mcsrranked import APIStatusError try: - user = client.users.get("Feinberg") -except APITimeoutError as e: - print(f"Request timed out: {e.message}") -``` - ---- - -## Import All Exceptions - -```python -from mcsrranked import ( - MCSRRankedError, - APIError, - APIStatusError, - APIConnectionError, - APITimeoutError, - BadRequestError, - AuthenticationError, - NotFoundError, - RateLimitError, -) + user = mcsrranked.users.get("invalid") +except APIStatusError as e: + print(f"Status: {e.status_code}") + print(f"Message: {e.message}") + print(f"Body: {e.body}") ``` diff --git a/docs/api/types.md b/docs/api/types.md index 25fc969..81f71b9 100644 --- a/docs/api/types.md +++ b/docs/api/types.md @@ -1,214 +1,105 @@ # Types Reference -All API responses are parsed into Pydantic models with full type hints. - ## Enums -### MatchType +::: mcsrranked.MatchType + +::: mcsrranked.SortOrder + +--- + +## Shared Types -```python -from mcsrranked import MatchType +::: mcsrranked.types.shared.UserProfile -MatchType.CASUAL # 1 -MatchType.RANKED # 2 -MatchType.PRIVATE # 3 -MatchType.EVENT # 4 -``` +::: mcsrranked.types.shared.Achievement -### SortOrder +::: mcsrranked.types.shared.MatchSeed -```python -from mcsrranked import SortOrder +::: mcsrranked.types.shared.EloChange -# Type alias for: Literal["newest", "oldest", "fastest", "slowest"] -``` +::: mcsrranked.types.shared.VodInfo --- ## User Types -### User - -Full user profile data. - -| Field | Type | Description | -|-------|------|-------------| -| `uuid` | `str` | UUID without dashes | -| `nickname` | `str` | Display name | -| `elo_rate` | `int \| None` | Current elo rating | -| `elo_rank` | `int \| None` | Current rank | -| `country` | `str \| None` | Country code (ISO 3166-1 alpha-2) | -| `role_type` | `int` | User role type | -| `achievements` | `AchievementsContainer` | User achievements | -| `timestamp` | `UserTimestamps \| None` | Activity timestamps | -| `statistics` | `UserStatistics` | Season and total stats | -| `connections` | `UserConnections` | Third-party connections | -| `season_result` | `SeasonResult \| None` | Current season data | -| `weekly_races` | `list[WeeklyRaceResult]` | Weekly race results | - -### UserProfile - -Basic user profile (used in matches, leaderboards). - -| Field | Type | Description | -|-------|------|-------------| -| `uuid` | `str` | UUID without dashes | -| `nickname` | `str` | Display name | -| `elo_rate` | `int \| None` | Elo rating | -| `elo_rank` | `int \| None` | Rank | -| `country` | `str \| None` | Country code | -| `role_type` | `int` | Role type | - -### UserStatistics - -| Field | Type | -|-------|------| -| `season` | `SeasonStats` | -| `total` | `TotalStats` | - -### MatchTypeStats - -Statistics for ranked/casual matches. - -| Field | Type | -|-------|------| -| `played_matches` | `int` | -| `wins` | `int` | -| `losses` | `int` | -| `draws` | `int` | -| `forfeits` | `int` | -| `highest_winstreak` | `int` | -| `current_winstreak` | `int` | -| `playtime` | `int` | -| `best_time` | `int \| None` | -| `best_time_id` | `int \| None` | -| `completions` | `int` | +::: mcsrranked.types.user.User + +::: mcsrranked.types.user.UserStatistics + +::: mcsrranked.types.user.MatchTypeStats + +::: mcsrranked.types.user.UserTimestamps + +::: mcsrranked.types.user.SeasonResult + +::: mcsrranked.types.user.PhaseResult + +::: mcsrranked.types.user.Connection + +::: mcsrranked.types.user.UserConnections + +::: mcsrranked.types.user.WeeklyRaceResult + +::: mcsrranked.types.user.UserSeasons --- ## Match Types -### MatchInfo - -| Field | Type | Description | -|-------|------|-------------| -| `id` | `int` | Match ID | -| `type` | `int` | Match type (1-4) | -| `season` | `int` | Season number | -| `category` | `str \| None` | Completion category | -| `date` | `int` | Unix timestamp (seconds) | -| `players` | `list[UserProfile]` | Match players | -| `spectators` | `list[UserProfile]` | Match spectators | -| `seed` | `MatchSeed \| None` | Seed information | -| `result` | `MatchResult \| None` | Match result | -| `forfeited` | `bool` | No completions | -| `decayed` | `bool` | Decayed match | -| `rank` | `MatchRank \| None` | Record ranking | -| `changes` | `list[EloChange]` | Elo changes | -| `tag` | `str \| None` | Special tag | -| `beginner` | `bool` | Beginner mode | -| `vod` | `list[VodInfo]` | VOD information | -| `completions` | `list[Completion]` | (Advanced) Completions | -| `timelines` | `list[Timeline]` | (Advanced) Timeline events | -| `replay_exist` | `bool` | (Advanced) Replay available | - -### MatchSeed - -| Field | Type | -|-------|------| -| `id` | `str \| None` | -| `overworld` | `str \| None` | -| `bastion` | `str \| None` | -| `end_towers` | `list[int]` | -| `variations` | `list[str]` | - -### MatchResult - -| Field | Type | -|-------|------| -| `uuid` | `str \| None` | -| `time` | `int` | - -### Timeline - -| Field | Type | -|-------|------| -| `uuid` | `str` | -| `time` | `int` | -| `type` | `str` | +::: mcsrranked.types.match.MatchInfo + +::: mcsrranked.types.match.MatchResult + +::: mcsrranked.types.match.MatchRank + +::: mcsrranked.types.match.Timeline + +::: mcsrranked.types.match.Completion + +::: mcsrranked.types.match.VersusStats + +::: mcsrranked.types.match.VersusResults --- ## Leaderboard Types -### EloLeaderboard +::: mcsrranked.types.leaderboard.EloLeaderboard + +::: mcsrranked.types.leaderboard.SeasonInfo -| Field | Type | -|-------|------| -| `season` | `SeasonInfo` | -| `users` | `list[LeaderboardUser]` | +::: mcsrranked.types.leaderboard.LeaderboardUser -### PhaseLeaderboard +::: mcsrranked.types.leaderboard.PhaseLeaderboard -| Field | Type | -|-------|------| -| `phase` | `PhaseInfo` | -| `users` | `list[PhaseLeaderboardUser]` | +::: mcsrranked.types.leaderboard.PhaseInfo -### RecordEntry +::: mcsrranked.types.leaderboard.PhaseLeaderboardUser -| Field | Type | -|-------|------| -| `rank` | `int` | -| `season` | `int` | -| `date` | `int` | -| `id` | `int` | -| `time` | `int` | -| `user` | `UserProfile` | -| `seed` | `MatchSeed \| None` | +::: mcsrranked.types.leaderboard.RecordEntry --- ## Live Types -### LiveData +::: mcsrranked.types.live.LiveData -| Field | Type | -|-------|------| -| `players` | `int` | -| `live_matches` | `list[LiveMatch]` | +::: mcsrranked.types.live.LiveMatch -### UserLiveMatch +::: mcsrranked.types.live.LiveMatchPlayer -| Field | Type | -|-------|------| -| `last_id` | `int \| None` | -| `type` | `int` | -| `status` | `str` | -| `time` | `int` | -| `players` | `list[UserProfile]` | -| `spectators` | `list[UserProfile]` | -| `timelines` | `list[Timeline]` | -| `completions` | `list[Completion]` | +::: mcsrranked.types.live.LivePlayerTimeline + +::: mcsrranked.types.live.UserLiveMatch --- ## Weekly Race Types -### WeeklyRace - -| Field | Type | -|-------|------| -| `id` | `int` | -| `seed` | `WeeklyRaceSeed` | -| `ends_at` | `int` | -| `leaderboard` | `list[RaceLeaderboardEntry]` | +::: mcsrranked.types.weekly_race.WeeklyRace -### RaceLeaderboardEntry +::: mcsrranked.types.weekly_race.WeeklyRaceSeed -| Field | Type | -|-------|------| -| `rank` | `int` | -| `player` | `UserProfile` | -| `time` | `int` | -| `replay_exist` | `bool` | +::: mcsrranked.types.weekly_race.RaceLeaderboardEntry diff --git a/mkdocs.yml b/mkdocs.yml index 6219bf0..07bde31 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,7 +42,22 @@ plugins: options: show_source: false show_root_heading: true - heading_level: 2 + show_bases: false + heading_level: 3 + members_order: source + show_root_full_path: false + show_signature_annotations: true + show_symbol_type_heading: true + show_symbol_type_toc: true + signature_crossrefs: true + summary: true + filters: + - "!^model_config$" + - "!^model_fields$" + - "!^model_computed_fields$" + extensions: + - griffe_pydantic: + schema: true markdown_extensions: - pymdownx.highlight: diff --git a/pyproject.toml b/pyproject.toml index 63c765c..4469206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ docs = [ "mkdocs>=1.6.0", "mkdocs-material>=9.5.0", "mkdocstrings[python]>=0.27.0", + "griffe-pydantic>=1.0.0", ] [project.urls] diff --git a/src/mcsrranked/_client.py b/src/mcsrranked/_client.py index 75840ce..b1cbce6 100644 --- a/src/mcsrranked/_client.py +++ b/src/mcsrranked/_client.py @@ -15,24 +15,7 @@ class MCSRRanked: - """Synchronous client for the MCSR Ranked API. - - Usage: - >>> client = MCSRRanked() - >>> user = client.users.get("Couriway") - >>> print(user.nickname, user.elo_rate) - - >>> # With context manager - >>> with MCSRRanked() as client: - ... matches = client.matches.list() - - >>> # With API key for expanded rate limits - >>> client = MCSRRanked(api_key="your-key") - - >>> # With private key for live data - >>> client = MCSRRanked(private_key="your-private-key") - >>> live = client.users.live("player") - """ + """Synchronous client for the MCSR Ranked API.""" def __init__( self, @@ -136,17 +119,7 @@ def __exit__(self, *args: Any) -> None: class AsyncMCSRRanked: - """Asynchronous client for the MCSR Ranked API. - - Usage: - >>> async with AsyncMCSRRanked() as client: - ... user = await client.users.get("Couriway") - ... print(user.nickname, user.elo_rate) - - >>> # With API key for expanded rate limits - >>> async with AsyncMCSRRanked(api_key="your-key") as client: - ... matches = await client.matches.list() - """ + """Asynchronous client for the MCSR Ranked API.""" def __init__( self, diff --git a/src/mcsrranked/_types.py b/src/mcsrranked/_types.py index 583cab9..5c465b1 100644 --- a/src/mcsrranked/_types.py +++ b/src/mcsrranked/_types.py @@ -8,9 +8,20 @@ class MatchType(IntEnum): """Match type enumeration.""" CASUAL = 1 + """Casual match.""" RANKED = 2 + """Ranked match.""" PRIVATE = 3 + """Private room match.""" EVENT = 4 + """Event match.""" SortOrder = Literal["newest", "oldest", "fastest", "slowest"] +"""Sort order for match listings. + +- `"newest"`: Most recent first +- `"oldest"`: Oldest first +- `"fastest"`: Fastest completion time first +- `"slowest"`: Slowest completion time first +""" diff --git a/src/mcsrranked/types/leaderboard.py b/src/mcsrranked/types/leaderboard.py index 46ea06d..6bef5f4 100644 --- a/src/mcsrranked/types/leaderboard.py +++ b/src/mcsrranked/types/leaderboard.py @@ -18,9 +18,11 @@ class SeasonInfo(BaseModel): """Season information.""" - number: int - starts_at: int = Field(alias="startsAt") - ends_at: int = Field(alias="endsAt") + number: int = Field(description="Season number") + starts_at: int = Field( + alias="startsAt", description="Unix timestamp of season start" + ) + ends_at: int = Field(alias="endsAt", description="Unix timestamp of season end") model_config = {"populate_by_name": True} @@ -28,9 +30,11 @@ class SeasonInfo(BaseModel): class LeaderboardSeasonResult(BaseModel): """Season result for leaderboard user.""" - elo_rate: int = Field(alias="eloRate") - elo_rank: int = Field(alias="eloRank") - phase_point: int = Field(alias="phasePoint") + elo_rate: int = Field(alias="eloRate", description="Final elo rating in season") + elo_rank: int = Field(alias="eloRank", description="Final elo rank in season") + phase_point: int = Field( + alias="phasePoint", description="Final phase points in season" + ) model_config = {"populate_by_name": True} @@ -38,7 +42,9 @@ class LeaderboardSeasonResult(BaseModel): class LeaderboardUser(UserProfile): """User entry in the elo leaderboard.""" - season_result: LeaderboardSeasonResult = Field(alias="seasonResult") + season_result: LeaderboardSeasonResult = Field( + alias="seasonResult", description="Season result data" + ) model_config = {"populate_by_name": True} @@ -46,8 +52,10 @@ class LeaderboardUser(UserProfile): class EloLeaderboard(BaseModel): """Elo leaderboard data.""" - season: SeasonInfo - users: list[LeaderboardUser] = Field(default_factory=list) + season: SeasonInfo = Field(description="Season information") + users: list[LeaderboardUser] = Field( + default_factory=list, description="Top 150 users by elo" + ) model_config = {"populate_by_name": True} @@ -55,9 +63,15 @@ class EloLeaderboard(BaseModel): class PhaseInfo(BaseModel): """Phase information.""" - season: int - number: int | None = None - ends_at: int | None = Field(default=None, alias="endsAt") + season: int = Field(description="Season number") + number: int | None = Field( + default=None, description="Current phase number. None for past seasons." + ) + ends_at: int | None = Field( + default=None, + alias="endsAt", + description="Unix timestamp of phase end. None for past seasons.", + ) model_config = {"populate_by_name": True} @@ -65,8 +79,14 @@ class PhaseInfo(BaseModel): class PhaseLeaderboardUser(UserProfile): """User entry in the phase points leaderboard.""" - season_result: LeaderboardSeasonResult = Field(alias="seasonResult") - pred_phase_point: int = Field(default=0, alias="predPhasePoint") + season_result: LeaderboardSeasonResult = Field( + alias="seasonResult", description="Season result data" + ) + pred_phase_point: int = Field( + default=0, + alias="predPhasePoint", + description="Predicted phase points for next phase", + ) model_config = {"populate_by_name": True} @@ -74,8 +94,10 @@ class PhaseLeaderboardUser(UserProfile): class PhaseLeaderboard(BaseModel): """Phase points leaderboard data.""" - phase: PhaseInfo - users: list[PhaseLeaderboardUser] = Field(default_factory=list) + phase: PhaseInfo = Field(description="Phase information") + users: list[PhaseLeaderboardUser] = Field( + default_factory=list, description="Top 100 users by phase points" + ) model_config = {"populate_by_name": True} @@ -83,12 +105,12 @@ class PhaseLeaderboard(BaseModel): class RecordEntry(BaseModel): """Record leaderboard entry.""" - rank: int - season: int - date: int - id: int - time: int - user: UserProfile - seed: MatchSeed | None = None + rank: int = Field(description="Record rank") + season: int = Field(description="Season number") + date: int = Field(description="Unix timestamp of the record") + id: int = Field(description="Match ID") + time: int = Field(description="Completion time in milliseconds") + user: UserProfile = Field(description="Player who set the record") + seed: MatchSeed | None = Field(default=None, description="Seed information") model_config = {"populate_by_name": True} diff --git a/src/mcsrranked/types/live.py b/src/mcsrranked/types/live.py index 70dc419..43f6245 100644 --- a/src/mcsrranked/types/live.py +++ b/src/mcsrranked/types/live.py @@ -17,8 +17,8 @@ class LivePlayerTimeline(BaseModel): """Live player timeline data.""" - time: int - type: str + time: int = Field(description="Match time of last split update in milliseconds") + type: str = Field(description="Timeline identifier of last split") model_config = {"populate_by_name": True} @@ -26,8 +26,14 @@ class LivePlayerTimeline(BaseModel): class LivePlayerData(BaseModel): """Live player data in a match.""" - live_url: str | None = Field(default=None, alias="liveUrl") - timeline: LivePlayerTimeline | None = None + live_url: str | None = Field( + default=None, + alias="liveUrl", + description="Live stream URL. None if player hasn't activated public stream.", + ) + timeline: LivePlayerTimeline | None = Field( + default=None, description="Last timeline update" + ) model_config = {"populate_by_name": True} @@ -35,9 +41,16 @@ class LivePlayerData(BaseModel): class LiveMatch(BaseModel): """Live match data.""" - current_time: int = Field(alias="currentTime") - players: list[UserProfile] = Field(default_factory=list) - data: dict[str, LivePlayerData] = Field(default_factory=dict) + current_time: int = Field( + alias="currentTime", description="Current match time in milliseconds" + ) + players: list[UserProfile] = Field( + default_factory=list, + description="Players with public stream activated", + ) + data: dict[str, LivePlayerData] = Field( + default_factory=dict, description="Player data keyed by UUID" + ) model_config = {"populate_by_name": True} @@ -45,7 +58,9 @@ class LiveMatch(BaseModel): class LiveMatchPlayer(UserProfile): """Player in a live match with stream data.""" - live_url: str | None = Field(default=None, alias="liveUrl") + live_url: str | None = Field( + default=None, alias="liveUrl", description="Live stream URL" + ) model_config = {"populate_by_name": True} @@ -53,8 +68,14 @@ class LiveMatchPlayer(UserProfile): class LiveData(BaseModel): """Live data response.""" - players: int - live_matches: list[LiveMatch] = Field(default_factory=list, alias="liveMatches") + players: int = Field( + description="Concurrent players connected to MCSR Ranked server" + ) + live_matches: list[LiveMatch] = Field( + default_factory=list, + alias="liveMatches", + description="Live matches with public streams", + ) model_config = {"populate_by_name": True} @@ -62,13 +83,29 @@ class LiveData(BaseModel): class UserLiveMatch(BaseModel): """Live match data for a specific user (from /users/{id}/live endpoint).""" - last_id: int | None = Field(default=None, alias="lastId") - type: int - status: str - time: int - players: list[UserProfile] = Field(default_factory=list) - spectators: list[UserProfile] = Field(default_factory=list) - timelines: list[Timeline] = Field(default_factory=list) - completions: list[Completion] = Field(default_factory=list) + last_id: int | None = Field( + default=None, + alias="lastId", + description="Match ID of previous match. Data resets when match ends.", + ) + type: int = Field(description="Match type") + status: str = Field( + description="Match status: idle, counting, generate, ready, running, or done" + ) + time: int = Field( + description="Current match time in milliseconds. 0 if not started." + ) + players: list[UserProfile] = Field( + default_factory=list, description="Match players" + ) + spectators: list[UserProfile] = Field( + default_factory=list, description="Match spectators" + ) + timelines: list[Timeline] = Field( + default_factory=list, description="Timeline events" + ) + completions: list[Completion] = Field( + default_factory=list, description="Match completions" + ) model_config = {"populate_by_name": True} diff --git a/src/mcsrranked/types/match.py b/src/mcsrranked/types/match.py index 56431ac..1ef2223 100644 --- a/src/mcsrranked/types/match.py +++ b/src/mcsrranked/types/match.py @@ -18,8 +18,8 @@ class MatchResult(BaseModel): """Match result data.""" - uuid: str | None = None - time: int + uuid: str | None = Field(default=None, description="Winner UUID without dashes") + time: int = Field(description="Winning time in milliseconds") model_config = {"populate_by_name": True} @@ -27,8 +27,10 @@ class MatchResult(BaseModel): class MatchRank(BaseModel): """Match record ranking.""" - season: int | None = None - all_time: int | None = Field(default=None, alias="allTime") + season: int | None = Field(default=None, description="Season record rank") + all_time: int | None = Field( + default=None, alias="allTime", description="All-time record rank" + ) model_config = {"populate_by_name": True} @@ -36,9 +38,9 @@ class MatchRank(BaseModel): class Timeline(BaseModel): """Match timeline event.""" - uuid: str - time: int - type: str + uuid: str = Field(description="Player UUID without dashes") + time: int = Field(description="Event time in milliseconds") + type: str = Field(description="Timeline event identifier") model_config = {"populate_by_name": True} @@ -46,8 +48,8 @@ class Timeline(BaseModel): class Completion(BaseModel): """Match completion data.""" - uuid: str - time: int + uuid: str = Field(description="Player UUID without dashes") + time: int = Field(description="Completion time in milliseconds") model_config = {"populate_by_name": True} @@ -55,26 +57,38 @@ class Completion(BaseModel): class MatchInfo(BaseModel): """Match information.""" - id: int - type: int - season: int - category: str | None = None - date: int - players: list[UserProfile] = Field(default_factory=list) - spectators: list[UserProfile] = Field(default_factory=list) - seed: MatchSeed | None = None - result: MatchResult | None = None - forfeited: bool = False - decayed: bool = False - rank: MatchRank | None = None - changes: list[EloChange] = Field(default_factory=list) - tag: str | None = None - beginner: bool = False - vod: list[VodInfo] = Field(default_factory=list) + id: int = Field(description="Match ID") + type: int = Field(description="Match type (1=casual, 2=ranked, 3=private, 4=event)") + season: int = Field(description="Season number") + category: str | None = Field(default=None, description="Completion category") + date: int = Field(description="Unix timestamp in seconds") + players: list[UserProfile] = Field( + default_factory=list, description="Match players" + ) + spectators: list[UserProfile] = Field( + default_factory=list, description="Match spectators" + ) + seed: MatchSeed | None = Field(default=None, description="Seed information") + result: MatchResult | None = Field(default=None, description="Match result") + forfeited: bool = Field( + default=False, description="Whether match had no completions" + ) + decayed: bool = Field(default=False, description="Whether match was decayed") + rank: MatchRank | None = Field(default=None, description="Record ranking") + changes: list[EloChange] = Field(default_factory=list, description="Elo changes") + tag: str | None = Field(default=None, description="Special match tag") + beginner: bool = Field(default=False, description="Whether beginner mode was used") + vod: list[VodInfo] = Field(default_factory=list, description="VOD information") # Advanced fields (only from /matches/{id} endpoint) - completions: list[Completion] = Field(default_factory=list) - timelines: list[Timeline] = Field(default_factory=list) - replay_exist: bool = Field(default=False, alias="replayExist") + completions: list[Completion] = Field( + default_factory=list, description="Match completions (advanced)" + ) + timelines: list[Timeline] = Field( + default_factory=list, description="Timeline events (advanced)" + ) + replay_exist: bool = Field( + default=False, alias="replayExist", description="Whether replay is available" + ) model_config = {"populate_by_name": True} @@ -82,7 +96,7 @@ class MatchInfo(BaseModel): class VersusResultStats(BaseModel): """Stats for versus results.""" - total: int = 0 + total: int = Field(default=0, description="Total matches") model_config = {"populate_by_name": True, "extra": "allow"} @@ -90,8 +104,14 @@ class VersusResultStats(BaseModel): class VersusResults(BaseModel): """Versus match results.""" - ranked: dict[str, int] = Field(default_factory=dict) - casual: dict[str, int] = Field(default_factory=dict) + ranked: dict[str, int] = Field( + default_factory=dict, + description="Ranked results. 'total' is match count, UUID keys are win counts.", + ) + casual: dict[str, int] = Field( + default_factory=dict, + description="Casual results. 'total' is match count, UUID keys are win counts.", + ) model_config = {"populate_by_name": True} @@ -99,8 +119,15 @@ class VersusResults(BaseModel): class VersusStats(BaseModel): """Versus statistics between two players.""" - players: list[UserProfile] = Field(default_factory=list) - results: VersusResults = Field(default_factory=VersusResults) - changes: dict[str, int] = Field(default_factory=dict) + players: list[UserProfile] = Field( + default_factory=list, description="The two players" + ) + results: VersusResults = Field( + default_factory=VersusResults, description="Match results by type" + ) + changes: dict[str, int] = Field( + default_factory=dict, + description="Total elo changes between players (keyed by UUID)", + ) model_config = {"populate_by_name": True} diff --git a/src/mcsrranked/types/shared.py b/src/mcsrranked/types/shared.py index 7074cd8..ccad54f 100644 --- a/src/mcsrranked/types/shared.py +++ b/src/mcsrranked/types/shared.py @@ -14,12 +14,20 @@ class UserProfile(BaseModel): """Basic user profile information.""" - uuid: str - nickname: str - role_type: int = Field(alias="roleType") - elo_rate: int | None = Field(default=None, alias="eloRate") - elo_rank: int | None = Field(default=None, alias="eloRank") - country: str | None = None + uuid: str = Field(description="UUID without dashes") + nickname: str = Field(description="Player display name") + role_type: int = Field(alias="roleType", description="User role type") + elo_rate: int | None = Field( + default=None, + alias="eloRate", + description="Elo rating for current season. None if placement matches not completed.", + ) + elo_rank: int | None = Field( + default=None, alias="eloRank", description="Rank for current season" + ) + country: str | None = Field( + default=None, description="Country code (lowercase ISO 3166-1 alpha-2)" + ) model_config = {"populate_by_name": True} @@ -27,12 +35,12 @@ class UserProfile(BaseModel): class Achievement(BaseModel): """User achievement data.""" - id: str - date: int - data: list[str | int] = Field(default_factory=list) - level: int - value: int | None = None - goal: int | None = None + id: str = Field(description="Achievement identifier") + date: int = Field(description="Unix timestamp when achievement was earned") + data: list[str | int] = Field(default_factory=list, description="Achievement data") + level: int = Field(description="Achievement level") + value: int | None = Field(default=None, description="Current progress value") + goal: int | None = Field(default=None, description="Target goal value") model_config = {"populate_by_name": True} @@ -40,11 +48,13 @@ class Achievement(BaseModel): class MatchSeed(BaseModel): """Match seed information.""" - id: str | None = None - overworld: str | None = None - bastion: str | None = None - end_towers: list[int] = Field(default_factory=list, alias="endTowers") - variations: list[str] = Field(default_factory=list) + id: str | None = Field(default=None, description="Seed identifier") + overworld: str | None = Field(default=None, description="Overworld seed") + bastion: str | None = Field(default=None, description="Bastion type") + end_towers: list[int] = Field( + default_factory=list, alias="endTowers", description="End tower positions" + ) + variations: list[str] = Field(default_factory=list, description="Seed variations") model_config = {"populate_by_name": True} @@ -52,9 +62,11 @@ class MatchSeed(BaseModel): class EloChange(BaseModel): """Elo change data for a player in a match.""" - uuid: str - change: int | None = None - elo_rate: int | None = Field(default=None, alias="eloRate") + uuid: str = Field(description="Player UUID without dashes") + change: int | None = Field(default=None, description="Elo change amount") + elo_rate: int | None = Field( + default=None, alias="eloRate", description="Elo rating after the match" + ) model_config = {"populate_by_name": True} @@ -62,8 +74,8 @@ class EloChange(BaseModel): class VodInfo(BaseModel): """VOD information for a match.""" - uuid: str - url: str - starts_at: int = Field(alias="startsAt") + uuid: str = Field(description="Player UUID without dashes") + url: str = Field(description="VOD URL") + starts_at: int = Field(alias="startsAt", description="VOD start timestamp offset") model_config = {"populate_by_name": True} diff --git a/src/mcsrranked/types/user.py b/src/mcsrranked/types/user.py index f6c3a48..607fd95 100644 --- a/src/mcsrranked/types/user.py +++ b/src/mcsrranked/types/user.py @@ -21,17 +21,29 @@ class MatchTypeStats(BaseModel): """Statistics for a specific match type (ranked/casual).""" - played_matches: int = Field(default=0, alias="playedMatches") - wins: int = 0 - losses: int = 0 - draws: int = 0 - forfeits: int = 0 - highest_winstreak: int = Field(default=0, alias="highestWinstreak") - current_winstreak: int = Field(default=0, alias="currentWinstreak") - playtime: int = 0 - best_time: int | None = Field(default=None, alias="bestTime") - best_time_id: int | None = Field(default=None, alias="bestTimeId") - completions: int = 0 + played_matches: int = Field( + default=0, alias="playedMatches", description="Total matches played" + ) + 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" + ) + current_winstreak: int = Field( + default=0, alias="currentWinstreak", description="Current win streak" + ) + playtime: int = Field(default=0, description="Total playtime 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") model_config = {"populate_by_name": True} @@ -39,8 +51,12 @@ class MatchTypeStats(BaseModel): class SeasonStats(BaseModel): """Season statistics container.""" - ranked: MatchTypeStats = Field(default_factory=MatchTypeStats) - casual: MatchTypeStats = Field(default_factory=MatchTypeStats) + ranked: MatchTypeStats = Field( + default_factory=MatchTypeStats, description="Ranked match statistics" + ) + casual: MatchTypeStats = Field( + default_factory=MatchTypeStats, description="Casual match statistics" + ) model_config = {"populate_by_name": True} @@ -48,8 +64,12 @@ class SeasonStats(BaseModel): class TotalStats(BaseModel): """All-time statistics container.""" - ranked: MatchTypeStats = Field(default_factory=MatchTypeStats) - casual: MatchTypeStats = Field(default_factory=MatchTypeStats) + ranked: MatchTypeStats = Field( + default_factory=MatchTypeStats, description="All-time ranked match statistics" + ) + casual: MatchTypeStats = Field( + default_factory=MatchTypeStats, description="All-time casual match statistics" + ) model_config = {"populate_by_name": True} @@ -57,8 +77,12 @@ class TotalStats(BaseModel): class UserStatistics(BaseModel): """User statistics for season and total.""" - season: SeasonStats = Field(default_factory=SeasonStats) - total: TotalStats = Field(default_factory=TotalStats) + season: SeasonStats = Field( + default_factory=SeasonStats, description="Current season statistics" + ) + total: TotalStats = Field( + default_factory=TotalStats, description="All-time statistics" + ) model_config = {"populate_by_name": True} @@ -66,10 +90,20 @@ class UserStatistics(BaseModel): class UserTimestamps(BaseModel): """User activity timestamps.""" - first_online: int = Field(alias="firstOnline") - last_online: int = Field(alias="lastOnline") - last_ranked: int | None = Field(default=None, alias="lastRanked") - next_decay: int | None = Field(default=None, alias="nextDecay") + first_online: int = Field( + alias="firstOnline", description="Unix timestamp of first connection" + ) + last_online: int = Field( + alias="lastOnline", description="Unix timestamp of last connection" + ) + last_ranked: int | None = Field( + default=None, + alias="lastRanked", + description="Unix timestamp of last ranked match", + ) + next_decay: int | None = Field( + default=None, alias="nextDecay", description="Unix timestamp of next elo decay" + ) model_config = {"populate_by_name": True} @@ -77,10 +111,10 @@ class UserTimestamps(BaseModel): class PhaseResult(BaseModel): """Phase result data.""" - phase: int - elo_rate: int = Field(alias="eloRate") - elo_rank: int = Field(alias="eloRank") - point: int + phase: int = Field(description="Phase number") + elo_rate: int = Field(alias="eloRate", description="Elo rating at phase end") + elo_rank: int = Field(alias="eloRank", description="Elo rank at phase end") + point: int = Field(description="Phase reward points") model_config = {"populate_by_name": True} @@ -88,9 +122,15 @@ class PhaseResult(BaseModel): class LastSeasonState(BaseModel): """Last state of a season.""" - elo_rate: int | None = Field(default=None, alias="eloRate") - elo_rank: int | None = Field(default=None, alias="eloRank") - phase_point: int | None = Field(default=None, alias="phasePoint") + elo_rate: int | None = Field( + default=None, alias="eloRate", description="Last elo rating of season" + ) + elo_rank: int | None = Field( + default=None, alias="eloRank", description="Last elo rank of season" + ) + phase_point: int | None = Field( + default=None, alias="phasePoint", description="Last phase points of season" + ) model_config = {"populate_by_name": True} @@ -98,10 +138,14 @@ class LastSeasonState(BaseModel): class SeasonResult(BaseModel): """User's season result data.""" - last: LastSeasonState - highest: int | None = None - lowest: int | None = None - phases: list[PhaseResult] = Field(default_factory=list) + last: LastSeasonState = Field(description="Final season state") + highest: int | None = Field( + default=None, description="Highest elo rating of season" + ) + lowest: int | None = Field(default=None, description="Lowest elo rating of season") + phases: list[PhaseResult] = Field( + default_factory=list, description="Phase results for the season" + ) model_config = {"populate_by_name": True} @@ -109,8 +153,8 @@ class SeasonResult(BaseModel): class Connection(BaseModel): """Third-party connection data.""" - id: str - name: str + id: str = Field(description="Connection identifier") + name: str = Field(description="Connection display name") model_config = {"populate_by_name": True} @@ -118,9 +162,9 @@ class Connection(BaseModel): class UserConnections(BaseModel): """User's third-party connections.""" - discord: Connection | None = None - twitch: Connection | None = None - youtube: Connection | None = None + discord: Connection | None = Field(default=None, description="Discord connection") + twitch: Connection | None = Field(default=None, description="Twitch connection") + youtube: Connection | None = Field(default=None, description="YouTube connection") model_config = {"populate_by_name": True} @@ -128,9 +172,9 @@ class UserConnections(BaseModel): class WeeklyRaceResult(BaseModel): """User's weekly race result.""" - id: int - time: int - rank: int + id: int = Field(description="Weekly race ID") + time: int = Field(description="Completion time in milliseconds") + rank: int = Field(description="Rank in the weekly race") model_config = {"populate_by_name": True} @@ -138,8 +182,12 @@ class WeeklyRaceResult(BaseModel): class AchievementsContainer(BaseModel): """Container for user achievements.""" - display: list[Achievement] = Field(default_factory=list) - total: list[Achievement] = Field(default_factory=list) + display: list[Achievement] = Field( + default_factory=list, description="Achievements displayed in-game" + ) + total: list[Achievement] = Field( + default_factory=list, description="All other achievements" + ) model_config = {"populate_by_name": True} @@ -147,19 +195,37 @@ class AchievementsContainer(BaseModel): class User(BaseModel): """Full user profile data.""" - uuid: str - nickname: str - role_type: int = Field(alias="roleType") - elo_rate: int | None = Field(default=None, alias="eloRate") - elo_rank: int | None = Field(default=None, alias="eloRank") - country: str | None = None - achievements: AchievementsContainer = Field(default_factory=AchievementsContainer) - timestamp: UserTimestamps | None = None - statistics: UserStatistics = Field(default_factory=UserStatistics) - connections: UserConnections = Field(default_factory=UserConnections) - season_result: SeasonResult | None = Field(default=None, alias="seasonResult") + uuid: str = Field(description="UUID without dashes") + nickname: str = Field(description="Player display name") + role_type: int = Field(alias="roleType", description="User role type") + elo_rate: int | None = Field( + default=None, + alias="eloRate", + description="Elo rating for current season. None if placement matches not completed.", + ) + elo_rank: int | None = Field( + default=None, alias="eloRank", description="Rank for current season" + ) + country: str | None = Field( + default=None, description="Country code (lowercase ISO 3166-1 alpha-2)" + ) + achievements: AchievementsContainer = Field( + default_factory=AchievementsContainer, description="User achievements" + ) + timestamp: UserTimestamps | None = Field( + default=None, description="Activity timestamps" + ) + statistics: UserStatistics = Field( + default_factory=UserStatistics, description="Season and all-time statistics" + ) + connections: UserConnections = Field( + default_factory=UserConnections, description="Third-party connections" + ) + season_result: SeasonResult | None = Field( + default=None, alias="seasonResult", description="Current season elo data" + ) weekly_races: list[WeeklyRaceResult] = Field( - default_factory=list, alias="weeklyRaces" + default_factory=list, alias="weeklyRaces", description="Weekly race results" ) model_config = {"populate_by_name": True} @@ -168,10 +234,12 @@ class User(BaseModel): class SeasonResultEntry(BaseModel): """Season result entry for user seasons endpoint.""" - last: LastSeasonState - highest: int - lowest: int - phases: list[PhaseResult] = Field(default_factory=list) + last: LastSeasonState = Field(description="Final season state") + highest: int = Field(description="Highest elo rating of season") + lowest: int = Field(description="Lowest elo rating of season") + phases: list[PhaseResult] = Field( + default_factory=list, description="Phase results for the season" + ) model_config = {"populate_by_name": True} @@ -179,14 +247,24 @@ class SeasonResultEntry(BaseModel): class UserSeasons(BaseModel): """User's season results across all seasons.""" - uuid: str - nickname: str - role_type: int = Field(alias="roleType") - elo_rate: int | None = Field(default=None, alias="eloRate") - elo_rank: int | None = Field(default=None, alias="eloRank") - country: str | None = None + uuid: str = Field(description="UUID without dashes") + nickname: str = Field(description="Player display name") + role_type: int = Field(alias="roleType", description="User role type") + elo_rate: int | None = Field( + default=None, + alias="eloRate", + description="Elo rating for current season. None if placement matches not completed.", + ) + elo_rank: int | None = Field( + default=None, alias="eloRank", description="Rank for current season" + ) + country: str | None = Field( + default=None, description="Country code (lowercase ISO 3166-1 alpha-2)" + ) season_results: dict[str, SeasonResultEntry] = Field( - default_factory=dict, alias="seasonResults" + default_factory=dict, + alias="seasonResults", + description="Season results keyed by season number", ) model_config = {"populate_by_name": True} diff --git a/src/mcsrranked/types/weekly_race.py b/src/mcsrranked/types/weekly_race.py index 6a8e7db..87e6576 100644 --- a/src/mcsrranked/types/weekly_race.py +++ b/src/mcsrranked/types/weekly_race.py @@ -14,10 +14,10 @@ class WeeklyRaceSeed(BaseModel): """Weekly race seed information.""" - overworld: str | None = None - nether: str | None = None - the_end: str | None = Field(default=None, alias="theEnd") - rng: str | None = None + overworld: str | None = Field(default=None, description="Overworld seed") + 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") model_config = {"populate_by_name": True} @@ -25,10 +25,12 @@ class WeeklyRaceSeed(BaseModel): class RaceLeaderboardEntry(BaseModel): """Entry in the weekly race leaderboard.""" - rank: int - player: UserProfile - time: int - replay_exist: bool = Field(default=False, alias="replayExist") + rank: int = Field(description="Leaderboard rank") + player: UserProfile = Field(description="Player profile") + time: int = Field(description="Completion time in milliseconds") + replay_exist: bool = Field( + default=False, alias="replayExist", description="Whether replay is available" + ) model_config = {"populate_by_name": True} @@ -36,9 +38,11 @@ class RaceLeaderboardEntry(BaseModel): class WeeklyRace(BaseModel): """Weekly race data.""" - id: int - seed: WeeklyRaceSeed - ends_at: int = Field(alias="endsAt") - leaderboard: list[RaceLeaderboardEntry] = Field(default_factory=list) + id: int = Field(description="Weekly race ID") + seed: WeeklyRaceSeed = Field(description="Race seed information") + ends_at: int = Field(alias="endsAt", description="Unix timestamp when race ends") + leaderboard: list[RaceLeaderboardEntry] = Field( + default_factory=list, description="Race leaderboard" + ) model_config = {"populate_by_name": True}