python-ballchasing 0.4.0__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.0 → python_ballchasing-0.5.0}/PKG-INFO +6 -5
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/README.md +4 -3
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/api.py +109 -83
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/constants.py +84 -81
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/typed/deep_group.py +27 -27
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/typed/deep_replay.py +16 -16
- {python_ballchasing-0.4.0 → 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.0/ballchasing/util.py → python_ballchasing-0.5.0/ballchasing/util/stats.py +7 -30
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/pyproject.toml +3 -3
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/PKG-INFO +6 -5
- {python_ballchasing-0.4.0 → 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.0/scripts/test_typed.py +0 -35
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/LICENSE +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/__init__.py +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/stats_info.tsv +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/typed/__init__.py +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/typed/shallow_group.py +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/ballchasing/typed/shallow_replay.py +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/dependency_links.txt +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/requires.txt +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/python_ballchasing.egg-info/top_level.txt +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/scripts/make_types.py +0 -0
- {python_ballchasing-0.4.0 → python_ballchasing-0.5.0}/scripts/test.py +0 -0
- {python_ballchasing-0.4.0 → 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,9 +88,10 @@ 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
95
|
Additionally, there's the option to put responses (replays, groups) into typed objects for better type hinting and validation.
|
|
95
96
|
It also attempts to fill in missing variables not returned by ballchasing (e.g. if a team has 0 goals they won't have a "goal" entry in the response)
|
|
96
97
|
The classes can also be found under the `typing` folder and used as a reference for what the API returns.
|
|
@@ -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,9 +71,10 @@ 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
78
|
Additionally, there's the option to put responses (replays, groups) into typed objects for better type hinting and validation.
|
|
78
79
|
It also attempts to fill in missing variables not returned by ballchasing (e.g. if a team has 0 goals they won't have a "goal" entry in the response)
|
|
79
80
|
The classes can also be found under the `typing` folder and used as a reference for what the API returns.
|
|
@@ -88,4 +89,4 @@ print(replay.blue.players[0].name) # Example of easy attribute access
|
|
|
88
89
|
|
|
89
90
|
group = api.get_group("g2-vs-bds-hwbf2eyolb", typed=True)
|
|
90
91
|
print(group.players[0].name)
|
|
91
|
-
```
|
|
92
|
+
```
|
|
@@ -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,56 +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
157
|
|
|
159
|
-
|
|
160
|
-
|
|
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)
|
|
169
|
+
|
|
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))
|
|
161
177
|
|
|
162
|
-
|
|
163
|
-
break
|
|
178
|
+
yield from batch
|
|
164
179
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
if not has_next:
|
|
181
|
+
break
|
|
182
|
+
if not prefetch:
|
|
183
|
+
future = executor.submit(fetch_page, dict(params))
|
|
168
184
|
|
|
169
185
|
def get_replays(
|
|
170
186
|
self,
|
|
171
187
|
*,
|
|
172
|
-
title:
|
|
173
|
-
player_name:
|
|
174
|
-
player_id:
|
|
175
|
-
playlist:
|
|
176
|
-
season:
|
|
177
|
-
match_result:
|
|
178
|
-
min_rank:
|
|
179
|
-
max_rank:
|
|
180
|
-
pro:
|
|
181
|
-
uploader:
|
|
182
|
-
group_id:
|
|
183
|
-
map_id:
|
|
184
|
-
created_before:
|
|
185
|
-
created_after:
|
|
186
|
-
replay_after:
|
|
187
|
-
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,
|
|
188
204
|
count: int = 150,
|
|
189
|
-
sort_by:
|
|
205
|
+
sort_by: AnyReplaySortBy | None = None,
|
|
190
206
|
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
191
207
|
deep: bool = False,
|
|
192
|
-
typed:
|
|
193
|
-
|
|
208
|
+
typed: bool | None = None,
|
|
209
|
+
deduplicate: bool = False,
|
|
210
|
+
) -> Iterator[dict | ShallowReplay | DeepReplay]:
|
|
194
211
|
"""
|
|
195
212
|
This endpoint lets you filter and retrieve replays. The implementation returns an iterator.
|
|
196
213
|
|
|
@@ -224,12 +241,14 @@ class BallchasingApi:
|
|
|
224
241
|
:param sort_dir: sort direction
|
|
225
242
|
:param deep: whether to get full stats for each replay (will be much slower).
|
|
226
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.
|
|
227
245
|
:return: an iterator over the replays returned by the API.
|
|
228
246
|
"""
|
|
229
247
|
url = f"{self.base_url}/replays"
|
|
230
248
|
params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist,
|
|
231
249
|
"season": season, "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank,
|
|
232
|
-
"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,
|
|
233
252
|
"created-before": to_rfc3339(created_before), "created-after": to_rfc3339(created_after),
|
|
234
253
|
"replay-date-after": to_rfc3339(replay_after), "replay-date-before": to_rfc3339(replay_before),
|
|
235
254
|
"count": count, "sort-by": sort_by, "sort-dir": sort_dir}
|
|
@@ -237,14 +256,20 @@ class BallchasingApi:
|
|
|
237
256
|
if typed is None:
|
|
238
257
|
typed = self.typed
|
|
239
258
|
|
|
240
|
-
iterator = self._iterable_from_request(url, params)
|
|
259
|
+
iterator = self._iterable_from_request(url, params, prefetch=not deep)
|
|
241
260
|
if deep:
|
|
242
|
-
iterator = (self.get_replay(r["id"]
|
|
243
|
-
|
|
244
|
-
|
|
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)
|
|
245
270
|
yield from iterator
|
|
246
271
|
|
|
247
|
-
def get_replay(self, replay_id: str, *, typed:
|
|
272
|
+
def get_replay(self, replay_id: str, *, typed: bool | None = None) -> dict | DeepReplay:
|
|
248
273
|
"""
|
|
249
274
|
Retrieve a given replay’s details and stats.
|
|
250
275
|
|
|
@@ -270,10 +295,10 @@ class BallchasingApi:
|
|
|
270
295
|
|
|
271
296
|
def upload_replay(
|
|
272
297
|
self,
|
|
273
|
-
replay_file:
|
|
298
|
+
replay_file: str | Path | BinaryIO,
|
|
274
299
|
*,
|
|
275
|
-
visibility:
|
|
276
|
-
group:
|
|
300
|
+
visibility: AnyVisibility | None = None,
|
|
301
|
+
group: str | None = None
|
|
277
302
|
) -> dict:
|
|
278
303
|
"""
|
|
279
304
|
Use this API to upload a replay file to ballchasing.com.
|
|
@@ -301,17 +326,17 @@ class BallchasingApi:
|
|
|
301
326
|
def get_groups(
|
|
302
327
|
self,
|
|
303
328
|
*,
|
|
304
|
-
name:
|
|
305
|
-
creator:
|
|
306
|
-
group:
|
|
307
|
-
created_before:
|
|
308
|
-
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,
|
|
309
334
|
count: int = 200,
|
|
310
335
|
sort_by: AnyGroupSortBy = GroupSortBy.CREATED,
|
|
311
336
|
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
312
337
|
deep: bool = False,
|
|
313
|
-
typed: bool = None,
|
|
314
|
-
) -> Iterator[
|
|
338
|
+
typed: bool | None = None,
|
|
339
|
+
) -> Iterator[dict | ShallowGroup | DeepGroup]:
|
|
315
340
|
"""
|
|
316
341
|
This endpoint lets you filter and retrieve replay groups.
|
|
317
342
|
|
|
@@ -334,7 +359,7 @@ class BallchasingApi:
|
|
|
334
359
|
url = f"{self.base_url}/groups/"
|
|
335
360
|
params = {"name": name, "creator": creator, "group": group, "created-before": to_rfc3339(created_before),
|
|
336
361
|
"created-after": to_rfc3339(created_after), "count": count, "sort-by": sort_by, "sort-dir": sort_dir}
|
|
337
|
-
iterator = self._iterable_from_request(url, params)
|
|
362
|
+
iterator = self._iterable_from_request(url, params, prefetch=not deep)
|
|
338
363
|
if typed is None:
|
|
339
364
|
typed = self.typed
|
|
340
365
|
if deep:
|
|
@@ -349,7 +374,7 @@ class BallchasingApi:
|
|
|
349
374
|
name: str,
|
|
350
375
|
player_identification: AnyPlayerIdentification,
|
|
351
376
|
team_identification: AnyTeamIdentification,
|
|
352
|
-
parent:
|
|
377
|
+
parent: str | None = None
|
|
353
378
|
) -> dict:
|
|
354
379
|
"""
|
|
355
380
|
Use this API to create a new replay group.
|
|
@@ -374,8 +399,8 @@ class BallchasingApi:
|
|
|
374
399
|
self,
|
|
375
400
|
group_id: str,
|
|
376
401
|
*,
|
|
377
|
-
typed:
|
|
378
|
-
) ->
|
|
402
|
+
typed: bool | None = None
|
|
403
|
+
) -> dict | DeepGroup:
|
|
379
404
|
"""
|
|
380
405
|
This endpoint retrieves a specific replay group info and stats given its id.
|
|
381
406
|
|
|
@@ -410,11 +435,11 @@ class BallchasingApi:
|
|
|
410
435
|
|
|
411
436
|
def get_group_replays(
|
|
412
437
|
self,
|
|
413
|
-
group:
|
|
438
|
+
group: str | dict | BasicGroup,
|
|
414
439
|
*,
|
|
415
440
|
deep: bool = False,
|
|
416
|
-
typed:
|
|
417
|
-
) -> Iterator[
|
|
441
|
+
typed: bool | None = None
|
|
442
|
+
) -> Iterator[dict | ShallowReplay | DeepReplay]:
|
|
418
443
|
"""
|
|
419
444
|
Finds all replays in a group, including child groups.
|
|
420
445
|
|
|
@@ -423,35 +448,36 @@ class BallchasingApi:
|
|
|
423
448
|
:param typed: whether to return a typed object (default is self.typed).
|
|
424
449
|
:return: an iterator over all the replays in the group.
|
|
425
450
|
"""
|
|
426
|
-
for path in self.get_group_tree(group, deep=deep, typed=typed):
|
|
427
|
-
group, replay = path
|
|
451
|
+
for path, replay in self.get_group_tree(group, deep=deep, typed=typed):
|
|
428
452
|
yield replay
|
|
429
453
|
|
|
430
454
|
def get_group_tree(
|
|
431
455
|
self,
|
|
432
|
-
group:
|
|
456
|
+
group: str | dict | BaseGroup,
|
|
433
457
|
*,
|
|
434
458
|
deep: bool = False,
|
|
435
|
-
typed:
|
|
436
|
-
):
|
|
459
|
+
typed: bool | None = None
|
|
460
|
+
) -> Iterator[tuple[list[str], dict | ShallowReplay | DeepReplay]]:
|
|
437
461
|
"""
|
|
438
|
-
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
|
+
|
|
439
464
|
:param group: the group id or a group dict.
|
|
440
465
|
:param deep: whether to get full stats for each replay and group (will be much slower).
|
|
441
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.
|
|
442
468
|
"""
|
|
443
469
|
if isinstance(group, str):
|
|
444
|
-
|
|
445
|
-
|
|
470
|
+
group_id = group
|
|
471
|
+
elif isinstance(group, BasicGroup):
|
|
446
472
|
group_id = group.id
|
|
447
473
|
else:
|
|
448
474
|
group_id = group["id"]
|
|
449
475
|
child_groups = self.get_groups(group=group_id, typed=typed)
|
|
450
476
|
for child in child_groups:
|
|
451
|
-
for path in self.get_group_tree(child, deep=deep, typed=typed):
|
|
452
|
-
yield group_id,
|
|
477
|
+
for path, replay in self.get_group_tree(child, deep=deep, typed=typed):
|
|
478
|
+
yield [group_id] + path, replay
|
|
453
479
|
for replay in self.get_replays(group_id=group_id, deep=deep, typed=typed):
|
|
454
|
-
yield group_id, replay
|
|
480
|
+
yield [group_id], replay
|
|
455
481
|
|
|
456
482
|
def download_replay(self, replay_id: str, path: str):
|
|
457
483
|
"""
|
|
@@ -494,7 +520,7 @@ class BallchasingApi:
|
|
|
494
520
|
res = self._request("/maps", "GET").json()
|
|
495
521
|
return res
|
|
496
522
|
|
|
497
|
-
def get_stats(self, replay:
|
|
523
|
+
def get_stats(self, replay: dict | str):
|
|
498
524
|
"""
|
|
499
525
|
Gets stats for players, teams and replay info.
|
|
500
526
|
|