infinitode.py 1.1.3__tar.gz → 1.1.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: infinitode.py
3
- Version: 1.1.3
3
+ Version: 1.1.4
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
@@ -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.1.3"
10
+ __version__ = "1.1.4"
11
11
  __license__ = """MIT License
12
12
 
13
- Copyright (c) 2024 Sprylos
13
+ Copyright (c) 2025 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
@@ -14,7 +14,7 @@ from typing import (
14
14
 
15
15
  # packages
16
16
  import aiohttp
17
- import bs4
17
+ from bs4 import BeautifulSoup, Comment
18
18
 
19
19
  # local
20
20
  from .errors import APIError, BadArgument
@@ -26,7 +26,7 @@ from .utils import async_expiring_cache, try_int
26
26
 
27
27
  __all__ = ("Session",)
28
28
 
29
- _log = logging.getLogger(__name__)
29
+ LOG = logging.getLogger(__name__)
30
30
 
31
31
  ID_REGEX = re.compile(r"U-([A-Z0-9]{4}-){2}[A-Z0-9]{6}")
32
32
 
@@ -45,13 +45,13 @@ DIFFICULTIES = ('EASY', 'NORMAL', 'ENDLESS_I')
45
45
  # fmt: on
46
46
 
47
47
 
48
- def _base_url(beta: bool = False) -> str:
48
+ def base_url(beta: bool = False) -> str:
49
49
  return f"https://{'beta.' if beta else ''}infinitode.prineside.com/"
50
50
 
51
51
 
52
52
  class Session:
53
53
  def __init__(self, session: Optional[aiohttp.ClientSession] = None) -> None:
54
- self._session = session or aiohttp.ClientSession()
54
+ self.__session = session or aiohttp.ClientSession()
55
55
 
56
56
  # async enter and exit allow for the fancy "with" statements
57
57
  # useful so you don't have to close the session yourself
@@ -63,11 +63,11 @@ class Session:
63
63
 
64
64
  async def close(self):
65
65
  """Closes the internal ClientSession."""
66
- await self._session.close()
66
+ await self.__session.close()
67
67
 
68
68
  # Rough parameter checking, "*" makes params keyword only
69
69
  @staticmethod
70
- def _kwarg_check(
70
+ def __kwarg_check(
71
71
  *,
72
72
  mapname: Optional[str] = None,
73
73
  playerid: Optional[str] = None,
@@ -87,20 +87,20 @@ class Session:
87
87
 
88
88
  # not being more specific with the payload type
89
89
  # so the typechecker stops annoying me
90
- async def _post(
90
+ async def __post(
91
91
  self, arg: str, data: Optional[Dict[str, Any]] = None, *, beta: bool = False
92
92
  ) -> Dict[str, Any]:
93
93
  """Internal post method to communicate with Rainy's API"""
94
- url = _base_url(beta) + f"?m=api&a={arg}&apiv=1&g=com.prineside.tdi2&v=282"
95
- _log.info("Sending POST request %s with data %s", arg, data)
96
- async with self._session.post(url, data=data) as r:
94
+ url = base_url(beta) + f"?m=api&a={arg}&apiv=1&g=com.prineside.tdi2&v=282"
95
+ LOG.info("Sending POST request %s with data %s", arg, data)
96
+ async with self.__session.post(url, data=data) as r:
97
97
  try:
98
98
  r.raise_for_status()
99
99
  except aiohttp.ClientResponseError:
100
100
  raise APIError("Something went wrong. Try again later")
101
101
 
102
102
  payload: Dict[str, Any] = await r.json()
103
- _log.debug("Response to POST request %s: %s", arg, payload)
103
+ LOG.debug("Response to POST request %s: %s", arg, payload)
104
104
 
105
105
  if payload["status"] == "success":
106
106
  return payload
@@ -121,10 +121,10 @@ class Session:
121
121
  Retrieves a Score of the given player.
122
122
  A valid playerid needs to be specified.
123
123
  """
124
- self._kwarg_check(
124
+ self.__kwarg_check(
125
125
  mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
126
126
  )
127
- payload = await self._post(
127
+ payload = await self.__post(
128
128
  "getLeaderboardsRank",
129
129
  data={
130
130
  "gamemode": "BASIC_LEVELS",
@@ -153,11 +153,11 @@ class Session:
153
153
  Retrieves a Leaderboard.
154
154
  The leaderboard contains the top 200 scores of the specified map.
155
155
  """
