Habiticalib 0.1.0a2__py3-none-any.whl → 0.2.0__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.
habiticalib/lib.py CHANGED
@@ -6,29 +6,40 @@ import asyncio
6
6
  from http import HTTPStatus
7
7
  from io import BytesIO
8
8
  import logging
9
- from typing import IO, TYPE_CHECKING, Self
9
+ from typing import IO, TYPE_CHECKING, Any, Self
10
10
 
11
11
  from aiohttp import ClientError, ClientResponseError, ClientSession
12
+ from habitipy.aio import HabitipyAsync # type: ignore[import-untyped]
12
13
  from PIL import Image
13
14
  from yarl import URL
14
15
 
15
- from .const import ASSETS_URL, BACKER_ONLY_GEAR, DEFAULT_URL
16
+ from .const import ASSETS_URL, BACKER_ONLY_GEAR, DEFAULT_URL, PAGE_LIMIT
16
17
  from .exceptions import (
17
18
  BadRequestError,
18
19
  NotAuthorizedError,
19
20
  NotFoundError,
20
21
  TooManyRequestsError,
21
22
  )
22
- from .helpers import extract_user_styles, get_user_agent, get_x_client, join_fields
23
+ from .helpers import (
24
+ deserialize_task,
25
+ extract_user_styles,
26
+ get_user_agent,
27
+ get_x_client,
28
+ join_fields,
29
+ )
23
30
  from .types import (
24
31
  Attributes,
25
- Class,
26
32
  Direction,
33
+ HabiticaClass,
27
34
  HabiticaClassSystemResponse,
35
+ HabiticaContentResponse,
28
36
  HabiticaErrorResponse,
37
+ HabiticaGroupMembersResponse,
29
38
  HabiticaLoginResponse,
39
+ HabiticaQuestResponse,
30
40
  HabiticaResponse,
31
41
  HabiticaScoreResponse,
42
+ HabiticaSleepResponse,
32
43
  HabiticaStatsResponse,
33
44
  HabiticaTagResponse,
34
45
  HabiticaTagsResponse,
@@ -254,14 +265,15 @@ class Habitica:
254
265
  self,
255
266
  task_type: TaskFilter | None = None,
256
267
  due_date: datetime | None = None,
257
- ) -> HabiticaResponse:
268
+ ) -> HabiticaTasksResponse:
258
269
  """Get the authenticated user's tasks.
259
270
 
260
271
  Parameters
261
272
  ----------
262
273
  task_type : TaskFilter | None
263
274
  The type of task to retrieve, defined in TaskFilter enum.
264
- If `None`, all task types will be retrieved (default is None).
275
+ If `None`, all task types will be retrieved except completed to-dos
276
+ (default is None).
265
277
 
266
278
  due_date : datetime | None
267
279
  Optional date to use for computing the nextDue field for each returned task.
@@ -378,14 +390,16 @@ class Habitica:
378
390
 
379
391
  Examples
380
392
  --------
381
- >>> new_task = Task(name="New Task", ...)
393
+ >>> new_task = Task(text="New Task", type=TaskType.TODO ...)
382
394
  >>> create_response = await habitica.create_task(new_task)
383
395
  >>> print(create_response.data) # Displays the created task information
384
396
  """
385
397
  url = self.url / "api/v3/tasks/user"
386
398
 
399
+ json = deserialize_task(task)
400
+
387
401
  return HabiticaTaskResponse.from_json(
388
- await self._request("post", url=url, json=task.to_dict()),
402
+ await self._request("post", url=url, json=json),
389
403
  )
390
404
 
391
405
  async def update_task(self, task_id: UUID, task: Task) -> HabiticaTaskResponse:
@@ -421,14 +435,16 @@ class Habitica:
421
435
  Examples
422
436
  --------
423
437
  >>> task_id = UUID("12345678-1234-5678-1234-567812345678")
424
- >>> updated_task = Task(name="Updated Task", ...)
438
+ >>> updated_task = Task(text="Updated Task", ...)
425
439
  >>> update_response = await habitica.update_task(task_id, updated_task)
