rivalsdata-api 1.2.0__tar.gz → 1.2.2__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.2
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
@@ -64,6 +64,7 @@ print(hero_id("Loki")) # 1016
64
64
  with RivalsDataClient() as rd:
65
65
  player = rd.get_player("GS-") # numeric UID works too
66
66
  print(player.name, player.level, player.rank_game_season)
67
+ print(player.win_rate) # Current-season competitive win rate
67
68
 
68
69
  # Player profile sections are lazy resource managers.
69
70
  hero_season = player.heroes.fetch(season=20)
@@ -85,20 +86,34 @@ with RivalsDataClient() as rd:
85
86
  print(hero_season[0].win_rate) # integer percent when wins/losses are present
86
87
  ```
87
88
 
89
+ The player overview's overall win rate (`player.win_rate`) is the **current-season
90
+ competitive win rate**. It uses the latest available competitive season with
91
+ usable counts when a direct source rate is absent; it does not combine seasons.
92
+ Hero and class stats use the season selector supplied to their own methods.
93
+
88
94
  Calculated player class statistics are available through
89
95
  `player.stats.classes(season=20)` and the MCP `get_player_stats` tool with
90
- `category="classes"`. Each row contains `player_class` (`tank`, `support`,
96
+ `category="classes"`. Pass a numeric season ID for that season, or
97
+ `season="all"` for combined all-seasons data, matching `player.heroes.fetch`.
98
+ Omitting the season uses the endpoint default. `player.stats.heroes` also
99
+ accepts `season="all"`. Each row contains `player_class` (`tank`, `support`,
91
100
  `dps`), the official role, hero IDs, and separate `competitive` and `quickplay`
92
101
  totals for games, wins, losses, and available MVP/SVP counts.
93
102
 
94
103
  ```python
95
104
  with RivalsDataClient() as rd:
96
- stats = rd.get_player("GS-").stats.classes(season=20)
105
+ player = rd.get_player("GS-")
106
+ stats = player.stats.classes(season=20)
107
+ all_seasons = player.stats.classes(season="all")
97
108
  for row in stats.classes:
98
109
  print(row.player_class, row.competitive.win_rate)
99
110
  print(stats.excluded) # Unknown roles or incomplete win/loss records
100
111
  ```
101
112
 
113
+ For MCP, use `get_player_stats(uid_or_name="GS-", category="classes", season=20)`
114
+ for one season, or `season="all"` for combined all-seasons stats. Both return
115
+ the same class response structure.
116
+
102
117
  Win rates are `total wins / (total wins + total losses)`, rounded to an integer
103
118
  percent. They are weighted by hero records, rather than averaging hero win
104
119
  rates. Switching heroes can make one match contribute to multiple records;
@@ -28,6 +28,7 @@ print(hero_id("Loki")) # 1016
28
28
  with RivalsDataClient() as rd:
29
29
  player = rd.get_player("GS-") # numeric UID works too
30
30
  print(player.name, player.level, player.rank_game_season)
31
+ print(player.win_rate) # Current-season competitive win rate
31
32
 
32
33
  # Player profile sections are lazy resource managers.
33
34
  hero_season = player.heroes.fetch(season=20)
@@ -49,20 +50,34 @@ with RivalsDataClient() as rd:
49
50
  print(hero_season[0].win_rate) # integer percent when wins/losses are present
50
51
  ```
51
52
 
53
+ The player overview's overall win rate (`player.win_rate`) is the **current-season
54
+ competitive win rate**. It uses the latest available competitive season with
55
+ usable counts when a direct source rate is absent; it does not combine seasons.
56
+ Hero and class stats use the season selector supplied to their own methods.
57
+
52
58
  Calculated player class statistics are available through
53
59
  `player.stats.classes(season=20)` and the MCP `get_player_stats` tool with
