rivalsdata-api 1.2.0__tar.gz → 1.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rivalsdata-api
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -87,18 +87,27 @@ print(hero_season[0].win_rate) # integer percent when wins/losses are present
87
87
 
88
88
  Calculated player class statistics are available through
89
89
  `player.stats.classes(season=20)` and the MCP `get_player_stats` tool with
90
- `category="classes"`. Each row contains `player_class` (`tank`, `support`,
90
+ `category="classes"`. Pass a numeric season ID for that season, or
91
+ `season="all"` for combined all-seasons data, matching `player.heroes.fetch`.
92
+ Omitting the season uses the endpoint default. `player.stats.heroes` also
93
+ accepts `season="all"`. Each row contains `player_class` (`tank`, `support`,
91
94
  `dps`), the official role, hero IDs, and separate `competitive` and `quickplay`
92
95
  totals for games, wins, losses, and available MVP/SVP counts.
93
96
 
94
97
  ```python
95
98
  with RivalsDataClient() as rd:
96
- stats = rd.get_player("GS-").stats.classes(season=20)
99
+ player = rd.get_player("GS-")
100
+ stats = player.stats.classes(season=20)
101
+ all_seasons = player.stats.classes(season="all")
97
102
  for row in stats.classes:
98
103
  print(row.player_class, row.competitive.win_rate)
99
104
  print(stats.excluded) # Unknown roles or incomplete win/loss records
100
105
  ```
101
106
 
107
+ For MCP, use `get_player_stats(uid_or_name="GS-", category="classes", season=20)`
108
+ for one season, or `season="all"` for combined all-seasons stats. Both return
109
+ the same class response structure.
110
+
102
111
  Win rates are `total wins / (total wins + total losses)`, rounded to an integer
103
112
  percent. They are weighted by hero records, rather than averaging hero win
104
113
  rates. Switching heroes can make one match contribute to multiple records;
@@ -51,18 +51,27 @@ print(hero_season[0].win_rate) # integer percent when wins/losses are present
51
51
 
52
52
  Calculated player class statistics are available through
53
53
  `player.stats.classes(season=20)` and the MCP `get_player_stats` tool with
54
- `category="classes"`. Each row contains `player_class` (`tank`, `support`,
54
+ `category="classes"`. Pass a numeric season ID for that season, or
55
+ `season="all"` for combined all-seasons data, matching `player.heroes.fetch`.
56
+ Omitting the season uses the endpoint default. `player.stats.heroes` also
57
+ accepts `season="all"`. Each row contains `player_class` (`tank`, `support`,
55
58
  `dps`), the official role, hero IDs, and separate `competitive` and `quickplay`
56
59
  totals for games, wins, losses, and available MVP/SVP counts.
57
60
 
58
61
  ```python
59
62
  with RivalsDataClient() as rd:
60
- stats = rd.get_player("GS-").stats.classes(season=20)
63
+ player = rd.get_player("GS-")
64
+ stats = player.stats.classes(season=20)
65
+ all_seasons = player.stats.classes(season="all")
61
66
  for row in stats.classes:
62
67
  print(row.player_class, row.competitive.win_rate)
63
68
  print(stats.excluded) # Unknown roles or incomplete win/loss records
64
69
  ```
65
70
 
71
+ For MCP, use `get_player_stats(uid_or_name="GS-", category="classes", season=20)`
72
+ for one season, or `season="all"` for combined all-seasons stats. Both return
73
+ the same class response structure.
74
+
66
75
  Win rates are `total wins / (total wins + total losses)`, rounded to an integer
67
76
  percent. They are weighted by hero records, rather than averaging hero win
68
77
  rates. Switching heroes can make one match contribute to multiple records;
@@ -53,7 +53,11 @@ none of these, it returns `None`.
53
53
  `player.stats.classes(season=...)` derives tank (Vanguard), support (Strategist),
54
54
  and DPS (Duelist) totals from `/player/stats/heroes`; it does not call a class
55
55
  endpoint. It sums games/wins/losses separately for competitive and quickplay,
56
- and MVP/SVP counts when every included row supplies them. Win rate is calculated
56
+ accepting a numeric season ID or `season="all"` (sent to the API as `-1`). The
57
+ all-seasons selection combines the returned hero records across seasons;
58
+ omitting the season keeps the endpoint default. `player.stats.heroes` and MCP
59
+ `get_player_stats` support the same selector. The method also sums
60
+ MVP/SVP counts when every included row supplies them. Win rate is calculated
57
61
  from summed wins and losses. Unknown classes and incomplete win/loss rows appear
58
62
  in `excluded`. Hero switching can count a single match in multiple hero records,
59
63
  so class totals are participation counts rather than distinct matches.
@@ -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.2.0`.
18
+ - Version: `1.2.1`.
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`.
@@ -55,7 +55,11 @@ Rows offer `.win_rate` and `.winrate` integer-percent access when data supports
55
55
  it; all original data remains in mapping access.
56
56
 
57
57
  `player.stats.classes(season=...)` groups observed hero IDs into tank, support,
58
- and DPS. It sums games, wins, losses, and available MVP/SVP counts separately
58
+ and DPS. Supply a numeric season ID for one season or `season="all"` for
59
+ combined all-seasons stats; the latter sends `season=-1` to the upstream API.
60
+ The detailed `player.stats.heroes` method and MCP stats tool accept the same
61
+ selector. Omitting the season keeps the endpoint default.
62
+ It sums games, wins, losses, and available MVP/SVP counts separately
59
63
  for competitive and quickplay, with win rates calculated from summed wins and
60
64
  losses. The response has typed `classes` rows and an `excluded` list for unknown
61
65
  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.2.0"
7
+ version = "1.2.1"
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"
@@ -153,4 +153,4 @@ __all__ = [
153
153
  "hero_id",
154
154
  "hero_name",
155
155
  ]
156
- __version__ = "1.2.0"
156
+ __version__ = "1.2.1"
@@ -408,19 +408,23 @@ def get_player_matches(
408
408
 
409
409
  @mcp.tool()
410
410
  def get_player_stats(
411
- uid_or_name: str, category: str = "heroes", season: int | None = None
411
+ uid_or_name: str, category: str = "heroes",
412
+ season: int | Literal["all"] | None = None,
412
413
  ) -> Any:
413
414
  """Get player stats: heroes, maps, bans, or calculated classes.
414
415
 
415
416
  Classes sum hero wins/losses by tank/support/dps and game mode; these are
416
417
  hero participation totals, which can count a match more than once.
418
+ Supply a season ID or "all" for combined all-seasons totals. Omitting the
419
+ season uses the endpoint default.
417
420
  """
418
421
  methods = {"heroes": "heroes", "maps": "maps", "bans": "bans", "classes": "classes"}
419
422
  if category not in methods:
420
423
  raise ValueError("category must be one of: heroes, maps, bans, classes")
421
424
  def fetch(client: RivalsDataClient, value: str, category: str,
422
- season: int | None) -> Any:
423
- return getattr(client.get_player(value).stats, methods[category])(season=season)
425
+ season: int | Literal["all"] | None) -> Any:
426
+ season_id = -1 if season == "all" else season
427
+ return getattr(client.get_player(value).stats, methods[category])(season=season_id)
424
428
  return _call(fetch, uid_or_name, category, season)
425
429
 
426
430
 
@@ -205,13 +205,25 @@ class PlayerHeroes(PlayerResource):
205
205
  class PlayerStats(PlayerResource):
206
206
  """Detailed per-player statistics tabs."""
207
207
 
208
- def heroes(self, *, season: int | None = None) -> list[HeroStatsRecord]:
209
- payload = {"season": season} if season is not None else {}
208
+ def heroes(
209
+ self, *, season: int | Literal["all"] | None = None
210
+ ) -> list[HeroStatsRecord]:
211
+ """Fetch mode-specific hero stats for a season or all seasons.
212
+
213
+ ``season="all"`` sends the API's all-seasons selector (-1). Omitting
214
+ ``season`` preserves the endpoint default.
215
+ """
216
+ season_id = -1 if season == "all" else season
217
+ payload = {"season": season_id} if season_id is not None else {}
210
218
  return _many(self._post("/player/stats/heroes", **payload), HeroStatsRecord)
211
219
 
212
- def classes(self, *, season: int | None = None) -> ClassStatsResponse:
220
+ def classes(
221
+ self, *, season: int | Literal["all"] | None = None
222
+ ) -> ClassStatsResponse:
213
223
  """Sum hero records by tank/support/dps, separately for each mode.
214
224
 
225
+ Supply a season ID or ``season="all"`` for combined all-seasons totals.
226
+ Omitting ``season`` preserves the endpoint default.
215
227
  Win rate uses total wins / (wins + losses), not the average of hero
216
228
  percentages. Counts describe hero participation: switching heroes can
217
229
  cause one match to contribute to multiple hero or class records.
@@ -1,3 +1,5 @@
1
+ import pytest
2
+
1
3
  from rivalsdata import (
2
4
  ClassModeStats,
3
5
  ClassStatsRecord,
@@ -55,3 +57,46 @@ def test_one_percent_is_not_treated_as_a_fraction():
55
57
  row = ClassModeStats({"wins": 1, "losses": 99, "win_rate": 1})
56
58
  assert row.win_rate == 1
57
59
  assert row.winrate == 1
60
+
61
+
62
+ class SeasonClient:
63
+ def _post_json(self, path, payload):
64
+ self.request = path, payload
65
+ wins = {None: 2, 20: 3, -1: 12}[payload.get("season")]
66
+ return [{"hero_id": 1016, "competitive": {"wins": wins, "losses": 1}}]
67
+
68
+
69
+ @pytest.mark.parametrize("season,payload,wins", [
70
+ (None, {"uid": 123}, 2),
71
+ (20, {"uid": 123, "season": 20}, 3),
72
+ ("all", {"uid": 123, "season": -1}, 12),
73
+ (-1, {"uid": 123, "season": -1}, 12),
74
+ ])
75
+ def test_class_season_selector_uses_the_selected_hero_data(season, payload, wins):
76
+ client = SeasonClient()
77
+ result = PlayerStats(client, 123).classes(season=season)
78
+ assert client.request == ("/player/stats/heroes", payload)
79
+ assert result.classes[1].competitive.wins == wins
80
+
81
+
82
+ def test_detailed_hero_stats_accept_the_same_all_seasons_selector():
83
+ client = SeasonClient()
84
+ rows = PlayerStats(client, 123).heroes(season="all")
85
+ assert client.request == ("/player/stats/heroes", {"uid": 123, "season": -1})
86
+ assert rows[0].competitive.wins == 12
87
+
88
+
89
+ def test_mcp_stats_exposes_all_seasons_and_forwards_the_selector(monkeypatch):
90
+ import asyncio
91
+ from types import SimpleNamespace
92
+
93
+ server = pytest.importorskip("rivalsdata.mcp_server", exc_type=ImportError)
94
+ client = SeasonClient()
95
+ client.get_player = lambda value: SimpleNamespace(stats=PlayerStats(client, 123))
96
+ monkeypatch.setattr(server, "_call", lambda fn, *a, **kw: fn(client, *a, **kw))
97
+ result = server.get_player_stats("123", category="classes", season="all")
98
+ assert client.request == ("/player/stats/heroes", {"uid": 123, "season": -1})
99
+ assert result.classes[1].competitive.wins == 12
100
+ tools = asyncio.run(server.mcp.list_tools())
101
+ schema = next(tool.inputSchema for tool in tools if tool.name == "get_player_stats")
102
+ assert {"const": "all", "type": "string"} in schema["properties"]["season"]["anyOf"]
File without changes