python-ballchasing 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- ballchasing/__init__.py +7 -0
- ballchasing/api.py +207 -119
- ballchasing/constants.py +68 -47
- ballchasing/typed/__init__.py +5 -0
- ballchasing/typed/deep_group.py +440 -0
- ballchasing/typed/deep_replay.py +286 -0
- ballchasing/typed/shallow_group.py +18 -0
- ballchasing/typed/shallow_replay.py +67 -0
- ballchasing/typed/shared.py +218 -0
- ballchasing/util.py +16 -4
- python_ballchasing-0.4.0.dist-info/METADATA +108 -0
- python_ballchasing-0.4.0.dist-info/RECORD +19 -0
- {python_ballchasing-0.2.0.dist-info → python_ballchasing-0.4.0.dist-info}/top_level.txt +1 -0
- scripts/make_types.py +194 -0
- scripts/test.py +52 -0
- scripts/test_typed.py +35 -0
- python_ballchasing-0.2.0.dist-info/METADATA +0 -36
- python_ballchasing-0.2.0.dist-info/RECORD +0 -10
- {python_ballchasing-0.2.0.dist-info → python_ballchasing-0.4.0.dist-info}/WHEEL +0 -0
- {python_ballchasing-0.2.0.dist-info → python_ballchasing-0.4.0.dist-info}/licenses/LICENSE +0 -0
ballchasing/__init__.py
CHANGED
ballchasing/api.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Union, List, BinaryIO, Iterator
|
|
5
6
|
from urllib.parse import parse_qs, urlparse
|
|
6
7
|
|
|
7
|
-
from requests import sessions, Response, ConnectionError
|
|
8
|
+
from requests import sessions, Response, ConnectionError, HTTPError
|
|
8
9
|
|
|
9
10
|
from ballchasing.constants import GroupSortBy, SortDir, AnyPlaylist, AnyMap, AnySeason, AnyRank, AnyReplaySortBy, \
|
|
10
11
|
AnySortDir, AnyVisibility, AnyGroupSortBy, AnyPlayerIdentification, AnyTeamIdentification, AnyMatchResult
|
|
11
|
-
from .
|
|
12
|
+
from ballchasing.typed import DeepReplay, ShallowReplay, DeepGroup, ShallowGroup
|
|
13
|
+
from .typed.shared import BaseGroup, BasicGroup
|
|
14
|
+
from .util import to_rfc3339, parse_replay_stats
|
|
12
15
|
|
|
13
16
|
DEFAULT_URL = "https://ballchasing.com/api"
|
|
14
17
|
|
|
@@ -18,12 +21,16 @@ class BallchasingApi:
|
|
|
18
21
|
Class for communication with ballchasing.com API (https://ballchasing.com/doc/api)
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
|
-
def __init__(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
auth_key: str,
|
|
27
|
+
*,
|
|
28
|
+
sleep_time_on_rate_limit: Optional[float] = None,
|
|
29
|
+
print_on_rate_limit: bool = False,
|
|
30
|
+
base_url=None,
|
|
31
|
+
do_initial_ping=True,
|
|
32
|
+
typed=False,
|
|
33
|
+
):
|
|
27
34
|
"""
|
|
28
35
|
|
|
29
36
|
:param auth_key: authentication key for API calls.
|
|
@@ -49,6 +56,7 @@ class BallchasingApi:
|
|
|
49
56
|
else:
|
|
50
57
|
self.sleep_time_on_rate_limit = sleep_time_on_rate_limit
|
|
51
58
|
self.print_on_rate_limit = print_on_rate_limit
|
|
59
|
+
self.typed = typed
|
|
52
60
|
|
|
53
61
|
@property
|
|
54
62
|
def steam_name(self):
|
|
@@ -74,11 +82,12 @@ class BallchasingApi:
|
|
|
74
82
|
self.ping()
|
|
75
83
|
return self._ping_result.get("quota")
|
|
76
84
|
|
|
77
|
-
def _request(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
def _request(
|
|
86
|
+
self,
|
|
87
|
+
url_or_endpoint: str,
|
|
88
|
+
method: str,
|
|
89
|
+
**params
|
|
90
|
+
) -> Response:
|
|
82
91
|
"""
|
|
83
92
|
Helper method for all requests.
|
|
84
93
|
|
|
@@ -92,7 +101,8 @@ class BallchasingApi:
|
|
|
92
101
|
headers = {"Authorization": self.auth_key}
|
|
93
102
|
url = f"{self.base_url}{url_or_endpoint}" if url_or_endpoint.startswith("/") else url_or_endpoint
|
|
94
103
|
max_retries = 8
|
|
95
|
-
|
|
104
|
+
retries = 0
|
|
105
|
+
while True:
|
|
96
106
|
try:
|
|
97
107
|
r: Response = self._session.request(method=method, url=url, headers=headers, **params)
|
|
98
108
|
if 200 <= r.status_code < 300:
|
|
@@ -108,13 +118,14 @@ class BallchasingApi:
|
|
|
108
118
|
elif self.sleep_time_on_rate_limit:
|
|
109
119
|
time.sleep(self.sleep_time_on_rate_limit)
|
|
110
120
|
else:
|
|
111
|
-
r.raise_for_status() # Raise an error for any other status code
|
|
121
|
+
r.raise_for_status() # Raise an error for any other status code'
|
|
112
122
|
except ConnectionError as e:
|
|
113
123
|
if retries >= max_retries - 1:
|
|
114
124
|
raise e
|
|
115
125
|
s = 2 ** retries
|
|
116
126
|
print(f"Connection error, trying again in {s} seconds...")
|
|
117
127
|
time.sleep(s)
|
|
128
|
+
retries += 1
|
|
118
129
|
|
|
119
130
|
def ping(self) -> dict:
|
|
120
131
|
"""
|
|
@@ -130,28 +141,56 @@ class BallchasingApi:
|
|
|
130
141
|
self._ping_result = result
|
|
131
142
|
return result
|
|
132
143
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
def _iterable_from_request(self, url, params):
|
|
145
|
+
# Shared by get_replays and get_groups
|
|
146
|
+
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
|
+
|
|
159
|
+
batch = d["list"][:request_count]
|
|
160
|
+
yield from batch
|
|
161
|
+
|
|
162
|
+
if "next" not in d:
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
next_url = d["next"]
|
|
166
|
+
remaining -= len(batch)
|
|
167
|
+
params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
|
|
168
|
+
|
|
169
|
+
def get_replays(
|
|
170
|
+
self,
|
|
171
|
+
*,
|
|
172
|
+
title: Optional[str] = None,
|
|
173
|
+
player_name: Optional[Union[str, List[str]]] = None,
|
|
174
|
+
player_id: Optional[Union[str, List[str]]] = None,
|
|
175
|
+
playlist: Optional[Union[AnyPlaylist, List[AnyPlaylist]]] = None,
|
|
176
|
+
season: Optional[Union[AnySeason, List[AnySeason]]] = None,
|
|
177
|
+
match_result: Optional[Union[AnyMatchResult, List[AnyMatchResult]]] = None,
|
|
178
|
+
min_rank: Optional[AnyRank] = None,
|
|
179
|
+
max_rank: Optional[AnyRank] = None,
|
|
180
|
+
pro: Optional[bool] = None,
|
|
181
|
+
uploader: Optional[str] = None,
|
|
182
|
+
group_id: Optional[Union[str, List[str]]] = None,
|
|
183
|
+
map_id: Optional[Union[AnyMap, List[AnyMap]]] = None,
|
|
184
|
+
created_before: Optional[Union[str, datetime]] = None,
|
|
185
|
+
created_after: Optional[Union[str, datetime]] = None,
|
|
186
|
+
replay_after: Optional[Union[str, datetime]] = None,
|
|
187
|
+
replay_before: Optional[Union[str, datetime]] = None,
|
|
188
|
+
count: int = 150,
|
|
189
|
+
sort_by: Optional[AnyReplaySortBy] = None,
|
|
190
|
+
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
191
|
+
deep: bool = False,
|
|
192
|
+
typed: Optional[bool] = None,
|
|
193
|
+
) -> Iterator[Union[dict, ShallowReplay, DeepReplay]]:
|
|
155
194
|
"""
|
|
156
195
|
This endpoint lets you filter and retrieve replays. The implementation returns an iterator.
|
|
157
196
|
|
|
@@ -184,42 +223,41 @@ class BallchasingApi:
|
|
|
184
223
|
:param sort_by: sort replays according the selected field
|
|
185
224
|
:param sort_dir: sort direction
|
|
186
225
|
:param deep: whether to get full stats for each replay (will be much slower).
|
|
226
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
187
227
|
:return: an iterator over the replays returned by the API.
|
|
188
228
|
"""
|
|
189
229
|
url = f"{self.base_url}/replays"
|
|
190
230
|
params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist,
|
|
191
231
|
"season": season, "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank,
|
|
192
232
|
"pro": pro, "uploader": uploader, "group": group_id, "map": map_id,
|
|
193
|
-
"created-before":
|
|
194
|
-
"replay-date-after":
|
|
195
|
-
"sort-by": sort_by, "sort-dir": sort_dir}
|
|
196
|
-
left = count
|
|
197
|
-
while left > 0:
|
|
198
|
-
request_count = min(left, 200)
|
|
199
|
-
params["count"] = request_count
|
|
200
|
-
d = self._request(url, "GET", params=params).json()
|
|
233
|
+
"created-before": to_rfc3339(created_before), "created-after": to_rfc3339(created_after),
|
|
234
|
+
"replay-date-after": to_rfc3339(replay_after), "replay-date-before": to_rfc3339(replay_before),
|
|
235
|
+
"count": count, "sort-by": sort_by, "sort-dir": sort_dir}
|
|
201
236
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
yield from batch
|
|
205
|
-
else:
|
|
206
|
-
yield from (self.get_replay(r["id"]) for r in batch)
|
|
237
|
+
if typed is None:
|
|
238
|
+
typed = self.typed
|
|
207
239
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
240
|
+
iterator = self._iterable_from_request(url, params)
|
|
241
|
+
if deep:
|
|
242
|
+
iterator = (self.get_replay(r["id"], typed=typed) for r in iterator)
|
|
243
|
+
elif typed:
|
|
244
|
+
iterator = (ShallowReplay(**r) for r in iterator)
|
|
245
|
+
yield from iterator
|
|
214
246
|
|
|
215
|
-
def get_replay(self, replay_id: str) -> dict:
|
|
247
|
+
def get_replay(self, replay_id: str, *, typed: Optional[bool] = None) -> Union[dict, DeepReplay]:
|
|
216
248
|
"""
|
|
217
249
|
Retrieve a given replay’s details and stats.
|
|
218
250
|
|
|
219
251
|
:param replay_id: the replay id.
|
|
252
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
220
253
|
:return: the result of the GET request.
|
|
221
254
|
"""
|
|
222
|
-
|
|
255
|
+
result = self._request(f"/replays/{replay_id}", "GET").json()
|
|
256
|
+
if typed is None:
|
|
257
|
+
typed = self.typed
|
|
258
|
+
if typed:
|
|
259
|
+
result = DeepReplay(**result)
|
|
260
|
+
return result
|
|
223
261
|
|
|
224
262
|
def patch_replay(self, replay_id: str, **params) -> None:
|
|
225
263
|
"""
|
|
@@ -230,18 +268,24 @@ class BallchasingApi:
|
|
|
230
268
|
"""
|
|
231
269
|
self._request(f"/replays/{replay_id}", "PATCH", json=params)
|
|
232
270
|
|
|
233
|
-
def upload_replay(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
271
|
+
def upload_replay(
|
|
272
|
+
self,
|
|
273
|
+
replay_file: Union[str, Path, BinaryIO],
|
|
274
|
+
*,
|
|
275
|
+
visibility: Optional[AnyVisibility] = None,
|
|
276
|
+
group: Optional[str] = None
|
|
277
|
+
) -> dict:
|
|
237
278
|
"""
|
|
238
279
|
Use this API to upload a replay file to ballchasing.com.
|
|
239
280
|
|
|
240
|
-
:param replay_file: replay file to upload.
|
|
281
|
+
:param replay_file: replay file to upload. Can be a file path (str or Path) or a file-like object.
|
|
241
282
|
:param visibility: to set the visibility of the uploaded replay.
|
|
242
283
|
:param group: to upload the replay to an existing group.
|
|
243
284
|
:return: the result of the POST request.
|
|
244
285
|
"""
|
|
286
|
+
if isinstance(replay_file, (str, Path)):
|
|
287
|
+
with open(replay_file, "rb") as f:
|
|
288
|
+
return self.upload_replay(f, visibility=visibility, group=group)
|
|
245
289
|
return self._request(f"/v2/upload", "POST", files={"file": replay_file},
|
|
246
290
|
params={"group": group, "visibility": visibility}).json()
|
|
247
291
|
|
|
@@ -254,16 +298,20 @@ class BallchasingApi:
|
|
|
254
298
|
"""
|
|
255
299
|
self._request(f"/replays/{replay_id}", "DELETE")
|
|
256
300
|
|
|
257
|
-
def get_groups(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
301
|
+
def get_groups(
|
|
302
|
+
self,
|
|
303
|
+
*,
|
|
304
|
+
name: Optional[str] = None,
|
|
305
|
+
creator: Optional[str] = None,
|
|
306
|
+
group: Optional[str] = None,
|
|
307
|
+
created_before: Optional[Union[str, datetime]] = None,
|
|
308
|
+
created_after: Optional[Union[str, datetime]] = None,
|
|
309
|
+
count: int = 200,
|
|
310
|
+
sort_by: AnyGroupSortBy = GroupSortBy.CREATED,
|
|
311
|
+
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
312
|
+
deep: bool = False,
|
|
313
|
+
typed: bool = None,
|
|
314
|
+
) -> Iterator[Union[dict, ShallowGroup, DeepGroup]]:
|
|
267
315
|
"""
|
|
268
316
|
This endpoint lets you filter and retrieve replay groups.
|
|
269
317
|
|
|
@@ -279,34 +327,30 @@ class BallchasingApi:
|
|
|
279
327
|
past the limit of 200 set by the API
|
|
280
328
|
:param sort_by: Sort groups according the selected field.
|
|
281
329
|
:param sort_dir: Sort direction.
|
|
330
|
+
:param deep: whether to get full stats for each group (will be much slower).
|
|
331
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
282
332
|
:return: an iterator over the groups returned by the API.
|
|
283
333
|
"""
|
|
284
334
|
url = f"{self.base_url}/groups/"
|
|
285
|
-
params = {"name": name, "creator": creator, "group": group, "created-before":
|
|
286
|
-
"created-after":
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def create_group(self,
|
|
305
|
-
name: str,
|
|
306
|
-
player_identification: AnyPlayerIdentification,
|
|
307
|
-
team_identification: AnyTeamIdentification,
|
|
308
|
-
parent: Optional[str] = None
|
|
309
|
-
) -> dict:
|
|
335
|
+
params = {"name": name, "creator": creator, "group": group, "created-before": to_rfc3339(created_before),
|
|
336
|
+
"created-after": to_rfc3339(created_after), "count": count, "sort-by": sort_by, "sort-dir": sort_dir}
|
|
337
|
+
iterator = self._iterable_from_request(url, params)
|
|
338
|
+
if typed is None:
|
|
339
|
+
typed = self.typed
|
|
340
|
+
if deep:
|
|
341
|
+
iterator = (self.get_group(g["id"], typed=typed) for g in iterator)
|
|
342
|
+
elif typed:
|
|
343
|
+
iterator = (ShallowGroup(**g) for g in iterator)
|
|
344
|
+
yield from iterator
|
|
345
|
+
|
|
346
|
+
def create_group(
|
|
347
|
+
self,
|
|
348
|
+
*,
|
|
349
|
+
name: str,
|
|
350
|
+
player_identification: AnyPlayerIdentification,
|
|
351
|
+
team_identification: AnyTeamIdentification,
|
|
352
|
+
parent: Optional[str] = None
|
|
353
|
+
) -> dict:
|
|
310
354
|
"""
|
|
311
355
|
Use this API to create a new replay group.
|
|
312
356
|
|
|
@@ -326,14 +370,25 @@ class BallchasingApi:
|
|
|
326
370
|
"team_identification": team_identification, "parent": parent}
|
|
327
371
|
return self._request(f"/groups", "POST", json=json).json()
|
|
328
372
|
|
|
329
|
-
def get_group(
|
|
373
|
+
def get_group(
|
|
374
|
+
self,
|
|
375
|
+
group_id: str,
|
|
376
|
+
*,
|
|
377
|
+
typed: Optional[bool] = None
|
|
378
|
+
) -> Union[dict, DeepGroup]:
|
|
330
379
|
"""
|
|
331
380
|
This endpoint retrieves a specific replay group info and stats given its id.
|
|
332
381
|
|
|
333
382
|
:param group_id: the group id.
|
|
383
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
334
384
|
:return: the group info with stats.
|
|
335
385
|
"""
|
|
336
|
-
|
|
386
|
+
result = self._request(f"/groups/{group_id}", "GET").json()
|
|
387
|
+
if typed is None:
|
|
388
|
+
typed = self.typed
|
|
389
|
+
if typed:
|
|
390
|
+
result = DeepGroup(**result)
|
|
391
|
+
return result
|
|
337
392
|
|
|
338
393
|
def patch_group(self, group_id: str, **params) -> None:
|
|
339
394
|
"""
|
|
@@ -353,46 +408,79 @@ class BallchasingApi:
|
|
|
353
408
|
"""
|
|
354
409
|
self._request(f"/groups/{group_id}", "DELETE")
|
|
355
410
|
|
|
356
|
-
def get_group_replays(
|
|
411
|
+
def get_group_replays(
|
|
412
|
+
self,
|
|
413
|
+
group: Union[str, dict, BasicGroup],
|
|
414
|
+
*,
|
|
415
|
+
deep: bool = False,
|
|
416
|
+
typed: Optional[bool] = None
|
|
417
|
+
) -> Iterator[Union[dict, ShallowReplay, DeepReplay]]:
|
|
357
418
|
"""
|
|
358
419
|
Finds all replays in a group, including child groups.
|
|
359
420
|
|
|
360
|
-
:param
|
|
421
|
+
:param group: the base group id, group dict, or BaseGroup object.
|
|
361
422
|
:param deep: whether or not to get full stats for each replay (will be much slower).
|
|
423
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
362
424
|
:return: an iterator over all the replays in the group.
|
|
363
425
|
"""
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
for replay in self.get_group_replays(child["id"], deep):
|
|
367
|
-
yield replay
|
|
368
|
-
for replay in self.get_replays(group_id=group_id, deep=deep):
|
|
426
|
+
for path in self.get_group_tree(group, deep=deep, typed=typed):
|
|
427
|
+
group, replay = path
|
|
369
428
|
yield replay
|
|
370
429
|
|
|
371
|
-
def
|
|
430
|
+
def get_group_tree(
|
|
431
|
+
self,
|
|
432
|
+
group: Union[str, dict, BaseGroup],
|
|
433
|
+
*,
|
|
434
|
+
deep: bool = False,
|
|
435
|
+
typed: Optional[bool] = None
|
|
436
|
+
):
|
|
437
|
+
"""
|
|
438
|
+
Finds all replays in a group, and includes the groups leading up to the replays.
|
|
439
|
+
:param group: the group id or a group dict.
|
|
440
|
+
:param deep: whether to get full stats for each replay and group (will be much slower).
|
|
441
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
442
|
+
"""
|
|
443
|
+
if isinstance(group, str):
|
|
444
|
+
group = self.get_group(group)
|
|
445
|
+
if isinstance(group, BasicGroup):
|
|
446
|
+
group_id = group.id
|
|
447
|
+
else:
|
|
448
|
+
group_id = group["id"]
|
|
449
|
+
child_groups = self.get_groups(group=group_id, typed=typed)
|
|
450
|
+
for child in child_groups:
|
|
451
|
+
for path in self.get_group_tree(child, deep=deep, typed=typed):
|
|
452
|
+
yield group_id, *path
|
|
453
|
+
for replay in self.get_replays(group_id=group_id, deep=deep, typed=typed):
|
|
454
|
+
yield group_id, replay
|
|
455
|
+
|
|
456
|
+
def download_replay(self, replay_id: str, path: str):
|
|
372
457
|
"""
|
|
373
458
|
Download a replay file.
|
|
374
459
|
|
|
375
460
|
:param replay_id: the replay id.
|
|
376
|
-
:param
|
|
461
|
+
:param path: the path to download the replay to. Can be a file path or a directory.
|
|
377
462
|
"""
|
|
378
463
|
r = self._request(f"/replays/{replay_id}/file", "GET")
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
464
|
+
if os.path.isdir(path):
|
|
465
|
+
# If path is a directory, use the replay id as the filename
|
|
466
|
+
filename = f"{replay_id}.replay"
|
|
467
|
+
path = os.path.join(path, filename)
|
|
468
|
+
with open(path, "wb") as f:
|
|
469
|
+
f.write(r.content)
|
|
382
470
|
|
|
383
|
-
def download_group(self, group_id: str, folder: str,
|
|
471
|
+
def download_group(self, group_id: str, folder: str, *, keep_tree_structure=True):
|
|
384
472
|
"""
|
|
385
473
|
Download an entire group.
|
|
386
474
|
|
|
387
475
|
:param group_id: the base group id.
|
|
388
476
|
:param folder: the folder in which to create the group folder.
|
|
389
|
-
:param
|
|
477
|
+
:param keep_tree_structure: whether to create new folders for child groups.
|
|
390
478
|
"""
|
|
391
479
|
folder = os.path.join(folder, group_id)
|
|
392
|
-
if
|
|
480
|
+
if keep_tree_structure:
|
|
393
481
|
os.makedirs(folder, exist_ok=True)
|
|
394
482
|
for child_group in self.get_groups(group=group_id):
|
|
395
|
-
self.download_group(child_group["id"], folder, True)
|
|
483
|
+
self.download_group(child_group["id"], folder, keep_tree_structure=True)
|
|
396
484
|
for replay in self.get_replays(group_id=group_id):
|
|
397
485
|
self.download_replay(replay["id"], folder)
|
|
398
486
|
else:
|
|
@@ -415,9 +503,9 @@ class BallchasingApi:
|
|
|
415
503
|
"""
|
|
416
504
|
|
|
417
505
|
if isinstance(replay, str):
|
|
418
|
-
replay = self.get_replay(replay)
|
|
506
|
+
replay = self.get_replay(replay, typed=False)
|
|
419
507
|
elif isinstance(replay, dict) and "title" not in replay:
|
|
420
|
-
replay = self.get_replay(replay["id"])
|
|
508
|
+
replay = self.get_replay(replay["id"], typed=False)
|
|
421
509
|
|
|
422
510
|
stats = parse_replay_stats(replay)
|
|
423
511
|
return stats
|