Habiticalib 0.1.0a3__tar.gz → 0.2.0a1__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 (44) hide show
  1. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/PKG-INFO +1 -1
  2. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/src/habiticalib/__init__.py +4 -0
  3. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/src/habiticalib/const.py +3 -1
  4. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/src/habiticalib/lib.py +445 -5
  5. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/src/habiticalib/types.py +34 -1
  6. habiticalib-0.1.0a3/tests/__snapshots__/test_user.ambr → habiticalib-0.2.0a1/tests/__snapshots__/test_lib.ambr +3 -0
  7. habiticalib-0.2.0a1/tests/fixtures/login.json +10 -0
  8. habiticalib-0.1.0a3/tests/test_user.py → habiticalib-0.2.0a1/tests/test_lib.py +12 -0
  9. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.cruft.json +0 -0
  10. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.editorconfig +0 -0
  11. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.github/FUNDING.yml +0 -0
  12. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.github/dependabot.yml +0 -0
  13. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.github/labels.yml +0 -0
  14. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.github/release-drafter.yml +0 -0
  15. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.github/workflows/build.yml +0 -0
  16. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.github/workflows/documentation.yml +0 -0
  17. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.github/workflows/draft.yml +0 -0
  18. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.github/workflows/labeler.yml +0 -0
  19. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.gitignore +0 -0
  20. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.pre-commit-config.yaml +0 -0
  21. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/.vscode/settings.json +0 -0
  22. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/LICENSE +0 -0
  23. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/README.md +0 -0
  24. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/docs/index.md +0 -0
  25. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/docs/reference/habiticalib.md +0 -0
  26. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/mkdocs.yml +0 -0
  27. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/pyproject.toml +0 -0
  28. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/src/habiticalib/exceptions.py +0 -0
  29. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/src/habiticalib/helpers.py +0 -0
  30. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/src/habiticalib/py.typed +0 -0
  31. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/__init__.py +0 -0
  32. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/__snapshots__/test_avatar.ambr +0 -0
  33. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/conftest.py +0 -0
  34. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user.json +0 -0
  35. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user_styles.json +0 -0
  36. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user_styles_kickstarter.json +0 -0
  37. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user_styles_seafoam.json +0 -0
  38. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user_styles_shinySeed.json +0 -0
  39. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user_styles_sleeping.json +0 -0
  40. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user_styles_snowball.json +0 -0
  41. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user_styles_spookySparkles.json +0 -0
  42. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/fixtures/user_styles_with_chair.json +0 -0
  43. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/test_avatar.py +0 -0
  44. {habiticalib-0.1.0a3 → habiticalib-0.2.0a1}/tests/test_init.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Habiticalib
3
- Version: 0.1.0a3
3
+ Version: 0.2.0a1
4
4
  Summary: Asynchronous Python client library for the Habitica API
5
5
  Project-URL: Documentation, https://tr4nt0r.github.io/habiticalib/
6
6
  Project-URL: Source, https://github.com/tr4nt0r/habiticalib
