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.
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/PKG-INFO +18 -3
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/README.md +17 -2
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/docs/API.md +11 -1
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/docs/PROJECT_CONTEXT.md +12 -2
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/pyproject.toml +1 -1
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/src/rivalsdata/__init__.py +1 -1
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/src/rivalsdata/mcp_server.py +14 -5
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/src/rivalsdata/models.py +5 -1
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/src/rivalsdata/resources.py +15 -3
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/tests/test_class_stats.py +45 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/.github/workflows/publish-pypi.yml +0 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/.gitignore +0 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/CONTRIBUTING.md +0 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/LICENSE +0 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/src/rivalsdata/client.py +0 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/src/rivalsdata/exceptions.py +0 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/src/rivalsdata/hero_ids.py +0 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/tests/test_hero_names.py +0 -0
- {rivalsdata_api-1.2.0 → rivalsdata_api-1.2.2}/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.2.
|
|
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"`.
|
|
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
|
-
|
|
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"`.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -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",
|
|
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
|
-
|
|
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
|
-
"""
|
|
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(
|
|
209
|
-
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|