python-ballchasing 0.4.1__py3-none-any.whl → 0.5.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/api.py CHANGED
@@ -1,17 +1,20 @@
1
1
  import os
2
2
  import time
3
+ from concurrent.futures import ThreadPoolExecutor
3
4
  from datetime import datetime
4
5
  from pathlib import Path
5
- from typing import Optional, Union, List, BinaryIO, Iterator
6
+ from typing import BinaryIO, Iterator
6
7
  from urllib.parse import parse_qs, urlparse
7
8
 
8
- from requests import sessions, Response, ConnectionError, HTTPError
9
+ from requests import sessions, Response, ConnectionError
9
10
 
10
- from ballchasing.constants import GroupSortBy, SortDir, AnyPlaylist, AnyMap, AnySeason, AnyRank, AnyReplaySortBy, \
11
- AnySortDir, AnyVisibility, AnyGroupSortBy, AnyPlayerIdentification, AnyTeamIdentification, AnyMatchResult
11
+ from ballchasing.constants import GroupSortBy, SortDir, AnySeason, AnyRank, AnyReplaySortBy, AnySortDir, \
12
+ AnyVisibility, AnyGroupSortBy, AnyPlayerIdentification, AnyTeamIdentification, AnyMatchResult, NoneOrMore
12
13
  from ballchasing.typed import DeepReplay, ShallowReplay, DeepGroup, ShallowGroup
13
14
  from .typed.shared import BaseGroup, BasicGroup
14
- from .util import to_rfc3339, parse_replay_stats
15
+ from .util.dates import to_rfc3339
16
+ from .util.iterators import deduplicate as deduplicator
17
+ from .util.stats import parse_replay_stats
15
18
 
16
19
  DEFAULT_URL = "https://ballchasing.com/api"
17
20
 
@@ -25,11 +28,11 @@ class BallchasingApi:
25
28
  self,
26
29
  auth_key: str,
27
30
  *,
28
- sleep_time_on_rate_limit: Optional[float] = None,
31
+ sleep_time_on_rate_limit: float | None = None,
29
32
  print_on_rate_limit: bool = False,
30
- base_url=None,
31
- do_initial_ping=True,
32
- typed=False,
33
+ base_url: str | None = None,
34
+ do_initial_ping: bool = True,
35
+ typed: bool = False,
33
36
  ):
34
37
  """
35
38
 
@@ -117,6 +120,8 @@ class BallchasingApi:
117
120
  time.sleep(retry_after)
118
121
  elif self.sleep_time_on_rate_limit:
119
122
  time.sleep(self.sleep_time_on_rate_limit)
123
+ elif r.status_code == 504:
124
+ raise ConnectionError("Gateway Timeout. The server did not respond in time.")
120
125
  else:
121
126
  r.raise_for_status() # Raise an error for any other status code'
122
127
  except ConnectionError as e:
@@ -141,58 +146,69 @@ class BallchasingApi:
141
146
  self._ping_result = result
142
147
  return result
143
148
 
144
- def _iterable_from_request(self, url, params):
145
- # Shared by get_replays and get_groups
149
+ def _iterable_from_request(self, url, params, prefetch=True):
150
+ # Shared by get_replays and get_groups.
151
+ # When prefetch=True, the next page is requested in a background
152
+ # thread *before* yielding, so network I/O overlaps with the
153
+ # consumer processing items. When prefetch=False (e.g. deep mode
154
+ # where the consumer also makes API calls), the next request is
155
+ # submitted *after* yielding to avoid doubling the request rate.
146
156
  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
157
 
161
- batch = d["list"][:request_count]
162
- yield from batch
158
+ def fetch_page(p):
159
+ return self._request(url, "GET", params=p).json()
160
+
161
+ with ThreadPoolExecutor(max_workers=1) as executor:
162
+ params["count"] = min(remaining, 200)
163
+ future = executor.submit(fetch_page, dict(params))
163
164
 
164
- if "next" not in d:
165
- break
165
+ while remaining > 0:
166
+ d = future.result()
167
+ batch = d["list"][:min(remaining, 200)]
168
+ remaining -= len(batch)
166
169
 
167
- next_url = d["next"]
168
- remaining -= len(batch)
169
- params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
170
+ has_next = "next" in d and remaining > 0
171
+ if has_next:
172
+ next_url = d["next"]
173
+ params["after"] = parse_qs(urlparse(next_url).query)["after"][0]
174
+ params["count"] = min(remaining, 200)
175
+ if prefetch:
176
+ future = executor.submit(fetch_page, dict(params))
177
+
178
+ yield from batch
179
+
180
+ if not has_next:
181
+ break
182
+ if not prefetch:
183
+ future = executor.submit(fetch_page, dict(params))
170
184
 
171
185
  def get_replays(
172
186
  self,
173
187
  *,
174
- title: Optional[str] = None,
175
- player_name: Optional[Union[str, List[str]]] = None,
176
- player_id: Optional[Union[str, List[str]]] = None,
177
- playlist: Optional[Union[AnyPlaylist, List[AnyPlaylist]]] = None,
178
- season: Optional[Union[AnySeason, List[AnySeason]]] = None,
179
- match_result: Optional[Union[AnyMatchResult, List[AnyMatchResult]]] = None,
180
- min_rank: Optional[AnyRank] = None,
181
- max_rank: Optional[AnyRank] = None,
182
- pro: Optional[bool] = None,
183
- uploader: Optional[str] = None,
184
- group_id: Optional[Union[str, List[str]]] = None,
185
- map_id: Optional[Union[AnyMap, List[AnyMap]]] = None,
186
- created_before: Optional[Union[str, datetime]] = None,
187
- created_after: Optional[Union[str, datetime]] = None,
188
- replay_after: Optional[Union[str, datetime]] = None,
189
- replay_before: Optional[Union[str, datetime]] = None,
188
+ title: NoneOrMore[str] = None,
189
+ player_name: NoneOrMore[str] = None,
190
+ player_id: NoneOrMore[str] = None,
191
+ playlist: NoneOrMore[str] = None,
192
+ season: NoneOrMore[AnySeason] = None,
193
+ match_result: NoneOrMore[AnyMatchResult] = None,
194
+ min_rank: AnyRank | None = None,
195
+ max_rank: AnyRank | None = None,
196
+ pro: bool | None = None,
197
+ uploader: str | None = None,
198
+ group_id: NoneOrMore[str] = None,
199
+ map_id: NoneOrMore[str] = None,
200
+ created_before: str | datetime | None = None,
201
+ created_after: str | datetime | None = None,
202
+ replay_after: str | datetime | None = None,
203
+ replay_before: str | datetime | None = None,
190
204
  count: int = 150,
191
- sort_by: Optional[AnyReplaySortBy] = None,
205
+ sort_by: AnyReplaySortBy | None = None,
192
206
  sort_dir: AnySortDir = SortDir.DESCENDING,
193
207
  deep: bool = False,
194
- typed: Optional[bool] = None,
195
- ) -> Iterator[Union[dict, ShallowReplay, DeepReplay]]:
208
+ typed: bool | None = None,
209
+ deduplicate: bool = False,
210
+ disable_prefetch: bool | None = None,
211
+ ) -> Iterator[dict | ShallowReplay | DeepReplay]:
196
212
  """
