python-ballchasing 0.3.0__tar.gz → 0.4.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.3.0 → python_ballchasing-0.4.0}/PKG-INFO +18 -2
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/README.md +16 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/ballchasing/__init__.py +7 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/ballchasing/api.py +142 -68
- python_ballchasing-0.4.0/ballchasing/typed/__init__.py +5 -0
- python_ballchasing-0.4.0/ballchasing/typed/deep_group.py +440 -0
- python_ballchasing-0.4.0/ballchasing/typed/deep_replay.py +286 -0
- python_ballchasing-0.4.0/ballchasing/typed/shallow_group.py +18 -0
- python_ballchasing-0.4.0/ballchasing/typed/shallow_replay.py +67 -0
- python_ballchasing-0.4.0/ballchasing/typed/shared.py +218 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/ballchasing/util.py +16 -4
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/pyproject.toml +2 -2
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/python_ballchasing.egg-info/PKG-INFO +18 -2
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/python_ballchasing.egg-info/SOURCES.txt +10 -1
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/python_ballchasing.egg-info/top_level.txt +1 -0
- python_ballchasing-0.4.0/scripts/make_types.py +194 -0
- python_ballchasing-0.4.0/scripts/test.py +52 -0
- python_ballchasing-0.4.0/scripts/test_typed.py +35 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/LICENSE +0 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/ballchasing/constants.py +0 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/ballchasing/stats_info.tsv +0 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/python_ballchasing.egg-info/dependency_links.txt +0 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/python_ballchasing.egg-info/requires.txt +0 -0
- {python_ballchasing-0.3.0 → python_ballchasing-0.4.0}/setup.cfg +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-ballchasing
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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.9
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Requires-Python: >=3.9
|
|
@@ -90,3 +90,19 @@ for group in groups:
|
|
|
90
90
|
for replay in replays:
|
|
91
91
|
api.download_replay(replay_id=replay["id"], folder="/path/to/destination/") # You could also download like this
|
|
92
92
|
```
|
|
93
|
+
|
|
94
|
+
Additionally, there's the option to put responses (replays, groups) into typed objects for better type hinting and validation.
|
|
95
|
+
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
|
+
The classes can also be found under the `typing` folder and used as a reference for what the API returns.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# When creating the API, you can specify a default setting for typing (defaults to False)
|
|
100
|
+
api = ballchasing.Api("Your token here", typed=True)
|
|
101
|
+
|
|
102
|
+
# or you can specify it explicitly
|
|
103
|
+
replay = api.get_replay("2627e02a-aa46-4e13-b66b-b76a32069a07", typed=True)
|
|
104
|
+
print(replay.blue.players[0].name) # Example of easy attribute access
|
|
105
|
+
|
|
106
|
+
group = api.get_group("g2-vs-bds-hwbf2eyolb", typed=True)
|
|
107
|
+
print(group.players[0].name)
|
|
108
|
+
```
|
|
@@ -73,3 +73,19 @@ for group in groups:
|
|
|
73
73
|
for replay in replays:
|
|
74
74
|
api.download_replay(replay_id=replay["id"], folder="/path/to/destination/") # You could also download like this
|
|
75
75
|
```
|
|
76
|
+
|
|
77
|
+
Additionally, there's the option to put responses (replays, groups) into typed objects for better type hinting and validation.
|
|
78
|
+
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
|
+
The classes can also be found under the `typing` folder and used as a reference for what the API returns.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# When creating the API, you can specify a default setting for typing (defaults to False)
|
|
83
|
+
api = ballchasing.Api("Your token here", typed=True)
|
|
84
|
+
|
|
85
|
+
# or you can specify it explicitly
|
|
86
|
+
replay = api.get_replay("2627e02a-aa46-4e13-b66b-b76a32069a07", typed=True)
|
|
87
|
+
print(replay.blue.players[0].name) # Example of easy attribute access
|
|
88
|
+
|
|
89
|
+
group = api.get_group("g2-vs-bds-hwbf2eyolb", typed=True)
|
|
90
|
+
print(group.players[0].name)
|
|
91
|
+
```
|
|
@@ -2,14 +2,16 @@ import os
|
|
|
2
2
|
import time
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional,
|
|
5
|
+
from typing import Optional, Union, List, BinaryIO, Iterator
|
|
6
6
|
from urllib.parse import parse_qs, urlparse
|
|
7
7
|
|
|
8
|
-
from requests import sessions, Response, ConnectionError
|
|
8
|
+
from requests import sessions, Response, ConnectionError, HTTPError
|
|
9
9
|
|
|
10
10
|
from ballchasing.constants import GroupSortBy, SortDir, AnyPlaylist, AnyMap, AnySeason, AnyRank, AnyReplaySortBy, \
|
|
11
11
|
AnySortDir, AnyVisibility, AnyGroupSortBy, AnyPlayerIdentification, AnyTeamIdentification, AnyMatchResult
|
|
12
|
-
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
|
|
13
15
|
|
|
14
16
|
DEFAULT_URL = "https://ballchasing.com/api"
|
|
15
17
|
|
|
@@ -22,10 +24,12 @@ class BallchasingApi:
|
|
|
22
24
|
def __init__(
|
|
23
25
|
self,
|
|
24
26
|
auth_key: str,
|
|
27
|
+
*,
|
|
25
28
|
sleep_time_on_rate_limit: Optional[float] = None,
|
|
26
29
|
print_on_rate_limit: bool = False,
|
|
27
30
|
base_url=None,
|
|
28
|
-
do_initial_ping=True
|
|
31
|
+
do_initial_ping=True,
|
|
32
|
+
typed=False,
|
|
29
33
|
):
|
|
30
34
|
"""
|
|
31
35
|
|
|
@@ -52,6 +56,7 @@ class BallchasingApi:
|
|
|
52
56
|
else:
|
|
53
57
|
self.sleep_time_on_rate_limit = sleep_time_on_rate_limit
|
|
54
58
|
self.print_on_rate_limit = print_on_rate_limit
|
|
59
|
+
self.typed = typed
|
|
55
60
|
|
|
56
61
|
@property
|
|
57
62
|
def steam_name(self):
|
|
@@ -136,8 +141,34 @@ class BallchasingApi:
|
|
|
136
141
|
self._ping_result = result
|
|
137
142
|
return result
|
|
138
143
|
|
|
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
|
+
|
|
139
169
|
def get_replays(
|
|
140
170
|
self,
|
|
171
|
+
*,
|
|
141
172
|
title: Optional[str] = None,
|
|
142
173
|
player_name: Optional[Union[str, List[str]]] = None,
|
|
143
174
|
player_id: Optional[Union[str, List[str]]] = None,
|
|
@@ -157,8 +188,9 @@ class BallchasingApi:
|
|
|
157
188
|
count: int = 150,
|
|
158
189
|
sort_by: Optional[AnyReplaySortBy] = None,
|
|
159
190
|
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
160
|
-
deep: bool = False
|
|
161
|
-
|
|
191
|
+
deep: bool = False,
|
|
192
|
+
typed: Optional[bool] = None,
|
|
193
|
+
) -> Iterator[Union[dict, ShallowReplay, DeepReplay]]:
|
|
162
194
|
"""
|
|
163
195
|
This endpoint lets you filter and retrieve replays. The implementation returns an iterator.
|
|
164
196
|
|
|
@@ -191,42 +223,41 @@ class BallchasingApi:
|
|
|
191
223
|
:param sort_by: sort replays according the selected field
|
|
192
224
|
:param sort_dir: sort direction
|
|
193
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).
|
|
194
227
|
:return: an iterator over the replays returned by the API.
|
|
195
228
|
"""
|
|
196
229
|
url = f"{self.base_url}/replays"
|
|
197
230
|
params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist,
|
|
198
231
|
"season": season, "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank,
|
|
199
232
|
"pro": pro, "uploader": uploader, "group": group_id, "map": map_id,
|
|
200
|
-
"created-before":
|
|
201
|
-
"replay-date-after":
|
|
202
|
-
"sort-by": sort_by, "sort-dir": sort_dir}
|
|
203
|
-
left = count
|
|
204
|
-
while left > 0:
|
|
205
|
-
request_count = min(left, 200)
|
|
206
|
-
params["count"] = request_count
|
|
207
|
-
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}
|
|
208
236
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
yield from batch
|
|
212
|
-
else:
|
|
213
|
-
yield from (self.get_replay(r["id"]) for r in batch)
|
|
237
|
+
if typed is None:
|
|
238
|
+
typed = self.typed
|
|
214
239
|
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
246
|
|
|
218
|
-
|
|
219
|
-
left -= len(batch)
|
|
220
|
-
params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
|
|
221
|
-
|
|
222
|
-
def get_replay(self, replay_id: str) -> dict:
|
|
247
|
+
def get_replay(self, replay_id: str, *, typed: Optional[bool] = None) -> Union[dict, DeepReplay]:
|
|
223
248
|
"""
|
|
224
249
|
Retrieve a given replay’s details and stats.
|
|
225
250
|
|
|
226
251
|
:param replay_id: the replay id.
|
|
252
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
227
253
|
:return: the result of the GET request.
|
|
228
254
|
"""
|
|
229
|
-
|
|
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
|
|
230
261
|
|
|
231
262
|
def patch_replay(self, replay_id: str, **params) -> None:
|
|
232
263
|
"""
|
|
@@ -240,6 +271,7 @@ class BallchasingApi:
|
|
|
240
271
|
def upload_replay(
|
|
241
272
|
self,
|
|
242
273
|
replay_file: Union[str, Path, BinaryIO],
|
|
274
|
+
*,
|
|
243
275
|
visibility: Optional[AnyVisibility] = None,
|
|
244
276
|
group: Optional[str] = None
|
|
245
277
|
) -> dict:
|
|
@@ -253,7 +285,7 @@ class BallchasingApi:
|
|
|
253
285
|
"""
|
|
254
286
|
if isinstance(replay_file, (str, Path)):
|
|
255
287
|
with open(replay_file, "rb") as f:
|
|
256
|
-
return self.upload_replay(f, visibility, group)
|
|
288
|
+
return self.upload_replay(f, visibility=visibility, group=group)
|
|
257
289
|
return self._request(f"/v2/upload", "POST", files={"file": replay_file},
|
|
258
290
|
params={"group": group, "visibility": visibility}).json()
|
|
259
291
|
|
|
@@ -268,6 +300,7 @@ class BallchasingApi:
|
|
|
268
300
|
|
|
269
301
|
def get_groups(
|
|
270
302
|
self,
|
|
303
|
+
*,
|
|
271
304
|
name: Optional[str] = None,
|
|
272
305
|
creator: Optional[str] = None,
|
|
273
306
|
group: Optional[str] = None,
|
|
@@ -275,8 +308,10 @@ class BallchasingApi:
|
|
|
275
308
|
created_after: Optional[Union[str, datetime]] = None,
|
|
276
309
|
count: int = 200,
|
|
277
310
|
sort_by: AnyGroupSortBy = GroupSortBy.CREATED,
|
|
278
|
-
sort_dir: AnySortDir = SortDir.DESCENDING
|
|
279
|
-
|
|
311
|
+
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
312
|
+
deep: bool = False,
|
|
313
|
+
typed: bool = None,
|
|
314
|
+
) -> Iterator[Union[dict, ShallowGroup, DeepGroup]]:
|
|
280
315
|
"""
|
|
281
316
|
This endpoint lets you filter and retrieve replay groups.
|
|
282
317
|
|
|
@@ -292,30 +327,25 @@ class BallchasingApi:
|
|
|
292
327
|
past the limit of 200 set by the API
|
|
293
328
|
:param sort_by: Sort groups according the selected field.
|
|
294
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).
|
|
295
332
|
:return: an iterator over the groups returned by the API.
|
|
296
333
|
"""
|
|
297
334
|
url = f"{self.base_url}/groups/"
|
|
298
|
-
params = {"name": name, "creator": creator, "group": group, "created-before":
|
|
299
|
-
"created-after":
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
yield from batch
|
|
309
|
-
|
|
310
|
-
if "next" not in d:
|
|
311
|
-
break
|
|
312
|
-
|
|
313
|
-
next_url = d["next"]
|
|
314
|
-
left -= len(batch)
|
|
315
|
-
params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
|
|
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
|
|
316
345
|
|
|
317
346
|
def create_group(
|
|
318
347
|
self,
|
|
348
|
+
*,
|
|
319
349
|
name: str,
|
|
320
350
|
player_identification: AnyPlayerIdentification,
|
|
321
351
|
team_identification: AnyTeamIdentification,
|
|
@@ -340,14 +370,25 @@ class BallchasingApi:
|
|
|
340
370
|
"team_identification": team_identification, "parent": parent}
|
|
341
371
|
return self._request(f"/groups", "POST", json=json).json()
|
|
342
372
|
|
|
343
|
-
def get_group(
|
|
373
|
+
def get_group(
|
|
374
|
+
self,
|
|
375
|
+
group_id: str,
|
|
376
|
+
*,
|
|
377
|
+
typed: Optional[bool] = None
|
|
378
|
+
) -> Union[dict, DeepGroup]:
|
|
344
379
|
"""
|
|
345
380
|
This endpoint retrieves a specific replay group info and stats given its id.
|
|
346
381
|
|
|
347
382
|
:param group_id: the group id.
|
|
383
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
348
384
|
:return: the group info with stats.
|
|
349
385
|
"""
|
|
350
|
-
|
|
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
|
|
351
392
|
|
|
352
393
|
def patch_group(self, group_id: str, **params) -> None:
|
|
353
394
|
"""
|
|
@@ -367,46 +408,79 @@ class BallchasingApi:
|
|
|
367
408
|
"""
|
|
368
409
|
self._request(f"/groups/{group_id}", "DELETE")
|
|
369
410
|
|
|
370
|
-
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]]:
|
|
371
418
|
"""
|
|
372
419
|
Finds all replays in a group, including child groups.
|
|
373
420
|
|
|
374
|
-
:param
|
|
421
|
+
:param group: the base group id, group dict, or BaseGroup object.
|
|
375
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).
|
|
376
424
|
:return: an iterator over all the replays in the group.
|
|
377
425
|
"""
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
for replay in self.get_group_replays(child["id"], deep):
|
|
381
|
-
yield replay
|
|
382
|
-
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
|
|
383
428
|
yield replay
|
|
384
429
|
|
|
385
|
-
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):
|
|
386
457
|
"""
|
|
387
458
|
Download a replay file.
|
|
388
459
|
|
|
389
460
|
:param replay_id: the replay id.
|
|
390
|
-
:param
|
|
461
|
+
:param path: the path to download the replay to. Can be a file path or a directory.
|
|
391
462
|
"""
|
|
392
463
|
r = self._request(f"/replays/{replay_id}/file", "GET")
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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)
|
|
396
470
|
|
|
397
|
-
def download_group(self, group_id: str, folder: str,
|
|
471
|
+
def download_group(self, group_id: str, folder: str, *, keep_tree_structure=True):
|
|
398
472
|
"""
|
|
399
473
|
Download an entire group.
|
|
400
474
|
|
|
401
475
|
:param group_id: the base group id.
|
|
402
476
|
:param folder: the folder in which to create the group folder.
|
|
403
|
-
:param
|
|
477
|
+
:param keep_tree_structure: whether to create new folders for child groups.
|
|
404
478
|
"""
|
|
405
479
|
folder = os.path.join(folder, group_id)
|
|
406
|
-
if
|
|
480
|
+
if keep_tree_structure:
|
|
407
481
|
os.makedirs(folder, exist_ok=True)
|
|
408
482
|
for child_group in self.get_groups(group=group_id):
|
|
409
|
-
self.download_group(child_group["id"], folder, True)
|
|
483
|
+
self.download_group(child_group["id"], folder, keep_tree_structure=True)
|
|
410
484
|
for replay in self.get_replays(group_id=group_id):
|
|
411
485
|
self.download_replay(replay["id"], folder)
|
|
412
486
|
else:
|
|
@@ -429,9 +503,9 @@ class BallchasingApi:
|
|
|
429
503
|
"""
|
|
430
504
|
|
|
431
505
|
if isinstance(replay, str):
|
|
432
|
-
replay = self.get_replay(replay)
|
|
506
|
+
replay = self.get_replay(replay, typed=False)
|
|
433
507
|
elif isinstance(replay, dict) and "title" not in replay:
|
|
434
|
-
replay = self.get_replay(replay["id"])
|
|
508
|
+
replay = self.get_replay(replay["id"], typed=False)
|
|
435
509
|
|
|
436
510
|
stats = parse_replay_stats(replay)
|
|
437
511
|
return stats
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Imports for the objects meant to be initialized directly
|
|
2
|
+
from ballchasing.typed.deep_group import DeepGroup
|
|
3
|
+
from ballchasing.typed.deep_replay import DeepReplay
|
|
4
|
+
from ballchasing.typed.shallow_group import ShallowGroup
|
|
5
|
+
from ballchasing.typed.shallow_replay import ShallowReplay
|