426
440
  >>> print(update_response.data) # Displays the updated task information
427
441
  """
428
442
  url = self.url / "api/v3/tasks" / str(task_id)
429
443
 
444
+ json = deserialize_task(task)
445
+
430
446
  return HabiticaTaskResponse.from_json(
431
- await self._request("put", url=url, json=task.to_dict()),
447
+ await self._request("put", url=url, json=json),
432
448
  )
433
449
 
434
450
  async def delete_task(self, task_id: UUID) -> HabiticaResponse:
@@ -445,7 +461,7 @@ class Habitica:
445
461
  Returns
446
462
  -------
447
463
  HabiticaTaskResponse
448
- A response object containing the data for the deleted task.
464
+ A response containing an empty data object.
449
465
 
450
466
  Raises
451
467
  ------
@@ -549,7 +565,7 @@ class Habitica:
549
565
  async def get_content(
550
566
  self,
551
567
  language: Language | None = None,
552
- ) -> HabiticaResponse:
568
+ ) -> HabiticaContentResponse:
553
569
  """
554
570
  Fetch game content from the Habitica API.
555
571
 
@@ -591,7 +607,7 @@ class Habitica:
591
607
  if language:
592
608
  params.update({"language": language.value})
593
609
 
594
- return HabiticaResponse.from_json(
610
+ return HabiticaContentResponse.from_json(
595
611
  await self._request("get", url=url, params=params),
596
612
  )
597
613
 
@@ -799,14 +815,14 @@ class Habitica:
799
815
 
800
816
  async def cast_skill(
801
817
  self,
802
- spell: Skill,
818
+ skill: Skill,
803
819
  target_id: UUID | None = None,
804
820
  ) -> HabiticaUserResponse:
805
821
  """Cast a skill (spell) in Habitica, optionally targeting a specific user, task or party.
806
822
 
807
823
  Parameters
808
824
  ----------
809
- spell : Skill
825
+ skill : Skill
810
826
  The skill (or spell) to be cast. This should be a valid `Skill` enum value.
811
827
  target_id : UUID, optional
812
828
  The unique identifier of the target for the skill. If the skill does not require a target,
@@ -832,23 +848,23 @@ class Habitica:
832
848
  TimeoutError
833
849
  If the connection times out.
834
850
  """
835
- url = self.url / "api/v3/class/cast" / spell
851
+ url = self.url / "api/v3/user/class/cast" / skill
836
852
  params = {}
837
853
 
838
854
  if target_id:
839
855
  params.update({"targetId": str(target_id)})
840
856
  return HabiticaUserResponse.from_json(
841
- await self._request("post", url=url, json=params),
857
+ await self._request("post", url=url, params=params),
842
858
  )
843
859
 
844
860
  async def toggle_sleep(
845
861
  self,
846
- ) -> HabiticaResponse:
862
+ ) -> HabiticaSleepResponse:
847
863
  """Toggles the user's sleep mode in Habitica.
848
864
 
849
865
  Returns
850
866
  -------
851
- HabiticaResponse
867
+ HabiticaSleepResponse
852
868
  A response object containing the result of the sleep mode toggle,
853
869
  and the new sleep state (True if sleeping, False if not).
854
870
 
@@ -865,7 +881,7 @@ class Habitica:
865
881
  """
866
882
  url = self.url / "api/v3/user/sleep"
867
883
 
868
- return HabiticaResponse.from_json(await self._request("post", url=url))
884
+ return HabiticaSleepResponse.from_json(await self._request("post", url=url))
869
885
 