197
213
  This endpoint lets you filter and retrieve replays. The implementation returns an iterator.
198
214
 
@@ -226,12 +242,15 @@ class BallchasingApi:
226
242
  :param sort_dir: sort direction
227
243
  :param deep: whether to get full stats for each replay (will be much slower).
228
244
  :param typed: whether to return a typed object (default is self.typed).
245
+ :param deduplicate: whether to deduplicate replays that seem to be the same game.
246
+ :param disable_prefetch: whether to disable prefetching replays.
229
247
  :return: an iterator over the replays returned by the API.
230
248
  """
231
249
  url = f"{self.base_url}/replays"
232
250
  params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist,
233
251
  "season": season, "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank,
234
- "pro": pro, "uploader": uploader, "group": group_id, "map": map_id,
252
+ "pro": str(pro).lower() if isinstance(pro, bool) else pro, "uploader": uploader, "group": group_id,
253
+ "map": map_id,
235
254
  "created-before": to_rfc3339(created_before), "created-after": to_rfc3339(created_after),
236
255
  "replay-date-after": to_rfc3339(replay_after), "replay-date-before": to_rfc3339(replay_before),
237
256
  "count": count, "sort-by": sort_by, "sort-dir": sort_dir}
@@ -239,14 +258,23 @@ class BallchasingApi:
239
258
  if typed is None:
240
259
  typed = self.typed
241
260
 
242
- iterator = self._iterable_from_request(url, params)
261
+ if disable_prefetch is None:
262
+ disable_prefetch = deep
263
+
264
+ iterator = self._iterable_from_request(url, params, prefetch=not disable_prefetch)
243
265
  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)
266
+ iterator = (self.get_replay(r["id"]) for r in iterator)
267
+ if deduplicate:
268
+ # Deep replays have match and replay IDs to deduplicate with. For shallow replays we check dates.
269
+ iterator = deduplicator(iterator, check_dates=not deep)
270
+ if typed:
271
+ if deep:
272
+ iterator = (DeepReplay(**r) for r in iterator)
273
+ else:
274
+ iterator = (ShallowReplay(**r) for r in iterator)
247
275
  yield from iterator
248
276
 
249
- def get_replay(self, replay_id: str, *, typed: Optional[bool] = None) -> Union[dict, DeepReplay]:
277
+ def get_replay(self, replay_id: str, *, typed: bool | None = None) -> dict | DeepReplay:
250
278
  """
251
279
  Retrieve a given replay’s details and stats.
252
280
 
@@ -272,10 +300,10 @@ class BallchasingApi:
272
300
 
273
301
  def upload_replay(
274
302
  self,
275
- replay_file: Union[str, Path, BinaryIO],
303
+ replay_file: str | Path | BinaryIO,
276
304
  *,
277
- visibility: Optional[AnyVisibility] = None,
278
- group: Optional[str] = None
305
+ visibility: AnyVisibility | None = None,
306
+ group: str | None = None
279
307
  ) -> dict:
280
308
  """
281
309
  Use this API to upload a replay file to ballchasing.com.
@@ -303,17 +331,18 @@ class BallchasingApi:
303
331
  def get_groups(
304
332
  self,
305
333
  *,
306
- name: Optional[str] = None,
307
- creator: Optional[str] = None,
308
- group: Optional[str] = None,
309
- created_before: Optional[Union[str, datetime]] = None,
310
- created_after: Optional[Union[str, datetime]] = None,
334
+ name: str | None = None,
335
+ creator: str | None = None,
336
+ group: str | None = None,
337
+ created_before: str | datetime | None = None,
338
+ created_after: str | datetime | None = None,
311
339
  count: int = 200,
312
340
  sort_by: AnyGroupSortBy = GroupSortBy.CREATED,
313
341
  sort_dir: AnySortDir = SortDir.DESCENDING,
314
342
  deep: bool = False,
315
- typed: bool = None,
316
- ) -> Iterator[Union[dict, ShallowGroup, DeepGroup]]:
343
+ typed: bool | None = None,
344
+ disable_prefetch: bool | None = None,
345
+ ) -> Iterator[dict | ShallowGroup | DeepGroup]:
317
346
  """
