python-ballchasing 0.1.2__py3-none-any.whl → 0.2.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 +67 -36
- ballchasing/api.py +427 -322
- ballchasing/constants.py +202 -0
- ballchasing/stats_info.tsv +143 -0
- ballchasing/util.py +100 -0
- python_ballchasing-0.2.0.dist-info/METADATA +36 -0
- python_ballchasing-0.2.0.dist-info/RECORD +10 -0
- {python_ballchasing-0.1.2.dist-info → python_ballchasing-0.2.0.dist-info}/WHEEL +1 -1
- {python_ballchasing-0.1.2.dist-info → python_ballchasing-0.2.0.dist-info/licenses}/LICENSE +21 -21
- python_ballchasing-0.1.2.dist-info/METADATA +0 -31
- python_ballchasing-0.1.2.dist-info/RECORD +0 -7
- {python_ballchasing-0.1.2.dist-info → python_ballchasing-0.2.0.dist-info}/top_level.txt +0 -0
ballchasing/api.py
CHANGED
|
@@ -1,322 +1,427 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
self.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
:param
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
:param
|
|
167
|
-
:param
|
|
168
|
-
:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
:param
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
:param
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"""
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
"""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
"""
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
:param
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
"""
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional, Iterator, Union, List
|
|
5
|
+
from urllib.parse import parse_qs, urlparse
|
|
6
|
+
|
|
7
|
+
from requests import sessions, Response, ConnectionError
|
|
8
|
+
|
|
9
|
+
from ballchasing.constants import GroupSortBy, SortDir, AnyPlaylist, AnyMap, AnySeason, AnyRank, AnyReplaySortBy, \
|
|
10
|
+
AnySortDir, AnyVisibility, AnyGroupSortBy, AnyPlayerIdentification, AnyTeamIdentification, AnyMatchResult
|
|
11
|
+
from .util import rfc3339, parse_replay_stats
|
|
12
|
+
|
|
13
|
+
DEFAULT_URL = "https://ballchasing.com/api"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BallchasingApi:
|
|
17
|
+
"""
|
|
18
|
+
Class for communication with ballchasing.com API (https://ballchasing.com/doc/api)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self,
|
|
22
|
+
auth_key: str,
|
|
23
|
+
sleep_time_on_rate_limit: Optional[float] = None,
|
|
24
|
+
print_on_rate_limit: bool = False,
|
|
25
|
+
base_url=None,
|
|
26
|
+
do_initial_ping=True):
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
:param auth_key: authentication key for API calls.
|
|
30
|
+
:param sleep_time_on_rate_limit: seconds to wait after being rate limited.
|
|
31
|
+
Default value is calculated depending on patron type.
|
|
32
|
+
:param print_on_rate_limit: whether or not to print upon rate limits.
|
|
33
|
+
"""
|
|
34
|
+
self.auth_key = auth_key
|
|
35
|
+
self._session = sessions.Session()
|
|
36
|
+
self._ping_result = None
|
|
37
|
+
self.rate_limit_count = 0
|
|
38
|
+
self.base_url = DEFAULT_URL if base_url is None else base_url
|
|
39
|
+
if do_initial_ping:
|
|
40
|
+
self.ping()
|
|
41
|
+
if sleep_time_on_rate_limit is None:
|
|
42
|
+
self.sleep_time_on_rate_limit = {
|
|
43
|
+
"regular": 3600 / 1000,
|
|
44
|
+
"gold": 3600 / 2000,
|
|
45
|
+
"diamond": 3600 / 5000,
|
|
46
|
+
"champion": 1 / 8,
|
|
47
|
+
"gc": 1 / 16
|
|
48
|
+
}.get(self.patron_type or "regular")
|
|
49
|
+
else:
|
|
50
|
+
self.sleep_time_on_rate_limit = sleep_time_on_rate_limit
|
|
51
|
+
self.print_on_rate_limit = print_on_rate_limit
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def steam_name(self):
|
|
55
|
+
if self._ping_result is None:
|
|
56
|
+
self.ping()
|
|
57
|
+
return self._ping_result.get("name")
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def steam_id(self):
|
|
61
|
+
if self._ping_result is None:
|
|
62
|
+
self.ping()
|
|
63
|
+
return self._ping_result.get("steam_id")
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def patron_type(self):
|
|
67
|
+
if self._ping_result is None:
|
|
68
|
+
self.ping()
|
|
69
|
+
return self._ping_result.get("type")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def quota(self):
|
|
73
|
+
if self._ping_result is None:
|
|
74
|
+
self.ping()
|
|
75
|
+
return self._ping_result.get("quota")
|
|
76
|
+
|
|
77
|
+
def _request(self,
|
|
78
|
+
url_or_endpoint: str,
|
|
79
|
+
method: str,
|
|
80
|
+
**params
|
|
81
|
+
) -> Response:
|
|
82
|
+
"""
|
|
83
|
+
Helper method for all requests.
|
|
84
|
+
|
|
85
|
+
:param url: url or endpoint for request.
|
|
86
|
+
:param method: the method to use.
|
|
87
|
+
:param params: parameters for GET request.
|
|
88
|
+
:return: the request result.
|
|
89
|
+
:raises ConnectionError: if the connection fails after max retries.
|
|
90
|
+
:raises HTTPError: if the request fails with a status code other than 2xx or 429.
|
|
91
|
+
"""
|
|
92
|
+
headers = {"Authorization": self.auth_key}
|
|
93
|
+
url = f"{self.base_url}{url_or_endpoint}" if url_or_endpoint.startswith("/") else url_or_endpoint
|
|
94
|
+
max_retries = 8
|
|
95
|
+
for retries in range(max_retries):
|
|
96
|
+
try:
|
|
97
|
+
r: Response = self._session.request(method=method, url=url, headers=headers, **params)
|
|
98
|
+
if 200 <= r.status_code < 300:
|
|
99
|
+
return r
|
|
100
|
+
elif r.status_code == 429:
|
|
101
|
+
self.rate_limit_count += 1
|
|
102
|
+
if self.print_on_rate_limit:
|
|
103
|
+
print(f"Rate limited at {url} ({self.rate_limit_count} total rate limits)")
|
|
104
|
+
retry_after = r.headers.get("Retry-After", '0')
|
|
105
|
+
retry_after = int(retry_after) if retry_after.isdigit() else None
|
|
106
|
+
if retry_after: # integer > 0
|
|
107
|
+
time.sleep(retry_after)
|
|
108
|
+
elif self.sleep_time_on_rate_limit:
|
|
109
|
+
time.sleep(self.sleep_time_on_rate_limit)
|
|
110
|
+
else:
|
|
111
|
+
r.raise_for_status() # Raise an error for any other status code
|
|
112
|
+
except ConnectionError as e:
|
|
113
|
+
if retries >= max_retries - 1:
|
|
114
|
+
raise e
|
|
115
|
+
s = 2 ** retries
|
|
116
|
+
print(f"Connection error, trying again in {s} seconds...")
|
|
117
|
+
time.sleep(s)
|
|
118
|
+
|
|
119
|
+
def ping(self) -> dict:
|
|
120
|
+
"""
|
|
121
|
+
Use this API to:
|
|
122
|
+
|
|
123
|
+
- check if your API key is correct
|
|
124
|
+
- check if ballchasing API is reachable
|
|
125
|
+
|
|
126
|
+
This method runs automatically at initialization and the steam name and id as well as patron type are stored.
|
|
127
|
+
:return: ping response.
|
|
128
|
+
"""
|
|
129
|
+
result = self._request("/", "GET").json()
|
|
130
|
+
self._ping_result = result
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
def get_replays(self,
|
|
134
|
+
title: Optional[str] = None,
|
|
135
|
+
player_name: Optional[Union[str, List[str]]] = None,
|
|
136
|
+
player_id: Optional[Union[str, List[str]]] = None,
|
|
137
|
+
playlist: Optional[Union[AnyPlaylist, List[AnyPlaylist]]] = None,
|
|
138
|
+
season: Optional[Union[AnySeason, List[AnySeason]]] = None,
|
|
139
|
+
match_result: Optional[Union[AnyMatchResult, List[AnyMatchResult]]] = None,
|
|
140
|
+
min_rank: Optional[AnyRank] = None,
|
|
141
|
+
max_rank: Optional[AnyRank] = None,
|
|
142
|
+
pro: Optional[bool] = None,
|
|
143
|
+
uploader: Optional[str] = None,
|
|
144
|
+
group_id: Optional[Union[str, List[str]]] = None,
|
|
145
|
+
map_id: Optional[Union[AnyMap, List[AnyMap]]] = None,
|
|
146
|
+
created_before: Optional[Union[str, datetime]] = None,
|
|
147
|
+
created_after: Optional[Union[str, datetime]] = None,
|
|
148
|
+
replay_after: Optional[Union[str, datetime]] = None,
|
|
149
|
+
replay_before: Optional[Union[str, datetime]] = None,
|
|
150
|
+
count: int = 150,
|
|
151
|
+
sort_by: Optional[AnyReplaySortBy] = None,
|
|
152
|
+
sort_dir: AnySortDir = SortDir.DESCENDING,
|
|
153
|
+
deep: bool = False
|
|
154
|
+
) -> Iterator[dict]:
|
|
155
|
+
"""
|
|
156
|
+
This endpoint lets you filter and retrieve replays. The implementation returns an iterator.
|
|
157
|
+
|
|
158
|
+
:param title: filter replays by title.
|
|
159
|
+
:param player_name: filter replays by a player’s name.
|
|
160
|
+
:param player_id: filter replays by a player’s platform id in the $platform:$id, e.g. steam:76561198141161044,
|
|
161
|
+
ps4:gamertag, … You can filter replays by multiple player ids, e.g ?player-id=steam:1&player-id=steam:2
|
|
162
|
+
:param playlist: filter replays by one or more playlists.
|
|
163
|
+
:param season: filter replays by season. Must be a number between 1 and 14 (for old seasons)
|
|
164
|
+
or f1, f2, … for the new free to play seasons
|
|
165
|
+
:param match_result: filter your replays by result.
|
|
166
|
+
:param min_rank: filter your replays based on players minimum rank.
|
|
167
|
+
:param max_rank: filter your replays based on players maximum rank.
|
|
168
|
+
:param pro: only include replays containing at least one pro player.
|
|
169
|
+
:param uploader: only include replays uploaded by the specified user. Accepts either the
|
|
170
|
+
numerical 76*************44 steam id, or the special value 'me'
|
|
171
|
+
:param group_id: only include replays belonging to the specified group. This only include replays immediately
|
|
172
|
+
under the specified group, but not replays in child groups
|
|
173
|
+
:param map_id: only include replays in the specified map. Check get_maps for the list of valid map codes
|
|
174
|
+
:param created_before: only include replays created (uploaded) before some date.
|
|
175
|
+
RFC3339 format, e.g. '2020-01-02T15:00:05+01:00'
|
|
176
|
+
:param created_after: only include replays created (uploaded) after some date.
|
|
177
|
+
RFC3339 format, e.g. '2020-01-02T15:00:05+01:00'
|
|
178
|
+
:param replay_after: only include replays for games that happened after some date.
|
|
179
|
+
RFC3339 format, e.g. '2020-01-02T15:00:05+01:00'
|
|
180
|
+
:param replay_before: only include replays for games that happened before some date.
|
|
181
|
+
RFC3339 format, e.g. '2020-01-02T15:00:05+01:00'
|
|
182
|
+
:param count: returns at most count replays. Since the implementation uses an iterator it supports iterating
|
|
183
|
+
past the limit of 200 set by the API
|
|
184
|
+
:param sort_by: sort replays according the selected field
|
|
185
|
+
:param sort_dir: sort direction
|
|
186
|
+
:param deep: whether to get full stats for each replay (will be much slower).
|
|
187
|
+
:return: an iterator over the replays returned by the API.
|
|
188
|
+
"""
|
|
189
|
+
url = f"{self.base_url}/replays"
|
|
190
|
+
params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist,
|
|
191
|
+
"season": season, "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank,
|
|
192
|
+
"pro": pro, "uploader": uploader, "group": group_id, "map": map_id,
|
|
193
|
+
"created-before": rfc3339(created_before), "created-after": rfc3339(created_after),
|
|
194
|
+
"replay-date-after": rfc3339(replay_after), "replay-date-before": rfc3339(replay_before),
|
|
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()
|
|
201
|
+
|
|
202
|
+
batch = d["list"][:request_count]
|
|
203
|
+
if not deep:
|
|
204
|
+
yield from batch
|
|
205
|
+
else:
|
|
206
|
+
yield from (self.get_replay(r["id"]) for r in batch)
|
|
207
|
+
|
|
208
|
+
if "next" not in d:
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
next_url = d["next"]
|
|
212
|
+
left -= len(batch)
|
|
213
|
+
params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
|
|
214
|
+
|
|
215
|
+
def get_replay(self, replay_id: str) -> dict:
|
|
216
|
+
"""
|
|
217
|
+
Retrieve a given replay’s details and stats.
|
|
218
|
+
|
|
219
|
+
:param replay_id: the replay id.
|
|
220
|
+
:return: the result of the GET request.
|
|
221
|
+
"""
|
|
222
|
+
return self._request(f"/replays/{replay_id}", "GET").json()
|
|
223
|
+
|
|
224
|
+
def patch_replay(self, replay_id: str, **params) -> None:
|
|
225
|
+
"""
|
|
226
|
+
This endpoint can patch one or more fields of the specified replay
|
|
227
|
+
|
|
228
|
+
:param replay_id: the replay id.
|
|
229
|
+
:param params: parameters for the PATCH request.
|
|
230
|
+
"""
|
|
231
|
+
self._request(f"/replays/{replay_id}", "PATCH", json=params)
|
|
232
|
+
|
|
233
|
+
def upload_replay(self,
|
|
234
|
+
replay_file,
|
|
235
|
+
visibility: Optional[AnyVisibility] = None,
|
|
236
|
+
group: Optional[str] = None) -> dict:
|
|
237
|
+
"""
|
|
238
|
+
Use this API to upload a replay file to ballchasing.com.
|
|
239
|
+
|
|
240
|
+
:param replay_file: replay file to upload.
|
|
241
|
+
:param visibility: to set the visibility of the uploaded replay.
|
|
242
|
+
:param group: to upload the replay to an existing group.
|
|
243
|
+
:return: the result of the POST request.
|
|
244
|
+
"""
|
|
245
|
+
return self._request(f"/v2/upload", "POST", files={"file": replay_file},
|
|
246
|
+
params={"group": group, "visibility": visibility}).json()
|
|
247
|
+
|
|
248
|
+
def delete_replay(self, replay_id: str) -> None:
|
|
249
|
+
"""
|
|
250
|
+
This endpoint deletes the specified replay.
|
|
251
|
+
WARNING: This operation is permanent and undoable.
|
|
252
|
+
|
|
253
|
+
:param replay_id: the replay id.
|
|
254
|
+
"""
|
|
255
|
+
self._request(f"/replays/{replay_id}", "DELETE")
|
|
256
|
+
|
|
257
|
+
def get_groups(self,
|
|
258
|
+
name: Optional[str] = None,
|
|
259
|
+
creator: Optional[str] = None,
|
|
260
|
+
group: Optional[str] = None,
|
|
261
|
+
created_before: Optional[Union[str, datetime]] = None,
|
|
262
|
+
created_after: Optional[Union[str, datetime]] = None,
|
|
263
|
+
count: int = 200,
|
|
264
|
+
sort_by: AnyGroupSortBy = GroupSortBy.CREATED,
|
|
265
|
+
sort_dir: AnySortDir = SortDir.DESCENDING
|
|
266
|
+
) -> Iterator[dict]:
|
|
267
|
+
"""
|
|
268
|
+
This endpoint lets you filter and retrieve replay groups.
|
|
269
|
+
|
|
270
|
+
:param name: filter groups by name
|
|
271
|
+
:param creator: only include groups created by the specified user.
|
|
272
|
+
Accepts either the numerical 76*************44 steam id, or the special value me
|
|
273
|
+
:param group: only include children of the specified group
|
|
274
|
+
:param created_before: only include groups created (uploaded) before some date.
|
|
275
|
+
RFC3339 format, e.g. 2020-01-02T15:00:05+01:00
|
|
276
|
+
:param created_after: only include groups created (uploaded) after some date.
|
|
277
|
+
RFC3339 format, e.g. 2020-01-02T15:00:05+01:00
|
|
278
|
+
:param count: returns at most count groups. Since the implementation uses an iterator it supports iterating
|
|
279
|
+
past the limit of 200 set by the API
|
|
280
|
+
:param sort_by: Sort groups according the selected field.
|
|
281
|
+
:param sort_dir: Sort direction.
|
|
282
|
+
:return: an iterator over the groups returned by the API.
|
|
283
|
+
"""
|
|
284
|
+
url = f"{self.base_url}/groups/"
|
|
285
|
+
params = {"name": name, "creator": creator, "group": group, "created-before": rfc3339(created_before),
|
|
286
|
+
"created-after": rfc3339(created_after), "sort-by": sort_by, "sort-dir": sort_dir}
|
|
287
|
+
|
|
288
|
+
left = count
|
|
289
|
+
while left > 0:
|
|
290
|
+
request_count = min(left, 200)
|
|
291
|
+
params["count"] = request_count
|
|
292
|
+
d = self._request(url, "GET", params=params).json()
|
|
293
|
+
|
|
294
|
+
batch = d["list"][:request_count]
|
|
295
|
+
yield from batch
|
|
296
|
+
|
|
297
|
+
if "next" not in d:
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
next_url = d["next"]
|
|
301
|
+
left -= len(batch)
|
|
302
|
+
params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
|
|
303
|
+
|
|
304
|
+
def create_group(self,
|
|
305
|
+
name: str,
|
|
306
|
+
player_identification: AnyPlayerIdentification,
|
|
307
|
+
team_identification: AnyTeamIdentification,
|
|
308
|
+
parent: Optional[str] = None
|
|
309
|
+
) -> dict:
|
|
310
|
+
"""
|
|
311
|
+
Use this API to create a new replay group.
|
|
312
|
+
|
|
313
|
+
:param name: the new group name.
|
|
314
|
+
:param player_identification: how to identify the same player across multiple replays.
|
|
315
|
+
Some tournaments (e.g. RLCS) make players use a pool of generic Steam accounts,
|
|
316
|
+
meaning the same player could end up using 2 different accounts in 2 series.
|
|
317
|
+
That's when the `by-name` comes in handy
|
|
318
|
+
:param team_identification: How to identify the same team across multiple replays.
|
|
319
|
+
Set to `by-distinct-players` if teams have a fixed roster of players for
|
|
320
|
+
every single game. In some tournaments/leagues, teams allow player rotations,
|
|
321
|
+
or a sub can replace another player, in which case use `by-player-clusters`.
|
|
322
|
+
:param parent: if set,the new group will be created as a child of the specified group
|
|
323
|
+
:return: the result of the POST request.
|
|
324
|
+
"""
|
|
325
|
+
json = {"name": name, "player_identification": player_identification,
|
|
326
|
+
"team_identification": team_identification, "parent": parent}
|
|
327
|
+
return self._request(f"/groups", "POST", json=json).json()
|
|
328
|
+
|
|
329
|
+
def get_group(self, group_id: str) -> dict:
|
|
330
|
+
"""
|
|
331
|
+
This endpoint retrieves a specific replay group info and stats given its id.
|
|
332
|
+
|
|
333
|
+
:param group_id: the group id.
|
|
334
|
+
:return: the group info with stats.
|
|
335
|
+
"""
|
|
336
|
+
return self._request(f"/groups/{group_id}", "GET").json()
|
|
337
|
+
|
|
338
|
+
def patch_group(self, group_id: str, **params) -> None:
|
|
339
|
+
"""
|
|
340
|
+
This endpoint can patch one or more fields of the specified group.
|
|
341
|
+
|
|
342
|
+
:param group_id: the group id
|
|
343
|
+
:param params: parameters for the PATCH request.
|
|
344
|
+
"""
|
|
345
|
+
self._request(f"/groups/{group_id}", "PATCH", json=params)
|
|
346
|
+
|
|
347
|
+
def delete_group(self, group_id: str) -> None:
|
|
348
|
+
"""
|
|
349
|
+
This endpoint deletes the specified group.
|
|
350
|
+
WARNING: This operation is permanent and undoable.
|
|
351
|
+
|
|
352
|
+
:param group_id: the group id.
|
|
353
|
+
"""
|
|
354
|
+
self._request(f"/groups/{group_id}", "DELETE")
|
|
355
|
+
|
|
356
|
+
def get_group_replays(self, group_id: str, deep: bool = False) -> Iterator[dict]:
|
|
357
|
+
"""
|
|
358
|
+
Finds all replays in a group, including child groups.
|
|
359
|
+
|
|
360
|
+
:param group_id: the base group id.
|
|
361
|
+
:param deep: whether or not to get full stats for each replay (will be much slower).
|
|
362
|
+
:return: an iterator over all the replays in the group.
|
|
363
|
+
"""
|
|
364
|
+
child_groups = self.get_groups(group=group_id)
|
|
365
|
+
for child in child_groups:
|
|
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):
|
|
369
|
+
yield replay
|
|
370
|
+
|
|
371
|
+
def download_replay(self, replay_id: str, folder: str):
|
|
372
|
+
"""
|
|
373
|
+
Download a replay file.
|
|
374
|
+
|
|
375
|
+
:param replay_id: the replay id.
|
|
376
|
+
:param folder: the folder to download into.
|
|
377
|
+
"""
|
|
378
|
+
r = self._request(f"/replays/{replay_id}/file", "GET")
|
|
379
|
+
with open(f"{folder}/{replay_id}.replay", "wb") as f:
|
|
380
|
+
for ch in r:
|
|
381
|
+
f.write(ch)
|
|
382
|
+
|
|
383
|
+
def download_group(self, group_id: str, folder: str, recursive=True):
|
|
384
|
+
"""
|
|
385
|
+
Download an entire group.
|
|
386
|
+
|
|
387
|
+
:param group_id: the base group id.
|
|
388
|
+
:param folder: the folder in which to create the group folder.
|
|
389
|
+
:param recursive: whether to create new folders for child groups.
|
|
390
|
+
"""
|
|
391
|
+
folder = os.path.join(folder, group_id)
|
|
392
|
+
if recursive:
|
|
393
|
+
os.makedirs(folder, exist_ok=True)
|
|
394
|
+
for child_group in self.get_groups(group=group_id):
|
|
395
|
+
self.download_group(child_group["id"], folder, True)
|
|
396
|
+
for replay in self.get_replays(group_id=group_id):
|
|
397
|
+
self.download_replay(replay["id"], folder)
|
|
398
|
+
else:
|
|
399
|
+
for replay in self.get_group_replays(group_id):
|
|
400
|
+
self.download_replay(replay["id"], folder)
|
|
401
|
+
|
|
402
|
+
def get_maps(self):
|
|
403
|
+
"""
|
|
404
|
+
Use this API to get the list of map codes to map names (map as in stadium).
|
|
405
|
+
"""
|
|
406
|
+
res = self._request("/maps", "GET").json()
|
|
407
|
+
return res
|
|
408
|
+
|
|
409
|
+
def get_stats(self, replay: Union[dict, str]):
|
|
410
|
+
"""
|
|
411
|
+
Gets stats for players, teams and replay info.
|
|
412
|
+
|
|
413
|
+
:param replay: the replay to get stats for. Can be a replay id (str) or a replay dict.
|
|
414
|
+
:return: a dictionary containing replay, team and player stats.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
if isinstance(replay, str):
|
|
418
|
+
replay = self.get_replay(replay)
|
|
419
|
+
elif isinstance(replay, dict) and "title" not in replay:
|
|
420
|
+
replay = self.get_replay(replay["id"])
|
|
421
|
+
|
|
422
|
+
stats = parse_replay_stats(replay)
|
|
423
|
+
return stats
|
|
424
|
+
|
|
425
|
+
def __repr__(self):
|
|
426
|
+
return f"BallchasingApi(key={self.auth_key},name={self.steam_name}," \
|
|
427
|
+
f"steam_id={self.steam_id},type={self.patron_type})"
|