python-ballchasing 0.4.1__tar.gz → 0.5.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.
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/PKG-INFO +5 -5
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/README.md +2 -2
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/api.py +109 -85
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/constants.py +84 -81
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/typed/deep_group.py +27 -27
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/typed/deep_replay.py +16 -16
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/typed/shared.py +33 -8
- python_ballchasing-0.5.0/ballchasing/util/__init__.py +4 -0
- python_ballchasing-0.5.0/ballchasing/util/dates.py +35 -0
- python_ballchasing-0.5.0/ballchasing/util/iterators.py +118 -0
- python_ballchasing-0.5.0/ballchasing/util/replays.py +185 -0
- python_ballchasing-0.4.1/ballchasing/util.py → python_ballchasing-0.5.0/ballchasing/util/stats.py +7 -30
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/pyproject.toml +3 -3
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/PKG-INFO +5 -5
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/SOURCES.txt +6 -1
- python_ballchasing-0.5.0/scripts/get_maps.py +147 -0
- python_ballchasing-0.5.0/scripts/test_typed.py +88 -0
- python_ballchasing-0.4.1/scripts/test_typed.py +0 -35
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/LICENSE +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/__init__.py +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/stats_info.tsv +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/typed/__init__.py +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/typed/shallow_group.py +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/ballchasing/typed/shallow_replay.py +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/dependency_links.txt +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/requires.txt +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/top_level.txt +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/scripts/make_types.py +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/scripts/test.py +0 -0
- {python_ballchasing-0.4.1 → python_ballchasing-0.5.0}/setup.cfg +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-ballchasing
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A Python wrapper around the Ballchasing API
|
|
5
5
|
Author-email: Rolv-Arild Braaten <rolv_arild@hotmail.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
Project-URL: Homepage, https://github.com/Rolv-Arild/python-ballchasing
|
|
8
8
|
Project-URL: Download, https://pypi.python.org/pypi/python-ballchasing
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: requests
|
|
@@ -78,7 +78,7 @@ for group in groups:
|
|
|
78
78
|
# Download the group
|
|
79
79
|
api.download_group(
|
|
80
80
|
group_id=group["id"],
|
|
81
|
-
|
|
81
|
+
path="/path/to/destination/"
|
|
82
82
|
recursive=True, # To download all the replays and retain the group structure with subfolders
|
|
83
83
|
)
|
|
84
84
|
|
|
@@ -88,7 +88,7 @@ for group in groups:
|
|
|
88
88
|
deep=True, # To get detailed replay info
|
|
89
89
|
)
|
|
90
90
|
for replay in replays:
|
|
91
|
-
api.download_replay(replay_id=replay["id"],
|
|
91
|
+
api.download_replay(replay_id=replay["id"], path="/path/to/destination/") # You could also download like this
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
---
|
|
@@ -61,7 +61,7 @@ for group in groups:
|
|
|
61
61
|
# Download the group
|
|
62
62
|
api.download_group(
|
|
63
63
|
group_id=group["id"],
|
|
64
|
-
|
|
64
|
+
path="/path/to/destination/"
|
|
65
65
|
recursive=True, # To download all the replays and retain the group structure with subfolders
|
|
66
66
|
)
|
|
67
67
|
|
|
@@ -71,7 +71,7 @@ for group in groups:
|
|
|
71
71
|
deep=True, # To get detailed replay info
|
|
72
72
|
)
|
|
73
73
|
for replay in replays:
|
|
74
|
-
api.download_replay(replay_id=replay["id"],
|
|
74
|
+
api.download_replay(replay_id=replay["id"], path="/path/to/destination/") # You could also download like this
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
---
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import BinaryIO, Iterator
|
|
6
7
|
from urllib.parse import parse_qs, urlparse
|
|
7
8
|
|
|
8
|
-
from requests import sessions, Response, ConnectionError
|
|
9
|
+
from requests import sessions, Response, ConnectionError
|
|
9
10
|
|
|
10
|
-
from ballchasing.constants import GroupSortBy, SortDir,
|
|
11
|
-
|
|
11
|
+
from ballchasing.constants import GroupSortBy, SortDir, AnySeason, AnyRank, AnyReplaySortBy, AnySortDir, \
|
|
12
|
+
AnyVisibility, AnyGroupSortBy, AnyPlayerIdentification, AnyTeamIdentification, AnyMatchResult, NoneOrMore
|
|
12
13
|
from ballchasing.typed import DeepReplay, ShallowReplay, DeepGroup, ShallowGroup
|
|
13
14
|
from .typed.shared import BaseGroup, BasicGroup
|
|
14
|
-
from .util import to_rfc3339
|
|
15
|
+
from .util.dates import to_rfc3339
|
|
16
|
+
from .util.iterators import deduplicate as deduplicator
|
|
17
|
+
from .util.stats import parse_replay_stats
|
|
15
18
|
|
|
16
19
|
DEFAULT_URL = "https://ballchasing.com/api"
|
|
17
20
|
|
|
@@ -25,11 +28,11 @@ class BallchasingApi:
|
|
|
25
28
|
self,
|
|
26
29
|
auth_key: str,
|
|
27
30
|
*,
|
|
28
|
-
sleep_time_on_rate_limit:
|
|
31
|
+
sleep_time_on_rate_limit: float | None = None,
|
|
29
32
|
print_on_rate_limit: bool = False,
|
|
30
|
-
base_url=None,
|
|
31
|
-
do_initial_ping=True,
|
|
32
|
-
typed=False,
|
|
33
|
+
base_url: str | None = None,
|
|
34
|
+
do_initial_ping: bool = True,
|
|
35
|
+
typed: bool = False,
|
|
33
36
|
):
|
|
34
37
|
"""
|
|
35
38
|
|
|
@@ -117,6 +120,8 @@ class BallchasingApi:
|
|
|
117
120
|
time.sleep(retry_after)
|
|
118
121
|
elif self.sleep_time_on_rate_limit:
|
|
119
122
|
time.sleep(self.sleep_time_on_rate_limit)
|
|
123
|
+
elif r.status_code == 504:
|
|
124
|
+
raise ConnectionError("Gateway Timeout. The server did not respond in time.")
|
|
120
125
|
else:
|
|
121
126
|
r.raise_for_status() # Raise an error for any other status code'
|
|
122
127
|
except ConnectionError as e:
|
|
@@ -141,58 +146,68 @@ class BallchasingApi:
|
|
|
141
146
|
self._ping_result = result
|
|
142
147
|
return result
|
|
143
148
|
|
|
144
|
-
def _iterable_from_request(self, url, params):
|
|
145
|
-
# Shared by get_replays and get_groups
|
|
149
|
+
def _iterable_from_request(self, url, params, prefetch=True):
|
|
150
|
+
# Shared by get_replays and get_groups.
|
|
151
|
+
# When prefetch=True, the next page is requested in a background
|
|
152
|
+
# thread *before* yielding, so network I/O overlaps with the
|
|
153
|
+
# consumer processing items. When prefetch=False (e.g. deep mode
|
|
154
|
+
# where the consumer also makes API calls), the next request is
|
|
155
|
+
# submitted *after* yielding to avoid doubling the request rate.
|
|
146
156
|
remaining = params["count"]
|
|
147
|
-
# return_length = True
|
|
148
|
-
while remaining > 0:
|
|
149
|
-
request_count = min(remaining, 200)
|
|
150
|
-
params["count"] = request_count
|
|
151
|
-
try:
|
|
152
|
-
d = self._request(url, "GET", params=params).json()
|
|
153
|
-
except HTTPError as e:
|
|
154
|
-
if e.response.status_code == 504:
|
|
155
|
-
# Gateway Timeout, retry
|
|
156
|
-
time.sleep(5)
|
|
157
|
-
continue
|
|
158
|
-
else:
|
|
159
|
-
raise e
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
def fetch_page(p):
|
|
159
|
+
return self._request(url, "GET", params=p).json()
|
|
160
|
+
|
|
161
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
162
|
+
params["count"] = min(remaining, 200)
|
|
163
|
+
future = executor.submit(fetch_page, dict(params))
|
|
164
|
+
|
|
165
|
+
while remaining > 0:
|
|
166
|
+
d = future.result()
|
|
167
|
+
batch = d["list"][:min(remaining, 200)]
|
|
168
|
+
remaining -= len(batch)
|
|
163
169
|
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
has_next = "next" in d and remaining > 0
|
|
171
|
+
if has_next:
|
|
172
|
+
next_url = d["next"]
|
|
173
|
+
params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
|
|
174
|
+
params["count"] = min(remaining, 200)
|
|
175
|
+
if prefetch:
|
|
176
|
+
future = executor.submit(fetch_page, dict(params))
|
|
166
177
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
yield from batch
|
|
179
|
+
|
|
180
|
+
if not has_next:
|
|
181
|
+
break
|
|
182
|
+
if not prefetch:
|
|
183
|
+
future = executor.submit(fetch_page, dict(params))
|
|
170
184
|
|
|
171
185
|
def get_replays(
|
|
172
186
|
self,
|
|
173
187
|
*,
|
|
174
|
-
title:
|
|
175
|
-
player_name:
|
|
176
|
-
player_id:
|
|
177
|
-
playlist:
|
|
178
|
-
season:
|
|
179
|
-
match_result:
|
|
180
|
-
min_rank:
|
|
181
|
-
max_rank:
|
|
182
|
-
pro:
|
|
183
|
-
uploader:
|
|
184
|
-
group_id:
|
|
185
|
-
map_id:
|
|
186
|
-
created_before:
|
|
187
|
-
created_after:
|
|
188
|
-
replay_after:
|
|
189
|
-
replay_before:
|
|
188
|
+
title: NoneOrMore[str] = None,
|
|
189
|
+
player_name: NoneOrMore[str] = None,
|
|
190
|
+
player_id: NoneOrMore[str] = None,
|
|
191
|
+
playlist: NoneOrMore[str] = None,
|
|
192
|
+
season: NoneOrMore[AnySeason] = None,
|
|
193
|
+
match_result: NoneOrMore[AnyMatchResult] = None,
|
|
194
|
+
min_rank: AnyRank | None = None,
|
|
195
|
+
max_rank: AnyRank | None = None,
|
|
196
|
+
pro: bool | None = None,
|
|
197
|
+
uploader: str | None = None,
|
|
198
|
+
group_id: NoneOrMore[str] = None,
|
|
199
|
+
map_id: NoneOrMore[str] = None,
|
|
200
|
+
created_before: str | datetime | None = None,
|
|
201
|
+
created_after: str | datetime | None = None,
|
|
202
|
+
replay_after: str | datetime | None = None,
|
|
203
|
+
replay_before: str | datetime | None = None,
|
|
190
204
|
count: int = 150,
|
|
191
|
-
sort_by:
|
|
205
|
+
sort_by: AnyReplaySortBy | None = None,
|
|
192
206
|
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
193
207
|
deep: bool = False,
|
|
194
|
-
typed:
|
|
195
|
-
|
|
208
|
+
typed: bool | None = None,
|
|
209
|
+
deduplicate: bool = False,
|
|
210
|
+
) -> Iterator[dict | ShallowReplay | DeepReplay]:
|
|
196
211
|
"""
|
|
197
212
|
This endpoint lets you filter and retrieve replays. The implementation returns an iterator.
|
|
198
213
|
|
|
@@ -226,12 +241,14 @@ class BallchasingApi:
|
|
|
226
241
|
:param sort_dir: sort direction
|
|
227
242
|
:param deep: whether to get full stats for each replay (will be much slower).
|
|
228
243
|
:param typed: whether to return a typed object (default is self.typed).
|
|
244
|
+
:param deduplicate: whether to deduplicate replays that seem to be the same game.
|
|
229
245
|
:return: an iterator over the replays returned by the API.
|
|
230
246
|
"""
|
|
231
247
|
url = f"{self.base_url}/replays"
|
|
232
248
|
params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist,
|
|
233
249
|
"season": season, "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank,
|
|
234
|
-
"pro": pro, "uploader": uploader, "group": group_id,
|
|
250
|
+
"pro": str(pro).lower() if isinstance(pro, bool) else pro, "uploader": uploader, "group": group_id,
|
|
251
|
+
"map": map_id,
|
|
235
252
|
"created-before": to_rfc3339(created_before), "created-after": to_rfc3339(created_after),
|
|
236
253
|
"replay-date-after": to_rfc3339(replay_after), "replay-date-before": to_rfc3339(replay_before),
|
|
237
254
|
"count": count, "sort-by": sort_by, "sort-dir": sort_dir}
|
|
@@ -239,14 +256,20 @@ class BallchasingApi:
|
|
|
239
256
|
if typed is None:
|
|
240
257
|
typed = self.typed
|
|
241
258
|
|
|
242
|
-
iterator = self._iterable_from_request(url, params)
|
|
259
|
+
iterator = self._iterable_from_request(url, params, prefetch=not deep)
|
|
243
260
|
if deep:
|
|
244
|
-
iterator = (self.get_replay(r["id"]
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
iterator = (self.get_replay(r["id"]) for r in iterator)
|
|
262
|
+
if deduplicate:
|
|
263
|
+
# Deep replays have match and replay IDs to deduplicate with. For shallow replays we check dates.
|
|
264
|
+
iterator = deduplicator(iterator, check_dates=not deep)
|
|
265
|
+
if typed:
|
|
266
|
+
if deep:
|
|
267
|
+
iterator = (DeepReplay(**r) for r in iterator)
|
|
268
|
+
else:
|
|
269
|
+
iterator = (ShallowReplay(**r) for r in iterator)
|
|
247
270
|
yield from iterator
|
|
248
271
|
|
|
249
|
-
def get_replay(self, replay_id: str, *, typed:
|
|
272
|
+
def get_replay(self, replay_id: str, *, typed: bool | None = None) -> dict | DeepReplay:
|
|
250
273
|
"""
|
|
251
274
|
Retrieve a given replay’s details and stats.
|
|
252
275
|
|
|
@@ -272,10 +295,10 @@ class BallchasingApi:
|
|
|
272
295
|
|
|
273
296
|
def upload_replay(
|
|
274
297
|
self,
|
|
275
|
-
replay_file:
|
|
298
|
+
replay_file: str | Path | BinaryIO,
|
|
276
299
|
*,
|
|
277
|
-
visibility:
|
|
278
|
-
group:
|
|
300
|
+
visibility: AnyVisibility | None = None,
|
|
301
|
+
group: str | None = None
|
|
279
302
|
) -> dict:
|
|
280
303
|
"""
|
|
281
304
|
Use this API to upload a replay file to ballchasing.com.
|
|
@@ -303,17 +326,17 @@ class BallchasingApi:
|
|
|
303
326
|
def get_groups(
|
|
304
327
|
self,
|
|
305
328
|
*,
|
|
306
|
-
name:
|
|
307
|
-
creator:
|
|
308
|
-
group:
|
|
309
|
-
created_before:
|
|
310
|
-
created_after:
|
|
329
|
+
name: str | None = None,
|
|
330
|
+
creator: str | None = None,
|
|
331
|
+
group: str | None = None,
|
|
332
|
+
created_before: str | datetime | None = None,
|
|
333
|
+
created_after: str | datetime | None = None,
|
|
311
334
|
count: int = 200,
|
|
312
335
|
sort_by: AnyGroupSortBy = GroupSortBy.CREATED,
|
|
313
336
|
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
314
337
|
deep: bool = False,
|
|
315
|
-
typed: bool = None,
|
|
316
|
-
) -> Iterator[
|
|
338
|
+
typed: bool | None = None,
|
|
339
|
+
) -> Iterator[dict | ShallowGroup | DeepGroup]:
|
|
317
340
|
"""
|
|
318
341
|
This endpoint lets you filter and retrieve replay groups.
|
|
319
342
|
|
|
@@ -336,7 +359,7 @@ class BallchasingApi:
|
|
|
336
359
|
url = f"{self.base_url}/groups/"
|
|
337
360
|
params = {"name": name, "creator": creator, "group": group, "created-before": to_rfc3339(created_before),
|
|
338
361
|
"created-after": to_rfc3339(created_after), "count": count, "sort-by": sort_by, "sort-dir": sort_dir}
|
|
339
|
-
iterator = self._iterable_from_request(url, params)
|
|
362
|
+
iterator = self._iterable_from_request(url, params, prefetch=not deep)
|
|
340
363
|
if typed is None:
|
|
341
364
|
typed = self.typed
|
|
342
365
|
if deep:
|
|
@@ -351,7 +374,7 @@ class BallchasingApi:
|
|
|
351
374
|
name: str,
|
|
352
375
|
player_identification: AnyPlayerIdentification,
|
|
353
376
|
team_identification: AnyTeamIdentification,
|
|
354
|
-
parent:
|
|
377
|
+
parent: str | None = None
|
|
355
378
|
) -> dict:
|
|
356
379
|
"""
|
|
357
380
|
Use this API to create a new replay group.
|
|
@@ -376,8 +399,8 @@ class BallchasingApi:
|
|
|
376
399
|
self,
|
|
377
400
|
group_id: str,
|
|
378
401
|
*,
|
|
379
|
-
typed:
|
|
380
|
-
) ->
|
|
402
|
+
typed: bool | None = None
|
|
403
|
+
) -> dict | DeepGroup:
|
|
381
404
|
"""
|
|
382
405
|
This endpoint retrieves a specific replay group info and stats given its id.
|
|
383
406
|
|
|
@@ -412,11 +435,11 @@ class BallchasingApi:
|
|
|
412
435
|
|
|
413
436
|
def get_group_replays(
|
|
414
437
|
self,
|
|
415
|
-
group:
|
|
438
|
+
group: str | dict | BasicGroup,
|
|
416
439
|
*,
|
|
417
440
|
deep: bool = False,
|
|
418
|
-
typed:
|
|
419
|
-
) -> Iterator[
|
|
441
|
+
typed: bool | None = None
|
|
442
|
+
) -> Iterator[dict | ShallowReplay | DeepReplay]:
|
|
420
443
|
"""
|
|
421
444
|
Finds all replays in a group, including child groups.
|
|
422
445
|
|
|
@@ -425,35 +448,36 @@ class BallchasingApi:
|
|
|
425
448
|
:param typed: whether to return a typed object (default is self.typed).
|
|
426
449
|
:return: an iterator over all the replays in the group.
|
|
427
450
|
"""
|
|
428
|
-
for path in self.get_group_tree(group, deep=deep, typed=typed):
|
|
429
|
-
group, replay = path
|
|
451
|
+
for path, replay in self.get_group_tree(group, deep=deep, typed=typed):
|
|
430
452
|
yield replay
|
|
431
453
|
|
|
432
454
|
def get_group_tree(
|
|
433
455
|
self,
|
|
434
|
-
group:
|
|
456
|
+
group: str | dict | BaseGroup,
|
|
435
457
|
*,
|
|
436
458
|
deep: bool = False,
|
|
437
|
-
typed:
|
|
438
|
-
):
|
|
459
|
+
typed: bool | None = None
|
|
460
|
+
) -> Iterator[tuple[list[str], dict | ShallowReplay | DeepReplay]]:
|
|
439
461
|
"""
|
|
440
|
-
Finds all replays in a group, and includes the
|
|
462
|
+
Finds all replays in a group, and includes the group path leading up to each replay.
|
|
463
|
+
|
|
441
464
|
:param group: the group id or a group dict.
|
|
442
465
|
:param deep: whether to get full stats for each replay and group (will be much slower).
|
|
443
466
|
:param typed: whether to return a typed object (default is self.typed).
|
|
467
|
+
:return: an iterator of (path, replay) tuples, where path is a list of group ids.
|
|
444
468
|
"""
|
|
445
469
|
if isinstance(group, str):
|
|
446
|
-
|
|
447
|
-
|
|
470
|
+
group_id = group
|
|
471
|
+
elif isinstance(group, BasicGroup):
|
|
448
472
|
group_id = group.id
|
|
449
473
|
else:
|
|
450
474
|
group_id = group["id"]
|
|
451
475
|
child_groups = self.get_groups(group=group_id, typed=typed)
|
|
452
476
|
for child in child_groups:
|
|
453
|
-
for path in self.get_group_tree(child, deep=deep, typed=typed):
|
|
454
|
-
yield group_id,
|
|
477
|
+
for path, replay in self.get_group_tree(child, deep=deep, typed=typed):
|
|
478
|
+
yield [group_id] + path, replay
|
|
455
479
|
for replay in self.get_replays(group_id=group_id, deep=deep, typed=typed):
|
|
456
|
-
yield group_id, replay
|
|
480
|
+
yield [group_id], replay
|
|
457
481
|
|
|
458
482
|
def download_replay(self, replay_id: str, path: str):
|
|
459
483
|
"""
|
|
@@ -496,7 +520,7 @@ class BallchasingApi:
|
|
|
496
520
|
res = self._request("/maps", "GET").json()
|
|
497
521
|
return res
|
|
498
522
|
|
|
499
|
-
def get_stats(self, replay:
|
|
523
|
+
def get_stats(self, replay: dict | str):
|
|
500
524
|
"""
|
|
501
525
|
Gets stats for players, teams and replay info.
|
|
502
526
|
|