318
347
  This endpoint lets you filter and retrieve replay groups.
319
348
 
@@ -331,12 +360,17 @@ class BallchasingApi:
331
360
  :param sort_dir: Sort direction.
332
361
  :param deep: whether to get full stats for each group (will be much slower).
333
362
  :param typed: whether to return a typed object (default is self.typed).
363
+ :param disable_prefetch: whether to disable prefetching.
334
364
  :return: an iterator over the groups returned by the API.
335
365
  """
336
366
  url = f"{self.base_url}/groups/"
337
367
  params = {"name": name, "creator": creator, "group": group, "created-before": to_rfc3339(created_before),
338
368
  "created-after": to_rfc3339(created_after), "count": count, "sort-by": sort_by, "sort-dir": sort_dir}
339
- iterator = self._iterable_from_request(url, params)
369
+
370
+ if disable_prefetch is None:
371
+ disable_prefetch = deep
372
+
373
+ iterator = self._iterable_from_request(url, params, prefetch=not disable_prefetch)
340
374
  if typed is None:
341
375
  typed = self.typed
342
376
  if deep:
@@ -351,7 +385,7 @@ class BallchasingApi:
351
385
  name: str,
352
386
  player_identification: AnyPlayerIdentification,
353
387
  team_identification: AnyTeamIdentification,
354
- parent: Optional[str] = None
388
+ parent: str | None = None
355
389
  ) -> dict:
356
390
  """
357
391
  Use this API to create a new replay group.
@@ -376,8 +410,8 @@ class BallchasingApi:
376
410
  self,
377
411
  group_id: str,
378
412
  *,
379
- typed: Optional[bool] = None
380
- ) -> Union[dict, DeepGroup]:
413
+ typed: bool | None = None
414
+ ) -> dict | DeepGroup:
381
415
  """
382
416
  This endpoint retrieves a specific replay group info and stats given its id.
383
417
 
@@ -412,11 +446,11 @@ class BallchasingApi:
412
446
 
413
447
  def get_group_replays(
414
448
  self,
415
- group: Union[str, dict, BasicGroup],
449
+ group: str | dict | BasicGroup,
416
450
  *,
417
451
  deep: bool = False,
418
- typed: Optional[bool] = None
419
- ) -> Iterator[Union[dict, ShallowReplay, DeepReplay]]:
452
+ typed: bool | None = None
453
+ ) -> Iterator[dict | ShallowReplay | DeepReplay]:
420
454
  """
421
455
  Finds all replays in a group, including child groups.
422
456
 
@@ -425,35 +459,36 @@ class BallchasingApi:
425
459
  :param typed: whether to return a typed object (default is self.typed).
426
460
  :return: an iterator over all the replays in the group.
427
461
  """
428
- for path in self.get_group_tree(group, deep=deep, typed=typed):
429
- group, replay = path
462
+ for path, replay in self.get_group_tree(group, deep=deep, typed=typed):
430
463
  yield replay
431
464
 
432
465
  def get_group_tree(
433
466
  self,
434
- group: Union[str, dict, BaseGroup],
467
+ group: str | dict | BaseGroup,
435
468
  *,
436
469
  deep: bool = False,
437
- typed: Optional[bool] = None
438
- ):
470
+ typed: bool | None = None
471
+ ) -> Iterator[tuple[list[str], dict | ShallowReplay | DeepReplay]]:
439
472
  """
440
- Finds all replays in a group, and includes the groups leading up to the replays.
473
+ Finds all replays in a group, and includes the group path leading up to each replay.
474
+
441
475
  :param group: the group id or a group dict.
442
476
  :param deep: whether to get full stats for each replay and group (will be much slower).
443
477
  :param typed: whether to return a typed object (default is self.typed).
478
+ :return: an iterator of (path, replay) tuples, where path is a list of group ids.
444
479
  """
445
480
  if isinstance(group, str):
446
- group = self.get_group(group)
447
- if isinstance(group, BasicGroup):
481
+ group_id = group
482
+ elif isinstance(group, BasicGroup):
448
483
  group_id = group.id
449
484
  else:
450
485
  group_id = group["id"]
451
486
  child_groups = self.get_groups(group=group_id, typed=typed)
452
487
  for child in child_groups:
453
- for path in self.get_group_tree(child, deep=deep, typed=typed):
454
- yield group_id, *path
488
+ for path, replay in self.get_group_tree(child, deep=deep, typed=typed):
489
+ yield [group_id] + path, replay
455
490
  for replay in self.get_replays(group_id=group_id, deep=deep, typed=typed):
456
- yield group_id, replay
491
+ yield [group_id], replay
457
492
 
458
493
  def download_replay(self, replay_id: str, path: str):
459
494
  """
@@ -496,7 +531,7 @@ class BallchasingApi:
496
531
  res = self._request("/maps", "GET").json()
497
532
  return res
498
533
 
