infinitode.py 1.0.4__tar.gz → 1.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/PKG-INFO +5 -2
  2. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/README.md +4 -1
  3. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode/__init__.py +2 -2
  4. infinitode.py-1.0.5/infinitode/badge.py +29 -0
  5. infinitode.py-1.0.5/infinitode/core.py +463 -0
  6. infinitode.py-1.0.5/infinitode/errors.py +25 -0
  7. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode/leaderboard.py +61 -54
  8. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode/player.py +79 -41
  9. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode/score.py +46 -41
  10. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode/utils.py +2 -5
  11. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode.py.egg-info/PKG-INFO +5 -2
  12. infinitode.py-1.0.4/infinitode/badge.py +0 -31
  13. infinitode.py-1.0.4/infinitode/core.py +0 -315
  14. infinitode.py-1.0.4/infinitode/errors.py +0 -22
  15. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/LICENSE +0 -0
  16. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode/py.typed +0 -0
  17. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode.py.egg-info/SOURCES.txt +0 -0
  18. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode.py.egg-info/dependency_links.txt +0 -0
  19. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode.py.egg-info/requires.txt +0 -0
  20. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/infinitode.py.egg-info/top_level.txt +0 -0
  21. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/setup.cfg +0 -0
  22. {infinitode.py-1.0.4 → infinitode.py-1.0.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: infinitode.py
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: A python wrapper for the Infinitode 2 API.
5
5
  Home-page: https://github.com/Sprylos/infinitode.py
6
6
  Author: Sprylos
@@ -116,7 +116,10 @@ async def main():
116
116
  player.nickname,
117
117
  player.level,
118
118
  player.xp,
119
- player.xp_max, # xp required per level (always 1000?)
119
+ player.xp_max, # xp required per level
120
+ player.season_level,
121
+ player.season_xp,
122
+ player.season_xp_max,
120
123
  player.total_score,
121
124
  player.total_rank,
122
125
  player.total_top,
@@ -105,7 +105,10 @@ async def main():
105
105
  player.nickname,
106
106
  player.level,
107
107
  player.xp,
108
- player.xp_max, # xp required per level (always 1000?)
108
+ player.xp_max, # xp required per level
109
+ player.season_level,
110
+ player.season_xp,
111
+ player.season_xp_max,
109
112
  player.total_score,
110
113
  player.total_rank,
111
114
  player.total_top,
@@ -7,10 +7,10 @@ An asynchronous wrapper for the Infinitode API.
7
7
 
8
8
  __author__ = "Sprylos"
9
9
  __title__ = "infinitode.py"
10
- __version__ = '1.0.4'
10
+ __version__ = "1.0.5"
11
11
  __license__ = """MIT License
12
12
 
13
- Copyright (c) 2022 Sprylos
13
+ Copyright (c) 2023 Sprylos
14
14
 
15
15
  Permission is hereby granted, free of charge, to any person obtaining a copy
16
16
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ("Badge",)
4
+
5
+
6
+ class Badge:
7
+ """Represents an in-game pinned badge."""
8
+
9
+ __slots__ = ("icon_img", "icon_color", "overlay_img", "overlay_color")
10
+
11
+ def __init__(
12
+ self, iconImg: str, iconColor: str, overlayImg: str, overlayColor: str
13
+ ) -> None:
14
+ self.icon_img = iconImg
15
+ self.icon_color = iconColor
16
+ self.overlay_img = overlayImg
17
+ self.overlay_color = overlayColor
18
+
19
+ # magic methods
20
+
21
+ def __repr__(self) -> str:
22
+ attrs = {
23
+ "icon_img": self.icon_img,
24
+ "icon_color": self.icon_color,
25
+ "overlay_img": self.overlay_img,
26
+ "overlay_color": self.overlay_color,
27
+ }
28
+ inner = " ".join(f"{k}={v}" for k, v in attrs.items())
29
+ return f"<{self.__class__.__name__} {inner}>"
@@ -0,0 +1,463 @@
1
+ from __future__ import annotations
2
+
3
+ # std
4
+ import re
5
+ import time
6
+ import asyncio
7
+ import logging
8
+ import datetime
9
+ from typing import (
10
+ Any,
11
+ Dict,
12
+ Optional,
13
+ Union,
14
+ )
15
+
16
+ # packages
17
+ import aiohttp
18
+ import bs4
19
+
20
+ # local
21
+ from .errors import APIError, BadArgument
22
+ from .leaderboard import Leaderboard
23
+ from .player import Player
24
+ from .utils import try_int
25
+ from .score import Score
26
+
27
+
28
+ __all__ = ("Session",)
29
+
30
+ _log = logging.getLogger(__name__)
31
+
32
+ ID_REGEX = re.compile(r"U-([A-Z0-9]{4}-){2}[A-Z0-9]{6}")
33
+
34
+ # fmt: off
35
+ LEVELS = (
36
+ '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.b1',
37
+ '2.1', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7', '2.8', '2.b1',
38
+ '3.1', '3.2', '3.3', '3.4', '3.5', '3.6', '3.7', '3.8', '3.b1',
39
+ '4.1', '4.2', '4.3', '4.4', '4.5', '4.6', '4.7', '4.8', '4.b1',
40
+ '5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7', '5.8', '5.b1', '5.b2',
41
+ '6.1', '6.2', '6.3', '6.4', '6.5', 'rumble', 'dev', 'zecred',
42
+ 'DQ1', 'DQ3', 'DQ4', 'DQ5', 'DQ7', 'DQ8', 'DQ9', 'DQ10', 'DQ11', 'DQ12',
43
+ )
44
+ # fmt: on
45
+
46
+
47
+ class Session:
48
+ def __init__(self, session: Optional[aiohttp.ClientSession] = None) -> None:
49
+ self._session = session or aiohttp.ClientSession()
50
+ self._cooldown: Dict[str, Any] = {
51
+ "DailyQuestInfo": 0.0,
52
+ "LatestNews": 0.0,
53
+ "DailyQuestLeaderboards": {},
54
+ "SkillPointLeaderboard": 0.0,
55
+ "BasicLevelsTopLeaderboards": {},
56
+ "Leaderboards": {},
57
+ "seasonal": 0.0,
58
+ }
59
+ self._DailyQuestLeaderboards: Dict[str, Leaderboard] = {}
60
+ self._BasicLevelsTopLeaderboards: Dict[str, Leaderboard] = {}
61
+ self._Leaderboards: Dict[str, Leaderboard] = {}
62
+ self._SkillPointLeaderboard: Leaderboard
63
+ self._seasonal: Leaderboard
64
+
65
+ # async enter and exit allow for the fancy "with" statements
66
+ # useful so you don't have to close the session yourself
67
+ async def __aenter__(self):
68
+ return self
69
+
70
+ async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
71
+ await self.close()
72
+
73
+ async def close(self):
74
+ """Closes the internal ClientSession."""
75
+ await self._session.close()
76
+
77
+ # Rough parameter checking, "*" makes params keyword only
78
+ @staticmethod
79
+ def _kwarg_check(
80
+ *,
81
+ mapname: Optional[str] = None,
82
+ playerid: Optional[str] = None,
83
+ mode: Optional[str] = None,
84
+ difficulty: Optional[str] = None,
85
+ ) -> None:
86
+ if mapname is not None and str(mapname) not in LEVELS:
87
+ raise BadArgument("Invalid map: " + mapname)
88
+ if playerid is not None and not ID_REGEX.match(playerid):
89
+ raise BadArgument("Invalid playerid: " + playerid)
90
+ if mode is not None and not mode in ("score", "waves"):
91
+ raise BadArgument(
92
+ "Invalid mode (must be either 'score' or 'waves'): " + mode
93
+ )
94
+ if difficulty is not None and not difficulty in ("EASY", "NORMAL", "ENDLESS_I"):
95
+ raise BadArgument(
96
+ "Invalid difficulty (must be one of 'EASY', 'NORMAL', 'ENDLESS_I': "
97
+ + difficulty
98
+ )
99
+
100
+ # not being more specific with the payload type
101
+ # so the typechecker stops annoying me
102
+ async def _post(
103
+ self, arg: str, data: Optional[Dict[str, Any]] = None
104
+ ) -> Dict[str, Any]:
105
+ """Internal post method to communicate with Rainy's API"""
106
+ url = f"https://infinitode.prineside.com/?m=api&a={arg}&apiv=1&g=com.prineside.tdi2&v=282"
107
+ _log.info("Sending POST request %s with data %s", arg, data)
108
+ async with self._session.post(url, data=data) as r:
109
+ try:
110
+ r.raise_for_status()
111
+ except aiohttp.ClientResponseError:
112
+ raise APIError("Something went wrong. Try again later")
113
+
114
+ payload: Dict[str, Any] = await r.json()
115
+ _log.debug("Response to POST request %s: %s", arg, payload)
116
+
117
+ if payload["status"] == "success":
118
+ return payload
119
+ else:
120
+ raise APIError(f'Error response from server: {payload["message"]}')
121
+
122
+ # all the mapnames are Any because any python object can be converted to a str
123
+ async def leaderboards_rank(
124
+ self,
125
+ mapname: Any,
126
+ playerid: str,
127
+ mode: str = "score",
128
+ difficulty: str = "NORMAL",
129
+ ) -> Score:
130
+ """
131
+ Retrieves a Score of the given player.
132
+ A valid playerid needs to be specified.
133
+ """
134
+ self._kwarg_check(
135
+ mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
136
+ )
137
+ payload = await self._post(
138
+ "getLeaderboardsRank",
139
+ data={
140
+ "gamemode": "BASIC_LEVELS",
141
+ "difficulty": difficulty,
142
+ "playerid": playerid,
143
+ "mapname": str(mapname),
144
+ "mode": mode,
145
+ },
146
+ )
147
+ return Score.from_payload(
148
+ "leaderboards_rank", mapname, mode, difficulty, playerid, payload
149
+ )
150
+
151
+ async def leaderboards(
152
+ self,
153
+ mapname: Any,
154
+ playerid: Optional[str] = None,
155
+ mode: str = "score",
156
+ difficulty: str = "NORMAL",
157
+ ) -> Leaderboard:
158
+ """
159
+ Retrieves a Leaderboard.
160
+ The leaderboard contains the top 200 scores of the specified map.
161
+ """
162
+ self._kwarg_check(
163
+ mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
164
+ )
165
+ key = f"{difficulty}{mode}{mapname}"
166
+ if not playerid and (
167
+ time.time() < self._cooldown["Leaderboards"].get(key, 0) + 60
168
+ ):
169
+ return self._Leaderboards[key]
170
+ payload = await self._post(
171
+ "getLeaderboards",
172
+ data={
173
+ "gamemode": "BASIC_LEVELS",
174
+ "difficulty": difficulty,
175
+ "playerid": playerid,
176
+ "mapname": str(mapname),
177
+ "mode": mode,
178
+ },
179
+ )
180
+ lb = Leaderboard.from_payload(
181
+ "leaderboards", mapname, mode, difficulty, playerid, payload
182
+ )
183
+ if lb.player is None:
184
+ self._Leaderboards[key] = lb
185
+ self._cooldown["Leaderboards"][key] = time.time()
186
+ return lb
187
+
188
+ async def runtime_leaderboards(
189
+ self,
190
+ mapname: Any,
191
+ playerid: str,
192
+ mode: str = "score",
193
+ difficulty: str = "NORMAL",
194
+ ) -> Leaderboard:
195
+ """
196
+ Retrieves a Runtime Leaderboard (The one displayed top right in-game).
197
+ A valid playerid needs to be specified.
198
+ The leaderboard contains the top 200 scores and one Score for each top% of the specified map.
199
+ """
200
+ self._kwarg_check(
201
+ mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
202
+ )
203
+ payload = await self._post(
204
+ "getRuntimeLeaderboards",
205
+ data={
206
+ "gamemode": "BASIC_LEVELS",
207
+ "difficulty": difficulty,
208
+ "playerid": playerid,
209
+ "mapname": str(mapname),
210
+ "mode": mode,
211
+ },
212
+ )
213
+ return Leaderboard.from_payload(
214
+ "runtime_leaderboards", mapname, mode, difficulty, playerid, payload
215
+ )
216
+
217
+ async def skill_point_leaderboard(
218
+ self, playerid: Optional[str] = None
219
+ ) -> Leaderboard:
220
+ """
221
+ Retrieves the Skill Point Leaderboard.
222
+ The leaderboard contains the top 3 skill point owners (looking at you, Eupho!).
223
+ """
224
+ if playerid is not None:
225
+ self._kwarg_check(playerid=playerid)
226
+ elif time.time() < self._cooldown["SkillPointLeaderboard"] + 60:
227
+ return self._SkillPointLeaderboard
228
+ payload = await self._post(
229
+ "getSkillPointLeaderboard", data={"playerid": playerid}
230
+ )
231
+ lb = Leaderboard.from_payload(
232
+ "skill_point_leaderboard", "SP", "score", "NORMAL", playerid, payload
233
+ )
234
+ if lb.player is None:
235
+ self._SkillPointLeaderboard = lb
236
+ self._cooldown["SkillPointLeaderboard"] = time.time()
237
+ return lb
238
+
239
+ async def daily_quest_leaderboards(
240
+ self,
241
+ date: Union[datetime.datetime, str, None] = None,
242
+ playerid: Optional[str] = None,
243
+ warning: bool = True,
244
+ ) -> Leaderboard:
245
+ """
246
+ Retrieves the Daily Quest Leaderboard for the given date.
247
+ If an invalid or no date is provided, the date will be set to the current date.
248
+ You may disable the invalid date warning by setting the warning param to False.
249
+ The leaderboard contains the top 200 DQ players of the given date.
250
+ """
251
+ if date is None:
252
+ date = datetime.datetime.utcnow().strftime("%Y-%m-%d")
253
+ elif isinstance(date, datetime.datetime):
254
+ date = date.strftime("%Y-%m-%d")
255
+ else:
256
+ try:
257
+ datetime.datetime.strptime(date, "%Y-%m-%d")
258
+ except ValueError:
259
+ if warning == True:
260
+ _log.warning(
261
+ "Invalid date in daily_quest_leaderboards (Use YYYY-MM-DD format): %s",
262
+ date,
263
+ )
264
+ date = datetime.datetime.utcnow().strftime("%Y-%m-%d")
265
+ if not playerid and (
266
+ time.time() < self._cooldown["DailyQuestLeaderboards"].get(date, 0) + 60
267
+ ):
268
+ return self._DailyQuestLeaderboards[date]
269
+ if playerid is not None:
270
+ self._kwarg_check(playerid=playerid)
271
+ payload = await self._post(
272
+ "getDailyQuestLeaderboards", data={"date": date, "playerid": playerid}
273
+ )
274
+ lb = Leaderboard.from_payload(
275
+ "daily_quest_leaderboards",
276
+ "DQ",
277
+ "score",
278
+ "NORMAL",
279
+ playerid,
280
+ payload,
281
+ date=date,
282
+ )
283
+ if lb.player is None:
284
+ self._DailyQuestLeaderboards[date] = lb
285
+ self._cooldown["DailyQuestLeaderboards"][date] = time.time()
286
+ return lb
287
+
288
+ async def seasonal_leaderboard(self) -> Leaderboard:
289
+ """
290
+ Retrieves the season Leaderboard.
291
+ The leaderboard contains the top 100 scores in the season.
292
+ This coroutine never takes arguments.
293
+ """
294
+ if time.time() < self._cooldown["seasonal"] + 60:
295
+ return self._seasonal
296
+ url = "https://infinitode.prineside.com/xdx/?url=seasonal_leaderboard"
297
+ _log.info("Sending GET request to %s", url)
298
+ r = await self._session.get(url=url)
299
+ try:
300
+ r.raise_for_status()
301
+ except aiohttp.ClientResponseError:
302
+ raise APIError("Bad Gateway.")
303
+ seasonal = bs4.BeautifulSoup(await r.text(), features="lxml")
304
+
305
+ # fmt: off
306
+ season = int(seasonal.select_one('label[i18n="season_formatted"]')['i18nf'].replace('["', '').replace('"]', '')) # type: ignore
307
+ player_count = int(seasonal.select('label[i18n="player_count_formatted"]')[ # type: ignore
308
+ 0]['i18nf'].replace('["', '').replace('"]', '').replace(',', '')) # type: ignore
309
+ lb = Leaderboard.from_payload(
310
+ 'seasonal_leaderboard', 'season', 'score', 'NORMAL', None, {
311
+ 'status': 'success',
312
+ 'player': {'total': player_count},
313
+ 'leaderboards': [
314
+ {
315
+ 'playerid': seasonal.select('label[color="LIGHT_BLUE:P300"]')[x]['click'].split('id=')[1], # type: ignore
316
+ 'nickname': seasonal.select('label[color="LIGHT_BLUE:P300"]')[x].text,
317
+ 'score': seasonal.select('label[nowrap="true"][text-align="right"]')[x].text.replace(',', '')
318
+ } for x in range(len(seasonal.select('div[x="90"]')))
319
+ ]
320
+ }, season=season
321
+ )
322
+ # fmt: on
323
+
324
+ self._seasonal = lb
325
+ self._cooldown["seasonal"] = time.time()
326
+ return lb
327
+
328
+ def _parse_player(self, content: str, playerid: str) -> Player:
329
+ data = bs4.BeautifulSoup(content, features="lxml")
330
+ t: Dict[str, Any] = {}
331
+ t["playerid"] = playerid
332
+ t["nickname"] = data.select_one("label:not([i18n])").text # type: ignore
333
+ totals = data.select_one('div[width="522"][height="140"][align="center"]')
334
+ if totals is None:
335
+ t.update({"total_score": 0, "total_rank": 0, "total_top": 0})
336
+ else:
337
+ totals = totals.select("label")
338
+ if len(totals) >= 4:
339
+ t["total_score"] = try_int(totals[1].text.replace(",", ""))
340
+ t["total_rank"] = try_int(totals[2].text.replace(",", ""))
341
+ t["total_top"] = totals[3].text.replace("- Top ", "")
342
+ else:
343
+ t.update({"total_score": 0, "total_rank": 0, "total_top": "0%"})
344
+ comments = data.findAll(text=lambda text: isinstance(text, bs4.Comment))
345
+ for x in comments:
346
+ if "Level:" in x:
347
+ t["level"] = int(x.split(">")[3].split("<")[0])
348
+ break
349
+ else:
350
+ t["level"] = 0
351
+ xp_data = data.select_one('div[width="330"][height="64"]')
352
+ if xp_data is None:
353
+ raise BadArgument("Invalid playerid: " + playerid)
354
+ xp_data = xp_data.select_one("label").text.split(" / ") # type: ignore
355
+ t["xp"] = int(xp_data[0])
356
+ t["xp_max"] = int(xp_data[1])
357
+ season_xp_data = data.select_one(
358
+ 'div[width="530"][align="center"][height="64"][pad-bottom="10"]'
359
+ )
360
+ if season_xp_data is None:
361
+ raise BadArgument("Invalid playerid: " + playerid)
362
+ season_xp_borders = season_xp_data.select_one("label").text.split(" / ") # type: ignore
363
+ t["season_xp"] = int(season_xp_borders[0])
364
+ t["season_xp_max"] = int(season_xp_borders[1])
365
+ season_level_data = season_xp_data.select_one(
366
+ 'div[x="466"][width="64"][height="64"]'
367
+ )
368
+ if season_level_data is None:
369
+ raise BadArgument("Invalid playerid: " + playerid)
370
+ t["season_level"] = season_level_data["data"].split(":")[1] # type: ignore
371
+
372
+ t["levels"] = {}
373
+ for x in data.select('div[width="800"][height="40"]')[1:]:
374
+ level_data = x.select("label")
375
+ level = level_data[0].text
376
+ if not x.select_one('label[i18n="not_ranked"]'):
377
+ rank = int(level_data[2].text.replace(",", ""))
378
+ score = int(level_data[1].text.replace(",", ""))
379
+ total = int(level_data[3].text.replace("/ ", "").replace(",", ""))
380
+ top = level_data[-1].text
381
+ else:
382
+ rank, score, total, top = 0, 0, 0, "-%"
383
+ t["levels"][level] = Score(
384
+ "player",
385
+ level,
386
+ "score",
387
+ "NORMAL",
388
+ playerid,
389
+ rank=rank,
390
+ score=score,
391
+ total=total,
392
+ top=top,
393
+ level=t["level"],
394
+ nickname=t["nickname"],
395
+ )
396
+ t["badges"] = {}
397
+ icos = [
398
+ "daily-game",
399
+ "invited-players",
400
+ "killed-enemies",
401
+ "mined-resources",
402
+ "skillful",
403
+ "of-merit",
404
+ "beta-tester-season-2",
405
+ f"high-leveled-{t['level'] // 10}",
406
+ ]
407
+ for x in data.select('div[width="80"][height="80"]'):
408
+ rar: str = x.select("img")[0]["src"].split("bg-")[1] # type: ignore
409
+ if rar in [
410
+ "not-received",
411
+ "common",
412
+ "rare",
413
+ "very-rare",
414
+ "epic",
415
+ "legendary",
416
+ "supreme",
417
+ "artifact",
418
+ ]:
419
+ ico: str = x.select("img")[1]["src"].split("icon-")[1] # type: ignore
420
+ if (
421
+ ico
422
+ in icos
423
+ + [
424
+ "youtube-author-" + rar,
425
+ f"season-level-{rar}-2",
426
+ ]
427
+ or ico[:8] == "season-1"
428
+ ):
429
+ col: str = x.select("img")[-1]["color"] # type: ignore
430
+ t["badges"][ico] = (rar, col)
431
+ labels = data.select('table[width="800"][align="center"]')[-1].select("label")
432
+ replays = labels[-3].string.split(" ") # type: ignore
433
+ t["replays"] = 0 if len(replays) != 4 else int(replays[3])
434
+ issues = labels[-2].string.split(" ") # type: ignore
435
+ t["issues"] = 0 if len(issues) != 6 else int(issues[0][3:])
436
+ sp = list(labels[-1].string.split("ned ")[1].split(" ")) # type: ignore
437
+ day = sp[0][:-2] if len(sp[0][:2]) == 2 else "0" + sp[0][:2]
438
+ t["created_at"] = datetime.datetime.strptime(
439
+ day + " " + sp[-2] + " " + sp[-1], "%d %B %Y"
440
+ ).strftime("%Y-%m-%d")
441
+
442
+ return Player(**t)
443
+
444
+ async def player(self, playerid: str) -> Player:
445
+ """
446
+ Retrieves the Player.
447
+ A valid playerid needs to be specified.
448
+ """
449
+ self._kwarg_check(playerid=playerid)
450
+ url = f"https://infinitode.prineside.com/xdx/index.php?url=profile/view&id={playerid}"
451
+ _log.info("Sending GET request to %s", url)
452
+ r = await self._session.get(url=url)
453
+ try:
454
+ r.raise_for_status()
455
+ except aiohttp.ClientResponseError:
456
+ raise APIError("Bad Gateway.")
457
+ loop = asyncio.get_event_loop()
458
+ try:
459
+ return await loop.run_in_executor(
460
+ None, self._parse_player, await r.text(), playerid
461
+ )
462
+ except Exception as exc:
463
+ raise BadArgument("Invalid playerid: " + playerid) from exc
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = (
4
+ "InfinitodeError",
5
+ "APIError",
6
+ "BadArgument",
7
+ )
8
+
9
+
10
+ class InfinitodeError(Exception):
11
+ """Base Infinitode Error."""
12
+
13
+ pass
14
+
15
+
16
+ class APIError(InfinitodeError):
17
+ """Error directly related to the communication with the API."""
18
+
19
+ pass
20
+
21
+
22
+ class BadArgument(InfinitodeError):
23
+ """Error raised when an invalid argument is passed."""
24
+
25
+ pass