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/api.py CHANGED
@@ -1,322 +1,427 @@
1
- import time
2
- from typing import Optional, Iterator
3
-
4
- from requests import sessions, Response
5
-
6
- DEFAULT_URL = "https://ballchasing.com/api"
7
-
8
-
9
- class Api:
10
- """
11
- Class for communication with ballchasing.com API (https://ballchasing.com/doc/api)
12
- """
13
- def __init__(self, auth_key: str, sleep_time_on_rate_limit: Optional[float] = None,
14
- print_on_rate_limit: bool = False, base_url=None, do_initial_ping=True):
15
- """
16
-
17
- :param auth_key: authentication key for API calls.
18
- :param sleep_time_on_rate_limit: seconds to wait after being rate limited.
19
- Default value is calculated depending on patron type.
20
- :param print_on_rate_limit: whether or not to print upon rate limits.
21
- """
22
- self.auth_key = auth_key
23
- self._session = sessions.Session()
24
- self.steam_name = None
25
- self.steam_id = None
26
- self.patron_type = None
27
- self.rate_limit_count = 0
28
- self.base_url = DEFAULT_URL if base_url is None else base_url
29
- if do_initial_ping:
30
- self.ping()
31
- if sleep_time_on_rate_limit is None:
32
- self.sleep_time_on_rate_limit = {
33
- "regular": 3600 / 1000,
34
- "gold": 3600 / 2000,
35
- "diamond": 3600 / 5000,
36
- "champion": 1 / 8,
37
- "gc": 1 / 16
38
- }.get(self.patron_type or "regular")
39
- else:
40
- self.sleep_time_on_rate_limit = sleep_time_on_rate_limit
41
- self.print_on_rate_limit = print_on_rate_limit
42
-
43
- def _request(self, url_or_endpoint: str, method: callable, **params) -> Response:
44
- """
45
- Helper method for all requests.
46
-
47
- :param url: url or endpoint for request.
48
- :param method: the method to use.
49
- :param params: parameters for GET request.
50
- :return: the request result.
51
- """
52
- headers = {"Authorization": self.auth_key}
53
- url = f"{self.base_url}{url_or_endpoint}" if url_or_endpoint.startswith("/") else url_or_endpoint
54
- while True:
55
- r = method(url, headers=headers, **params)
56
- if 200 <= r.status_code < 300:
57
- return r
58
- elif r.status_code == 429:
59
- if self.print_on_rate_limit:
60
- print(429, url, self.rate_limit_count)
61
- if self.sleep_time_on_rate_limit:
62
- time.sleep(self.sleep_time_on_rate_limit)
63
- self.rate_limit_count += 1
64
- elif r.status_code < 500:
65
- raise ValueError(r, r.json())
66
-
67
- def ping(self):
68
- """
69
- Use this API to:
70
-
71
- - check if your API key is correct
72
- - check if ballchasing API is reachable
73
-
74
- This method runs automatically at initialization and the steam name and id as well as patron type are stored.
75
- :return: ping response.
76
- """
77
- result = self._request("/", self._session.get).json()
78
- self.steam_name = result["name"]
79
- self.steam_id = result["steam_id"]
80
- self.patron_type = result["type"]
81
- return result
82
-
83
- def get_replays(self, title: Optional[str] = None, player_name: Optional[str] = None, player_id: Optional[str] = None,
84
- playlist: Optional[str] = None, season: Optional[int] = None, match_result: Optional[str] = None,
85
- min_rank: Optional[str] = None, max_rank: Optional[str] = None, pro: Optional[bool] = None,
86
- uploader: Optional[str] = None, group_id: Optional[str] = None,
87
- created_before: Optional[str] = None, created_after: Optional[str] = None,
88
- replay_after: Optional[str] = None, replay_before: Optional[str] = None, count: int = 200,
89
- sort_by: Optional[str] = None, sort_dir: str = "desc") -> Iterator[dict]:
90
- """
91
- This endpoint lets you filter and retrieve replays. The implementation returns an iterator.
92
-
93
- :param title: filter replays by title.
94
- :param player_name: filter replays by a player’s name.
95
- :param player_id: filter replays by a player’s platform id in the $platform:$id, e.g. steam:76561198141161044,
96
- ps4:gamertag, … You can filter replays by multiple player ids, e.g ?player-id=steam:1&player-id=steam:2
97
- :param playlist: filter replays by one or more playlists.
98
- :param season: filter replays by season.
99
- :param match_result: filter your replays by result.
100
- :param min_rank: filter your replays based on players minimum rank.
101
- :param max_rank: filter your replays based on players maximum rank.
102
- :param pro: only include replays containing at least one pro player.
103
- :param uploader: only include replays uploaded by the specified user. Accepts either the
104
- numerical 76*************44 steam id, or the special value 'me'
105
- :param group_id: only include replays belonging to the specified group. This only include replays immediately
106
- under the specified group, but not replays in child groups
107
- :param created_before: only include replays created (uploaded) before some date.
108
- RFC3339 format, e.g. '2020-01-02T15:00:05+01:00'
109
- :param created_after: only include replays created (uploaded) after some date.
110
- RFC3339 format, e.g. '2020-01-02T15:00:05+01:00'
111
- :param replay_after: only include replays for games that happened after some date.
112
- RFC3339 format, e.g. '2020-01-02T15:00:05+01:00'
113
- :param replay_before: only include replays for games that happened before some date.
114
- RFC3339 format, e.g. '2020-01-02T15:00:05+01:00'
115
- :param count: returns at most count replays. Since the implementation uses an iterator it supports iterating
116
- past the limit of 200 set by the API
117
- :param sort_by: sort replays according the selected field
118
- :param sort_dir: sort direction
119
- :return: an iterator over the replays returned by the API.
120
- """
121
- params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist, "season": season,
122
- "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank, "pro": pro,
123
- "uploader": uploader, "group": group_id, "created-before": created_before,
124
- "created-after": created_after, "replay-date-after": replay_after,
125
- "replay-date-before": replay_before,
126
- "sort-by": sort_by, "sort-dir": sort_dir}
127
- n = 0
128
- left = count
129
- while left > 0:
130
- request_count = min(left, 200)
131
- params["count"] = request_count
132
- d = self._request("/replays", self._session.get, params=params).json()
133
-
134
- for replay in d["list"][:request_count]:
135
- yield replay
136
- n += 1
137
- if "next" in d and n < count:
138
- url = d["next"]
139
- left -= request_count
140
- params = {}
141
- else:
142
- break
143
-
144
- def get_replay(self, replay_id: str) -> dict:
145
- """
146
- Retrieve a given replay’s details and stats.
147
-
148
- :param replay_id: the replay id.
149
- :return: the result of the GET request.
150
- """
151
- return self._request(f"/replays/{replay_id}", self._session.get).json()
152
-
153
- def patch_replay(self, replay_id: str, **params) -> None:
154
- """
155
- This endpoint can patch one or more fields of the specified replay
156
-
157
- :param replay_id: the replay id.
158
- :param params: parameters for the PATCH request.
159
- """
160
- self._request(f"/replays/{replay_id}", self._session.patch, json=params)
161
-
162
- def upload_replay(self, replay_file, visibility: Optional[str] = None) -> dict:
163
- """
164
- Use this API to upload a replay file to ballchasing.com.
165
-
166
- :param replay_file: replay file to upload.
167
- :param visibility: to set the visibility of the uploaded replay.
168
- :return: the result of the POST request.
169
- """
170
- return self._request(f"/v2/upload", self._session.post, files={"file": replay_file},
171
- params={"visibility": visibility}).json()
172
-
173
- def delete_replay(self, replay_id: str) -> None:
174
- """
175
- This endpoint deletes the specified replay.
176
- WARNING: This operation is permanent and undoable.
177
-
178
- :param replay_id: the replay id.
179
- """
180
- self._request(f"/replays/{replay_id}", self._session.delete)
181
-
182
- def get_groups(self, name: Optional[str] = None, creator: Optional[str] = None, group: Optional[str] = None,
183
- created_before: Optional[str] = None, created_after: Optional[str] = None, count: int = 200,
184
- sort_by: str = "created", sort_dir: str = "desc") -> Iterator[dict]:
185
- """
186
- This endpoint lets you filter and retrieve replay groups.
187
-
188
- :param name: filter groups by name
189
- :param creator: only include groups created by the specified user.
190
- Accepts either the numerical 76*************44 steam id, or the special value me
191
- :param group: only include children of the specified group
192
- :param created_before: only include groups created (uploaded) before some date.
193
- RFC3339 format, e.g. 2020-01-02T15:00:05+01:00
194
- :param created_after: only include groups created (uploaded) after some date.
195
- RFC3339 format, e.g. 2020-01-02T15:00:05+01:00
196
- :param count: returns at most count groups. Since the implementation uses an iterator it supports iterating
197
- past the limit of 200 set by the API
198
- :param sort_by: Sort groups according the selected field.
199
- :param sort_dir: Sort direction.
200
- :return: an iterator over the groups returned by the API.
201
- """
202
- url = f"{self.base_url}/groups/"
203
- params = {"name": name, "creator": creator, "group": group, "created-before": created_before,
204
- "created-after": created_after, "sort-by": sort_by, "sort-dir": sort_dir}
205
- n = 0
206
- left = count
207
- while left > 0:
208
- request_count = min(left, 200)
209
- params["count"] = request_count
210
- d = self._request(url, self._session.get, params=params).json()
211
-
212
- for group in d["list"][:request_count]:
213
- yield group
214
- n += 1
215
- if "next" in d and n < count:
216
- url = d["next"]
217
- left -= request_count
218
- params = {}
219
- else:
220
- break
221
-
222
- def create_group(self, name: str, player_identification: str, team_identification: str,
223
- parent: Optional[str] = None) -> dict:
224
- """
225
- Use this API to create a new replay group.
226
-
227
- :param name: the new group name.
228
- :param player_identification: how to identify the same player across multiple replays.
229
- Some tournaments (e.g. RLCS) make players use a pool of generic Steam accounts,
230
- meaning the same player could end up using 2 different accounts in 2 series.
231
- That's when the `by-name` comes in handy
232
- :param team_identification: How to identify the same team across multiple replays.
233
- Set to `by-distinct-players` if teams have a fixed roster of players for
234
- every single game. In some tournaments/leagues, teams allow player rotations,
235
- or a sub can replace another player, in which case use `by-player-clusters`.
236
- :param parent: if set,the new group will be created as a child of the specified group
237
- :return: the result of the POST request.
238
- """
239
- json = {"name": name, "player_identification": player_identification,
240
- "team_identification": team_identification, "parent": parent}
241
- return self._request(f"/groups", self._session.post, json=json).json()
242
-
243
- def get_group(self, group_id: str) -> dict:
244
- """
245
- This endpoint retrieves a specific replay group info and stats given its id.
246
-
247
- :param group_id: the group id.
248
- :return: the group info with stats.
249
- """
250
- return self._request(f"/groups/{group_id}", self._session.get).json()
251
-
252
- def patch_group(self, group_id: str, **params) -> None:
253
- """
254
- This endpoint can patch one or more fields of the specified group.
255
-
256
- :param group_id: the group id
257
- :param params: parameters for the PATCH request.
258
- """
259
- self._request(f"/groups/{group_id}", self._session.patch, json=params)
260
-
261
- def delete_group(self, group_id: str) -> None:
262
- """
263
- This endpoint deletes the specified group.
264
- WARNING: This operation is permanent and undoable.
265
-
266
- :param group_id: the group id.
267
- """
268
- self._request(f"/groups/{group_id}", self._session.delete)
269
-
270
- def get_group_replays(self, group_id: str) -> Iterator[dict]:
271
- """
272
- Finds all replays in a group, including child groups.
273
-
274
- :param group_id: the base group id.
275
- :return: an iterator over all the replays in the group.
276
- """
277
- child_groups = self.get_groups(group=group_id)
278
- for child in child_groups:
279
- for replay in self.get_group_replays(child["id"]):
280
- yield replay
281
- for replay in self.get_replays(group_id=group_id):
282
- yield replay
283
-
284
- def download_replay(self, replay_id: str, folder: str):
285
- """
286
- Download a replay file.
287
-
288
- :param replay_id: the replay id.
289
- :param folder: the folder to download into.
290
- """
291
- r = self._request(f"/replays/{replay_id}/file", self._session.get)
292
- with open(f"{folder}/{replay_id}.replay", "wb") as f:
293
- for ch in r:
294
- f.write(ch)
295
-
296
- def __str__(self):
297
- return f"BallchasingApi[key={self.auth_key},name={self.steam_name}," \
298
- f"steam_id={self.steam_id},type={self.patron_type}]"
299
-
300
-
301
- if __name__ == '__main__':
302
- # Basic initial tests
303
- import sys
304
- token = sys.argv[1]
305
- api = Api(token)
306
- print(api)
307
- # api.delete_replay("a22a8c81-fadd-4453-914e-ae54c2b8391f")
308
- upload_response = api.upload_replay(open("4E2B22344F748C6EB4922DB8CC8AC282.replay", "rb"))
309
- replays_response = api.get_replays()
310
- replay_response = api.get_replay(next(replays_response)["id"])
311
-
312
- groups_response = api.get_groups()
313
- group_response = api.get_group(next(groups_response)["id"])
314
-
315
- create_group_response = api.create_group(f"test-{time.time()}", "by-id", "by-distinct-players")
316
- api.patch_group(create_group_response["id"], team_identification="by-player-clusters")
317
-
318
- api.patch_replay(upload_response["id"], group=create_group_response["id"])
319
-
320
- api.delete_group(create_group_response["id"])
321
- api.delete_replay(upload_response["id"])
322
- print("Nice")
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})"