infinitode.py 1.1.4__tar.gz → 1.1.6__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.4
3
+ Version: 1.1.6
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,7 +7,7 @@ An asynchronous wrapper for the Infinitode API.
7
7
 
8
8
  __author__ = "Sprylos"
9
9
  __title__ = "infinitode.py"
10
- __version__ = "1.1.4"
10
+ __version__ = "1.1.6"
11
11
  __license__ = """MIT License
12
12
 
13
13
  Copyright (c) 2025 Sprylos
@@ -51,7 +51,7 @@ def base_url(beta: bool = False) -> str:
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,10 @@ 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
- # Rough parameter checking, "*" makes params keyword only
69
68
  @staticmethod
70
- def __kwarg_check(
69
+ def _kwarg_check(
71
70
  *,
72
71
  mapname: Optional[str] = None,
73
72
  playerid: Optional[str] = None,
@@ -87,13 +86,13 @@ class Session:
87
86
 
88
87
  # not being more specific with the payload type
89
88
  # so the typechecker stops annoying me
90
- async def __post(
89
+ async def _post(
91
90
  self, arg: str, data: Optional[Dict[str, Any]] = None, *, beta: bool = False
92
91
  ) -> Dict[str, Any]:
93
92
  """Internal post method to communicate with Rainy's API"""
94
93
  url = base_url(beta) + f"?m=api&a={arg}&apiv=1&g=com.prineside.tdi2&v=282"
95
94
  LOG.info("Sending POST request %s with data %s", arg, data)
96
- async with self.__session.post(url, data=data) as r:
95
+ async with self._session.post(url, data=data) as r:
97
96
  try:
98
97
  r.raise_for_status()
99
98
  except aiohttp.ClientResponseError:
@@ -121,10 +120,10 @@ class Session:
121
120
  Retrieves a Score of the given player.
122
121
  A valid playerid needs to be specified.
123
122
  """
124
- self.__kwarg_check(
123
+ self._kwarg_check(
125
124
  mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
126
125
  )
127
- payload = await self.__post(
126
+ payload = await self._post(
128
127
  "getLeaderboardsRank",
129
128
  data={
130
129
  "gamemode": "BASIC_LEVELS",
@@ -153,11 +152,11 @@ class Session:
153
152
  Retrieves a Leaderboard.
154
153
  The leaderboard contains the top 200 scores of the specified map.
155
154
  """