54
- `category="classes"`. Each row contains `player_class` (`tank`, `support`,
60
+ `category="classes"`. Pass a numeric season ID for that season, or
61
+ `season="all"` for combined all-seasons data, matching `player.heroes.fetch`.
62
+ Omitting the season uses the endpoint default. `player.stats.heroes` also
63
+ accepts `season="all"`. Each row contains `player_class` (`tank`, `support`,
55
64
  `dps`), the official role, hero IDs, and separate `competitive` and `quickplay`
56
65
  totals for games, wins, losses, and available MVP/SVP counts.
57
66
 
58
67
  ```python
59
68
  with RivalsDataClient() as rd:
60
- stats = rd.get_player("GS-").stats.classes(season=20)
69
+ player = rd.get_player("GS-")
70
+ stats = player.stats.classes(season=20)
71
+ all_seasons = player.stats.classes(season="all")
61
72
  for row in stats.classes:
62
73
  print(row.player_class, row.competitive.win_rate)
63
74
  print(stats.excluded) # Unknown roles or incomplete win/loss records
64
75
  ```
65
76
 
77
+ For MCP, use `get_player_stats(uid_or_name="GS-", category="classes", season=20)`
78
+ for one season, or `season="all"` for combined all-seasons stats. Both return
79
+ the same class response structure.
80
+
66
81
  Win rates are `total wins / (total wins + total losses)`, rounded to an integer
67
82
  percent. They are weighted by hero records, rather than averaging hero win
68
83
  rates. Switching heroes can make one match contribute to multiple records;
@@ -48,12 +48,22 @@ integer percentage from a direct win-rate field, `wins`/`losses`, or the
48
48
  competitive profile row's `win_count`/`battle_count`. If the source provides
49
49
  none of these, it returns `None`.
50
50
 
51
+ The player overview's overall win rate (`Player.win_rate`) refers to the current
52
+ competitive season, using the latest available competitive season with usable
53
+ counts when a direct source rate is absent. It does not sum historical seasons.
54
+ MCP overview/dashboard descriptions identify this scope, and the dashboard labels
55
+ the value as a season win rate. Hero and class rates follow their season selectors.
56
+
51
57
  ## Observed UI and routes
52
58
 
53
59
  `player.stats.classes(season=...)` derives tank (Vanguard), support (Strategist),
54
60
  and DPS (Duelist) totals from `/player/stats/heroes`; it does not call a class
55
61
  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
62
+ accepting a numeric season ID or `season="all"` (sent to the API as `-1`). The
63
+ all-seasons selection combines the returned hero records across seasons;
64
+ omitting the season keeps the endpoint default. `player.stats.heroes` and MCP
65
+ `get_player_stats` support the same selector. The method also sums
66
+ MVP/SVP counts when every included row supplies them. Win rate is calculated
57
67
  from summed wins and losses. Unknown classes and incomplete win/loss rows appear
58
68
  in `excluded`. Hero switching can count a single match in multiple hero records,
59
69
  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.2`.
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`.
@@ -54,8 +54,18 @@ Favorites requires numeric UIDs and returns player summary rows.
54
54
  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
+ The player overview's overall win rate (`player.win_rate`) is for the current
58
+ competitive season (latest available season with usable counts when there is no
59
+ direct source rate), not an all-seasons aggregate. Keep this scope explicit in
60
+ docs, MCP descriptions, and dashboard labels. Hero/class stats have separate
61
+ season selectors.
62
+
57
63
  `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
64
+ and DPS. Supply a numeric season ID for one season or `season="all"` for
65
+ combined all-seasons stats; the latter sends `season=-1` to the upstream API.
66
+ The detailed `player.stats.heroes` method and MCP stats tool accept the same
67
+ selector. Omitting the season keeps the endpoint default.
68
+ It sums games, wins, losses, and available MVP/SVP counts separately
59
69
  for competitive and quickplay, with win rates calculated from summed wins and
60
70
  losses. The response has typed `classes` rows and an `excluded` list for unknown
61
71
  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.2"
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.2"
@@ -108,7 +108,11 @@ def search_players(name: str) -> dict[str, Any]:
108
108
 
109
109
  @mcp.tool()
110
110
  def get_player(uid_or_name: str) -> dict[str, Any]:
111
- """Get a public player overview by numeric UID or in-game name."""
111
+ """Get a public player overview by numeric UID or in-game name.
112
+
113
+ The overview's overall win rate refers to the current competitive season
114
+ (latest available season with usable counts), not all seasons combined.
115
+ """
112
116
  return _call(lambda client, value: client.get_player(value).raw, uid_or_name)
113
117
 
114
118
 
@@ -141,6 +145,7 @@ def show_player_dashboard(
141
145
  combining live-match, hero, and match-history pulls. MCP Apps hosts can
142
146
  render the returned dashboard resource; all values are escaped before HTML
143
147
  output.
148
+ The competitive record and win rate are for the latest available season.
144
149
  """
145
150
  with RivalsDataClient() as client:
146
151
  player = client.get_player(uid_or_name)
@@ -168,7 +173,7 @@ def show_player_dashboard(
168
173
  competitive_summary = (
169
174
  f'<div class="competitive-line">'
170
175
  f'<span>{battles:,} competitive games</span>'
171
- + (f'<span>{rate}% win rate</span>' if rate is not None else "")
176
+ + (f'<span>{rate}% season win rate</span>' if rate is not None else "")
172
177
  + (f'<span>{escape(str(score))} RP</span>' if score is not None else "")
173
178
  + '</div>'
174
179
  )
@@ -408,19 +413,23 @@ def get_player_matches(
408
413
 
409
414
  @mcp.tool()
410
415
  def get_player_stats(
411
- uid_or_name: str, category: str = "heroes", season: int | None = None
416
+ uid_or_name: str, category: str = "heroes",
417
+ season: int | Literal["all"] | None = None,
412
418
  ) -> Any:
413
419
  """Get player stats: heroes, maps, bans, or calculated classes.
414
420
 
415
421
  Classes sum hero wins/losses by tank/support/dps and game mode; these are
416
422
  hero participation totals, which can count a match more than once.
423
+ Supply a season ID or "all" for combined all-seasons totals. Omitting the
424
+ season uses the endpoint default.
417
425
  """
418
426
  methods = {"heroes": "heroes", "maps": "maps", "bans": "bans", "classes": "classes"}
419
427
  if category not in methods:
420
428
  raise ValueError("category must be one of: heroes, maps, bans, classes")
421
429
  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)
430
+ season: int | Literal["all"] | None) -> Any:
431
+ season_id = -1 if season == "all" else season
432
+ return getattr(client.get_player(value).stats, methods[category])(season=season_id)
424
433
  return _call(fetch, uid_or_name, category, season)
425
434
 
426
435
 
@@ -983,7 +983,11 @@ class Player(DataModel):
983
983
 
984
984
  @property
985
985
  def win_rate(self) -> int | None:
986
- """Overall competitive win rate when the profile includes wins/losses."""
986
+ """Current-season competitive win rate when profile counts are available.
987
+
988
+ Uses a direct source rate or the latest available competitive season
989
+ with usable counts. This property does not aggregate across seasons.
990
+ """
987
991
  direct = StatRecord(self._data).win_rate
988
992
  if direct is not None:
989
993
  return direct
@@ -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