156
- self._kwarg_check(
156
+ self.__kwarg_check(
157
157
  mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
158
158
  )
159
159
 
160
- payload = await self._post(
160
+ payload = await self.__post(
161
161
  "getLeaderboards",
162
162
  data={
163
163
  "gamemode": "BASIC_LEVELS",
@@ -189,10 +189,10 @@ class Session:
189
189
  A valid playerid needs to be specified.
190
190
  The leaderboard contains the top 200 scores and one Score for each top% of the specified map.
191
191
  """
192
- self._kwarg_check(
192
+ self.__kwarg_check(
193
193
  mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
194
194
  )
195
- payload = await self._post(
195
+ payload = await self.__post(
196
196
  "getRuntimeLeaderboards",
197
197
  data={
198
198
  "gamemode": "BASIC_LEVELS",
@@ -216,9 +216,9 @@ class Session:
216
216
  The leaderboard contains the top 3 skill point owners (looking at you, Eupho!).
217
217
  """
218
218
  if playerid is not None:
219
- self._kwarg_check(playerid=playerid)
219
+ self.__kwarg_check(playerid=playerid)
220
220
 
221
- payload = await self._post(
221
+ payload = await self.__post(
222
222
  "getSkillPointLeaderboard", data={"playerid": playerid}, beta=beta
223
223
  )
224
224
  lb = Leaderboard.from_payload(
@@ -251,16 +251,16 @@ class Session:
251
251
  datetime.datetime.strptime(date, "%Y-%m-%d")
252
252
  except ValueError:
253
253
  if warning is True:
254
- _log.warning(
254
+ LOG.warning(
255
255
  "Invalid date in daily_quest_leaderboards (Use YYYY-MM-DD format): %s",
256
256
  date,
257
257
  )
258
258
  date = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
259
259
 
260
260
  if playerid is not None:
261
- self._kwarg_check(playerid=playerid)
261
+ self.__kwarg_check(playerid=playerid)
262
262
 
263
- payload = await self._post(
263
+ payload = await self.__post(
264
264
  "getDailyQuestLeaderboards",
265
265
  data={"date": date, "playerid": playerid},
266
266
  beta=beta,
@@ -284,16 +284,16 @@ class Session:
284
284
  The leaderboard contains the top 100 scores in the season.
285
285
  This coroutine never takes arguments.
286
286
  """
287
- url = _base_url(beta) + "xdx/?url=seasonal_leaderboard"
288
- _log.info("Sending GET request to %s", url)
287
+ url = base_url(beta) + "xdx/?url=seasonal_leaderboard"
288
+ LOG.info("Sending GET request to %s", url)
289
289
 
290
- r = await self._session.get(url=url)
290
+ r = await self.__session.get(url=url)
291
291
  try:
292
292
  r.raise_for_status()
293
293
  except aiohttp.ClientResponseError:
294
294
  raise APIError("Bad Gateway.")
295
295
 
296
- seasonal = bs4.BeautifulSoup(await r.text(), features="lxml")
296
+ seasonal = BeautifulSoup(await r.text(), features="lxml")
297
297
 
298
298
  # fmt: off
299
299
  season = int(seasonal.select_one('label[i18n="season_formatted"]')['i18nf'].replace('["', '').replace('"]', '')) # type: ignore
@@ -316,56 +316,102 @@ class Session:
316
316
 
317
317
  return lb
318
318
 
319
- def _parse_player(self, content: str, playerid: str, beta: bool) -> Player:
320
- data = bs4.BeautifulSoup(content, features="lxml")
319
+ @async_expiring_cache()
320
+ async def player(self, playerid: str, beta: bool = False) -> Player:
321
+ """
322
+ Retrieves the Player.
323
+ A valid playerid needs to be specified.
324
+ """
325
+ self.__kwarg_check(playerid=playerid)
326
+ url = base_url(beta) + "xdx/index.php?url=profile/view&id=" + playerid
327
+ LOG.info("Sending GET request to %s", url)
328
+
329
+ r = await self.__session.get(url=url)
330
+ try:
331
+ r.raise_for_status()
332
+ except aiohttp.ClientResponseError:
333
+ raise APIError("Bad Gateway.")
334
+
335
+ loop = asyncio.get_event_loop()
336
+ try:
337
+ return await loop.run_in_executor(
338
+ None, self.__parse_player, await r.text(), playerid, beta
339
+ )
340
+ except Exception as exc:
341
+ raise BadArgument("Invalid playerid: " + playerid) from exc
342
+
343
+ @classmethod
344
+ def __parse_player(cls, content: str, playerid: str, beta: bool) -> Player:
345
+ data = BeautifulSoup(content, features="lxml")
346
+
321
347
  t: Dict[str, Any] = {}
322
348
  t["playerid"] = playerid
323
349
  t["beta"] = beta
324
350
  t["nickname"] = data.select_one("label:not([i18n])").text # type: ignore
351
+
352
+ cls.__parse_totals(data, t)
353
+ cls.__parse_level_from_comments(data, t)
354
+ cls.__parse_xp_data(data, t)
355
+ cls.__parse_levels(data, t)
356
+ cls.__parse_badges(data, t)
357
+ cls.__parse_misc(data, t)
358
+
359
+ return Player(**t)
360
+
361
+ @staticmethod
362
+ def __parse_totals(data: BeautifulSoup, t: Dict[str, Any]) -> None:
325
363
  totals = data.select_one('div[width="522"][height="140"][align="center"]')
326
364
  if totals is None:
327
365
  t.update({"total_score": 0, "total_rank": 0, "total_top": 0})
366
+ return
367
+
368
+ totals = totals.select("label")
369
+ if len(totals) >= 4:
370
+ t["total_score"] = try_int(totals[1].text.replace(",", ""))
371
+ t["total_rank"] = try_int(totals[2].text.replace(",", ""))
372
+ t["total_top"] = totals[3].text.replace("- Top ", "")
328
373
  else:
329
- totals = totals.select("label")
330
- if len(totals) >= 4:
331
- t["total_score"] = try_int(totals[1].text.replace(",", ""))
332
- t["total_rank"] = try_int(totals[2].text.replace(",", ""))
333
- t["total_top"] = totals[3].text.replace("- Top ", "")
334
- else:
335
- t.update({"total_score": 0, "total_rank": 0, "total_top": "0%"})
336
- comments = data.findAll(text=lambda text: isinstance(text, bs4.Comment))
374
+ t.update({"total_score": 0, "total_rank": 0, "total_top": "0%"})
375
+
376
+ @staticmethod
377
+ def __parse_level_from_comments(data: BeautifulSoup, t: Dict[str, Any]) -> None:
378
+ comments = data.findAll(text=lambda text: isinstance(text, Comment))
337
379
  for x in comments:
338
380
  if "Level:" in x:
339
381
  t["level"] = int(x.split(">")[3].split("<")[0])
340
382
  break
341
383
  else:
342
384
  t["level"] = 1
385
+
386
+ @staticmethod
387
+ def __parse_xp_data(data: BeautifulSoup, t: Dict[str, Any]) -> None:
343
388
  xp_data = data.select_one('div[width="330"][height="64"]')
344
- if xp_data is None:
345
- raise BadArgument("Invalid playerid: " + playerid)
346
389
  xp_data = xp_data.select_one("label").text.split(" / ") # type: ignore
347
390
  t["xp"] = int(xp_data[0])
348
391
  t["xp_max"] = int(xp_data[1])
349
392
  season_xp_data = data.select_one(
350
393
  'div[width="530"][align="center"][height="64"][pad-bottom="10"]'
351
394
  )
395
+
352
396
  if season_xp_data is None:
353
- t["season_xp"] = 0
354
- t["season_xp_max"] = 500
397
+ t.update({"season_xp": 0, "season_xp_max": 500, "season_level": 1})
398
+ return
399
+
400
+ season_xp_borders = season_xp_data.select_one("label").text.split(" / ") # type: ignore
401
+ t["season_xp"] = int(season_xp_borders[0])
402
+ t["season_xp_max"] = int(season_xp_borders[1])
403
+ season_level_data = season_xp_data.select_one(
404
+ 'div[x="466"][width="64"][height="64"]'
405
+ )
406
+ if season_level_data is None:
355
407
  t["season_level"] = 1
356
408
  else:
357
- season_xp_borders = season_xp_data.select_one("label").text.split(" / ") # type: ignore
358
- t["season_xp"] = int(season_xp_borders[0])
359
- t["season_xp_max"] = int(season_xp_borders[1])
360
- season_level_data = season_xp_data.select_one(
361
- 'div[x="466"][width="64"][height="64"]'
362
- )
363
- if season_level_data is None:
364
- t["season_level"] = 1
365
- else:
366
- t["season_level"] = int(season_level_data["data"].split(":")[1]) # type: ignore
409
+ t["season_level"] = int(season_level_data["data"].split(":")[1]) # type: ignore
367
410
 
411
+ @staticmethod
412
+ def __parse_levels(data: BeautifulSoup, t: Dict[str, Any]) -> None:
368
413
  t["levels"] = {}
414
+
369
415
  for x in data.select('div[width="800"][height="40"]')[1:]:
370
416
  level_data = x.select("label")
371
417
  level = level_data[0].text
@@ -381,7 +427,7 @@ class Session:
381
427
  level,
382
428
  "score",
383
429
  "NORMAL",
384
- playerid,
430
+ t["playerid"],
385
431
  rank=rank,
386
432
  score=score,
387
433
  total=total,
@@ -389,7 +435,11 @@ class Session:
389
435
  level=t["level"],
390
436
  nickname=t["nickname"],
391
437
  )
438
+
439
+ @staticmethod
440
+ def __parse_badges(data: BeautifulSoup, t: Dict[str, Any]) -> None:
392
441
  t["badges"] = {}
442
+
393
443
  icos = [
394
444
  "daily-game",
395
445
  "invited-players",
@@ -400,63 +450,40 @@ class Session:
400
450
  "beta-tester-season-2",
401
451
  f"high-leveled-{t['level'] // 10 if t['level'] < 100 else 10}",
402
452
  ]
453
+ rars = (
454
+ "not-received",
455
+ "common",
456
+ "rare",
457
+ "very-rare",
458
+ "epic",
459
+ "legendary",
460
+ "supreme",
461
+ "artifact",
462
+ )
463
+
403
464
  for x in data.select('div[width="80"][height="80"]'):
404
465
  rar: str = x.select("img")[0]["src"].split("bg-")[1] # type: ignore
405
- if rar in [
406
- "not-received",
407
- "common",
408
- "rare",
409
- "very-rare",
410
- "epic",
411
- "legendary",
412
- "supreme",
413
- "artifact",
414
- ]:
466
+ if rar in rars:
415
467
  ico: str = x.select("img")[1]["src"].split("icon-")[1] # type: ignore
416
- if (
417
- ico
418
- in icos
419
- + [
420
- "youtube-author-" + rar,
421
- f"season-level-{rar}-2",
422
- ]
423
- or ico[:8] == "season-1"
424
- ):
468
+ if ico in icos + [
469
+ "youtube-author-" + rar,
470
+ f"season-level-{rar}-2",
471
+ f"season-level-{rar}-3",
472
+ ] or ico[:8] in ("season-1", "season-2"):
425
473
  col: str = x.select("img")[-1]["color"] # type: ignore
426
474
  t["badges"][ico] = (rar, col)
475
+
476
+ @staticmethod
477
+ def __parse_misc(data: BeautifulSoup, t: Dict[str, Any]) -> None:
427
478
  labels = data.select('table[width="800"][align="center"]')[-1].select("label")
428
479
  replays = labels[-3].string.split(" ") # type: ignore
429
480
  t["replays"] = 0 if len(replays) != 4 else int(replays[3])
481
+
430
482
  issues = labels[-2].string.split(" ") # type: ignore
431
483
  t["issues"] = 0 if len(issues) != 6 else int(issues[0][3:])
484
+
432
485
  sp = list(labels[-1].string.split("ned ")[1].split(" ")) # type: ignore
433
486
  day = sp[0][:-2] if len(sp[0][:2]) == 2 else "0" + sp[0][:2]
434
487
  t["created_at"] = datetime.datetime.strptime(
435
488
  day + " " + sp[-2] + " " + sp[-1], "%d %B %Y"
436
489
  ).strftime("%Y-%m-%d")
437
-
438
- return Player(**t)
439
-
440
- @async_expiring_cache()
441
- async def player(self, playerid: str, beta: bool = False) -> Player:
442
- """
443
- Retrieves the Player.
444
- A valid playerid needs to be specified.
445
- """
446
- self._kwarg_check(playerid=playerid)
447
- url = _base_url(beta) + "xdx/index.php?url=profile/view&id=" + playerid
448
- _log.info("Sending GET request to %s", url)
449
-
450
- r = await self._session.get(url=url)
451
- try:
452
- r.raise_for_status()
453
- except aiohttp.ClientResponseError:
454
- raise APIError("Bad Gateway.")
455
-
456
- loop = asyncio.get_event_loop()
457
- try:
458
- return await loop.run_in_executor(
459
- None, self._parse_player, await r.text(), playerid, beta
460
- )
461
- except Exception as exc:
462
- raise BadArgument("Invalid playerid: " + playerid) from exc
@@ -154,7 +154,7 @@ class Leaderboard(Sequence[Score]):
154
154
  # magic methods
155
155
 
156
156
  def __repr__(self) -> str:
157
- attrs = {
157
+ attrs: dict[str, Any] = {
158
158
  "method": self._method,
159
159
  "mapname": self._mapname,
160
160
  "mode": self._mode,
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  # std
4
- from typing import Dict, Optional, Union, Tuple, TYPE_CHECKING
4
+ from typing import Any, Dict, Optional, Union, Tuple, TYPE_CHECKING
5
5
 
6
6
  # local
7
7
  from .score import Score
@@ -166,7 +166,7 @@ class Player:
166
166
  @property
167
167
  def avatar_link(self):
168
168
  """The link to the player's avatar. Invalid URL if the user doesn't have a pfp."""
169
- return AVATAR_URL.format('beta' if self._beta else '', self._playerid)
169
+ return AVATAR_URL.format("beta" if self._beta else "", self._playerid)
170
170
 
171
171
  @property
172
172
  def daily_quest(self):
@@ -219,18 +219,19 @@ class Player:
219
219
  if session is None:
220
220
  if self._daily_quest is None:
221
221
  return
222
-
222
+
223
223
  # daily_quest is MISSING
224
224
  raise InfinitodeError(
225
225
  "You need to provide a Session to fetch the daily quest score."
226
226
  )
227
-
228
- lb = await session.daily_quest_leaderboards(playerid=self._playerid, beta=self._beta)
227
+
228
+ lb = await session.daily_quest_leaderboards(
229
+ playerid=self._playerid, beta=self._beta
230
+ )
229
231
  self._daily_quest = lb.player
230
232
 
231
233
  return self._daily_quest
232
234
 
233
-
234
235
  async def fetch_skill_point(
235
236
  self, session: Optional[Session] = None
236
237
  ) -> Optional[Score]:
@@ -240,26 +241,27 @@ class Player:
240
241
  """
241
242
  if self._skill_point:
242
243
  return self._skill_point
243
-
244
+
244
245
  if session is None:
245
246
  if self._skill_point is None:
246
247
  return
247
-
248
+
248
249
  # skill_point is MISSING
249
250
  raise InfinitodeError(
250
251
  "You need to provide a Session to fetch the skill point score."
251
252
  )
252
253
 
253
- lb = await session.skill_point_leaderboard( playerid=self._playerid, beta=self._beta)
254
+ lb = await session.skill_point_leaderboard(
255
+ playerid=self._playerid, beta=self._beta
256
+ )
254
257
  self._skill_point = lb.player
255
258
 
256
259
  return self._skill_point
257
260
 
258
-
259
261
  # magic methods
260
262
 
261
263
  def __repr__(self) -> str:
262
- attrs = {
264
+ attrs: dict[str, Any] = {
263
265
  "playerid": self._playerid,
264
266
  "nickname": self._nickname,
265
267
  "total_rank": self._total_rank,
@@ -174,7 +174,7 @@ class Score:
174
174
  raise InfinitodeError(
175
175
  "The score is not valid for formatting (There is no nickname attached to this score)."
176
176
  )
177
-
177
+
178
178
  return "#{:<5} {:<22} {:>0,}".format(
179
179
  self._rank,
180
180
  self._nickname if len(self._nickname) < 21 else f"{self._nickname[:19]}...",
@@ -188,7 +188,7 @@ class Score:
188
188
  # magic methods
189
189
 
190
190
  def __repr__(self) -> str:
191
- attrs = {
191
+ attrs: dict[str, Any] = {
192
192
  "method": self._method,
193
193
  "mapname": self._mapname,
194
194
  "mode": self._mode,
@@ -34,7 +34,7 @@ def async_expiring_cache(seconds: int = 60) -> Any:
34
34
  value, timestamp = cache[key]
35
35
  if time.time() < timestamp + seconds:
36
36
  return value
37
-
37
+
38
38
  coro = func(*args, **kwargs)
39
39
  value = asyncio.ensure_future(coro)
40
40
  cache[key] = (value, time.time())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: infinitode.py
3
- Version: 1.1.3
3
+ Version: 1.1.4
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
File without changes
File without changes
File without changes