156
- self.__kwarg_check(
155
+ self._kwarg_check(
157
156
  mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
158
157
  )
159
158
 
160
- payload = await self.__post(
159
+ payload = await self._post(
161
160
  "getLeaderboards",
162
161
  data={
163
162
  "gamemode": "BASIC_LEVELS",
@@ -189,10 +188,10 @@ class Session:
189
188
  A valid playerid needs to be specified.
190
189
  The leaderboard contains the top 200 scores and one Score for each top% of the specified map.
191
190
  """
192
- self.__kwarg_check(
191
+ self._kwarg_check(
193
192
  mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
194
193
  )
195
- payload = await self.__post(
194
+ payload = await self._post(
196
195
  "getRuntimeLeaderboards",
197
196
  data={
198
197
  "gamemode": "BASIC_LEVELS",
@@ -216,9 +215,9 @@ class Session:
216
215
  The leaderboard contains the top 3 skill point owners (looking at you, Eupho!).
217
216
  """
218
217
  if playerid is not None:
219
- self.__kwarg_check(playerid=playerid)
218
+ self._kwarg_check(playerid=playerid)
220
219
 
221
- payload = await self.__post(
220
+ payload = await self._post(
222
221
  "getSkillPointLeaderboard", data={"playerid": playerid}, beta=beta
223
222
  )
224
223
  lb = Leaderboard.from_payload(
@@ -248,7 +247,7 @@ class Session:
248
247
  date = date.strftime("%Y-%m-%d")
249
248
  else:
250
249
  try:
251
- datetime.datetime.strptime(date, "%Y-%m-%d")
250
+ date_obj = datetime.datetime.strptime(date, "%Y-%m-%d")
252
251
  except ValueError:
253
252
  if warning is True:
254
253
  LOG.warning(
@@ -256,11 +255,13 @@ class Session:
256
255
  date,
257
256
  )
258
257
  date = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
258
+ else:
259
+ date = date_obj.strftime("%Y-%m-%d") # allows for missing leading zeros
259
260
 
260
261
  if playerid is not None:
261
- self.__kwarg_check(playerid=playerid)
262
+ self._kwarg_check(playerid=playerid)
262
263
 
263
- payload = await self.__post(
264
+ payload = await self._post(
264
265
  "getDailyQuestLeaderboards",
265
266
  data={"date": date, "playerid": playerid},
266
267
  beta=beta,
@@ -278,7 +279,7 @@ class Session:
278
279
  return lb
279
280
 
280
281
  @async_expiring_cache()
281
- async def seasonal_leaderboard(self, beta: bool = False) -> Leaderboard:
282
+ async def seasonal_leaderboard(self, *, beta: bool = False) -> Leaderboard:
282
283
  """
283
284
  Retrieves the season Leaderboard.
284
285
  The leaderboard contains the top 100 scores in the season.
@@ -287,7 +288,7 @@ class Session:
287
288
  url = base_url(beta) + "xdx/?url=seasonal_leaderboard"
288
289
  LOG.info("Sending GET request to %s", url)
289
290
 
290
- r = await self.__session.get(url=url)
291
+ r = await self._session.get(url=url)
291
292
  try:
292
293
  r.raise_for_status()
293
294
  except aiohttp.ClientResponseError:
@@ -317,16 +318,32 @@ class Session:
317
318
  return lb
318
319
 
319
320
  @async_expiring_cache()
320
- async def player(self, playerid: str, beta: bool = False) -> Player:
321
+ async def player(
322
+ self,
323
+ playerid: str | None = None,
324
+ nickname: str | None = None,
325
+ *,
326
+ beta: bool = False,
327
+ ) -> Player:
321
328
  """
322
329
  Retrieves the Player.
323
330
  A valid playerid needs to be specified.
324
331
  """
325
- self.__kwarg_check(playerid=playerid)
326
- url = base_url(beta) + "xdx/index.php?url=profile/view&id=" + playerid
332
+ if playerid and nickname:
333
+ raise BadArgument("You can't specify both playerid and nickname.")
334
+
335
+ url = base_url(beta)
336
+ if nickname:
337
+ url = url + "xdx/index.php?url=profile/view&nickname=" + nickname
338
+ elif playerid:
339
+ self._kwarg_check(playerid=playerid)
340
+ url = url + "xdx/index.php?url=profile/view&id=" + playerid
341
+ else:
342
+ raise BadArgument("You need to specify either playerid or nickname.")
343
+
327
344
  LOG.info("Sending GET request to %s", url)
328
345
 
329
- r = await self.__session.get(url=url)
346
+ r = await self._session.get(url=url)
330
347
  try:
331
348
  r.raise_for_status()
332
349
  except aiohttp.ClientResponseError:
@@ -335,31 +352,33 @@ class Session:
335
352
  loop = asyncio.get_event_loop()
336
353
  try:
337
354
  return await loop.run_in_executor(
338
- None, self.__parse_player, await r.text(), playerid, beta
355
+ None, self._parse_player, await r.text(), beta
339
356
  )
340
357
  except Exception as exc:
341
- raise BadArgument("Invalid playerid: " + playerid) from exc
358
+ raise BadArgument(
359
+ f"Invalid playerid/nickname: {playerid or nickname}"
360
+ ) from exc
342
361
 
343
362
  @classmethod
344
- def __parse_player(cls, content: str, playerid: str, beta: bool) -> Player:
363
+ def _parse_player(cls, content: str, beta: bool) -> Player:
345
364
  data = BeautifulSoup(content, features="lxml")
346
365
 
347
366
  t: Dict[str, Any] = {}
348
- t["playerid"] = playerid
349
367
  t["beta"] = beta
368
+ t["playerid"] = data.select_one("label:not([i18n],[font-min-size])").text # type: ignore
350
369
  t["nickname"] = data.select_one("label:not([i18n])").text # type: ignore
351
370
 
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)
371
+ cls._parse_totals(data, t)
372
+ cls._parse_level_from_comments(data, t)
373
+ cls._parse_xp_data(data, t)
374
+ cls._parse_levels(data, t)
375
+ cls._parse_badges(data, t)
376
+ cls._parse_misc(data, t)
358
377
 
359
378
  return Player(**t)
360
379
 
361
380
  @staticmethod
362
- def __parse_totals(data: BeautifulSoup, t: Dict[str, Any]) -> None:
381
+ def _parse_totals(data: BeautifulSoup, t: Dict[str, Any]) -> None:
363
382
  totals = data.select_one('div[width="522"][height="140"][align="center"]')
364
383
  if totals is None:
365
384
  t.update({"total_score": 0, "total_rank": 0, "total_top": 0})
@@ -374,7 +393,7 @@ class Session:
374
393
  t.update({"total_score": 0, "total_rank": 0, "total_top": "0%"})
375
394
 
376
395
  @staticmethod
377
- def __parse_level_from_comments(data: BeautifulSoup, t: Dict[str, Any]) -> None:
396
+ def _parse_level_from_comments(data: BeautifulSoup, t: Dict[str, Any]) -> None:
378
397
  comments = data.findAll(text=lambda text: isinstance(text, Comment))
379
398
  for x in comments:
380
399
  if "Level:" in x:
@@ -384,7 +403,7 @@ class Session:
384
403
  t["level"] = 1
385
404
 
386
405
  @staticmethod
387
- def __parse_xp_data(data: BeautifulSoup, t: Dict[str, Any]) -> None:
406
+ def _parse_xp_data(data: BeautifulSoup, t: Dict[str, Any]) -> None:
388
407
  xp_data = data.select_one('div[width="330"][height="64"]')
389
408
  xp_data = xp_data.select_one("label").text.split(" / ") # type: ignore
390
409
  t["xp"] = int(xp_data[0])
@@ -409,7 +428,7 @@ class Session:
409
428
  t["season_level"] = int(season_level_data["data"].split(":")[1]) # type: ignore
410
429
 
411
430
  @staticmethod
412
- def __parse_levels(data: BeautifulSoup, t: Dict[str, Any]) -> None:
431
+ def _parse_levels(data: BeautifulSoup, t: Dict[str, Any]) -> None:
413
432
  t["levels"] = {}
414
433
 
415
434
  for x in data.select('div[width="800"][height="40"]')[1:]:
@@ -437,7 +456,7 @@ class Session:
437
456
  )
438
457
 
439
458
  @staticmethod
440
- def __parse_badges(data: BeautifulSoup, t: Dict[str, Any]) -> None:
459
+ def _parse_badges(data: BeautifulSoup, t: Dict[str, Any]) -> None:
441
460
  t["badges"] = {}
442
461
 
443
462
  icos = [
@@ -474,7 +493,7 @@ class Session:
474
493
  t["badges"][ico] = (rar, col)
475
494
 
476
495
  @staticmethod
477
- def __parse_misc(data: BeautifulSoup, t: Dict[str, Any]) -> None:
496
+ def _parse_misc(data: BeautifulSoup, t: Dict[str, Any]) -> None:
478
497
  labels = data.select('table[width="800"][align="center"]')[-1].select("label")
479
498
  replays = labels[-3].string.split(" ") # type: ignore
480
499
  t["replays"] = 0 if len(replays) != 4 else int(replays[3])
@@ -67,10 +67,8 @@ class Score:
67
67
  self._nickname = nickname
68
68
  self._pinned_badge: Optional[Badge] = (
69
69
  Badge(**pinnedBadge) if pinnedBadge is not None else None
70
- ) # nopep8
71
- self._position: Optional[int] = (
72
- int(position) if position is not None else None
73
- ) # nopep8
70
+ )
71
+ self._position: Optional[int] = int(position) if position is not None else None
74
72
  self._top = top
75
73
  # apparently some payloads provide total, but i think it is pointless as a property
76
74
  # so i will keep it as a private attribute just in case it will ever be needed
@@ -20,10 +20,7 @@ def try_int(string: str, /, *, default: int = 0) -> int:
20
20
 
21
21
 
22
22
  def async_expiring_cache(seconds: int = 60) -> Any:
23
- """Decorator to cache coroutines return value for the given amount of seconds.
24
-
25
- It is not perfect because
26
- """
23
+ """Decorator to cache coroutines return value for the given amount of seconds."""
27
24
 
28
25
  def decorator(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, asyncio.Task[T]]:
29
26
  cache: Dict[Tuple[Any, ...], Tuple[asyncio.Task[T], float]] = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: infinitode.py
3
- Version: 1.1.4
3
+ Version: 1.1.6
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