870
886
  async def revive(
871
887
  self,
@@ -889,7 +905,7 @@ class Habitica:
889
905
 
890
906
  return HabiticaResponse.from_json(await self._request("post", url=url))
891
907
 
892
- async def change_class(self, Class: Class) -> HabiticaClassSystemResponse: # noqa: N803
908
+ async def change_class(self, Class: HabiticaClass) -> HabiticaClassSystemResponse: # noqa: N803
893
909
  """Change the user's class in Habitica.
894
910
 
895
911
  This method sends a request to the Habitica API to change the user's class
@@ -920,7 +936,7 @@ class Habitica:
920
936
 
921
937
  Examples
922
938
  --------
923
- >>> new_class = Class.WARRIOR
939
+ >>> new_class = HabiticaClass.WARRIOR
924
940
  >>> change_response = await habitica.change_class(new_class)
925
941
  >>> print(change_response.data.stats) # Displays the user's stats after class change
926
942
  """
@@ -1068,7 +1084,7 @@ class Habitica:
1068
1084
  url = self.url / "api/v3/tags"
1069
1085
 
1070
1086
  return HabiticaTagsResponse.from_json(
1071
- await self._request("post", url=url),
1087
+ await self._request("get", url=url),
1072
1088
  )
1073
1089
 
1074
1090
  async def get_tag(self, tag_id: UUID) -> HabiticaTagResponse:
@@ -1106,7 +1122,7 @@ class Habitica:
1106
1122
  url = self.url / "api/v3/tags" / str(tag_id)
1107
1123
 
1108
1124
  return HabiticaTagResponse.from_json(
1109
- await self._request("post", url=url),
1125
+ await self._request("get", url=url),
1110
1126
  )
1111
1127
 
1112
1128
  async def delete_tag(self, tag_id: UUID) -> HabiticaResponse:
@@ -1266,10 +1282,447 @@ class Habitica:
1266
1282
  url = self.url / "api/v3/reorder-tags"
1267
1283
  json = {"tagId": str(tag_id), "to": to}
1268
1284
 
1269
- return HabiticaTagResponse.from_json(
1285
+ return HabiticaResponse.from_json(
1270
1286
  await self._request("post", url=url, json=json),
1271
1287
  )
1272
1288
 
1289
+ async def get_group_members(
1290
+ self,
1291
+ group_id: UUID | None = None,
1292
+ *,
1293
+ limit: int | None = None,
1294
+ tasks: bool = False,
1295
+ public_fields: bool = False,
1296
+ last_id: UUID | None = None,
1297
+ ) -> HabiticaGroupMembersResponse:
1298
+ """Get members of the party or a specific group.
1299
+
1300
+ This method retrieves a list of members for a party or a specified group
1301
+ from the Habitica API. Additional options allow including tasks or public
1302
+ through results if necessary to collect all members. If the API rate limit is
1303
+ exceeded, the method will pause for the duration specified in the `retry-after`
1304
+ header and retry the request.
1305
+
1306
+
1307
+ Parameters
1308
+ ----------
1309
+ group_id : UUID, optional
1310
+ The UUID of the group. Defaults to the user's party if not specified.
1311
+ limit : int, optional
1312
+ Maximum number of members per request (default: 30, max: 60).
1313
+ tasks : bool, optional
1314
+ Whether to include tasks associated with the group.
1315
+ public_fields : bool, optional
1316
+ Whether to include all public fields for group members.
1317
+ last_id : UUID, optional
1318
+ For paginated requests, the UUID of the last member retrieved.
1319
+
1320
+ Returns
1321
+ -------
1322
+ HabiticaGroupMembersResponse
1323
+ A response object containing the group member data.
1324
+
1325
+ Raises
1326
+ ------
1327
+ aiohttp.ClientResponseError
1328
+ For HTTP-related errors, such as HTTP 400 or 500 response status.
1329
+ aiohttp.ClientConnectionError
1330
+ If the connection to the API fails.
1331
+ aiohttp.ClientError
1332
+ For any other exceptions raised by aiohttp during the request.
1333
+ TimeoutError
1334
+ If the connection times out.
1335
+
1336
+ Examples
1337
+ --------
1338
+ >>> members_response = await habitica.get_group_members()
1339
+ >>> for member in members_response.data:
1340
+ ... print(member.profile.name)
1341
+ """
1342
+
1343
+ if limit is not None and (limit < 1 or limit > PAGE_LIMIT):
1344
+ msg = f"The 'limit' parameter must be between 1 and {PAGE_LIMIT}."
1345
+ raise ValueError(msg)
1346
+
1347
+ group = "party" if not group_id else str(group_id)
1348
+ url = self.url / "api/v3/groups" / group / "members"
1349
+
1350
+ params: dict[str, str | int] = {}
1351
+
1352
+ if tasks:
1353
+ params["includeTasks"] = "true"
1354
+ if public_fields:
1355
+ params["includeAllPublicFields"] = "true"
1356
+ if last_id:
1357
+ params["lastId"] = str(last_id)
1358
+ if limit:
1359
+ params["limit"] = limit
1360
+
1361
+ while True:
1362
+ try:
1363
+ response = HabiticaGroupMembersResponse.from_json(
1364
+ await self._request("get", url=url, params=params),
1365
+ )
1366
+ break
1367
+ except TooManyRequestsError as e:
1368
+ await asyncio.sleep(e.retry_after)
1369
+
1370
+ if len(response.data) == limit:
1371
+ next_page = await self.get_group_members(
1372
+ group_id=group_id,
1373
+ limit=limit,
1374
+ tasks=tasks,
1375
+ public_fields=public_fields,
1376
+ last_id=response.data[-1].id,
1377
+ )
1378
+ response.data.extend(next_page.data)
1379
+
1380
+ return response
1381
+
1382
+ async def abort_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1383
+ """Abort an active quest for the party or a specific group.
1384
+
1385
+ Prematurely terminates an ongoing quest, causing all progress to be lost.
1386
+ The quest scroll will be returned to the owner's inventory.
1387
+ Only the quest leader or group leader is allowed to perform this action.
1388
+
1389
+ Parameters
1390
+ ----------
1391
+ group_id : UUID, optional
1392
+ The UUID of the specific group whose quest should be aborted.
1393
+ Defaults to the user's party if not specified.
1394
+
1395
+ Returns
1396
+ -------
1397
+ HabiticaQuestResponse
1398
+ A response object containing updated quest data of the group or party.
1399
+
1400
+ Raises
1401
+ ------
1402
+ NotFoundError
1403
+ If the specified group or quest could not be found.
1404
+ NotAuthorizedError
1405
+ If the user does not have permission to abort the quest.
1406
+ aiohttp.ClientResponseError
1407
+ For HTTP-related errors, such as HTTP 400 or 500 response status.
1408
+ aiohttp.ClientConnectionError
1409
+ If the connection to the API fails.
1410
+ aiohttp.ClientError
1411
+ For any other exceptions raised by aiohttp during the request.
1412
+ TimeoutError
1413
+ If the connection times out.
1414
+
1415
+ Examples
1416
+ --------
1417
+ Abort the party's current quest:
1418
+ >>> response = await habitica.abort_quest()
1419
+ >>> print(response.success) # True if the quest was successfully aborted.
1420
+
1421
+ Abort a quest for a specific group:
1422
+ >>> group_id = UUID("12345678-1234-5678-1234-567812345678")
1423
+ >>> response = await habitica.abort_quest(group_id)
1424
+ >>> print(response.success) # True if the quest was successfully aborted.
1425
+ """
1426
+ group = "party" if not group_id else str(group_id)
1427
+ url = self.url / "api/v3/groups" / group / "quests/abort"
1428
+
1429
+ return HabiticaQuestResponse.from_json(
1430
+ await self._request("post", url=url),
1431
+ )
1432
+
1433
+ async def accept_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1434
+ """Accept a pending invitation to a quest from the party or a specific group.
1435
+
1436
+ Allows a user to accept an invitation to participate in a quest within a
1437
+ specified group.
1438
+
1439
+ Parameters
1440
+ ----------
1441
+ group_id : UUID, optional
1442
+ The UUID of the group for which the quest invitation is being accepted.
1443
+ Defaults to the user's party if not specified.
1444
+
1445
+
1446
+ Returns
1447
+ -------
1448
+ HabiticaQuestResponse
1449
+ A response object containing updated quest data of the group or party.
1450
+
1451
+ Raises
1452
+ ------
1453
+ NotFoundError
1454
+ If the specified group or quest could not be found.
1455
+ aiohttp.ClientResponseError
1456
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1457
+ aiohttp.ClientConnectionError
1458
+ If the connection to the API fails.
1459
+ aiohttp.ClientError
1460
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1461
+ TimeoutError
1462
+ If the connection to the API times out.
1463
+
1464
+ Examples
1465
+ --------
1466
+ Accept a pending quest invitation from the party:
1467
+ >>> response = await habitica.accept_quest()
1468
+ >>> print(response.success) # True if the quest invitation was successfully accepted.
1469
+ """
1470
+ group = "party" if not group_id else str(group_id)
1471
+ url = self.url / "api/v3/groups" / group / "quests/accept"
1472
+
1473
+ return HabiticaQuestResponse.from_json(
1474
+ await self._request("post", url=url),
1475
+ )
1476
+
1477
+ async def reject_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1478
+ """Reject a pending quest invitation from the party or a specific group.
1479
+
1480
+ Allows a user to reject an invitation to participate in a quest within a
1481
+ specified group. The user will not join the quest and will be excluded from
1482
+ its progress and rewards.
1483
+
1484
+ Parameters
1485
+ ----------
1486
+ group_id : UUID, optional
1487
+ The UUID of the group for which the quest invitation is being rejected.
1488
+ Defaults to the user's party if not specified.
1489
+
1490
+
1491
+ Returns
1492
+ -------
1493
+ HabiticaQuestResponse
1494
+ A response object containing updated quest data of the group or party.
1495
+
1496
+ Raises
1497
+ ------
1498
+ NotFoundError
1499
+ If the specified group or quest could not be found.
1500
+ aiohttp.ClientResponseError
1501
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1502
+ aiohttp.ClientConnectionError
1503
+ If the connection to the API fails.
1504
+ aiohttp.ClientError
1505
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1506
+ TimeoutError
1507
+ If the connection to the API times out.
1508
+
1509
+ Examples
1510
+ --------
1511
+ Reject a pending quest invitation from the party:
1512
+ >>> response = await habitica.reject_quest()
1513
+ >>> print(response.success) # True if the quest invitation was successfully rejected.
1514
+
1515
+ Reject a pending quest invitation from a specific group:
1516
+ >>> group_id = UUID("12345678-1234-5678-1234-567812345678")
1517
+ >>> response = await habitica.reject_quest(group_id)
1518
+ >>> print(response.success) # True if the quest invitation was successfully rejected.
1519
+ """
1520
+ group = "party" if not group_id else str(group_id)
1521
+ url = self.url / "api/v3/groups" / group / "quests/reject"
1522
+
1523
+ return HabiticaQuestResponse.from_json(
1524
+ await self._request("post", url=url),
1525
+ )
1526
+
1527
+ async def cancel_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1528
+ """Cancel a pending quest for the party or a specific group.
1529
+
1530
+ Cancel a quest that has not yet startet. All accepted and pending invitations
1531
+ will be canceled and the quest roll returned to the owner's inventory.
1532
+ Only quest leader or group leader can perform this action.
1533
+
1534
+ Parameters
1535
+ ----------
1536
+ group_id : UUID, optional
1537
+ The UUID of the group for which the quest is being canceled.
1538
+ Defaults to the user's party if not specified.
1539
+
1540
+ Returns
1541
+ -------
1542
+ HabiticaQuestResponse
1543
+ A response object containing details about the canceled quest.
1544
+
1545
+ Raises
1546
+ ------
1547
+ NotFoundError
1548
+ If the specified group or quest could not be found.
1549
+ NotAuthorizedError
1550
+ If the user does not have permission to cancel the quest.
1551
+ aiohttp.ClientResponseError
1552
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1553
+ aiohttp.ClientConnectionError
1554
+ If the connection to the API fails.
1555
+ aiohttp.ClientError
1556
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1557
+ TimeoutError
1558
+ If the connection to the API times out.
1559
+
1560
+ Examples
1561
+ --------
1562
+ Cancel a pending quest for the party:
1563
+ >>> response = await habitica.cancel_quest()
1564
+ >>> print(response.success) # True if the quest was successfully canceled.
1565
+ """
1566
+ group = "party" if not group_id else str(group_id)
1567
+ url = self.url / "api/v3/groups" / group / "quests/cancel"
1568
+
1569
+ return HabiticaQuestResponse.from_json(
1570
+ await self._request("post", url=url),
1571
+ )
1572
+
1573
+ async def start_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1574
+ """Force-start a quest for the party or a specific group.
1575
+
1576
+ Begins a quest immediately, bypassing any pending invitations that haven't been
1577
+ accepted or rejected.
1578
+ Only quest leader or group leader can perform this action.
1579
+
1580
+ Parameters
1581
+ ----------
1582
+ group_id : UUID, optional
1583
+ The UUID of the group for which the quest should be started.
1584
+ Defaults to the user's party if not specified.
1585
+
1586
+
1587
+ Returns
1588
+ -------
1589
+ HabiticaQuestResponse
1590
+ A response object containing updated quest data of the group or party.
1591
+
1592
+ Raises
1593
+ ------
1594
+ NotFoundError
1595
+ If the specified group or quest could not be found.
1596
+ NotAuthorizedError
1597
+ If the user does not have permission to start the quest.
1598
+ aiohttp.ClientResponseError
1599
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1600
+ aiohttp.ClientConnectionError
1601
+ If the connection to the API fails.
1602
+ aiohttp.ClientError
1603
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1604
+ TimeoutError
1605
+ If the connection to the API times out.
1606
+
1607
+ Examples
1608
+ --------
1609
+ Cancel a pending quest for the party:
1610
+ >>> response = await habitica.cancel_quest()
1611
+ >>> print(response.success) # True if the quest was successfully canceled.
1612
+ """
1613
+ group = "party" if not group_id else str(group_id)
1614
+ url = self.url / "api/v3/groups" / group / "quests/force-start"
1615
+
1616
+ return HabiticaQuestResponse.from_json(
1617
+ await self._request("post", url=url),
1618
+ )
1619
+
1620
+ async def invite_quest(
1621
+ self,
1622
+ group_id: UUID | None = None,
1623
+ *,
1624
+ quest_key: str,
1625
+ ) -> HabiticaQuestResponse:
1626
+ """Invite members of the party or a specific group to participate in a quest.
1627
+
1628
+ Sends invitations for a quest to all eligible members of the specified group.
1629
+ The quest is started when all members accept or reject the invitation.
1630
+
1631
+ Parameters
1632
+ ----------
1633
+ group_id : UUID, optional
1634
+ The UUID of the group for which the quest invitations should be sent.
1635
+ Defaults to the user's party if not specified.
1636
+ quest_key : str
1637
+ The unique key identifying the quest to invite members to.
1638
+
1639
+ Returns
1640
+ -------
1641
+ HabiticaQuestResponse
1642
+ A response object containing updated quest data of the group or party.
1643
+
1644
+ Raises
1645
+ ------
1646
+ NotFoundError
1647
+ If the specified group or quest could not be found.
1648
+ aiohttp.ClientResponseError
1649
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1650
+ aiohttp.ClientConnectionError
1651
+ If the connection to the API fails.
1652
+ aiohttp.ClientError
1653
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1654
+ TimeoutError
1655
+ If the connection to the API times out.
1656
+
1657
+ Examples
1658
+ --------
1659
+ Send a quest invitation to the party:
1660
+ >>> response = await habitica.invite_quest(quest_key="dilatory_derby")
1661
+ >>> print(response.success) # True if invitations were successfully sent.
1662
+
1663
+ Send a quest invitation to a specific group:
1664
+ >>> group_id = UUID("12345678-1234-5678-1234-567812345678")
1665
+ >>> response = await habitica.invite_quest(group_id, quest_key="golden_knight")
1666
+ >>> print(response.success) # True if invitations were successfully sent.
1667
+ """
1668
+ group = "party" if not group_id else str(group_id)
1669
+ url = self.url / "api/v3/groups" / group / "quests/invite" / quest_key
1670
+
1671
+ return HabiticaQuestResponse.from_json(
1672
+ await self._request("post", url=url),
1673
+ )
1674
+
1675
+ async def leave_quest(self, group_id: UUID | None = None) -> HabiticaQuestResponse:
1676
+ """Leave the current quest from the party or a specific group.
1677
+
1678
+ Allows a user to exit an ongoing quest they are part of. This action removes
1679
+ them from the quest but does not affect its progress for other participants.
1680
+ Users who leave a quest will not contribute to its completion or receive rewards.
1681
+
1682
+ Parameters
1683
+ ----------
1684
+ group_id : UUID, optional
1685
+ The UUID of the group associated with the quest the user is leaving.
1686
+ Defaults to the user's party if not specified.
1687
+ quest_key : str
1688
+ The unique key identifying the quest to invite members to.
1689
+
1690
+ Returns
1691
+ -------
1692
+ HabiticaQuestResponse
1693
+ A response object containing updated quest data of the group or party.
1694
+
1695
+ Raises
1696
+ ------
1697
+ NotFoundError
1698
+ If the specified group or quest could not be found.
1699
+ aiohttp.ClientResponseError
1700
+ Raised for HTTP-related errors, such as HTTP 400 or 500 response status.
1701
+ aiohttp.ClientConnectionError
1702
+ If the connection to the API fails.
1703
+ aiohttp.ClientError
1704
+ Raised for any other exceptions encountered by `aiohttp` during the request.
1705
+ TimeoutError
1706
+ If the connection to the API times out.
1707
+
1708
+ Examples
1709
+ --------
1710
+ Leave the current quest in the user's party:
1711
+ >>> response = await habitica.leave_quest()
1712
+ >>> print(response.success) # True if the user successfully left the quest.
1713
+
1714
+ Leave the current quest in a specific group:
1715
+ >>> group_id = UUID("12345678-1234-5678-1234-567812345678")
1716
+ >>> response = await habitica.leave_quest(group_id)
1717
+ >>> print(response.success) # True if the user successfully left the quest.
1718
+ """
1719
+ group = "party" if not group_id else str(group_id)
1720
+ url = self.url / "api/v3/groups" / group / "quests/leave"
1721
+
1722
+ return HabiticaQuestResponse.from_json(
1723
+ await self._request("post", url=url),
1724
+ )
1725
+
1273
1726
  def _cache_asset(self, asset: str, asset_data: IO[bytes]) -> None:
1274
1727
  """Cache an asset and maintain the cache size limit by removing older entries.
1275
1728
 
@@ -1546,3 +1999,33 @@ class Habitica:
1546
1999
  image.save(fp, fmt)
1547
2000
 
1548
2001
  return user_styles
2002
+
2003
+ async def habitipy(self) -> HabitipyAsync:
2004
+ """Create a Habitipy instance."""
2005
+
2006
+ _session = self._session
2007
+ _headers = self._headers
2008
+ loop = asyncio.get_running_loop()
2009
+
2010
+ class HAHabitipyAsync(HabitipyAsync):
2011
+ """Closure API class to hold session."""
2012
+
2013
+ def __call__(self, **kwargs) -> Any:
2014
+ """Pass session to habitipy."""
2015
+ return super().__call__(_session, **kwargs)
2016
+
2017
+ def _make_headers(self) -> dict[str, str]:
2018
+ """Inject headers."""
2019
+ headers = super()._make_headers()
2020
+ headers.update(_headers)
2021
+ return headers
2022
+
2023
+ return await loop.run_in_executor(
2024
+ None,
2025
+ HAHabitipyAsync,
2026
+ {
2027
+ "url": str(self.url),
2028
+ "login": self._headers.get("X-API-USER"),
2029
+ "password": self._headers.get("X-API-KEY"),
2030
+ }, # type: ignore[var-annotated]
2031
+ )