499
- def get_stats(self, replay: Union[dict, str]):
534
+ def get_stats(self, replay: dict | str):
500
535
  """
501
536
  Gets stats for players, teams and replay info.
502
537
 
ballchasing/constants.py CHANGED
@@ -1,22 +1,24 @@
1
- from typing import Literal, get_args, AnyStr, Union
2
-
3
- AnyPlaylist = Union[
4
- AnyStr,
5
- Literal[
6
- "unranked-duels", "unranked-doubles", "unranked-standard", "unranked-chaos",
7
- "private", "season", "offline", "local-lobby",
8
- "ranked-duels", "ranked-doubles", "ranked-solo-standard", "ranked-standard",
9
- "snowday", "rocketlabs", "hoops", "rumble", "tournament", "dropshot",
10
- "ranked-hoops", "ranked-rumble", "ranked-dropshot", "ranked-snowday",
11
- "dropshot-rumble", "heatseeker", "gridiron", "spooky-cube"
12
- ]
13
- ]
1
+ from typing import Literal, get_args, TypeAlias, TypeVar, Sequence
14
2
 
15
3
 
16
4
  def _get_literals(type_):
17
5
  return get_args(get_args(type_)[1])
18
6
 
19
7
 
8
+ T = TypeVar('T')
9
+ OneOrMore: TypeAlias = T | Sequence[T]
10
+ NoneOrMore: TypeAlias = OneOrMore[T] | None
11
+
12
+ AnyPlaylist: TypeAlias = str | Literal[
13
+ "unranked-duels", "unranked-doubles", "unranked-standard", "unranked-chaos",
14
+ "private", "season", "offline", "local-lobby",
15
+ "ranked-duels", "ranked-doubles", "ranked-solo-standard", "ranked-standard",
16
+ "snowday", "rocketlabs", "hoops", "rumble", "tournament", "dropshot",
17
+ "ranked-hoops", "ranked-rumble", "ranked-dropshot", "ranked-snowday",
18
+ "dropshot-rumble", "heatseeker", "gridiron", "spooky-cube"
19
+ ]
20
+
21
+
20
22
  class Playlist:
21
23
  ALL = (UNRANKED_DUELS, UNRANKED_DOUBLES, UNRANKED_STANDARD, UNRANKED_CHAOS, PRIVATE, SEASON, OFFLINE, LOCAL_LOBBY,
22
24
  RANKED_DUELS, RANKED_DOUBLES, RANKED_SOLO_STANDARD, RANKED_STANDARD, SNOWDAY, ROCKETLABS, HOOPS, RUMBLE,
@@ -31,20 +33,17 @@ class Playlist:
31
33
  MISC = (PRIVATE, SEASON, OFFLINE, LOCAL_LOBBY)
32
34
 
33
35
 
34
- AnyRank = Union[
35
- AnyStr,
36
- Literal[
37
- "unranked",
38
- "bronze-1", "bronze-2", "bronze-3",
39
- "silver-1", "silver-2", "silver-3",
40
- "gold-1", "gold-2", "gold-3",
41
- "platinum-1", "platinum-2", "platinum-3",
42
- "diamond-1", "diamond-2", "diamond-3",
43
- "champion-1", "champion-2", "champion-3",
44
- "grand-champion", # Legacy. Seems to be interchangeable with "grand-champion-1"
45
- "grand-champion-1", "grand-champion-2", "grand-champion-3",
46
- "supersonic-legend"
47
- ],
36
+ AnyRank: TypeAlias = str | Literal[
37
+ "unranked",
38
+ "bronze-1", "bronze-2", "bronze-3",
39
+ "silver-1", "silver-2", "silver-3",
40
+ "gold-1", "gold-2", "gold-3",
41
+ "platinum-1", "platinum-2", "platinum-3",
42
+ "diamond-1", "diamond-2", "diamond-3",
43
+ "champion-1", "champion-2", "champion-3",
44
+ "grand-champion", # Legacy. Seems to be interchangeable with "grand-champion-1"
45
+ "grand-champion-1", "grand-champion-2", "grand-champion-3",
46
+ "supersonic-legend"
48
47
  ]
49
48
 
50
49
 
@@ -70,13 +69,11 @@ class Rank:
70
69
  GRAND_CHAMPION = (GRAND_CHAMPION_1, GRAND_CHAMPION_2, GRAND_CHAMPION_3)
71
70
 
72
71
 
73
- AnySeason = Union[
74
- AnyStr,
75
- Literal[
76
- "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14",
77
- "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9",
78
- "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19"
79
- ]
72
+ AnySeason: TypeAlias = str | Literal[
73
+ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14",
74
+ "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9",
75
+ "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19",
76
+ "f20", "f21", "f22"
80
77
  ]
81
78
 
82
79
 
@@ -87,79 +84,81 @@ class Season:
87
84
  SEASON_13_LEGACY, SEASON_14_LEGACY,
88
85
  SEASON_1_FTP, SEASON_2_FTP, SEASON_3_FTP, SEASON_4_FTP, SEASON_5_FTP, SEASON_6_FTP, SEASON_7_FTP,
89
86
  SEASON_8_FTP, SEASON_9_FTP, SEASON_10_FTP, SEASON_11_FTP, SEASON_12_FTP, SEASON_13_FTP, SEASON_14_FTP,
90
- SEASON_15_FTP, SEASON_16_FTP, SEASON_17_FTP, SEASON_18_FTP, SEASON_19_FTP
87
+ SEASON_15_FTP, SEASON_16_FTP, SEASON_17_FTP, SEASON_18_FTP, SEASON_19_FTP, SEASON_20_FTP, SEASON_21_FTP,
88
+ SEASON_22_FTP
91
89
  ) = _get_literals(AnySeason)
92
90
  LEGACY = ALL[:14]
93
91
  FREE_TO_PLAY = ALL[14:]
94
92
 
95
93
 
96
- AnyMatchResult = Union[AnyStr, Literal["win", "loss"]]
94
+ AnyMatchResult: TypeAlias = str | Literal["win", "loss"]
97
95
 
98
96
 
99
97
  class MatchResult:
100
98
  WIN, LOSS = _get_literals(AnyMatchResult)
101
99
 
102
100
 
103
- AnyReplaySortBy = Union[AnyStr, Literal["replay-date", "upload-date"]]
101
+ AnyReplaySortBy: TypeAlias = str | Literal["replay-date", "upload-date"]
104
102
 
105
103
 
106
104
  class ReplaySortBy:
107
105
  REPLAY_DATE, UPLOAD_DATE = _get_literals(AnyReplaySortBy)
108
106
 
109
107
 
110
- AnyGroupSortBy = Union[AnyStr, Literal["created", "name"]]
108
+ AnyGroupSortBy: TypeAlias = str | Literal["created", "name"]
111
109
 
112
110
 
113
111
  class GroupSortBy:
114
112
  CREATED, NAME = _get_literals(AnyGroupSortBy)
115
113
 
116
114
 
117
- AnySortDir = Union[AnyStr, Literal["asc", "desc"]]
115
+ AnySortDir: TypeAlias = str | Literal["asc", "desc"]
118
116
 
119
117
 
120
118
  class SortDir:
121
119
  ASCENDING, DESCENDING = ASC, DESC = _get_literals(AnySortDir)
122
120
 
123
121
 
124
- AnyVisibility = Union[AnyStr, Literal["public", "unlisted", "private"]]
122
+ AnyVisibility: TypeAlias = str | Literal["public", "unlisted", "private"]
125
123
 
126
124
 
127
125
  class Visibility:
128
126
  PUBLIC, UNLISTED, PRIVATE = _get_literals(AnyVisibility)
129
127
 
130
128
 
131
- AnyPlayerIdentification = Union[AnyStr, Literal["by-id", "by-name"]]
129
+ AnyPlayerIdentification: TypeAlias = str | Literal["by-id", "by-name"]
132
130
 
133
131
 
134
132
  class PlayerIdentification:
135
133
  BY_ID, BY_NAME = _get_literals(AnyPlayerIdentification)
136
134
 
137
135
 
138
- AnyTeamIdentification = Union[AnyStr, Literal["by-distinct-players", "by-player-clusters"]]
136
+ AnyTeamIdentification: TypeAlias = str | Literal["by-distinct-players", "by-player-clusters"]
139
137
 
140
138
 
141
139
  class TeamIdentification:
142
140
  BY_DISTINCT_PLAYERS, BY_PLAYER_CLUSTERS = _get_literals(AnyTeamIdentification)
143
141
 
144
142
 
145
- AnyMap = Union[AnyStr, Literal[
143
+ AnyMap: TypeAlias = str | Literal[
146
144
  "arc_darc_p", "arc_p", "arc_standard_p", "bb_p", "beach_night_grs_p", "beach_night_p", "beach_p", "beachvolley",
147
145
  "chn_stadium_day_p", "chn_stadium_p", "cs_day_p", "cs_hw_p", "cs_p", "eurostadium_dusk_p", "eurostadium_night_p",
148
146
  "eurostadium_p", "eurostadium_rainy_p", "eurostadium_snownight_p", "farm_grs_p", "farm_hw_p", "farm_night_p",
149
147
  "farm_p", "farm_upsidedown_p", "ff_dusk_p", "fni_stadium_p", "haunted_trainstation_p", "hoopsstadium_p",
150
- "hoopsstreet_p", "ko_calavera_p", "ko_carbon_p", "ko_quadron_p", "labs_basin_p", "labs_circlepillars_p",
148
+ "hoopsstreet_p", "ko_calavera_p", "ko_carbon_p", "ko_quadron_p", "labs_4v4_arena15_blackout_p",
149
+ "labs_4v4_arena15_eurostadium_night_p", "labs_4v4_arena15_retro_p", "labs_basin_p", "labs_circlepillars_p",
151
150
  "labs_corridor_p", "labs_cosmic_p", "labs_cosmic_v4_p", "labs_doublegoal_p", "labs_doublegoal_v2_p",
152
151
  "labs_galleon_mast_p", "labs_galleon_p", "labs_holyfield_p", "labs_holyfield_space_p", "labs_octagon_02_p",
153
- "labs_octagon_p", "labs_pillarglass_p", "labs_pillarheat_p", "labs_pillarwings_p", "labs_underpass_p",
154
- "labs_underpass_v0_p", "labs_utopia_p", "music_p", "neotokyo_arcade_p", "neotokyo_hax_p", "neotokyo_p",
155
- "neotokyo_standard_p", "neotokyo_toon_p", "outlaw_oasis_p", "outlaw_p", "park_bman_p", "park_night_p", "park_p",
156
- "park_rainy_p", "park_snowy_p", "shattershot_p", "stadium_10a_p", "stadium_day_p", "stadium_foggy_p", "stadium_p",
157
- "stadium_race_day_p", "stadium_winter_p", "street_p", "swoosh_p", "throwbackhockey_p", "throwbackstadium_p",
158
- "trainstation_dawn_p", "trainstation_night_p", "trainstation_p", "trainstation_spooky_p", "uf_day_p",
159
- "underwater_grs_p", "underwater_p", "utopiastadium_dusk_p", "utopiastadium_lux_p", "utopiastadium_p",
160
- "utopiastadium_snow_p", "wasteland_grs_p", "wasteland_night_p", "wasteland_night_s_p", "wasteland_p",
161
- "wasteland_s_p", "woods_night_p", "woods_p"
162
- ]]
152
+ "labs_octagon_b2b_02_p", "labs_octagon_p", "labs_pillarglass_p", "labs_pillarheat_p", "labs_pillarwings_p",
153
+ "labs_underpass_p", "labs_underpass_v0_p", "labs_utopia_p", "mall_day_p", "music_p", "neotokyo_arcade_p",
154
+ "neotokyo_hax_p", "neotokyo_p", "neotokyo_standard_p", "neotokyo_toon_p", "outlaw_oasis_p", "outlaw_p",
155
+ "paname_dusk_p", "park_bman_p", "park_night_p", "park_p", "park_rainy_p", "park_snowy_p", "shattershot_p",
156
+ "stadium_10a_p", "stadium_day_p", "stadium_foggy_p", "stadium_p", "stadium_race_day_p", "stadium_winter_p",
157
+ "street_p", "swoosh_p", "throwbackhockey_p", "throwbackstadium_p", "trainstation_dawn_p", "trainstation_night_p",
158
+ "trainstation_p", "trainstation_spooky_p", "uf_day_p", "underwater_grs_p", "underwater_p", "utopiastadium_dusk_p",
159
+ "utopiastadium_lux_p", "utopiastadium_p", "utopiastadium_snow_p", "wasteland_grs_p", "wasteland_night_p",
160
+ "wasteland_night_s_p", "wasteland_p", "wasteland_s_p", "woods_night_p", "woods_p"
161
+ ]
163
162
 
164
163
 
165
164
  class Map:
@@ -168,17 +167,18 @@ class Map:
168
167
  CHN_STADIUM_DAY_P, CHN_STADIUM_P, CS_DAY_P, CS_HW_P, CS_P, EUROSTADIUM_DUSK_P, EUROSTADIUM_NIGHT_P,
169
168
  EUROSTADIUM_P, EUROSTADIUM_RAINY_P, EUROSTADIUM_SNOWNIGHT_P, FARM_GRS_P, FARM_HW_P, FARM_NIGHT_P, FARM_P,
170
169
  FARM_UPSIDEDOWN_P, FF_DUSK_P, FNI_STADIUM_P, HAUNTED_TRAINSTATION_P, HOOPSSTADIUM_P, HOOPSSTREET_P,
171
- KO_CALAVERA_P, KO_CARBON_P, KO_QUADRON_P, LABS_BASIN_P, LABS_CIRCLEPILLARS_P, LABS_CORRIDOR_P, LABS_COSMIC_P,
172
- LABS_COSMIC_V4_P, LABS_DOUBLEGOAL_P, LABS_DOUBLEGOAL_V2_P, LABS_GALLEON_MAST_P, LABS_GALLEON_P,
173
- LABS_HOLYFIELD_P, LABS_HOLYFIELD_SPACE_P, LABS_OCTAGON_02_P, LABS_OCTAGON_P, LABS_PILLARGLASS_P,
174
- LABS_PILLARHEAT_P, LABS_PILLARWINGS_P, LABS_UNDERPASS_P, LABS_UNDERPASS_V0_P, LABS_UTOPIA_P, MUSIC_P,
175
- NEOTOKYO_ARCADE_P, NEOTOKYO_HAX_P, NEOTOKYO_P, NEOTOKYO_STANDARD_P, NEOTOKYO_TOON_P, OUTLAW_OASIS_P, OUTLAW_P,
176
- PARK_BMAN_P, PARK_NIGHT_P, PARK_P, PARK_RAINY_P, PARK_SNOWY_P, SHATTERSHOT_P, STADIUM_10A_P, STADIUM_DAY_P,
177
- STADIUM_FOGGY_P, STADIUM_P, STADIUM_RACE_DAY_P, STADIUM_WINTER_P, STREET_P, SWOOSH_P, THROWBACKHOCKEY_P,
178
- THROWBACKSTADIUM_P, TRAINSTATION_DAWN_P, TRAINSTATION_NIGHT_P, TRAINSTATION_P, TRAINSTATION_SPOOKY_P, UF_DAY_P,
179
- UNDERWATER_GRS_P, UNDERWATER_P, UTOPIASTADIUM_DUSK_P, UTOPIASTADIUM_LUX_P, UTOPIASTADIUM_P,
180
- UTOPIASTADIUM_SNOW_P, WASTELAND_GRS_P, WASTELAND_NIGHT_P, WASTELAND_NIGHT_S_P, WASTELAND_P, WASTELAND_S_P,
181
- WOODS_NIGHT_P, WOODS_P
170
+ KO_CALAVERA_P, KO_CARBON_P, KO_QUADRON_P, LABS_4V4_ARENA15_BLACKOUT_P, LABS_4V4_ARENA15_EUROSTADIUM_NIGHT_P,
171
+ LABS_4V4_ARENA15_RETRO_P, LABS_BASIN_P, LABS_CIRCLEPILLARS_P, LABS_CORRIDOR_P, LABS_COSMIC_P, LABS_COSMIC_V4_P,
172
+ LABS_DOUBLEGOAL_P, LABS_DOUBLEGOAL_V2_P, LABS_GALLEON_MAST_P, LABS_GALLEON_P, LABS_HOLYFIELD_P,
173
+ LABS_HOLYFIELD_SPACE_P, LABS_OCTAGON_02_P, LABS_OCTAGON_B2B_02_P, LABS_OCTAGON_P, LABS_PILLARGLASS_P,
174
+ LABS_PILLARHEAT_P, LABS_PILLARWINGS_P, LABS_UNDERPASS_P, LABS_UNDERPASS_V0_P, LABS_UTOPIA_P, MALL_DAY_P,
175
+ MUSIC_P, NEOTOKYO_ARCADE_P, NEOTOKYO_HAX_P, NEOTOKYO_P, NEOTOKYO_STANDARD_P, NEOTOKYO_TOON_P, OUTLAW_OASIS_P,
176
+ OUTLAW_P, PANAME_DUSK_P, PARK_BMAN_P, PARK_NIGHT_P, PARK_P, PARK_RAINY_P, PARK_SNOWY_P, SHATTERSHOT_P,
177
+ STADIUM_10A_P, STADIUM_DAY_P, STADIUM_FOGGY_P, STADIUM_P, STADIUM_RACE_DAY_P, STADIUM_WINTER_P, STREET_P,
178
+ SWOOSH_P, THROWBACKHOCKEY_P, THROWBACKSTADIUM_P, TRAINSTATION_DAWN_P, TRAINSTATION_NIGHT_P, TRAINSTATION_P,
179
+ TRAINSTATION_SPOOKY_P, UF_DAY_P, UNDERWATER_GRS_P, UNDERWATER_P, UTOPIASTADIUM_DUSK_P, UTOPIASTADIUM_LUX_P,
180
+ UTOPIASTADIUM_P, UTOPIASTADIUM_SNOW_P, WASTELAND_GRS_P, WASTELAND_NIGHT_P, WASTELAND_NIGHT_S_P, WASTELAND_P,
181
+ WASTELAND_S_P, WOODS_NIGHT_P, WOODS_P
182
182
  ) = _get_literals(AnyMap)
183
183
  NAMES = (
184
184
  "Starbase ARC (Aftermath)", "Starbase ARC", "Starbase ARC (Standard)", "Champions Field (NFL)",
@@ -187,12 +187,13 @@ class Map:
187
187
  "Mannfield (Dusk)", "Mannfield (Night)", "Mannfield", "Mannfield (Stormy)", "Mannfield (Snowy)",
188
188
  "Farmstead (Pitched)", "Farmstead (Spooky)", "Farmstead (Night)", "Farmstead", "Farmstead (The Upside Down)",
189
189
  "Estadio Vida (Dusk)", "Forbidden Temple (Fire & Ice)", "Urban Central (Haunted)", "Dunk House",
190
- "The Block (Dusk)", "Calavera", "Carbon", "Quadron", "Basin", "Pillars", "Corridor", "Cosmic", "Cosmic",
191
- "Double Goal", "Double Goal", "Galleon Retro", "Galleon", "Loophole", "Holyfield", "Octagon", "Octagon",
192
- "Hourglass", "Barricade", "Colossus", "Underpass", "Underpass", "Utopia Retro", "Neon Fields",
190
+ "The Block (Dusk)", "Calavera", "Carbon", "Quadron", "Midnight Metro (Quads)", "Mannfield (Quads)",
191
+ "Sunset Dunes (Quads)", "Basin", "Pillars", "Corridor", "Cosmic", "Cosmic", "Double Goal", "Double Goal",
192
+ "Galleon Retro", "Galleon", "Loophole", "Holyfield", "Octagon", "Roadblock", "Octagon", "Hourglass",
193
+ "Barricade", "Colossus", "Underpass", "Underpass", "Utopia Retro", "Boostfield Mall", "Neon Fields",
193
194
  "Neo Tokyo (Arcade)", "Neo Tokyo (Hacked)", "Neo Tokyo", "Neo Tokyo (Standard)", "Neo Tokyo (Comic)",
194
- "Deadeye Canyon (Oasis)", "Deadeye Canyon", "Beckwith Park (Night)", "Beckwith Park (Midnight)",
195
- "Beckwith Park", "Beckwith Park (Stormy)", "Beckwith Park (Snowy)", "Core 707",
195
+ "Deadeye Canyon (Oasis)", "Deadeye Canyon", "Parc De Paris", "Beckwith Park (Night)",
196
+ "Beckwith Park (Midnight)", "Beckwith Park", "Beckwith Park (Stormy)", "Beckwith Park (Snowy)", "Core 707",
196
197
  "DFH Stadium (10th Anniversary)", "DFH Stadium (Day)", "DFH Stadium (Stormy)", "DFH Stadium",
197
198
  "DFH Stadium (Circuit)", "DFH Stadium (Snowy)", "Sovereign Heights (Dusk)", "Champions Field (Nike FC)",
198
199
  "Throwback Stadium (Snowy)", "Throwback Stadium", "Urban Central (Dawn)", "Urban Central (Night)",
@@ -205,19 +206,21 @@ class Map:
205
206
  STANDARD_MAPS = (
206
207
  ARC_DARC_P, ARC_STANDARD_P, BEACH_NIGHT_GRS_P, BEACH_NIGHT_P, BEACH_P, CHN_STADIUM_DAY_P, CHN_STADIUM_P,
207
208
  CS_DAY_P, CS_HW_P, CS_P, EUROSTADIUM_DUSK_P, EUROSTADIUM_NIGHT_P, EUROSTADIUM_P, EUROSTADIUM_RAINY_P,
208
- EUROSTADIUM_SNOWNIGHT_P, FARM_GRS_P, FARM_NIGHT_P, FARM_P, FARM_UPSIDEDOWN_P, FF_DUSK_P, FNI_STADIUM_P, MUSIC_P,
209
- NEOTOKYO_ARCADE_P, NEOTOKYO_HAX_P, NEOTOKYO_STANDARD_P, OUTLAW_OASIS_P, OUTLAW_P, PARK_NIGHT_P, PARK_P,
210
- PARK_RAINY_P, PARK_SNOWY_P, STADIUM_10A_P, STADIUM_DAY_P, STADIUM_FOGGY_P, STADIUM_P, STADIUM_RACE_DAY_P,
211
- STADIUM_WINTER_P, STREET_P, TRAINSTATION_DAWN_P, TRAINSTATION_NIGHT_P, TRAINSTATION_P, UF_DAY_P,
212
- UNDERWATER_GRS_P, UNDERWATER_P, UTOPIASTADIUM_DUSK_P, UTOPIASTADIUM_LUX_P, UTOPIASTADIUM_P,
213
- UTOPIASTADIUM_SNOW_P, WASTELAND_GRS_P, WASTELAND_NIGHT_P, WASTELAND_NIGHT_S_P, WASTELAND_P, WASTELAND_S_P,
214
- WOODS_NIGHT_P, WOODS_P
209
+ EUROSTADIUM_SNOWNIGHT_P, FARM_GRS_P, FARM_NIGHT_P, FARM_P, FARM_UPSIDEDOWN_P, FF_DUSK_P, FNI_STADIUM_P,
210
+ MALL_DAY_P, MUSIC_P, NEOTOKYO_ARCADE_P, NEOTOKYO_HAX_P, NEOTOKYO_STANDARD_P, OUTLAW_OASIS_P, OUTLAW_P,
211
+ PANAME_DUSK_P, PARK_NIGHT_P, PARK_P, PARK_RAINY_P, PARK_SNOWY_P, STADIUM_10A_P, STADIUM_DAY_P, STADIUM_FOGGY_P,
212
+ STADIUM_P, STADIUM_RACE_DAY_P, STADIUM_WINTER_P, STREET_P, TRAINSTATION_DAWN_P, TRAINSTATION_NIGHT_P,
213
+ TRAINSTATION_P, UF_DAY_P, UNDERWATER_GRS_P, UNDERWATER_P, UTOPIASTADIUM_DUSK_P, UTOPIASTADIUM_LUX_P,
214
+ UTOPIASTADIUM_P, UTOPIASTADIUM_SNOW_P, WASTELAND_GRS_P, WASTELAND_NIGHT_P, WASTELAND_NIGHT_S_P, WASTELAND_P,
215
+ WASTELAND_S_P, WOODS_NIGHT_P, WOODS_P
215
216
  )
216
217
  NON_STANDARD_MAPS = (
217
218
  ARC_P, BB_P, BEACHVOLLEY, FARM_HW_P, HAUNTED_TRAINSTATION_P, HOOPSSTADIUM_P, HOOPSSTREET_P, KO_CALAVERA_P,
218
- KO_CARBON_P, KO_QUADRON_P, LABS_BASIN_P, LABS_CIRCLEPILLARS_P, LABS_CORRIDOR_P, LABS_COSMIC_P, LABS_COSMIC_V4_P,
219
+ KO_CARBON_P, KO_QUADRON_P, LABS_4V4_ARENA15_BLACKOUT_P, LABS_4V4_ARENA15_EUROSTADIUM_NIGHT_P,
220
+ LABS_4V4_ARENA15_RETRO_P, LABS_BASIN_P, LABS_CIRCLEPILLARS_P, LABS_CORRIDOR_P, LABS_COSMIC_P, LABS_COSMIC_V4_P,
219
221
  LABS_DOUBLEGOAL_P, LABS_DOUBLEGOAL_V2_P, LABS_GALLEON_MAST_P, LABS_GALLEON_P, LABS_HOLYFIELD_P,
220
- LABS_HOLYFIELD_SPACE_P, LABS_OCTAGON_02_P, LABS_OCTAGON_P, LABS_PILLARGLASS_P, LABS_PILLARHEAT_P,
221
- LABS_PILLARWINGS_P, LABS_UNDERPASS_P, LABS_UNDERPASS_V0_P, LABS_UTOPIA_P, NEOTOKYO_P, NEOTOKYO_TOON_P,
222
- PARK_BMAN_P, SHATTERSHOT_P, SWOOSH_P, THROWBACKHOCKEY_P, THROWBACKSTADIUM_P, TRAINSTATION_SPOOKY_P
222
+ LABS_HOLYFIELD_SPACE_P, LABS_OCTAGON_02_P, LABS_OCTAGON_B2B_02_P, LABS_OCTAGON_P, LABS_PILLARGLASS_P,
223
+ LABS_PILLARHEAT_P, LABS_PILLARWINGS_P, LABS_UNDERPASS_P, LABS_UNDERPASS_V0_P, LABS_UTOPIA_P, NEOTOKYO_P,
224
+ NEOTOKYO_TOON_P, PARK_BMAN_P, SHATTERSHOT_P, SWOOSH_P, THROWBACKHOCKEY_P, THROWBACKSTADIUM_P,
225
+ TRAINSTATION_SPOOKY_P
223
226
  )