python-ballchasing 0.3.0__py3-none-any.whl → 0.4.1__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 +144 -68
- 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.3.0.dist-info → python_ballchasing-0.4.1.dist-info}/METADATA +19 -2
- python_ballchasing-0.4.1.dist-info/RECORD +19 -0
- {python_ballchasing-0.3.0.dist-info → python_ballchasing-0.4.1.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.3.0.dist-info/RECORD +0 -10
- {python_ballchasing-0.3.0.dist-info → python_ballchasing-0.4.1.dist-info}/WHEEL +0 -0
- {python_ballchasing-0.3.0.dist-info → python_ballchasing-0.4.1.dist-info}/licenses/LICENSE +0 -0
ballchasing/__init__.py
CHANGED
ballchasing/api.py
CHANGED
|
@@ -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,36 @@ 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
|
+
else:
|
|
159
|
+
raise e
|
|
160
|
+
|
|
161
|
+
batch = d["list"][:request_count]
|
|
162
|
+
yield from batch
|
|
163
|
+
|
|
164
|
+
if "next" not in d:
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
next_url = d["next"]
|
|
168
|
+
remaining -= len(batch)
|
|
169
|
+
params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
|
|
170
|
+
|
|
139
171
|
def get_replays(
|
|
140
172
|
self,
|
|
173
|
+
*,
|
|
141
174
|
title: Optional[str] = None,
|
|
142
175
|
player_name: Optional[Union[str, List[str]]] = None,
|
|
143
176
|
player_id: Optional[Union[str, List[str]]] = None,
|
|
@@ -157,8 +190,9 @@ class BallchasingApi:
|
|
|
157
190
|
count: int = 150,
|
|
158
191
|
sort_by: Optional[AnyReplaySortBy] = None,
|
|
159
192
|
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
160
|
-
deep: bool = False
|
|
161
|
-
|
|
193
|
+
deep: bool = False,
|
|
194
|
+
typed: Optional[bool] = None,
|
|
195
|
+
) -> Iterator[Union[dict, ShallowReplay, DeepReplay]]:
|
|
162
196
|
"""
|
|
163
197
|
This endpoint lets you filter and retrieve replays. The implementation returns an iterator.
|
|
164
198
|
|
|
@@ -191,42 +225,41 @@ class BallchasingApi:
|
|
|
191
225
|
:param sort_by: sort replays according the selected field
|
|
192
226
|
:param sort_dir: sort direction
|
|
193
227
|
:param deep: whether to get full stats for each replay (will be much slower).
|
|
228
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
194
229
|
:return: an iterator over the replays returned by the API.
|
|
195
230
|
"""
|
|
196
231
|
url = f"{self.base_url}/replays"
|
|
197
232
|
params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist,
|
|
198
233
|
"season": season, "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank,
|
|
199
234
|
"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()
|
|
235
|
+
"created-before": to_rfc3339(created_before), "created-after": to_rfc3339(created_after),
|
|
236
|
+
"replay-date-after": to_rfc3339(replay_after), "replay-date-before": to_rfc3339(replay_before),
|
|
237
|
+
"count": count, "sort-by": sort_by, "sort-dir": sort_dir}
|
|
208
238
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
yield from batch
|
|
212
|
-
else:
|
|
213
|
-
yield from (self.get_replay(r["id"]) for r in batch)
|
|
239
|
+
if typed is None:
|
|
240
|
+
typed = self.typed
|
|
214
241
|
|
|
215
|
-
|
|
216
|
-
|
|
242
|
+
iterator = self._iterable_from_request(url, params)
|
|
243
|
+
if deep:
|
|
244
|
+
iterator = (self.get_replay(r["id"], typed=typed) for r in iterator)
|
|
245
|
+
elif typed:
|
|
246
|
+
iterator = (ShallowReplay(**r) for r in iterator)
|
|
247
|
+
yield from iterator
|
|
217
248
|
|
|
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:
|
|
249
|
+
def get_replay(self, replay_id: str, *, typed: Optional[bool] = None) -> Union[dict, DeepReplay]:
|
|
223
250
|
"""
|
|
224
251
|
Retrieve a given replay’s details and stats.
|
|
225
252
|
|
|
226
253
|
:param replay_id: the replay id.
|
|
254
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
227
255
|
:return: the result of the GET request.
|
|
228
256
|
"""
|
|
229
|
-
|
|
257
|
+
result = self._request(f"/replays/{replay_id}", "GET").json()
|
|
258
|
+
if typed is None:
|
|
259
|
+
typed = self.typed
|
|
260
|
+
if typed:
|
|
261
|
+
result = DeepReplay(**result)
|
|
262
|
+
return result
|
|
230
263
|
|
|
231
264
|
def patch_replay(self, replay_id: str, **params) -> None:
|
|
232
265
|
"""
|
|
@@ -240,6 +273,7 @@ class BallchasingApi:
|
|
|
240
273
|
def upload_replay(
|
|
241
274
|
self,
|
|
242
275
|
replay_file: Union[str, Path, BinaryIO],
|
|
276
|
+
*,
|
|
243
277
|
visibility: Optional[AnyVisibility] = None,
|
|
244
278
|
group: Optional[str] = None
|
|
245
279
|
) -> dict:
|
|
@@ -253,7 +287,7 @@ class BallchasingApi:
|
|
|
253
287
|
"""
|
|
254
288
|
if isinstance(replay_file, (str, Path)):
|
|
255
289
|
with open(replay_file, "rb") as f:
|
|
256
|
-
return self.upload_replay(f, visibility, group)
|
|
290
|
+
return self.upload_replay(f, visibility=visibility, group=group)
|
|
257
291
|
return self._request(f"/v2/upload", "POST", files={"file": replay_file},
|
|
258
292
|
params={"group": group, "visibility": visibility}).json()
|
|
259
293
|
|
|
@@ -268,6 +302,7 @@ class BallchasingApi:
|
|
|
268
302
|
|
|
269
303
|
def get_groups(
|
|
270
304
|
self,
|
|
305
|
+
*,
|
|
271
306
|
name: Optional[str] = None,
|
|
272
307
|
creator: Optional[str] = None,
|
|
273
308
|
group: Optional[str] = None,
|
|
@@ -275,8 +310,10 @@ class BallchasingApi:
|
|
|
275
310
|
created_after: Optional[Union[str, datetime]] = None,
|
|
276
311
|
count: int = 200,
|
|
277
312
|
sort_by: AnyGroupSortBy = GroupSortBy.CREATED,
|
|
278
|
-
sort_dir: AnySortDir = SortDir.DESCENDING
|
|
279
|
-
|
|
313
|
+
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
314
|
+
deep: bool = False,
|
|
315
|
+
typed: bool = None,
|
|
316
|
+
) -> Iterator[Union[dict, ShallowGroup, DeepGroup]]:
|
|
280
317
|
"""
|
|
281
318
|
This endpoint lets you filter and retrieve replay groups.
|
|
282
319
|
|
|
@@ -292,30 +329,25 @@ class BallchasingApi:
|
|
|
292
329
|
past the limit of 200 set by the API
|
|
293
330
|
:param sort_by: Sort groups according the selected field.
|
|
294
331
|
:param sort_dir: Sort direction.
|
|
332
|
+
:param deep: whether to get full stats for each group (will be much slower).
|
|
333
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
295
334
|
:return: an iterator over the groups returned by the API.
|
|
296
335
|
"""
|
|
297
336
|
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]
|
|
337
|
+
params = {"name": name, "creator": creator, "group": group, "created-before": to_rfc3339(created_before),
|
|
338
|
+
"created-after": to_rfc3339(created_after), "count": count, "sort-by": sort_by, "sort-dir": sort_dir}
|
|
339
|
+
iterator = self._iterable_from_request(url, params)
|
|
340
|
+
if typed is None:
|
|
341
|
+
typed = self.typed
|
|
342
|
+
if deep:
|
|
343
|
+
iterator = (self.get_group(g["id"], typed=typed) for g in iterator)
|
|
344
|
+
elif typed:
|
|
345
|
+
iterator = (ShallowGroup(**g) for g in iterator)
|
|
346
|
+
yield from iterator
|
|
316
347
|
|
|
317
348
|
def create_group(
|
|
318
349
|
self,
|
|
350
|
+
*,
|
|
319
351
|
name: str,
|
|
320
352
|
player_identification: AnyPlayerIdentification,
|
|
321
353
|
team_identification: AnyTeamIdentification,
|
|
@@ -340,14 +372,25 @@ class BallchasingApi:
|
|
|
340
372
|
"team_identification": team_identification, "parent": parent}
|
|
341
373
|
return self._request(f"/groups", "POST", json=json).json()
|
|
342
374
|
|
|
343
|
-
def get_group(
|
|
375
|
+
def get_group(
|
|
376
|
+
self,
|
|
377
|
+
group_id: str,
|
|
378
|
+
*,
|
|
379
|
+
typed: Optional[bool] = None
|
|
380
|
+
) -> Union[dict, DeepGroup]:
|
|
344
381
|
"""
|
|
345
382
|
This endpoint retrieves a specific replay group info and stats given its id.
|
|
346
383
|
|
|
347
384
|
:param group_id: the group id.
|
|
385
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
348
386
|
:return: the group info with stats.
|
|
349
387
|
"""
|
|
350
|
-
|
|
388
|
+
result = self._request(f"/groups/{group_id}", "GET").json()
|
|
389
|
+
if typed is None:
|
|
390
|
+
typed = self.typed
|
|
391
|
+
if typed:
|
|
392
|
+
result = DeepGroup(**result)
|
|
393
|
+
return result
|
|
351
394
|
|
|
352
395
|
def patch_group(self, group_id: str, **params) -> None:
|
|
353
396
|
"""
|
|
@@ -367,46 +410,79 @@ class BallchasingApi:
|
|
|
367
410
|
"""
|
|
368
411
|
self._request(f"/groups/{group_id}", "DELETE")
|
|
369
412
|
|
|
370
|
-
def get_group_replays(
|
|
413
|
+
def get_group_replays(
|
|
414
|
+
self,
|
|
415
|
+
group: Union[str, dict, BasicGroup],
|
|
416
|
+
*,
|
|
417
|
+
deep: bool = False,
|
|
418
|
+
typed: Optional[bool] = None
|
|
419
|
+
) -> Iterator[Union[dict, ShallowReplay, DeepReplay]]:
|
|
371
420
|
"""
|
|
372
421
|
Finds all replays in a group, including child groups.
|
|
373
422
|
|
|
374
|
-
:param
|
|
423
|
+
:param group: the base group id, group dict, or BaseGroup object.
|
|
375
424
|
:param deep: whether or not to get full stats for each replay (will be much slower).
|
|
425
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
376
426
|
:return: an iterator over all the replays in the group.
|
|
377
427
|
"""
|
|
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):
|
|
428
|
+
for path in self.get_group_tree(group, deep=deep, typed=typed):
|
|
429
|
+
group, replay = path
|
|
383
430
|
yield replay
|
|
384
431
|
|
|
385
|
-
def
|
|
432
|
+
def get_group_tree(
|
|
433
|
+
self,
|
|
434
|
+
group: Union[str, dict, BaseGroup],
|
|
435
|
+
*,
|
|
436
|
+
deep: bool = False,
|
|
437
|
+
typed: Optional[bool] = None
|
|
438
|
+
):
|
|
439
|
+
"""
|
|
440
|
+
Finds all replays in a group, and includes the groups leading up to the replays.
|
|
441
|
+
:param group: the group id or a group dict.
|
|
442
|
+
:param deep: whether to get full stats for each replay and group (will be much slower).
|
|
443
|
+
:param typed: whether to return a typed object (default is self.typed).
|
|
444
|
+
"""
|
|
445
|
+
if isinstance(group, str):
|
|
446
|
+
group = self.get_group(group)
|
|
447
|
+
if isinstance(group, BasicGroup):
|
|
448
|
+
group_id = group.id
|
|
449
|
+
else:
|
|
450
|
+
group_id = group["id"]
|
|
451
|
+
child_groups = self.get_groups(group=group_id, typed=typed)
|
|
452
|
+
for child in child_groups:
|
|
453
|
+
for path in self.get_group_tree(child, deep=deep, typed=typed):
|
|
454
|
+
yield group_id, *path
|
|
455
|
+
for replay in self.get_replays(group_id=group_id, deep=deep, typed=typed):
|
|
456
|
+
yield group_id, replay
|
|
457
|
+
|
|
458
|
+
def download_replay(self, replay_id: str, path: str):
|
|
386
459
|
"""
|
|
387
460
|
Download a replay file.
|
|
388
461
|
|
|
389
462
|
:param replay_id: the replay id.
|
|
390
|
-
:param
|
|
463
|
+
:param path: the path to download the replay to. Can be a file path or a directory.
|
|
391
464
|
"""
|
|
392
465
|
r = self._request(f"/replays/{replay_id}/file", "GET")
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
466
|
+
if os.path.isdir(path):
|
|
467
|
+
# If path is a directory, use the replay id as the filename
|
|
468
|
+
filename = f"{replay_id}.replay"
|
|
469
|
+
path = os.path.join(path, filename)
|
|
470
|
+
with open(path, "wb") as f:
|
|
471
|
+
f.write(r.content)
|
|
396
472
|
|
|
397
|
-
def download_group(self, group_id: str, folder: str,
|
|
473
|
+
def download_group(self, group_id: str, folder: str, *, keep_tree_structure=True):
|
|
398
474
|
"""
|
|
399
475
|
Download an entire group.
|
|
400
476
|
|
|
401
477
|
:param group_id: the base group id.
|
|
402
478
|
:param folder: the folder in which to create the group folder.
|
|
403
|
-
:param
|
|
479
|
+
:param keep_tree_structure: whether to create new folders for child groups.
|
|
404
480
|
"""
|
|
405
481
|
folder = os.path.join(folder, group_id)
|
|
406
|
-
if
|
|
482
|
+
if keep_tree_structure:
|
|
407
483
|
os.makedirs(folder, exist_ok=True)
|
|
408
484
|
for child_group in self.get_groups(group=group_id):
|
|
409
|
-
self.download_group(child_group["id"], folder, True)
|
|
485
|
+
self.download_group(child_group["id"], folder, keep_tree_structure=True)
|
|
410
486
|
for replay in self.get_replays(group_id=group_id):
|
|
411
487
|
self.download_replay(replay["id"], folder)
|
|
412
488
|
else:
|
|
@@ -429,9 +505,9 @@ class BallchasingApi:
|
|
|
429
505
|
"""
|
|
430
506
|
|
|
431
507
|
if isinstance(replay, str):
|
|
432
|
-
replay = self.get_replay(replay)
|
|
508
|
+
replay = self.get_replay(replay, typed=False)
|
|
433
509
|
elif isinstance(replay, dict) and "title" not in replay:
|
|
434
|
-
replay = self.get_replay(replay["id"])
|
|
510
|
+
replay = self.get_replay(replay["id"], typed=False)
|
|
435
511
|
|
|
436
512
|
stats = parse_replay_stats(replay)
|
|
437
513
|
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
|