@@ -16,9 +16,11 @@ from .types import (
16
16
  HabiticaClass,
17
17
  HabiticaClassSystemResponse,
18
18
  HabiticaErrorResponse,
19
+ HabiticaGroupMembersResponse,
19
20
  HabiticaLoginResponse,
20
21
  HabiticaResponse,
21
22
  HabiticaScoreResponse,
23
+ HabiticaSleepResponse,
22
24
  HabiticaStatsResponse,
23
25
  HabiticaTagResponse,
24
26
  HabiticaTagsResponse,
@@ -48,9 +50,11 @@ __all__ = [
48
50
  "HabiticaClassSystemResponse",
49
51
  "HabiticaErrorResponse",
50
52
  "HabiticaException",
53
+ "HabiticaGroupMembersResponse",
51
54
  "HabiticaLoginResponse",
52
55
  "HabiticaResponse",
53
56
  "HabiticaScoreResponse",
57
+ "HabiticaSleepResponse",
54
58
  "HabiticaStatsResponse",
55
59
  "HabiticaTagResponse",
56
60
  "HabiticaTagsResponse",
@@ -1,6 +1,6 @@
1
1
  """Constants for Habiticalib."""
2
2
 
3
- __version__ = "0.1.0a3"
3
+ __version__ = "0.2.0a1"
4
4
 
5
5
  DEFAULT_URL = "https://habitica.com/"
6
6
  ASSETS_URL = "https://habitica-assets.s3.amazonaws.com/mobileApp/images/"
@@ -14,3 +14,5 @@ BACKER_ONLY_GEAR = {
14
14
  "shield_special_ks2019": "BackerOnly-Equip-MythicGryphonShield.gif",
15
15
  "weapon_special_ks2019": "BackerOnly-Equip-MythicGryphonGlaive.gif",
16
16
  }
17
+
18
+ PAGE_LIMIT = 60
@@ -12,7 +12,7 @@ from aiohttp import ClientError, ClientResponseError, ClientSession
12
12
  from PIL import Image
13
13
  from yarl import URL
14
14
 
15
- from .const import ASSETS_URL, BACKER_ONLY_GEAR, DEFAULT_URL
15
+ from .const import ASSETS_URL, BACKER_ONLY_GEAR, DEFAULT_URL, PAGE_LIMIT
16
16
  from .exceptions import (
17
17
  BadRequestError,
18
18
  NotAuthorizedError,
@@ -32,9 +32,12 @@ from .types import (
32
32
  HabiticaClass,
33
33
  HabiticaClassSystemResponse,
34
34
  HabiticaErrorResponse,
35
+ HabiticaGroupMembersResponse,
35
36
  HabiticaLoginResponse,
37
+ HabiticaQuestResponse,
36
38
  HabiticaResponse,
37
39
  HabiticaScoreResponse,
40
+ HabiticaSleepResponse,
38
41
  HabiticaStatsResponse,
39
42
  HabiticaTagResponse,
40
43
  HabiticaTagsResponse,
@@ -848,17 +851,17 @@ class Habitica:
848
851
  if target_id:
849
852
  params.update({"targetId": str(target_id)})
850
853
  return HabiticaUserResponse.from_json(
851
- await self._request("post", url=url, json=params),
854
+ await self._request("post", url=url, params=params),
852
855
  )
853
856
 
854
857
  async def toggle_sleep(
855
858
  self,
856
- ) -> HabiticaResponse:
859
+ ) -> HabiticaSleepResponse:
857
860
  """Toggles the user's sleep mode in Habitica.
858
861
 
859
862
  Returns
860
863
  -------
861
- HabiticaResponse
864
+ HabiticaSleepResponse
862
865
  A response object containing the result of the sleep mode toggle,
863
866
  and the new sleep state (True if sleeping, False if not).
864
867
 
@@ -875,7 +878,7 @@ class Habitica:
875
878
  """
876
879
  url = self.url / "api/v3/user/sleep"
877
880
 
878
- return HabiticaResponse.from_json(await self._request("post", url=url))
881
+ return HabiticaSleepResponse.from_json(await self._request("post", url=url))
879
882
 
880
883
  async def revive(
881
884
  self,
@@ -1280,6 +1283,443 @@ class Habitica:
1280
1283
  await self._request("post", url=url, json=json),
1281
1284
  )
1282
1285
 
1286
+ async def get_group_members(
1287
+ self,
1288
+ group_id: UUID | None = None,
1289
+ *,
1290
+ limit: int | None = None,
1291
+ tasks: bool = False,
1292
+ public_fields: bool = False,
1293
+ last_id: UUID | None = None,
1294
+ ) -> HabiticaGroupMembersResponse:
1295
+ """Get members of the party or a specific group.
1296
+
1297
+ This method retrieves a list of members for a party or a specified group
1298
+ from the Habitica API. Additional options allow including tasks or public
1299
+ through results if necessary to collect all members. If the API rate limit is
1300
+ exceeded, the method will pause for the duration specified in the `retry-after`
1301
+ header and retry the request.
1302
+
1303
+
1304
+ Parameters
1305
+ ----------
1306
+ group_id : UUID, optional
1307
+ The UUID of the group. Defaults to the user's party if not specified.
1308
+ limit : int, optional
1309
+ Maximum number of members per request (default: 30, max: 60).
1310
+ tasks : bool, optional
1311
+ Whether to include tasks associated with the group.
1312
+ public_fields : bool, optional
1313
+ Whether to include all public fields for group members.
1314
+ last_id : UUID, optional
1315
+ For paginated requests, the UUID of the last member retrieved.
1316
+
1317
+ Returns
1318
+ -------
1319
+ HabiticaGroupMembersResponse
1320
+ A response object containing the group member data.
1321
+
1322
+ Raises
1323
+ ------
1324
+ aiohttp.ClientResponseError
1325
+ For HTTP-related errors, such as HTTP 400 or 500 response status.
1326
+ aiohttp.ClientConnectionError
1327
+ If the connection to the API fails.
1328
+ aiohttp.ClientError
1329
+ For any other exceptions raised by aiohttp during the request.
1330
+ TimeoutError
1331
+ If the connection times out.
1332
+
1333
+ Examples
1334
+ --------
1335
+ >>> members_response = await habitica.get_group_members()
1336
+ >>> for member in members_response.data:
1337
+ ... print(member.profile.name)
1338
+ """
1339
+
1340
+ if limit is not None and (limit < 1 or limit > PAGE_LIMIT):
1341
+ msg = f"The 'limit' parameter must be between 1 and {PAGE_LIMIT}."
1342
+ raise ValueError(msg)
1343
+
1344
+ group = "party" if not group_id else str(group_id)
1345
+ url = self.url / "api/v3/groups" / group / "members"
1346
+
1347
+ params: dict[str, str | int] = {}
1348
+
1349
+ if tasks:
1350
+ params["includeTasks"] = "true"
1351
+ if public_fields:
1352
+ params["includeAllPublicFields"] = "true"
1353
+ if last_id:
1354
+ params["lastId"] = str(last_id)
1355
+ if limit:
1356
+ params["limit"] = limit
1357
+
1358
+ while True:
1359
+ try:
1360
+ response = HabiticaGroupMembersResponse.from_json(
1361
+ await self._request("get", url=url, params=params),
1362
+ )
1363
+ break
1364
+ except TooManyRequestsError as e:
1365
+ await asyncio.sleep(e.retry_after)
1366
+
1367
+ if len(response.data) == limit:
1368
+ next_page = await self.get_group_members(
1369
+ group_id=group_id,
1370
+ limit=limit,
1371
+ tasks=tasks,
1372
+ public_fields=public_fields,
1373
+ last_id=response.data[-1].id,
1374
+ )
1375
+ response.data.extend(next_page.data)
1376
+
1377
+ return response
1378
+
1379
+ async def abort_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1380
+ """Abort an active quest for the party or a specific group.
1381
+
1382
+ Prematurely terminates an ongoing quest, causing all progress to be lost.
1383
+ The quest scroll will be returned to the owner's inventory.
1384
+ Only the quest leader or group leader is allowed to perform this action.
1385
+
1386
+ Parameters
1387
+ ----------
1388
+ group_id : UUID, optional
1389
+ The UUID of the specific group whose quest should be aborted.
1390
+ Defaults to the user's party if not specified.
1391
+
1392
+ Returns
1393
+ -------
1394
+ HabiticaQuestResponse
1395
+ A response object containing updated quest data of the group or party.
1396
+
1397
+ Raises
1398
+ ------
1399
+ NotFoundError
1400
+ If the specified group or quest could not be found.
1401
+ NotAuthorizedError
1402
+ If the user does not have permission to abort the quest.
1403
+ aiohttp.ClientResponseError
1404
+ For HTTP-related errors, such as HTTP 400 or 500 response status.
1405
+ aiohttp.ClientConnectionError
1406
+ If the connection to the API fails.
1407
+ aiohttp.ClientError
1408
+ For any other exceptions raised by aiohttp during the request.
1409
+ TimeoutError
1410
+ If the connection times out.
1411
+
1412
+ Examples
1413
+ --------
1414
+ Abort the party's current quest:
1415
+ >>> response = await habitica.abort_quest()
1416
+ >>> print(response.success) # True if the quest was successfully aborted.
1417
+
1418
+ Abort a quest for a specific group:
1419
+ >>> group_id = UUID("12345678-1234-5678-1234-567812345678")
1420
+ >>> response = await habitica.abort_quest(group_id)
1421
+ >>> print(response.success) # True if the quest was successfully aborted.
1422
+ """
1423
+ group = "party" if not group_id else str(group_id)
1424
+ url = self.url / "api/v3/groups" / group / "quests/abort"
1425
+
1426
+ return HabiticaQuestResponse.from_json(
1427
+ await self._request("post", url=url),
1428
+ )
1429
+
1430
+ async def accept_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1431
+ """Accept a pending invitation to a quest from the party or a specific group.
1432
+
1433
+ Allows a user to accept an invitation to participate in a quest within a
1434
+ specified group.
1435
+
1436
+ Parameters
1437
+ ----------
1438
+ group_id : UUID, optional
1439
+ The UUID of the group for which the quest invitation is being accepted.
1440
+ Defaults to the user's party if not specified.
1441
+
1442
+
1443
+ Returns
1444
+ -------
1445
+ HabiticaQuestResponse
1446
+ A response object containing updated quest data of the group or party.
1447
+
1448
+ Raises
1449
+ ------
1450
+ NotFoundError
1451
+ If the specified group or quest could not be found.
1452
+ aiohttp.ClientResponseError
1453
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1454
+ aiohttp.ClientConnectionError
1455
+ If the connection to the API fails.
1456
+ aiohttp.ClientError
1457
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1458
+ TimeoutError
1459
+ If the connection to the API times out.
1460
+
1461
+ Examples
1462
+ --------
1463
+ Accept a pending quest invitation from the party:
1464
+ >>> response = await habitica.accept_quest()
1465
+ >>> print(response.success) # True if the quest invitation was successfully accepted.
1466
+ """
1467
+ group = "party" if not group_id else str(group_id)
1468
+ url = self.url / "api/v3/groups" / group / "quests/accept"
1469
+
1470
+ return HabiticaQuestResponse.from_json(
1471
+ await self._request("post", url=url),
1472
+ )
1473
+
1474
+ async def reject_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1475
+ """Reject a pending quest invitation from the party or a specific group.
1476
+
1477
+ Allows a user to reject an invitation to participate in a quest within a
1478
+ specified group. The user will not join the quest and will be excluded from
1479
+ its progress and rewards.
1480
+
1481
+ Parameters
1482
+ ----------
1483
+ group_id : UUID, optional
1484
+ The UUID of the group for which the quest invitation is being rejected.
1485
+ Defaults to the user's party if not specified.
1486
+
1487
+
1488
+ Returns
1489
+ -------
1490
+ HabiticaQuestResponse
1491
+ A response object containing updated quest data of the group or party.
1492
+
1493
+ Raises
1494
+ ------
1495
+ NotFoundError
1496
+ If the specified group or quest could not be found.
1497
+ aiohttp.ClientResponseError
1498
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1499
+ aiohttp.ClientConnectionError
1500
+ If the connection to the API fails.
1501
+ aiohttp.ClientError
1502
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1503
+ TimeoutError
1504
+ If the connection to the API times out.
1505
+
1506
+ Examples
1507
+ --------
1508
+ Reject a pending quest invitation from the party:
1509
+ >>> response = await habitica.reject_quest()
1510
+ >>> print(response.success) # True if the quest invitation was successfully rejected.
1511
+
1512
+ Reject a pending quest invitation from a specific group:
1513
+ >>> group_id = UUID("12345678-1234-5678-1234-567812345678")
1514
+ >>> response = await habitica.reject_quest(group_id)
1515
+ >>> print(response.success) # True if the quest invitation was successfully rejected.
1516
+ """
1517
+ group = "party" if not group_id else str(group_id)
1518
+ url = self.url / "api/v3/groups" / group / "quests/reject"
1519
+
1520
+ return HabiticaQuestResponse.from_json(
1521
+ await self._request("post", url=url),
1522
+ )
1523
+
1524
+ async def cancel_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1525
+ """Cancel a pending quest for the party or a specific group.
1526
+
1527
+ Cancel a quest that has not yet startet. All accepted and pending invitations
1528
+ will be canceled and the quest roll returned to the owner's inventory.
1529
+ Only quest leader or group leader can perform this action.
1530
+
1531
+ Parameters
1532
+ ----------
1533
+ group_id : UUID, optional
1534
+ The UUID of the group for which the quest is being canceled.
1535
+ Defaults to the user's party if not specified.
1536
+
1537
+ Returns
1538
+ -------
1539
+ HabiticaQuestResponse
1540
+ A response object containing details about the canceled quest.
1541
+
1542
+ Raises
1543
+ ------
1544
+ NotFoundError
1545
+ If the specified group or quest could not be found.
1546
+ NotAuthorizedError
1547
+ If the user does not have permission to cancel the quest.
1548
+ aiohttp.ClientResponseError
1549
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1550
+ aiohttp.ClientConnectionError
1551
+ If the connection to the API fails.
1552
+ aiohttp.ClientError
1553
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1554
+ TimeoutError
1555
+ If the connection to the API times out.
1556
+
1557
+ Examples
1558
+ --------
1559
+ Cancel a pending quest for the party:
1560
+ >>> response = await habitica.cancel_quest()
1561
+ >>> print(response.success) # True if the quest was successfully canceled.
1562
+ """
1563
+ group = "party" if not group_id else str(group_id)
1564
+ url = self.url / "api/v3/groups" / group / "quests/cancel"
1565
+
1566
+ return HabiticaQuestResponse.from_json(
1567
+ await self._request("post", url=url),
1568
+ )
1569
+
1570
+ async def start_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1571
+ """Force-start a quest for the party or a specific group.
1572
+
1573
+ Begins a quest immediately, bypassing any pending invitations that haven't been
1574
+ accepted or rejected.
1575
+ Only quest leader or group leader can perform this action.
1576
+
1577
+ Parameters
1578
+ ----------
1579
+ group_id : UUID, optional
1580
+ The UUID of the group for which the quest should be started.
1581
+ Defaults to the user's party if not specified.
1582
+
1583
+
1584
+ Returns
1585
+ -------
1586
+ HabiticaQuestResponse
1587
+ A response object containing updated quest data of the group or party.
1588
+
1589
+ Raises
1590
+ ------
1591
+ NotFoundError
1592
+ If the specified group or quest could not be found.
1593
+ NotAuthorizedError
1594
+ If the user does not have permission to start the quest.
1595
+ aiohttp.ClientResponseError
1596
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1597
+ aiohttp.ClientConnectionError
1598
+ If the connection to the API fails.
1599
+ aiohttp.ClientError
1600
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1601
+ TimeoutError
1602
+ If the connection to the API times out.
1603
+
1604
+ Examples
1605
+ --------
1606
+ Cancel a pending quest for the party:
1607
+ >>> response = await habitica.cancel_quest()
1608
+ >>> print(response.success) # True if the quest was successfully canceled.
1609
+ """
1610
+ group = "party" if not group_id else str(group_id)
1611
+ url = self.url / "api/v3/groups" / group / "quests/force-start"
1612
+
1613
+ return HabiticaQuestResponse.from_json(
1614
+ await self._request("post", url=url),
1615
+ )
1616
+
1617
+ async def invite_quest(
1618
+ self,
1619
+ group_id: UUID | None = None,
1620
+ *,
1621
+ quest_key: str,
1622
+ ) -> HabiticaQuestResponse:
1623
+ """Invite members of the party or a specific group to participate in a quest.
1624
+
1625
+ Sends invitations for a quest to all eligible members of the specified group.
1626
+ The quest is started when all members accept or reject the invitation.
1627
+
1628
+ Parameters
1629
+ ----------
1630
+ group_id : UUID, optional
1631
+ The UUID of the group for which the quest invitations should be sent.
1632
+ Defaults to the user's party if not specified.
1633
+ quest_key : str
1634
+ The unique key identifying the quest to invite members to.
1635
+
1636
+ Returns
1637
+ -------
1638
+ HabiticaQuestResponse
1639
+ A response object containing updated quest data of the group or party.
1640
+
1641
+ Raises
1642
+ ------
1643
+ NotFoundError
1644
+ If the specified group or quest could not be found.
1645
+ aiohttp.ClientResponseError
1646
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1647
+ aiohttp.ClientConnectionError
1648
+ If the connection to the API fails.
1649
+ aiohttp.ClientError
1650
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1651
+ TimeoutError
1652
+ If the connection to the API times out.
1653
+
1654
+ Examples
1655
+ --------
1656
+ Send a quest invitation to the party:
1657
+ >>> response = await habitica.invite_quest(quest_key="dilatory_derby")
1658
+ >>> print(response.success) # True if invitations were successfully sent.
1659
+
1660
+ Send a quest invitation to a specific group:
1661
+ >>> group_id = UUID("12345678-1234-5678-1234-567812345678")
1662
+ >>> response = await habitica.invite_quest(group_id, quest_key="golden_knight")
1663
+ >>> print(response.success) # True if invitations were successfully sent.
1664
+ """
1665
+ group = "party" if not group_id else str(group_id)
1666
+ url = self.url / "api/v3/groups" / group / "quests/invite" / quest_key
1667
+
1668
+ return HabiticaQuestResponse.from_json(
1669
+ await self._request("post", url=url),
1670
+ )
1671
+
1672
+ async def leave_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1673
+ """Leave the current quest from the party or a specific group.
1674
+
1675
+ Allows a user to exit an ongoing quest they are part of. This action removes
1676
+ them from the quest but does not affect its progress for other participants.
1677
+ Users who leave a quest will not contribute to its completion or receive rewards.
1678
+
1679
+ Parameters
1680
+ ----------
1681
+ group_id : UUID, optional
1682
+ The UUID of the group associated with the quest the user is leaving.
1683
+ Defaults to the user's party if not specified.
1684
+ quest_key : str
1685
+ The unique key identifying the quest to invite members to.
1686
+
1687
+ Returns
1688
+ -------
1689
+ HabiticaQuestResponse
1690
+ A response object containing updated quest data of the group or party.
1691
+
1692
+ Raises
1693
+ ------
1694
+ NotFoundError
1695
+ If the specified group or quest could not be found.
1696
+ aiohttp.ClientResponseError
1697
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1698
+ aiohttp.ClientConnectionError
1699
+ If the connection to the API fails.
1700
+ aiohttp.ClientError
1701
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1702
+ TimeoutError
1703
+ If the connection to the API times out.
1704
+
1705
+ Examples
1706
+ --------
1707
+ Leave the current quest in the user's party:
1708
+ >>> response = await habitica.leave_quest()
1709
+ >>> print(response.success) # True if the user successfully left the quest.
1710
+
1711
+ Leave the current quest in a specific group:
1712
+ >>> group_id = UUID("12345678-1234-5678-1234-567812345678")
1713
+ >>> response = await habitica.leave_quest(group_id)
1714
+ >>> print(response.success) # True if the user successfully left the quest.
1715
+ """
1716
+ group = "party" if not group_id else str(group_id)
1717
+ url = self.url / "api/v3/groups" / group / "quests/leave"
1718
+
1719
+ return HabiticaQuestResponse.from_json(
1720
+ await self._request("post", url=url),
1721
+ )
1722
+
1283
1723
  def _cache_asset(self, asset: str, asset_data: IO[bytes]) -> None:
1284
1724
  """Cache an asset and maintain the cache size limit by removing older entries.
1285
1725
 
@@ -777,6 +777,13 @@ class HabiticaUserResponse(HabiticaResponse):
777
777
  data: UserData
778
778
 
779
779
 
780
+ @dataclass(kw_only=True)
781
+ class HabiticaGroupMembersResponse(HabiticaResponse):
782
+ """Representation of a group members data response."""
783
+
784
+ data: list[UserData]
785
+
786
+
780
787
  @dataclass(kw_only=True)
781
788
  class CompletedBy:
782
789
  """Task group completedby data."""
@@ -1107,6 +1114,25 @@ class HabiticaTagResponse(HabiticaResponse, DataClassORJSONMixin):
1107
1114
  data: TagsUser
1108
1115
 
1109
1116
 
1117
+ @dataclass(kw_only=True)
1118
+ class QuestData:
1119
+ """Quest data."""
1120
+
1121
+ progress: ProgressQuest = field(default_factory=ProgressQuest)
1122
+ active: bool = False
1123
+ members: dict[str, bool | None]
1124
+ extra: dict | None = None
1125
+ key: str
1126
+ leader: UUID | None = None
1127
+
1128
+
1129
+ @dataclass(kw_only=True)
1130
+ class HabiticaQuestResponse(HabiticaResponse, DataClassORJSONMixin):
1131
+ """Representation of a quest response."""
1132
+
1133
+ data: QuestData
1134
+
1135
+
1110
1136
  @dataclass
1111
1137
  class ChangeClassData:
1112
1138
  """Change class data."""
@@ -1131,6 +1157,13 @@ class HabiticaTaskOrderResponse(HabiticaResponse):
1131
1157
  data: list[UUID] = field(default_factory=list)
1132
1158
 
1133
1159
 
1160
+ @dataclass
1161
+ class HabiticaSleepResponse(HabiticaResponse):
1162
+ """Representation of a sleep response."""
1163
+
1164
+ data: bool
1165
+
1166
+
1134
1167
  class TaskFilter(StrEnum):
1135
1168
  """Enum representing the valid types of tasks for requests."""
1136
1169
 
@@ -1187,7 +1220,7 @@ class Skill(StrEnum):
1187
1220
  VALOROUS_PRESENCE = "valorousPresence"
1188
1221
  INTIMIDATING_GAZE = "intimidate"
1189
1222
  # Rogue skills
1190
- PICKPOCKET = "Pickpocket"
1223
+ PICKPOCKET = "pickPocket"
1191
1224
  BACKSTAB = "backStab"
1192
1225
  TOOLS_OF_THE_TRADE = "toolsOfTrade"
1193
1226
  STEALTH = "stealth"
@@ -1,4 +1,7 @@
1
1
  # serializer version: 1
2
+ # name: test_login
3
+ HabiticaLoginResponse(data=LoginData(id=UUID('a380546a-94be-4b8e-8a0b-23e0d5c03303'), apiToken='cd0e5985-17de-4b4f-849e-5d506c5e4382', newUser=False, username='test-username', passwordResetCode=None), success=True, notifications=[], userV=None, appVersion='5.30.0')
4
+ # ---
2
5
  # name: test_user
3
6
  HabiticaUserResponse(data=UserData(id=UUID('c18e1853-bded-47a9-82e2-adfdad08894d'), preferences=PreferencesUser(hair=HairPreferences(color='red', base=3, bangs=1, beard=0, mustache=0, flower=1), emailNotifications=EmailNotificationsPreferences(unsubscribeFromAll=False, newPM=True, kickedGroup=True, wonChallenge=True, giftedGems=True, giftedSubscription=True, invitedParty=True, invitedGuild=True, questStarted=True, invitedQuest=True, importantAnnouncements=True, weeklyRecaps=True, onboarding=True, majorUpdates=True, subscriptionReminders=True, contentRelease=True), pushNotifications=PushNotificationsPreferences(unsubscribeFromAll=False, newPM=True, wonChallenge=True, giftedGems=True, giftedSubscription=True, invitedParty=True, invitedGuild=True, questStarted=True, invitedQuest=True, majorUpdates=True, mentionParty=True, mentionJoinedGuild=True, mentionUnjoinedGuild=True, partyActivity=True, contentRelease=True), suppressModals=SuppressModalsPreferences(levelUp=False, hatchPet=False, raisePet=False, streak=False), tasks=TasksPreferences(activeFilter=ActiveFilterTask(habit='all', daily='all', todo='remaining', reward='all'), groupByChallenge=False, confirmScoreNotes=False, mirrorGroupTasks=[]), dayStart=0, size='slim', hideHeader=False, skin='915533', shirt='blue', timezoneOffset=-120, sound='rosstavoTheme', chair='none', allocationMode='flat', autoEquip=True, costume=False, dateFormat='MM/dd/yyyy', sleep=False, stickyHeader=True, disableClasses=False, newTaskEdit=False, dailyDueDefaultView=False, advancedCollapsed=False, toolbarCollapsed=False, reverseChatOrder=False, developerMode=False, displayInviteToPartyWhenPartyIs1=True, background='violet', automaticAllocation=None, webhooks={}, improvementCategories=[], timezoneOffsetAtLastCron=None, language='de'), flags=FlagsUser(customizationsNotification=False, tour=TourFlags(intro=-1, classes=-1, stats=-1, tavern=-1, party=-1, guilds=-1, challenges=-1, market=-1, pets=-1, mounts=-1, hall=-1, equipment=-1, groupPlans=-1), showTour=True, tutorial=TutorialFlags(common=CommonTutorial(habits=True, dailies=True, todos=True, rewards=True, party=True, pets=True, gems=True, skills=True, classes=True, tavern=True, equipment=True, items=True, mounts=True, inbox=True, stats=True), ios=IosTutorial(addTask=False, editTask=False, deleteTask=False, filterTask=False, groupPets=False, inviteParty=False, reorderTask=False)), dropsEnabled=False, itemsEnabled=False, lastNewStuffRead='', rewrite=True, classSelected=False, rebirthEnabled=False, levelDrops={}, recaptureEmailsPhase=0, weeklyRecapEmailsPhase=0, lastWeeklyRecap=datetime.datetime(2024, 10, 19, 18, 43, 39, 782000, tzinfo=datetime.timezone.utc), communityGuidelinesAccepted=False, cronCount=0, welcomed=True, armoireEnabled=True, armoireOpened=False, armoireEmpty=False, cardReceived=False, warnedLowHealth=False, verifiedUsername=True, newStuff=False, thirdPartyTools=None, mathUpdates=None, lastFreeRebirth=None, chatRevoked=None, chatShadowMuted=None, lastWeeklyRecapDiscriminator=None, onboardingEmailsPhase=None), auth=AuthUser(local=LocalAuth(email='test@example.com', username='test', lowerCaseUsername='test', has_password=True), timestamps=LocalTimestamps(created=datetime.datetime(2024, 10, 19, 18, 43, 39, 782000, tzinfo=datetime.timezone.utc), loggedin=datetime.datetime(2024, 10, 19, 18, 43, 39, 782000, tzinfo=datetime.timezone.utc), updated=datetime.datetime(2024, 10, 19, 18, 44, 51, 37000, tzinfo=datetime.timezone.utc)), facebook={}, google={}, apple={}), achievements=AchievementsUser(ultimateGearSets=UltimateGearSetsAchievments(healer=False, wizard=False, rogue=False, warrior=False), streak=0, challenges=[], perfect=0, quests=QuestsAchievments(bewilder=None, burnout=None, stressbeast=None, harpy=None, atom3=None, vice3=None, vice1=None, gryphon=None, evilsanta2=None, evilsanta=None, dilatory_derby=None, dilatory=None, atom2=None, atom1=None, dysheartener=None), backToBasics=None, dustDevil=None, primedForPainting=None, completedTask=None, createdTask=None, fedPet=None, hatchedPet=None, purchasedEquipment=None, tickledPink=None, goodAsGold=None, boneCollector=None, seeingRed=None, violetsAreBlue=None, shadyCustomer=None, joinedGuild=None, joinedChallenge=None, partyUp=None), backer=BackerUser(tier=None, npc=None, tokensApplied=None), contributor=ContributorUser(contributions=None, level=None, text=None), permissions=PermissionsUser(fullAccess=None, news=None, userSupport=None, challengeAdmin=None, moderator=None, coupons=None), purchased=PurchasedUser(plan=PlanPurchased(consecutive=ConsecutivePlan(trinkets=0, gemCapExtra=0, offset=0, count=0), mysteryItems=[], gemsBought=0, extraMonths=0, dateUpdated=None, perkMonthCount=-1, quantity=1), txnCount=0, background={'violet': True}, shirt={}, hair={}, skin={}, ads=False, mobileChat=None), history=HistoryUser(todos=[], exp=[]), items=ItemsUser(gear=GearItems(equipped=EquippedGear(weapon='weapon_special_fall2024Warrior', armor='armor_special_fall2024Warrior', head='head_special_fall2024Warrior', shield='shield_special_fall2024Warrior', back='back_mystery_201402', headAccessory='headAccessory_special_pinkHeadband', eyewear='eyewear_special_pinkHalfMoon', body='body_mystery_202003'), costume=EquippedGear(weapon=None, armor='armor_base_0', head='head_base_0', shield='shield_base_0', back=None, headAccessory=None, eyewear=None, body=None), owned={'headAccessory_special_blackHeadband': True, 'headAccessory_special_blueHeadband': True, 'headAccessory_special_greenHeadband': True, 'headAccessory_special_pinkHeadband': True, 'headAccessory_special_redHeadband': True, 'headAccessory_special_whiteHeadband': True, 'headAccessory_special_yellowHeadband': True, 'eyewear_special_blackTopFrame': True, 'eyewear_special_blueTopFrame': True, 'eyewear_special_greenTopFrame': True, 'eyewear_special_pinkTopFrame': True, 'eyewear_special_redTopFrame': True, 'eyewear_special_whiteTopFrame': True, 'eyewear_special_yellowTopFrame': True, 'eyewear_special_blackHalfMoon': True, 'eyewear_special_blueHalfMoon': True, 'eyewear_special_greenHalfMoon': True, 'eyewear_special_pinkHalfMoon': True, 'eyewear_special_redHalfMoon': True, 'eyewear_special_whiteHalfMoon': True, 'eyewear_special_yellowHalfMoon': True}), special=SpecialItems(birthdayReceived=[], birthday=0, thankyouReceived=[], thankyou=0, greetingReceived=[], greeting=0, nyeReceived=[], nye=0, valentineReceived=[], valentine=0, seafoam=0, shinySeed=0, spookySparkles=0, snowball=0, congrats=0, congratsReceived=[], getwell=0, getwellReceived=[], goodluck=0, goodluckReceived=[]), lastDrop=LastDropItems(count=0, date=datetime.datetime(2024, 10, 19, 18, 43, 39, 784000, tzinfo=datetime.timezone.utc)), currentMount='Velociraptor-Base', currentPet='Rat-Shade', quests={'dustbunnies': 1}, mounts={}, food={}, hatchingPotions={}, eggs={}, pets={}), invitations=InvitationsUser(party={}, guilds=[], parties=[]), party=PartyUser(quest=QuestParty(progress=ProgressQuest(up=0.0, down=0.0, collect={}, collectedItems=0), RSVPNeeded=False, key=None, completed=None), order='level', orderAscending='ascending', _id=None), profile=ProfileUser(blurb=None, imageUrl=None, name='test'), stats=StatsUser(buffs=BuffsStats(Str=0, per=0, con=0, stealth=0, streaks=False, seafoam=False, shinySeed=False, snowball=False, spookySparkles=False, Int=0), training=TrainingStats(Str=0.0, per=0, con=0, Int=0), hp=50.0, mp=10.0, exp=0, gp=0.0, lvl=1, Class=<HabiticaClass.WARRIOR: 'warrior'>, points=0, Str=0, con=0, per=0, toNextLevel=25, maxHealth=50, maxMP=30, Int=0), notifications=[], tags=[TagsUser(id=UUID('5358c71f-fe0f-4583-84a3-109b70c699fd'), name='Arbeit', challenge=None, group=None), TagsUser(id=UUID('45870b8b-3e1e-4e13-a722-6afcacdf689f'), name='Training', challenge=None, group=None), TagsUser(id=UUID('3e49b9db-0cc5-4070-b860-e16f1e4806a4'), name='Gesundheit + Wohlbefinden', challenge=None, group=None), TagsUser(id=UUID('0d85319e-7b9c-43e2-81ed-2a706521d5e3'), name='Schule', challenge=None, group=None), TagsUser(id=UUID('b7cf86bf-97ac-42d3-b5d0-5ffe998b62b4'), name='Teams', challenge=None, group=None), TagsUser(id=UUID('c584d792-0a9b-4646-a6ca-a67bb6ef3c36'), name='Hausarbeiten', challenge=None, group=None), TagsUser(id=UUID('0602fd13-edf2-46a7-99c0-c5084046efbb'), name='Kreativität', challenge=None, group=None)], inbox=InboxUser(newMessages=0, optOut=False, blocks=[], messages={}), tasksOrder=TasksOrderUser(habits=[UUID('21b10675-e238-4462-be11-1a4f8012fc9c'), UUID('69909140-a920-4b83-b018-b9156760aca3'), UUID('7e0679cf-e9cd-4604-b429-d061ea16ce72'), UUID('270df162-d47d-488b-9c1d-4fc4f0e2b2d2'), UUID('80ad00b5-622e-4d1a-af4d-6a6c199f3571'), UUID('0712669d-8374-4211-84ea-715cbcbae9c1'), UUID('56dee98d-038a-43f3-a514-59b1ed9a52ee'), UUID('25df4765-754c-47b1-a011-83b31f70c4fb')], dailys=[UUID('e22e9ef0-15be-478a-81c7-f9fdbb4fac10'), UUID('2a74baa2-3acd-426c-bce4-6b4bb145a8d5'), UUID('e18d0899-7c59-41c0-affd-318b8850c3e5'), UUID('3d0cecac-73c1-462b-ab39-a39cea107e6b'), UUID('1ed21cd0-e6f5-4707-b1b4-fb376d462387'), UUID('9117ad5e-2cda-43e0-a07c-0b175d32c0eb')], todos=[UUID('ac13c62c-9375-4809-a357-0e42b01a1b43'), UUID('2388ccad-0387-4ea6-9968-2d10d994e903'), UUID('5c6efb91-6ae3-4cad-8b19-2dd2fc52a965'), UUID('80afe0b9-e367-4473-8117-b8bc7115f54a'), UUID('4cc21c36-886c-4a43-b580-877e958a37fd'), UUID('10a45996-7133-4ca1-84d8-abf1093d9dcc'), UUID('fb867459-ae50-4cee-9017-c13206c6dfa2')], rewards=[UUID('4b18ccf6-3934-43a5-a153-31af8b2a36d9')]), extra={}, pushDevices=[], webhooks=[], loginIncentives=0, invitesSent=0, pinnedItems=[PinnedItemsUser(path='gear.flat.weapon_warrior_0', Type='marketGear'), PinnedItemsUser(path='gear.flat.armor_warrior_1', Type='marketGear'), PinnedItemsUser(path='gear.flat.shield_warrior_1', Type='marketGear'), PinnedItemsUser(path='gear.flat.head_warrior_1', Type='marketGear'), PinnedItemsUser(path='potion', Type='potion'), PinnedItemsUser(path='armoire', Type='armoire')], pinnedItemsOrder=[], unpinnedItems=[], secret=None, balance=0.0, lastCron=datetime.datetime(2024, 10, 19, 18, 43, 39, 784000, tzinfo=datetime.timezone.utc), needsCron=False, challenges=[], guilds=[], newMessages={}), success=True, notifications=[], userV=5, appVersion='5.28.8')
4
7
  # ---
@@ -0,0 +1,10 @@
1
+ {
2
+ "success": true,
3
+ "data": {
4
+ "id": "a380546a-94be-4b8e-8a0b-23e0d5c03303",
5
+ "apiToken": "cd0e5985-17de-4b4f-849e-5d506c5e4382",
6
+ "newUser": false,
7
+ "username": "test-username"
8
+ },
9
+ "appVersion": "5.30.0"
10
+ }
@@ -9,6 +9,18 @@ from habiticalib import Habitica
9
9
  from .conftest import load_fixture
10
10
 
11
11
 
12
+ async def test_login(mock_aiohttp: aioresponses, snapshot: SnapshotAssertion) -> None:
13
+ """Test login."""
14
+ mock_aiohttp.post(
15
+ "https://habitica.com/api/v3/user/auth/local/login",
16
+ body=load_fixture("login.json"),
17
+ )
18
+ async with ClientSession() as session:
19
+ habitica = Habitica(session, "test", "test")
20
+ response = await habitica.login("test-username", "test-password")
21
+ assert response == snapshot
22
+
23
+
12
24
  async def test_user(mock_aiohttp: aioresponses, snapshot: SnapshotAssertion) -> None:
13
25
  """Test default user agent is set."""
14
26
  mock_aiohttp.get("https://habitica.com/api/v3/user", body=load_fixture("user.json"))
File without changes
File without changes
File without changes
File without changes
File without changes