rivalsdata-api 1.2.3__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/PKG-INFO +24 -1
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/README.md +23 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/docs/API.md +29 -1
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/docs/PROJECT_CONTEXT.md +12 -2
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/pyproject.toml +1 -1
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/src/rivalsdata/__init__.py +1 -1
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/src/rivalsdata/mcp_server.py +15 -3
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/src/rivalsdata/models.py +9 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/src/rivalsdata/resources.py +24 -5
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/tests/test_class_stats.py +31 -2
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/.github/workflows/publish-pypi.yml +0 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/.gitignore +0 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/CONTRIBUTING.md +0 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/LICENSE +0 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/src/rivalsdata/client.py +0 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/src/rivalsdata/exceptions.py +0 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/src/rivalsdata/hero_ids.py +0 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/tests/test_hero_names.py +0 -0
- {rivalsdata_api-1.2.3 → rivalsdata_api-1.3.0}/tests/test_typed_responses.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rivalsdata-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Python client and MCP server for public Marvel Rivals stats from RivalsData
|
|
5
5
|
Project-URL: Homepage, https://github.com/GS-Rionnag/rivalsdata-api
|
|
6
6
|
Project-URL: Repository, https://github.com/GS-Rionnag/rivalsdata-api
|
|
@@ -114,6 +114,29 @@ For MCP, use `get_player_stats(uid_or_name="GS-", category="classes", season=20)
|
|
|
114
114
|
for one season, or `season="all"` for combined all-seasons stats. Both return
|
|
115
115
|
the same class response structure.
|
|
116
116
|
|
|
117
|
+
Detailed hero stats require a mode and match the website's selected tab:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
competitive = player.stats.heroes(mode="competitive", season="all")
|
|
121
|
+
quickplay = player.stats.heroes(mode="quickplay", season=20)
|
|
122
|
+
print(competitive[0].competitive.games)
|
|
123
|
+
print(competitive[0].rank) # Hero leaderboard position, or None if unavailable
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Only heroes with data for the chosen mode are returned, with that mode's nested
|
|
127
|
+
stats and a `mode` label; the other mode is omitted. Rows are sorted by the
|
|
128
|
+
selected mode's games played descending, with ties retaining the JSON order.
|
|
129
|
+
The source returns both modes in one response; filtering and sorting happen
|
|
130
|
+
in this package, as they do on the website. Existing calls to
|
|
131
|
+
`player.stats.heroes()` must now supply `mode`. MCP also requires `mode` when
|
|
132
|
+
`get_player_stats` uses `category="heroes"`; other categories do not require it.
|
|
133
|
+
The separate summary method `player.heroes.fetch()` keeps its existing behavior.
|
|
134
|
+
|
|
135
|
+
Hero stats include the source's top-level `rank`, matching the **#N** displayed
|
|
136
|
+
in the left-hand hero card. It is preserved for either mode and all-seasons
|
|
137
|
+
requests when supplied by the source; it is not recalculated as a quickplay or
|
|
138
|
+
all-seasons leaderboard position. Missing ranks are returned as `None`.
|
|
139
|
+
|
|
117
140
|
Win rates are `total wins / (total wins + total losses)`, rounded to an integer
|
|
118
141
|
percent. They are weighted by hero records, rather than averaging hero win
|
|
119
142
|
rates. The response's `metadata` identifies the source, formula, and requested
|
|
@@ -78,6 +78,29 @@ For MCP, use `get_player_stats(uid_or_name="GS-", category="classes", season=20)
|
|
|
78
78
|
for one season, or `season="all"` for combined all-seasons stats. Both return
|
|
79
79
|
the same class response structure.
|
|
80
80
|
|
|
81
|
+
Detailed hero stats require a mode and match the website's selected tab:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
competitive = player.stats.heroes(mode="competitive", season="all")
|
|
85
|
+
quickplay = player.stats.heroes(mode="quickplay", season=20)
|
|
86
|
+
print(competitive[0].competitive.games)
|
|
87
|
+
print(competitive[0].rank) # Hero leaderboard position, or None if unavailable
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Only heroes with data for the chosen mode are returned, with that mode's nested
|
|
91
|
+
stats and a `mode` label; the other mode is omitted. Rows are sorted by the
|
|
92
|
+
selected mode's games played descending, with ties retaining the JSON order.
|
|
93
|
+
The source returns both modes in one response; filtering and sorting happen
|
|
94
|
+
in this package, as they do on the website. Existing calls to
|
|
95
|
+
`player.stats.heroes()` must now supply `mode`. MCP also requires `mode` when
|
|
96
|
+
`get_player_stats` uses `category="heroes"`; other categories do not require it.
|
|
97
|
+
The separate summary method `player.heroes.fetch()` keeps its existing behavior.
|
|
98
|
+
|
|
99
|
+
Hero stats include the source's top-level `rank`, matching the **#N** displayed
|
|
100
|
+
in the left-hand hero card. It is preserved for either mode and all-seasons
|
|
101
|
+
requests when supplied by the source; it is not recalculated as a quickplay or
|
|
102
|
+
all-seasons leaderboard position. Missing ranks are returned as `None`.
|
|
103
|
+
|
|
81
104
|
Win rates are `total wins / (total wins + total losses)`, rounded to an integer
|
|
82
105
|
percent. They are weighted by hero records, rather than averaging hero win
|
|
83
106
|
rates. The response's `metadata` identifies the source, formula, and requested
|
|
@@ -21,7 +21,7 @@ and `.raw` so upstream additions are not discarded.
|
|
|
21
21
|
| Player match history | `POST /player/matches/cached` (or `/player/matches`) | `uid`, `cursor`, optional `season`; cached endpoint also accepts `mode`, `hero`, `teammate` | Cached response keys observed: `matches`, `next_cursor`, `source`; match rows include `assists`, `deaths`, `game_mode_id`, `game_play_mode_id`, `hero_id`, `is_mvp`, `is_svp`, `is_win`, `kills`, `map_id`, `match_uid`, `os`, `placement`, `platform`, `rank_level`, `rank_score`, `score_change`, `season`, `team_score`, `timestamp`, `winner_camp`. A sample profile marked its history private; a private profile can return no visible match rows. |
|
|
22
22
|
| Player teammates | `POST /player/teammates` | `uid`, optional `season`, `mode` | Array rows: `games`, `icon`, `losses`, `name`, `teammate_uid`, `wins`. |
|
|
23
23
|
| Player proficiency | `POST /player/proficiency` | `uid` | Object keyed by account id (sample: `11001_{uid}`); each account has `hero_proficiency_infos` keyed by hero id, with `proficiency_level` and `proficiency_point`. |
|
|
24
|
-
| Player hero stats | `POST /player/stats/heroes` | `uid`, optional `season` |
|
|
24
|
+
| Player hero stats | `POST /player/stats/heroes` | `uid`, optional `season`; wrapper requires `mode` (not sent upstream) | Source rows: `competitive`, `hero_id`, `quickplay`, `rank`. Each mode includes games, wins/losses, KDA, MVP/SVP counts, accuracy, and `per_10`/`per_game` combat averages. Wrapper selects one mode, sorts by its games, and includes `rank` (or `None`). |
|
|
25
25
|
| Player map stats | `POST /player/stats/maps` | `uid`, optional `season` | Array rows: `map`, `competitive`, `quickplay`; each mode has games, wins, losses, winrate. |
|
|
26
26
|
| Player ban stats | `POST /player/stats/bans` | `uid`, optional `season` | Array rows: `hero_id`, `matches`, `wins`, `losses`, `winrate`. |
|
|
27
27
|
| Player punishments | `POST /player/punishments` | `uid` | Object keys: `chat`, `login`, `rank`; non-null entries include `expire`, `name`, `reason`, `time`, `uid`. |
|
|
@@ -77,6 +77,34 @@ unverified. Warnings flag those limitations and any excluded records.
|
|
|
77
77
|
The roster at `/stats` on 2026-09-30 identifies Deadpool variants as 10571
|
|
78
78
|
(tank), 10572 (DPS), and 10573 (support), Daredevil as 1055, and Angela as 1056.
|
|
79
79
|
|
|
80
|
+
### Detailed hero mode selection and ordering
|
|
81
|
+
|
|
82
|
+
`player.stats.heroes(mode="competitive", season="all")` requires either
|
|
83
|
+
`"competitive"` or `"quickplay"`. Missing mode raises `TypeError`; unsupported
|
|
84
|
+
values raise `ValueError`. The upstream `/player/stats/heroes` request still
|
|
85
|
+
contains only UID and optional season, because the source returns both modes
|
|
86
|
+
together. The wrapper filters heroes without the selected mode's data, omits
|
|
87
|
+
the other mode's key, adds `mode`, and sorts by selected-mode `games` descending.
|
|
88
|
+
Equal game counts retain source order. Access stats through the selected nested
|
|
89
|
+
key, such as `row.competitive` or `row.quickplay`.
|
|
90
|
+
|
|
91
|
+
`row.rank` is the source-supplied hero leaderboard position from the top-level
|
|
92
|
+
JSON `rank` field, or `None` when unavailable. On 2026-10-01, GS-'s left-hand
|
|
93
|
+
Loki card showed #408; both `/player/heroes` and `/player/stats/heroes` supplied
|
|
94
|
+
`rank: 408` for hero 1016. The detailed Stats tab did not display that field.
|
|
95
|
+
Mode filtering preserves it. All-seasons requests may also return a rank, but
|
|
96
|
+
the wrapper does not infer its leaderboard period or recalculate it per mode.
|
|
97
|
+
|
|
98
|
+
Camoufox inspection of GS-'s All Seasons page on 2026-10-01 found the JSON sorted
|
|
99
|
+
by combined competitive + quickplay games. The Competitive tab's 40 rows and
|
|
100
|
+
Quickplay tab's 39 rows each exactly matched sorting by that mode's games.
|
|
101
|
+
Switching tabs sent no additional hero-stats request.
|
|
102
|
+
|
|
103
|
+
MCP `get_player_stats(category="heroes", mode=..., season=...)` requires mode
|
|
104
|
+
for heroes and rejects missing or invalid values before making a request.
|
|
105
|
+
Other categories retain their behavior; class stats still aggregate both modes
|
|
106
|
+
from the complete source response. `player.heroes.fetch()` is unchanged.
|
|
107
|
+
|
|
80
108
|
### Character playtime investigation (2026-09-30)
|
|
81
109
|
|
|
82
110
|
Using Camoufox (the package's optional browser dependency), the public GS-
|
|
@@ -15,7 +15,7 @@ as public, stable methods. Keep requests respectful and conservative.
|
|
|
15
15
|
## Current package
|
|
16
16
|
|
|
17
17
|
- Distribution: `rivalsdata-api`; import: `rivalsdata`.
|
|
18
|
-
- Version: `1.
|
|
18
|
+
- Version: `1.3.0`.
|
|
19
19
|
- Python `>=3.10`, Hatchling build, `src/` layout.
|
|
20
20
|
- Runtime HTTP dependency: `curl-cffi`; optional browser fallback: Camoufox.
|
|
21
21
|
- Public entry point: `RivalsDataClient`.
|
|
@@ -65,7 +65,17 @@ and DPS. Supply a numeric season ID for one season or `season="all"` for
|
|
|
65
65
|
combined all-seasons stats; the latter sends `season=-1` to the upstream API.
|
|
66
66
|
The detailed `player.stats.heroes` method and MCP stats tool accept the same
|
|
67
67
|
selector. Omitting the season keeps the endpoint default.
|
|
68
|
-
|
|
68
|
+
Detailed `player.stats.heroes` now requires `mode="competitive"` or
|
|
69
|
+
`mode="quickplay"`; it filters to that mode, omits the opposite mode key, and
|
|
70
|
+
sorts by selected-mode games descending (stable ties), matching the website.
|
|
71
|
+
MCP requires mode for category heroes; other categories do not require it.
|
|
72
|
+
Class aggregation uses a private full-response fetch so both class modes stay
|
|
73
|
+
available. Hero summaries (`player.heroes.fetch`) remain unchanged.
|
|
74
|
+
Detailed hero rows always expose `rank` (None if absent), preserving the
|
|
75
|
+
source's top-level hero leaderboard position shown as #N in the left card.
|
|
76
|
+
It is not recalculated for quickplay or all seasons. GS-'s Loki was #408 in
|
|
77
|
+
both the card and both hero endpoint responses on 2026-10-01.
|
|
78
|
+
Class calculation sums games, wins, losses, and available MVP/SVP counts separately
|
|
69
79
|
for competitive and quickplay, with win rates calculated from summed wins and
|
|
70
80
|
losses. The response has typed `classes` rows and an `excluded` list for unknown
|
|
71
81
|
roles or incomplete win/loss data. MCP exposes it through `get_player_stats`
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rivalsdata-api"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "Python client and MCP server for public Marvel Rivals stats from RivalsData"
|
|
9
9
|
keywords = ["marvel-rivals", "rivalsdata", "game-stats", "api-client", "mcp"]
|
|
10
10
|
readme = "README.md"
|
|
@@ -415,9 +415,15 @@ def get_player_matches(
|
|
|
415
415
|
def get_player_stats(
|
|
416
416
|
uid_or_name: str, category: str = "heroes",
|
|
417
417
|
season: int | Literal["all"] | None = None,
|
|
418
|
+
mode: Literal["competitive", "quickplay"] | None = None,
|
|
418
419
|
) -> Any:
|
|
419
420
|
"""Get player stats: heroes, maps, bans, or calculated classes.
|
|
420
421
|
|
|
422
|
+
Heroes REQUIRE mode="competitive" or mode="quickplay". Only that mode's
|
|
423
|
+
hero records are returned, sorted by its games played descending to match
|
|
424
|
+
the website. Mode is not used for the other categories.
|
|
425
|
+
Hero rows include rank: the source's hero leaderboard position, also shown
|
|
426
|
+
as #N in the left-hand profile card, or null when the source has no rank.
|
|
421
427
|
Classes sum hero wins/losses by tank/support/dps and game mode; these are
|
|
422
428
|
summed hero records, which may overlap within a match. The response metadata
|
|
423
429
|
explains the calculation and scope; class totals cannot establish a player's
|
|
@@ -428,11 +434,17 @@ def get_player_stats(
|
|
|
428
434
|
methods = {"heroes": "heroes", "maps": "maps", "bans": "bans", "classes": "classes"}
|
|
429
435
|
if category not in methods:
|
|
430
436
|
raise ValueError("category must be one of: heroes, maps, bans, classes")
|
|
437
|
+
if category == "heroes" and mode not in ("competitive", "quickplay"):
|
|
438
|
+
raise ValueError("heroes require mode=competitive or mode=quickplay")
|
|
431
439
|
def fetch(client: RivalsDataClient, value: str, category: str,
|
|
432
|
-
season: int | Literal["all"] | None
|
|
440
|
+
season: int | Literal["all"] | None,
|
|
441
|
+
mode: Literal["competitive", "quickplay"] | None) -> Any:
|
|
433
442
|
season_id = -1 if season == "all" else season
|
|
434
|
-
|
|
435
|
-
|
|
443
|
+
filters: dict[str, Any] = {"season": season_id}
|
|
444
|
+
if category == "heroes":
|
|
445
|
+
filters["mode"] = mode
|
|
446
|
+
return getattr(client.get_player(value).stats, methods[category])(**filters)
|
|
447
|
+
return _call(fetch, uid_or_name, category, season, mode)
|
|
436
448
|
|
|
437
449
|
|
|
438
450
|
@mcp.tool()
|
|
@@ -388,12 +388,21 @@ class HeroModeStats(StatRecord):
|
|
|
388
388
|
|
|
389
389
|
|
|
390
390
|
class HeroStatsRecord(Character):
|
|
391
|
+
"""Detailed hero stats including the source's leaderboard position.
|
|
392
|
+
|
|
393
|
+
``rank`` is the number shown as #N in the profile's left-hand hero card.
|
|
394
|
+
It is shared source metadata, not a rank calculated for the selected mode.
|
|
395
|
+
None means the source did not supply a position.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
mode: str | None
|
|
391
399
|
competitive: HeroModeStats | None
|
|
392
400
|
quickplay: HeroModeStats | None
|
|
393
401
|
rank: int | str | None
|
|
394
402
|
|
|
395
403
|
def __init__(self, data: Mapping[str, Any] | None = None, **values: Any) -> None:
|
|
396
404
|
super().__init__(data, **values)
|
|
405
|
+
self._data.setdefault("rank", None)
|
|
397
406
|
for key in ("competitive", "quickplay"):
|
|
398
407
|
if isinstance(self._data.get(key), Mapping):
|
|
399
408
|
self._data[key] = HeroModeStats(self._data[key])
|
|
@@ -206,13 +206,32 @@ class PlayerStats(PlayerResource):
|
|
|
206
206
|
"""Detailed per-player statistics tabs."""
|
|
207
207
|
|
|
208
208
|
def heroes(
|
|
209
|
-
self, *,
|
|
209
|
+
self, *, mode: Literal["competitive", "quickplay"],
|
|
210
|
+
season: int | Literal["all"] | None = None,
|
|
210
211
|
) -> list[HeroStatsRecord]:
|
|
211
|
-
"""Fetch
|
|
212
|
+
"""Fetch heroes for a required mode, in the website's most-played order.
|
|
212
213
|
|
|
214
|
+
The source supplies both modes together. This method keeps only heroes
|
|
215
|
+
with the selected mode's data, removes the other mode, and sorts by
|
|
216
|
+
selected-mode games descending. Ties retain the source order.
|
|
217
|
+
Each row includes ``rank`` (the source's hero leaderboard position,
|
|
218
|
+
also shown in the left-hand profile card), or None when unavailable.
|
|
213
219
|
``season="all"`` sends the API's all-seasons selector (-1). Omitting
|
|
214
220
|
``season`` preserves the endpoint default.
|
|
215
221
|
"""
|
|
222
|
+
if mode not in ("competitive", "quickplay"):
|
|
223
|
+
raise ValueError("mode must be competitive or quickplay")
|
|
224
|
+
heroes = [hero for hero in self._hero_records(season=season)
|
|
225
|
+
if hero.get(mode) is not None]
|
|
226
|
+
heroes.sort(key=lambda hero: hero[mode].get("games", 0) or 0, reverse=True)
|
|
227
|
+
other_mode = "quickplay" if mode == "competitive" else "competitive"
|
|
228
|
+
return [HeroStatsRecord({key: value for key, value in hero.raw.items()
|
|
229
|
+
if key != other_mode}, mode=mode) for hero in heroes]
|
|
230
|
+
|
|
231
|
+
def _hero_records(
|
|
232
|
+
self, *, season: int | Literal["all"] | None = None
|
|
233
|
+
) -> list[HeroStatsRecord]:
|
|
234
|
+
"""Fetch both upstream modes for class aggregation and hero filtering."""
|
|
216
235
|
season_id = -1 if season == "all" else season
|
|
217
236
|
payload = {"season": season_id} if season_id is not None else {}
|
|
218
237
|
return _many(self._post("/player/stats/heroes", **payload), HeroStatsRecord)
|
|
@@ -238,7 +257,7 @@ class PlayerStats(PlayerResource):
|
|
|
238
257
|
("dps", "Duelist"))
|
|
239
258
|
}
|
|
240
259
|
excluded = []
|
|
241
|
-
for hero in self.
|
|
260
|
+
for hero in self._hero_records(season=season):
|
|
242
261
|
identifier = hero.get("hero_id")
|
|
243
262
|
name = hero_class(identifier)
|
|
244
263
|
if name is None:
|
|
@@ -275,8 +294,8 @@ class PlayerStats(PlayerResource):
|
|
|
275
294
|
group[mode] = totals
|
|
276
295
|
all_seasons = season in ("all", -1)
|
|
277
296
|
warnings = [
|
|
278
|
-
"Hero records may overlap within a match. Class totals must not "
|
|
279
|
-
|
|
297
|
+
("Hero records may overlap within a match. Class totals must not "
|
|
298
|
+
"be used to calculate the player's overall match win rate.")
|
|
280
299
|
]
|
|
281
300
|
if all_seasons:
|
|
282
301
|
warnings.append(
|
|
@@ -7,7 +7,7 @@ from rivalsdata import (
|
|
|
7
7
|
hero_class,
|
|
8
8
|
hero_name,
|
|
9
9
|
)
|
|
10
|
-
from rivalsdata.resources import PlayerStats
|
|
10
|
+
from rivalsdata.resources import PlayerHeroes, PlayerStats
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class HeroClient:
|
|
@@ -42,6 +42,7 @@ def test_weighted_class_totals_modes_and_exclusions():
|
|
|
42
42
|
assert groups["dps"].quickplay.win_rate is None
|
|
43
43
|
assert len(result.excluded) == 3
|
|
44
44
|
assert result.to_dict()["classes"][0]["competitive"]["win_rate"] == 83
|
|
45
|
+
assert any("excluded" in warning for warning in result.metadata.warnings)
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def test_roles_use_observed_ids_and_do_not_guess_generic_deadpool():
|
|
@@ -77,13 +78,39 @@ def test_class_season_selector_uses_the_selected_hero_data(season, payload, wins
|
|
|
77
78
|
result = PlayerStats(client, 123).classes(season=season)
|
|
78
79
|
assert client.request == ("/player/stats/heroes", payload)
|
|
79
80
|
assert result.classes[1].competitive.wins == wins
|
|
81
|
+
metadata = result.to_dict()["metadata"]
|
|
82
|
+
assert metadata["counts_basis"] == "summed_hero_records"
|
|
83
|
+
assert metadata["unique_matches_verified"] is False
|
|
84
|
+
assert metadata["hero_switch_attribution"] == "unknown"
|
|
85
|
+
assert metadata["season"] == ("all" if season in ("all", -1) else season)
|
|
86
|
+
assert metadata["season_scope"] == (
|
|
87
|
+
"all" if season in ("all", -1) else
|
|
88
|
+
"endpoint_default" if season is None else "season"
|
|
89
|
+
)
|
|
90
|
+
assert len(metadata["warnings"]) == (2 if season in ("all", -1) else 1)
|
|
80
91
|
|
|
81
92
|
|
|
82
93
|
def test_detailed_hero_stats_accept_the_same_all_seasons_selector():
|
|
83
94
|
client = SeasonClient()
|
|
84
|
-
rows = PlayerStats(client, 123).heroes(season="all")
|
|
95
|
+
rows = PlayerStats(client, 123).heroes(mode="competitive", season="all")
|
|
85
96
|
assert client.request == ("/player/stats/heroes", {"uid": 123, "season": -1})
|
|
86
97
|
assert rows[0].competitive.wins == 12
|
|
98
|
+
assert rows[0].mode == "competitive"
|
|
99
|
+
assert "quickplay" not in rows[0]
|
|
100
|
+
with pytest.raises(TypeError):
|
|
101
|
+
PlayerStats(client, 123).heroes(season="all")
|
|
102
|
+
with pytest.raises(ValueError):
|
|
103
|
+
PlayerStats(client, 123).heroes(mode="invalid", season="all")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.parametrize("season,season_id", [(None, None), (20, 20), ("all", -1)])
|
|
107
|
+
def test_hero_summary_season_selector(season, season_id):
|
|
108
|
+
client = SeasonClient()
|
|
109
|
+
PlayerHeroes(client, 123).fetch(season=season)
|
|
110
|
+
expected = {"uid": 123}
|
|
111
|
+
if season_id is not None:
|
|
112
|
+
expected["season"] = season_id
|
|
113
|
+
assert client.request == ("/player/heroes", expected)
|
|
87
114
|
|
|
88
115
|
|
|
89
116
|
def test_mcp_stats_exposes_all_seasons_and_forwards_the_selector(monkeypatch):
|
|
@@ -97,6 +124,8 @@ def test_mcp_stats_exposes_all_seasons_and_forwards_the_selector(monkeypatch):
|
|
|
97
124
|
result = server.get_player_stats("123", category="classes", season="all")
|
|
98
125
|
assert client.request == ("/player/stats/heroes", {"uid": 123, "season": -1})
|
|
99
126
|
assert result.classes[1].competitive.wins == 12
|
|
127
|
+
assert result.metadata.season == "all"
|
|
128
|
+
assert result.metadata.season_scope == "all"
|
|
100
129
|
tools = asyncio.run(server.mcp.list_tools())
|
|
101
130
|
schema = next(tool.inputSchema for tool in tools if tool.name == "get_player_stats")
|
|
102
131
|
assert {"const": "all", "type": "string"} in schema["properties"]["season"]["anyOf"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|