Habiticalib 0.1.0a1__py3-none-any.whl → 0.1.0a3__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/__init__.py CHANGED
@@ -1,12 +1,71 @@
1
1
  """Modern asynchronous Python client library for the Habitica API."""
2
2
 
3
- from .exceptions import HabiticaException, NotAuthorizedError, NotFoundError
3
+ from .const import ASSETS_URL, DEFAULT_URL, __version__
4
+ from .exceptions import (
5
+ BadRequestError,
6
+ HabiticaException,
7
+ NotAuthorizedError,
8
+ NotFoundError,
9
+ TooManyRequestsError,
10
+ )
4
11
  from .lib import Habitica
12
+ from .types import (
13
+ Attributes,
14
+ Direction,
15
+ Frequency,
16
+ HabiticaClass,
17
+ HabiticaClassSystemResponse,
18
+ HabiticaErrorResponse,
19
+ HabiticaLoginResponse,
20
+ HabiticaResponse,
21
+ HabiticaScoreResponse,
22
+ HabiticaStatsResponse,
23
+ HabiticaTagResponse,
24
+ HabiticaTagsResponse,
25
+ HabiticaTaskOrderResponse,
26
+ HabiticaTaskResponse,
27
+ HabiticaTasksResponse,
28
+ HabiticaUserExport,
29
+ HabiticaUserResponse,
30
+ Language,
31
+ Skill,
32
+ Task,
33
+ TaskFilter,
34
+ TaskType,
35
+ UserStyles,
36
+ )
5
37
 
6
- __version__ = "0.1.0a1"
7
38
  __all__ = [
39
+ "__version__",
40
+ "ASSETS_URL",
41
+ "Attributes",
42
+ "BadRequestError",
43
+ "DEFAULT_URL",
44
+ "Direction",
45
+ "Frequency",
8
46
  "Habitica",
47
+ "HabiticaClass",
48
+ "HabiticaClassSystemResponse",
49
+ "HabiticaErrorResponse",
9
50
  "HabiticaException",
51
+ "HabiticaLoginResponse",
52
+ "HabiticaResponse",
53
+ "HabiticaScoreResponse",
54
+ "HabiticaStatsResponse",
55
+ "HabiticaTagResponse",
56
+ "HabiticaTagsResponse",
57
+ "HabiticaTaskOrderResponse",
58
+ "HabiticaTaskResponse",
59
+ "HabiticaTasksResponse",
60
+ "HabiticaUserExport",
61
+ "HabiticaUserResponse",
62
+ "Language",
10
63
  "NotAuthorizedError",
11
64
  "NotFoundError",
65
+ "Skill",
66
+ "Task",
67
+ "TaskFilter",
68
+ "TaskType",
69
+ "TooManyRequestsError",
70
+ "UserStyles",
12
71
  ]
habiticalib/const.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Constants for Habiticalib."""
2
2
 
3
+ __version__ = "0.1.0a3"
4
+
3
5
  DEFAULT_URL = "https://habitica.com/"
4
6
  ASSETS_URL = "https://habitica-assets.s3.amazonaws.com/mobileApp/images/"
5
7
 
habiticalib/exceptions.py CHANGED
@@ -1,16 +1,35 @@
1
1
  """Exceptions for Habiticalib."""
2
2
 
3
+ from datetime import datetime
3
4
  from typing import Self
4
5
 
6
+ from multidict import CIMultiDictProxy
7
+
5
8
  from habiticalib.types import HabiticaErrorResponse
6
9
 
7
10
 
8
11
  class HabiticaException(Exception): # noqa: N818
9
12
  """Base class for Habitica errors."""
10
13
 
11
- def __init__(self: Self, error: HabiticaErrorResponse) -> None:
14
+ def __init__(
15
+ self: Self,
16
+ error: HabiticaErrorResponse,
17
+ headers: CIMultiDictProxy,
18
+ ) -> None:
12
19
  """Initialize the Exception."""
13
20
  self.error = error
21
+ self.rate_limit: int | None = (
22
+ int(r) if (r := headers.get("x-ratelimit-limit")) else None
23
+ )
24
+ self.rate_limit_remaining: int | None = (
25
+ int(r) if (r := headers.get("x-ratelimit-remaining")) else None
26
+ )
27
+ self.rate_limit_reset: datetime | None = (
28
+ datetime.strptime(r[:33], "%a %b %d %Y %H:%M:%S %Z%z")
29
+ if (r := headers.get("x-ratelimit-reset"))
30
+ else None
31
+ )
32
+ self.retry_after: int = round(float(headers.get("retry-after", 0)))
14
33
 
15
34
  super().__init__(error.message)
16
35
 
@@ -25,3 +44,7 @@ class NotFoundError(HabiticaException):
25
44
 
26
45
  class BadRequestError(HabiticaException):
27
46
  """BadRequest error."""
47
+
48
+
49
+ class TooManyRequestsError(HabiticaException):
50
+ """TooManyRequests error."""
habiticalib/helpers.py CHANGED
@@ -1,13 +1,15 @@
1
1
  """Helper functions for Habiticalib."""
2
2
 
3
- from dataclasses import asdict
4
- from importlib.metadata import version
3
+ from dataclasses import asdict, is_dataclass
4
+ from datetime import date, datetime
5
+ from enum import Enum
5
6
  import platform
7
+ from typing import Any
6
8
  import uuid
7
9
 
8
10
  import aiohttp
9
11
 
10
- from .const import DEVELOPER_ID
12
+ from .const import DEVELOPER_ID, __version__
11
13
  from .types import HabiticaUserResponse, UserData, UserStyles
12
14
 
13
15
 
@@ -58,7 +60,7 @@ def get_user_agent() -> str:
58
60
  os_info = f"{os_name} {os_release} ({os_version}); {arch}"
59
61
 
60
62
  return (
61
- f"Habiticalib/{version("habiticalib")} ({os_info}) "
63
+ f"Habiticalib/{__version__} ({os_info}) "
62
64
  f"aiohttp/{aiohttp.__version__} Python/{platform.python_version()} "
63
65
  " +https://github.com/tr4nt0r/habiticalib)"
64
66
  )
@@ -106,10 +108,31 @@ def get_x_client(x_client: str | None = None) -> str:
106
108
 
107
109
  return x_client
108
110
 
109
- return f"{DEVELOPER_ID} - Habiticalib/{version("habiticalib")}"
111
+ return f"{DEVELOPER_ID} - Habiticalib/{__version__}"
110
112
 
111
113
 
112
114
  def extract_user_styles(user_data: HabiticaUserResponse) -> UserStyles:
113
115
  """Extract user styles from a user data object."""
114
116
  data: UserData = user_data.data
115
117
  return UserStyles.from_dict(asdict(data))
118
+
119
+
120
+ def deserialize_task(value: Any) -> Any: # noqa: PLR0911
121
+ """Recursively convert Enums to values, dates to ISO strings, UUIDs to strings."""
122
+
123
+ if is_dataclass(value) and not isinstance(value, type):
124
+ # Convert dataclass to dict and recursively deserialize
125
+ return deserialize_task(asdict(value))
126
+ if isinstance(value, Enum):
127
+ return value.value # Convert Enum to its value
128
+ if isinstance(value, uuid.UUID):
129
+ return str(value) # Convert UUID to string
130
+ if isinstance(value, datetime | date):
131
+ return value.isoformat() # Convert datetime/date to ISO string
132
+ if isinstance(value, list):
133
+ # Recursively apply deserialization to each item in the list
134
+ return [deserialize_task(item) for item in value]
135
+ if isinstance(value, dict):
136
+ # Recursively apply deserialization to each key-value pair in the dictionary
137
+ return {k: deserialize_task(v) for k, v in value.items()}
138
+ return value # Return other types unchanged
habiticalib/lib.py CHANGED
@@ -13,12 +13,23 @@ from PIL import Image
13
13
  from yarl import URL
14
14
 
15
15
  from .const import ASSETS_URL, BACKER_ONLY_GEAR, DEFAULT_URL
16
- from .exceptions import BadRequestError, NotAuthorizedError, NotFoundError
17
- from .helpers import extract_user_styles, get_user_agent, get_x_client, join_fields
16
+ from .exceptions import (
17
+ BadRequestError,
18
+ NotAuthorizedError,
19
+ NotFoundError,
20
+ TooManyRequestsError,
21
+ )
22
+ from .helpers import (
23
+ deserialize_task,
24
+ extract_user_styles,
25
+ get_user_agent,
26
+ get_x_client,
27
+ join_fields,
28
+ )
18
29
  from .types import (
19
30
  Attributes,
20
- Class,
21
31
  Direction,
32
+ HabiticaClass,
22
33
  HabiticaClassSystemResponse,
23
34
  HabiticaErrorResponse,
24
35
  HabiticaLoginResponse,
@@ -87,7 +98,7 @@ class Habitica:
87
98
  msg = "Both 'api_user' and 'api_key' must be provided together."
88
99
  raise ValueError(msg)
89
100
 
90
- self.url = URL(url if url else DEFAULT_URL) / "api"
101
+ self.url = URL(url if url else DEFAULT_URL)
91
102
 
92
103
  self._assets_cache: dict[str, IO[bytes]] = {}
93
104
  self._cache_order: list[str] = []
@@ -102,15 +113,19 @@ class Habitica:
102
113
  ) as r:
103
114
  if r.status == HTTPStatus.UNAUTHORIZED:
104
115
  raise NotAuthorizedError(
105
- HabiticaErrorResponse.from_json(await r.text()),
116
+ HabiticaErrorResponse.from_json(await r.text()), r.headers
106
117
  )
107
118
  if r.status == HTTPStatus.NOT_FOUND:
108
119
  raise NotFoundError(
109
- HabiticaErrorResponse.from_json(await r.text()),
120
+ HabiticaErrorResponse.from_json(await r.text()), r.headers
110
121
  )
111
122
  if r.status == HTTPStatus.BAD_REQUEST:
112
123
  raise BadRequestError(
113
- HabiticaErrorResponse.from_json(await r.text()),
124
+ HabiticaErrorResponse.from_json(await r.text()), r.headers
125
+ )
126
+ if r.status == HTTPStatus.TOO_MANY_REQUESTS:
127
+ raise TooManyRequestsError(
128
+ HabiticaErrorResponse.from_json(await r.text()), r.headers
114
129
  )
115
130
  r.raise_for_status()
116
131
  return await r.text()
@@ -169,7 +184,7 @@ class Habitica:
169
184
  >>> response.data.apiToken
170
185
  'api-token'
171
186
  """
172
- url = self.url / "v3/user/auth/local/login"
187
+ url = self.url / "api/v3/user/auth/local/login"
173
188
  data = {
174
189
  "username": username,
175
190
  "password": password,
@@ -229,7 +244,7 @@ class Habitica:
229
244
  >>> response = await habitica.get_user(user_fields="achievements,items.mounts")
230
245
  >>> response.data # Access the returned data from the response
231
246
  """
232
- url = self.url / "v3/user"
247
+ url = self.url / "api/v3/user"
233
248
  params = {}
234
249
 
235
250
  if user_fields:
@@ -290,7 +305,7 @@ class Habitica:
290
305
 
291
306
  >>> await habitica.get_tasks(TaskType.HABITS, due_date=datetime(2024, 10, 15))
292
307
  """
293
- url = self.url / "v3/tasks/user"
308
+ url = self.url / "api/v3/tasks/user"
294
309
  params = {}
295
310
 
296
311
  if task_type:
@@ -334,7 +349,7 @@ class Habitica:
334
349
  >>> task_response = await habitica.get_task(task_id)
335
350
  >>> print(task_response.data) # Displays the retrieved task information
336
351
  """
337
- url = self.url / "v3/tasks" / str(task_id)
352
+ url = self.url / "api/v3/tasks" / str(task_id)
338
353
 
339
354
  return HabiticaTaskResponse.from_json(
340
355
  await self._request("get", url=url),
@@ -369,14 +384,16 @@ class Habitica:
369
384
 
370
385
  Examples
371
386
  --------
372
- >>> new_task = Task(name="New Task", ...)
387
+ >>> new_task = Task(text="New Task", type=TaskType.TODO ...)
373
388
  >>> create_response = await habitica.create_task(new_task)
374
389
  >>> print(create_response.data) # Displays the created task information
375
390
  """
376
- url = self.url / "v3/tasks/user"
391
+ url = self.url / "api/v3/tasks/user"
392
+
393
+ json = deserialize_task(task)
377
394
 
378
395
  return HabiticaTaskResponse.from_json(
379
- await self._request("post", url=url, json=task.to_dict()),
396
+ await self._request("post", url=url, json=json),
380
397
  )
381
398
 
382
399
  async def update_task(self, task_id: UUID, task: Task) -> HabiticaTaskResponse:
@@ -412,14 +429,16 @@ class Habitica:
412
429
  Examples
413
430
  --------
414
431
  >>> task_id = UUID("12345678-1234-5678-1234-567812345678")
415
- >>> updated_task = Task(name="Updated Task", ...)
432
+ >>> updated_task = Task(text="Updated Task", ...)
416
433
  >>> update_response = await habitica.update_task(task_id, updated_task)
417
434
  >>> print(update_response.data) # Displays the updated task information
418
435
  """
419
- url = self.url / "v3/tasks" / str(task_id)
436
+ url = self.url / "api/v3/tasks" / str(task_id)
437
+
438
+ json = deserialize_task(task)
420
439
 
421
440
  return HabiticaTaskResponse.from_json(
422
- await self._request("put", url=url, json=task.to_dict()),
441
+ await self._request("put", url=url, json=json),
423
442
  )
424
443
 
425
444
  async def delete_task(self, task_id: UUID) -> HabiticaResponse:
@@ -455,7 +474,7 @@ class Habitica:
455
474
  >>> delete_response = await habitica.delete_task(task_id)
456
475
  >>> print(delete_response.success) # True if successfully deleted
457
476
  """
458
- url = self.url / "v3/tasks" / str(task_id)
477
+ url = self.url / "api/v3/tasks" / str(task_id)
459
478
 
460
479
  return HabiticaResponse.from_json(
461
480
  await self._request("delete", url=url),
@@ -497,7 +516,7 @@ class Habitica:
497
516
  >>> reorder_response = await habitica.reorder_task(task_id, 2)
498
517
  >>> print(reorder_response.data) # Displays a list of task IDs in the new order
499
518
  """
500
- url = self.url / "v3/tasks" / str(task_id) / "move/to" / str(to)
519
+ url = self.url / "api/v3/tasks" / str(task_id) / "move/to" / str(to)
501
520
 
502
521
  return HabiticaTaskOrderResponse.from_json(
503
522
  await self._request("post", url=url),
@@ -531,7 +550,7 @@ class Habitica:
531
550
  TimeoutError
532
551
  If the connection times out.
533
552
  """
534
- url = self.url.parent / "export/userdata.json"
553
+ url = self.url / "export/userdata.json"
535
554
 
536
555
  return HabiticaUserExport.from_json(
537
556
  await self._request("get", url=url),
@@ -576,7 +595,7 @@ class Habitica:
576
595
  TimeoutError
577
596
  If the connection times out.
578
597
  """
579
- url = self.url / "v3/content"
598
+ url = self.url / "api/v3/content"
580
599
  params = {}
581
600
 
582
601
  if language:
@@ -612,7 +631,7 @@ class Habitica:
612
631
  TimeoutError
613
632
  If the connection times out.
614
633
  """
615
- url = self.url / "v3/cron"
634
+ url = self.url / "api/v3/cron"
616
635
  return HabiticaResponse.from_json(await self._request("post", url=url))
617
636
 
618
637
  async def allocate_single_stat_point(
@@ -656,7 +675,7 @@ class Habitica:
656
675
  Allocate a single stat point to Strength (default):
657
676
  >>> await habitica.allocate_single_stat_point()
658
677
  """
659
- url = self.url / "v3/user/allocate"
678
+ url = self.url / "api/v3/user/allocate"
660
679
  params = {"stat": stat}
661
680
 
662
681
  return HabiticaStatsResponse.from_json(
@@ -690,7 +709,7 @@ class Habitica:
690
709
  TimeoutError
691
710
  If the connection times out.
692
711
  """
693
- url = self.url / "v3/user/allocate-now"
712
+ url = self.url / "api/v3/user/allocate-now"
694
713
 
695
714
  return HabiticaStatsResponse.from_json(
696
715
  await self._request("post", url=url),
@@ -743,7 +762,7 @@ class Habitica:
743
762
  Allocate 2 points to INT and 1 point to STR:
744
763
  >>> await allocate_bulk_stat_points(int_points=2, str_points=1)
745
764
  """
746
- url = self.url / "v3/user/allocate-bulk"
765
+ url = self.url / "api/v3/user/allocate-bulk"
747
766
  json = {
748
767
  "stats": {
749
768
  "int": int_points,
@@ -782,7 +801,7 @@ class Habitica:
782
801
  TimeoutError
783
802
  If the connection times out.
784
803
  """
785
- url = self.url / "v3/user/buy-health-potion"
804
+ url = self.url / "api/v3/user/buy-health-potion"
786
805
 
787
806
  return HabiticaStatsResponse.from_json(
788
807
  await self._request("post", url=url),
@@ -790,14 +809,14 @@ class Habitica:
790
809
 
791
810
  async def cast_skill(
792
811
  self,
793
- spell: Skill,
812
+ skill: Skill,
794
813
  target_id: UUID | None = None,
795
814
  ) -> HabiticaUserResponse:
796
815
  """Cast a skill (spell) in Habitica, optionally targeting a specific user, task or party.
797
816
 
798
817
  Parameters
799
818
  ----------
800
- spell : Skill
819
+ skill : Skill
801
820
  The skill (or spell) to be cast. This should be a valid `Skill` enum value.
802
821
  target_id : UUID, optional
803
822
  The unique identifier of the target for the skill. If the skill does not require a target,
@@ -823,7 +842,7 @@ class Habitica:
823
842
  TimeoutError
824
843
  If the connection times out.
825
844
  """
826
- url = self.url / "v3/class/cast" / spell
845
+ url = self.url / "api/v3/user/class/cast" / skill
827
846
  params = {}
828
847
 
829
848
  if target_id:
@@ -854,7 +873,7 @@ class Habitica:
854
873
  TimeoutError
855
874
  If the connection times out.
856
875
  """
857
- url = self.url / "v3/user/sleep"
876
+ url = self.url / "api/v3/user/sleep"
858
877
 
859
878
  return HabiticaResponse.from_json(await self._request("post", url=url))
860
879
 
@@ -876,11 +895,11 @@ class Habitica:
876
895
  TimeoutError
877
896
  If the connection times out.
878
897
  """
879
- url = self.url / "v3/user/revive"
898
+ url = self.url / "api/v3/user/revive"
880
899
 
881
900
  return HabiticaResponse.from_json(await self._request("post", url=url))
882
901
 
883
- async def change_class(self, Class: Class) -> HabiticaClassSystemResponse: # noqa: N803
902
+ async def change_class(self, Class: HabiticaClass) -> HabiticaClassSystemResponse: # noqa: N803
884
903
  """Change the user's class in Habitica.
885
904
 
886
905
  This method sends a request to the Habitica API to change the user's class
@@ -911,11 +930,11 @@ class Habitica:
911
930
 
912
931
  Examples
913
932
  --------
914
- >>> new_class = Class.WARRIOR
933
+ >>> new_class = HabiticaClass.WARRIOR
915
934
  >>> change_response = await habitica.change_class(new_class)
916
935
  >>> print(change_response.data.stats) # Displays the user's stats after class change
917
936
  """
918
- url = self.url / "v3/user/change-class"
937
+ url = self.url / "api/v3/user/change-class"
919
938
  params = {"class": Class.value}
920
939
 
921
940
  return HabiticaClassSystemResponse.from_json(
@@ -948,7 +967,7 @@ class Habitica:
948
967
  >>> disable_response = await habitica.disable_classes()
949
968
  >>> print(disable_response.data.stats) # Displays the user's stats after disabling the class system
950
969
  """
951
- url = self.url / "v3/user/disable-classes"
970
+ url = self.url / "api/v3/user/disable-classes"
952
971
 
953
972
  return HabiticaClassSystemResponse.from_json(
954
973
  await self._request("post", url=url)
@@ -981,7 +1000,7 @@ class Habitica:
981
1000
  >>> delete_response = await habitica.delete_completed_todos()
982
1001
  >>> print(delete_response.success) # True if successfully cleared completed to-dos
983
1002
  """
984
- url = self.url / "v3/tasks/clearCompletedTodos"
1003
+ url = self.url / "api/v3/tasks/clearCompletedTodos"
985
1004
 
986
1005
  return HabiticaClassSystemResponse.from_json(
987
1006
  await self._request("post", url=url)
@@ -1023,7 +1042,7 @@ class Habitica:
1023
1042
  TimeoutError
1024
1043
  If the connection times out.
1025
1044
  """
1026
- url = self.url / "v3/tasks" / str(task_id) / "score" / direction.value
1045
+ url = self.url / "api/v3/tasks" / str(task_id) / "score" / direction.value
1027
1046
 
1028
1047
  return HabiticaScoreResponse.from_json(
1029
1048
  await self._request("post", url=url),
@@ -1056,10 +1075,10 @@ class Habitica:
1056
1075
  >>> tags_response = await habitica.get_tags()
1057
1076
  >>> print(tags_response.data)
1058
1077
  """
1059
- url = self.url / "v3/tags"
1078
+ url = self.url / "api/v3/tags"
1060
1079
 
1061
1080
  return HabiticaTagsResponse.from_json(
1062
- await self._request("post", url=url),
1081
+ await self._request("get", url=url),
1063
1082
  )
1064
1083
 
1065
1084
  async def get_tag(self, tag_id: UUID) -> HabiticaTagResponse:
@@ -1094,10 +1113,10 @@ class Habitica:
1094
1113
  >>> tag_response = await habitica.get_tag()
1095
1114
  >>> print(tag_response.data)
1096
1115
  """
1097
- url = self.url / "v3/tags" / str(tag_id)
1116
+ url = self.url / "api/v3/tags" / str(tag_id)
1098
1117
 
1099
1118
  return HabiticaTagResponse.from_json(
1100
- await self._request("post", url=url),
1119
+ await self._request("get", url=url),
1101
1120
  )
1102
1121
 
1103
1122
  async def delete_tag(self, tag_id: UUID) -> HabiticaResponse:
@@ -1133,7 +1152,7 @@ class Habitica:
1133
1152
  >>> delete_response = await habitica.delete_tag(tag_id)
1134
1153
  >>> print(delete_response.success) # True if successfully deleted
1135
1154
  """
1136
- url = self.url / "v3/tags" / str(tag_id)
1155
+ url = self.url / "api/v3/tags" / str(tag_id)
1137
1156
 
1138
1157
  return HabiticaTagResponse.from_json(
1139
1158
  await self._request("delete", url=url),
@@ -1172,7 +1191,7 @@ class Habitica:
1172
1191
  >>> new_tag_response = await habitica.create_tag("New Tag Name")
1173
1192
  >>> print(new_tag_response.data.id) # Displays the id of the new tag
1174
1193
  """
1175
- url = self.url / "v3/tags"
1194
+ url = self.url / "api/v3/tags"
1176
1195
  json = {"name": name}
1177
1196
  return HabiticaTagResponse.from_json(
1178
1197
  await self._request("post", url=url, json=json),
@@ -1213,7 +1232,7 @@ class Habitica:
1213
1232
  >>> update_response = await habitica.update_tag(tag_id, "New Tag Name")
1214
1233
  >>> print(update_response.data) # Displays the updated tag information
1215
1234
  """
1216
- url = self.url / "v3/tags" / str(tag_id)
1235
+ url = self.url / "api/v3/tags" / str(tag_id)
1217
1236
  json = {"name": name}
1218
1237
  return HabiticaTagResponse.from_json(
1219
1238
  await self._request("put", url=url, json=json),
@@ -1254,10 +1273,10 @@ class Habitica:
1254
1273
  >>> reorder_response = await habitica.reorder_tag(tag_id, 2)
1255
1274
  >>> print(reorder_response.success) # True if reorder is successful
1256
1275
  """
1257
- url = self.url / "v3/reorder-tags"
1276
+ url = self.url / "api/v3/reorder-tags"
1258
1277
  json = {"tagId": str(tag_id), "to": to}
1259
1278
 
1260
- return HabiticaTagResponse.from_json(
1279
+ return HabiticaResponse.from_json(
1261
1280
  await self._request("post", url=url, json=json),
1262
1281
  )
1263
1282
 
habiticalib/types.py CHANGED
@@ -4,26 +4,27 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  from dataclasses import dataclass, field
7
+ import datetime as dt
7
8
  from datetime import UTC, datetime
8
- from enum import StrEnum
9
- from typing import Any
9
+ from enum import Enum, StrEnum
10
+ from typing import Any, NotRequired, TypedDict
10
11
  from uuid import UUID # noqa: TCH003
11
12
 
12
13
  from mashumaro import field_options
13
- from mashumaro.config import BaseConfig
14
14
  from mashumaro.mixins.orjson import DataClassORJSONMixin
15
15
 
16
16
 
17
17
  def serialize_datetime(date: str | int | None) -> datetime | None:
18
18
  """Convert an iso date to a datetime.date object."""
19
19
  if isinstance(date, int):
20
- datetime.fromtimestamp(date / 1000, tz=UTC)
20
+ return datetime.fromtimestamp(date / 1000, tz=UTC)
21
21
  if isinstance(date, str):
22
22
  try:
23
23
  return datetime.fromisoformat(date)
24
24
  except ValueError:
25
25
  # sometimes nextDue dates are JavaScript datetime strings
26
26
  # instead of iso: "Mon May 06 2024 00:00:00 GMT+0200"
27
+ # This was fixed in Habitica v5.28.9, nextDue dates are now isoformat
27
28
  try:
28
29
  return datetime.strptime(date, "%a %b %d %Y %H:%M:%S %Z%z")
29
30
  except ValueError:
@@ -60,6 +61,7 @@ class LoginData:
60
61
  apiToken: str
61
62
  newUser: bool
62
63
  username: str
64
+ passwordResetCode: str | None = None
63
65
 
64
66
 
65
67
  @dataclass(kw_only=True)
@@ -73,27 +75,27 @@ class HabiticaLoginResponse(HabiticaResponse):
73
75
  class LocalAuth:
74
76
  """Auth local data."""
75
77
 
76
- email: str
77
- username: str
78
- lowerCaseUsername: str
79
- has_password: bool
78
+ email: str | None = None
79
+ username: str | None = None
80
+ lowerCaseUsername: str | None = None
81
+ has_password: bool | None = None
80
82
 
81
83
 
82
84
  @dataclass(kw_only=True)
83
85
  class LocalTimestamps:
84
86
  """Timestamps local data."""
85
87
 
86
- created: datetime
87
- loggedin: datetime
88
- updated: datetime
88
+ created: datetime | None = None
89
+ loggedin: datetime | None = None
90
+ updated: datetime | None = None
89
91
 
90
92
 
91
93
  @dataclass(kw_only=True)
92
94
  class AuthUser:
93
95
  """User auth data."""
94
96
 
95
- local: LocalAuth | None = None
96
- timestamps: LocalTimestamps | None = None
97
+ local: LocalAuth = field(default_factory=LocalAuth)
98
+ timestamps: LocalTimestamps = field(default_factory=LocalTimestamps)
97
99
  facebook: dict | None = None
98
100
  google: dict | None = None
99
101
  apple: dict | None = None
@@ -627,6 +629,15 @@ class TrainingStats:
627
629
  Int: int | None = field(default=None, metadata=field_options(alias="int"))
628
630
 
629
631
 
632
+ class HabiticaClass(StrEnum):
633
+ """Habitica's player classes."""
634
+
635
+ WARRIOR = "warrior"
636
+ ROGUE = "rogue"
637
+ MAGE = "wizard"
638
+ HEALER = "healer"
639
+
640
+
630
641
  @dataclass(kw_only=True)
631
642
  class StatsUser:
632
643
  """Stats user data."""
@@ -638,7 +649,9 @@ class StatsUser:
638
649
  exp: int | None = None
639
650
  gp: float | None = None
640
651
  lvl: int | None = None
641
- Class: str = field(default="warrior", metadata=field_options(alias="class"))
652
+ Class: HabiticaClass | None = field(
653
+ default=None, metadata=field_options(alias="class")
654
+ )
642
655
  points: int | None = None
643
656
  Str: int | None = field(default=None, metadata=field_options(alias="str"))
644
657
  con: int | None = None
@@ -649,6 +662,13 @@ class StatsUser:
649
662
  Int: int | None = field(default=None, metadata=field_options(alias="int"))
650
663
 
651
664
 
665
+ field(
666
+ metadata=field_options(
667
+ deserialize=serialize_datetime,
668
+ )
669
+ )
670
+
671
+
652
672
  @dataclass(kw_only=True)
653
673
  class TagsUser:
654
674
  """Tags user data."""
@@ -673,10 +693,10 @@ class InboxUser:
673
693
  class TasksOrderUser:
674
694
  """TasksOrder user data."""
675
695
 
676
- habits: list[str] = field(default_factory=list)
677
- dailys: list[str] = field(default_factory=list)
678
- todos: list[str] = field(default_factory=list)
679
- rewards: list[str] = field(default_factory=list)
696
+ habits: list[UUID] = field(default_factory=list)
697
+ dailys: list[UUID] = field(default_factory=list)
698
+ todos: list[UUID] = field(default_factory=list)
699
+ rewards: list[UUID] = field(default_factory=list)
680
700
 
681
701
 
682
702
  @dataclass(kw_only=True)
@@ -716,7 +736,7 @@ class UserData:
716
736
 
717
737
  id: UUID | None = None
718
738
  preferences: PreferencesUser = field(default_factory=PreferencesUser)
719
- flags: FlagsUser | None = None
739
+ flags: FlagsUser = field(default_factory=FlagsUser)
720
740
  auth: AuthUser = field(default_factory=AuthUser)
721
741
  achievements: AchievementsUser = field(default_factory=AchievementsUser)
722
742
  backer: BackerUser = field(default_factory=BackerUser)
@@ -745,8 +765,8 @@ class UserData:
745
765
  balance: float | None = None
746
766
  lastCron: datetime | None = None
747
767
  needsCron: bool | None = None
748
- challenges: list[str] = field(default_factory=list)
749
- guilds: list[str] = field(default_factory=list)
768
+ challenges: list[UUID] = field(default_factory=list)
769
+ guilds: list[UUID] = field(default_factory=list)
750
770
  newMessages: dict[str, bool] = field(default_factory=dict)
751
771
 
752
772
 
@@ -849,38 +869,31 @@ class Frequency(StrEnum):
849
869
  YEARLY = "yearly"
850
870
 
851
871
 
852
- @dataclass(kw_only=True)
853
- class Task(DataClassORJSONMixin):
872
+ class Task(TypedDict("Task", {"type": NotRequired[TaskType]}), total=True):
854
873
  """Representation of a task."""
855
874
 
856
- class Config(BaseConfig):
857
- """Config."""
858
-
859
- omit_none = True
860
-
861
- text: str | None = None
862
- attribute: Attributes | None = None
863
- alias: str | None = None
864
- notes: str | None = None
865
- tags: list[UUID] | None = None
866
- collapseChecklist: bool | None = None
867
- date: datetime | None = None
868
- priority: float | None = None
869
- reminders: list[Reminders] | None = None
870
- checklist: list[str] | None = None
871
- task_type: TaskType | None = None
872
- up: bool | None = None
873
- down: bool | None = None
874
- counterUp: int | None = None
875
- counterDown: int | None = None
876
- startDate: datetime | None = None
877
- frequency: Frequency | None = None
878
- everyX: int | None = None
879
- repeat: Repeat | None = None
880
- daysOfMonth: list[int] | None = None
881
- weeksOfMonth: list[int] | None = None
882
- completed: bool | None = None
883
- streak: int | None = None
875
+ text: NotRequired[str]
876
+ attribute: NotRequired[Attributes]
877
+ alias: NotRequired[str]
878
+ notes: NotRequired[str]
879
+ tags: NotRequired[list[UUID]]
880
+ collapseChecklist: NotRequired[bool]
881
+ date: NotRequired[datetime | dt.date | None]
882
+ priority: NotRequired[TaskPriority]
883
+ reminders: NotRequired[list[Reminders]]
884
+ checklist: NotRequired[list[str]]
885
+ up: NotRequired[bool]
886
+ down: NotRequired[bool]
887
+ counterUp: NotRequired[int]
888
+ counterDown: NotRequired[int]
889
+ startDate: NotRequired[datetime | dt.date]
890
+ frequency: NotRequired[Frequency]
891
+ everyX: NotRequired[int]
892
+ repeat: NotRequired[Repeat]
893
+ daysOfMonth: NotRequired[list[int]]
894
+ weeksOfMonth: NotRequired[list[int]]
895
+ completed: NotRequired[bool]
896
+ streak: NotRequired[int]
884
897
 
885
898
 
886
899
  @dataclass(kw_only=True)
@@ -894,7 +907,7 @@ class TaskData:
894
907
  notes: str | None = None
895
908
  tags: list[UUID] | None = None
896
909
  value: float | None = None
897
- priority: float | None = None
910
+ priority: TaskPriority | None = None
898
911
  attribute: Attributes | None = None
899
912
  byHabitica: bool | None = None
900
913
  createdAt: datetime | None = None
@@ -978,7 +991,7 @@ class StatsUserStyles:
978
991
  """Stats user styles data."""
979
992
 
980
993
  buffs: BuffsUserStyles = field(default_factory=BuffsUserStyles)
981
- Class: str = field(default="warrior", metadata=field_options(alias="class"))
994
+ Class: HabiticaClass = field(default=HabiticaClass.WARRIOR)
982
995
 
983
996
 
984
997
  @dataclass(kw_only=True)
@@ -1051,7 +1064,7 @@ class DropTmpScore:
1051
1064
  canDrop: bool | None = None
1052
1065
  value: int | None = None
1053
1066
  key: str | None = None
1054
- type: str | None = None
1067
+ Type: str | None = field(default=None, metadata=field_options(alias="type"))
1055
1068
  dialog: str | None = None
1056
1069
 
1057
1070
 
@@ -1063,12 +1076,21 @@ class TmpScore:
1063
1076
  drop: DropTmpScore = field(default_factory=DropTmpScore)
1064
1077
 
1065
1078
 
1079
+ @dataclass
1080
+ class ScoreData(StatsUser):
1081
+ """Scora data."""
1082
+
1083
+ delta: float | None = None
1084
+ tmp: TmpScore = field(
1085
+ default_factory=TmpScore, metadata=field_options(alias="_tmp")
1086
+ )
1087
+
1088
+
1066
1089
  @dataclass(kw_only=True)
1067
- class HabiticaScoreResponse(StatsUser, DataClassORJSONMixin):
1090
+ class HabiticaScoreResponse(HabiticaResponse, DataClassORJSONMixin):
1068
1091
  """Representation of a score response."""
1069
1092
 
1070
- delta: float | None = None
1071
- _tmp: TmpScore = field(default_factory=TmpScore)
1093
+ data: ScoreData
1072
1094
 
1073
1095
 
1074
1096
  @dataclass(kw_only=True)
@@ -1106,7 +1128,7 @@ class HabiticaClassSystemResponse(HabiticaResponse, DataClassORJSONMixin):
1106
1128
  class HabiticaTaskOrderResponse(HabiticaResponse):
1107
1129
  """Representation of a reorder task response."""
1108
1130
 
1109
- data: TasksOrderUser = field(default_factory=TasksOrderUser)
1131
+ data: list[UUID] = field(default_factory=list)
1110
1132
 
1111
1133
 
1112
1134
  class TaskFilter(StrEnum):
@@ -1186,17 +1208,17 @@ class Skill(StrEnum):
1186
1208
  PETAL_FREE_POTION = "petalFreePotion"
1187
1209
 
1188
1210
 
1189
- class Class(StrEnum):
1190
- """Habitica's player classes."""
1191
-
1192
- WARRIOR = "warrior"
1193
- ROGUE = "rogue"
1194
- MAGE = "mage"
1195
- HEALER = "healer"
1196
-
1197
-
1198
1211
  class Direction(StrEnum):
1199
1212
  """Direction to score a task."""
1200
1213
 
1201
1214
  UP = "up"
1202
1215
  DOWN = "down"
1216
+
1217
+
1218
+ class TaskPriority(Enum):
1219
+ """Task difficulties."""
1220
+
1221
+ TRIVIAL = 0.1
1222
+ EASY = 1
1223
+ MEDIUM = 1.5
1224
+ HARD = 2
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.3
2
+ Name: Habiticalib
3
+ Version: 0.1.0a3
4
+ Summary: Asynchronous Python client library for the Habitica API
5
+ Project-URL: Documentation, https://tr4nt0r.github.io/habiticalib/
6
+ Project-URL: Source, https://github.com/tr4nt0r/habiticalib
7
+ Author-email: Manfred Dennerlein Rodelo <manfred@dennerlein.name>
8
+ License: MIT License
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Requires-Python: >=3.12
13
+ Requires-Dist: aiohttp~=3.9
14
+ Requires-Dist: mashumaro~=3.13
15
+ Requires-Dist: orjson~=3.10
16
+ Requires-Dist: pillow~=11.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Habiticalib
20
+
21
+ <p align="center">
22
+ <em>Modern asynchronous Python client library for the Habitica API</em>
23
+ </p>
24
+
25
+ [![build](https://github.com/tr4nt0r/habiticalib/workflows/Build/badge.svg)](https://github.com/tr4nt0r/habiticalib/actions)
26
+ [![codecov](https://codecov.io/gh/tr4nt0r/habiticalib/graph/badge.svg?token=iEsZ1Ktj7d)](https://codecov.io/gh/tr4nt0r/habiticalib)
27
+ [![PyPI version](https://badge.fury.io/py/habiticalib.svg)](https://badge.fury.io/py/habiticalib)
28
+
29
+ **Habiticalib** is a Python library for interacting with the [Habitica API](https://habitica.com). It provides an organized, typed interface to work with Habitica’s features, including tasks, user data, and avatars. The goal of this library is to simplify integration with Habitica.
30
+
31
+ ## Key features
32
+
33
+ - **Asynchronous**: The library is fully asynchronous, allowing non-blocking API calls.
34
+ - **Fully typed with Dataclasses**: The library is fully typed using Python `dataclasses`. It handles serialization with `mashumaro` and `orjson` for efficient conversion between Habitica API JSON data and Python objects.
35
+ - **Dynamic avatar image generation**: Habiticalib can fetch all necessary assets (like equipped items, pets, and mounts) and combine them into a single avatar image. This image can be saved to disk or returned as a byte buffer for further processing.
36
+ **Fetch user data**: Retrieve and manage user data such as stats, preferences, and items. User data is structured with dataclasses to make it easy to work with.
37
+ - **Task management**: Support for creating, updating, and retrieving Habitica tasks (to-dos, dailies, habits, rewards) is provided.
38
+ - **Task status updates**: The library allows updates for task statuses, habit scoring, and daily completion.
39
+ - **Tags**: Habiticalib supports the creation, updating and deletion of tags.
40
+ - **Stat allocation, class cystem and sleep**: The library offers methods for stat point allocation and switching between Habitica classes. It also provides the ability to disable the class system and pausing damage(resting in the inn)
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install habiticalib
46
+ ```
47
+
48
+ ## Getting started
49
+ Here’s an example to demonstrate basic usage:
50
+
51
+ ```python
52
+ import asyncio
53
+
54
+ from aiohttp import ClientSession
55
+
56
+ from habiticalib import Habitica, TaskType
57
+
58
+
59
+ async def main():
60
+ async with ClientSession() as session:
61
+ habitica = Habitica(session)
62
+
63
+ # Login to Habitica
64
+ habitica.login(username="your_username", password="your_password")
65
+
66
+ # Fetch user data
67
+ user_data = await habitica.user()
68
+ print(f"Your current health: {user_data.stats.hp}")
69
+
70
+ # Fetch all tasks (to-dos, dailies, habits, and rewards)
71
+ tasks = await habitica.get_tasks()
72
+ print("All tasks:")
73
+ for task in tasks:
74
+ print(f"- {task.text} (type: {task.type})")
75
+
76
+ # Fetch only to-dos
77
+ todos = await habitica.get_tasks(task_type=TaskType.TODO)
78
+ print("\nTo-Do tasks:")
79
+ for todo in todos:
80
+ print(f"- {todo.text} (due: {todo.date})")
81
+
82
+ # Fetch only dailies
83
+ dailies = await habitica.tasks(task_type=TaskType.DAILY)
84
+ print("\nDailies:")
85
+ for daily in dailies:
86
+ print(f"- {daily.text}")
87
+
88
+ asyncio.run(main())
89
+ ```
90
+
91
+ ## Documentation
92
+
93
+ For full documentation and detailed usage examples, please visit the [Habiticalib documentation](https://tr4nt0r.github.io/habiticalib/).
94
+
95
+ ## License
96
+
97
+ This project is licensed under the terms of the MIT license.
@@ -0,0 +1,11 @@
1
+ habiticalib/__init__.py,sha256=rwRSTk3JFaSnVKRniTC82tA5JKPSuQ-AD_mAok2QU_g,1561
2
+ habiticalib/const.py,sha256=aeFLubB_xXy_3ChIDmT9Atp_aOe58oOqcAs6vepIsoI,609
3
+ habiticalib/exceptions.py,sha256=oVFCGbHkVn0UpIKIPZPzXfvzs9US4R05ebdEn6cOpqM,1350
4
+ habiticalib/helpers.py,sha256=IRZLYWkDVLI0iVBgBMmvZ6L83KCUl-CnzGhUR_tP6Fg,4576
5
+ habiticalib/lib.py,sha256=MMs3BINdbZUszUy_DCWvFWDZKy2or1UA5R47X3GeUwI,54274
6
+ habiticalib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ habiticalib/types.py,sha256=7xpxpvpDQBxsyuD241vQxeXjk6zuEC4yXzw-Of7G7Bw,33187
8
+ habiticalib-0.1.0a3.dist-info/METADATA,sha256=fmMClL_kOz3sWYotJu9QZT49CfV6sEcq9sq0bfV7YZ8,4156
9
+ habiticalib-0.1.0a3.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
10
+ habiticalib-0.1.0a3.dist-info/licenses/LICENSE,sha256=oIinIOSJ49l1iVIRI3XGXFWt6SF7a83kEFBAY8ORwNI,1084
11
+ habiticalib-0.1.0a3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.26.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,92 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: Habiticalib
3
- Version: 0.1.0a1
4
- Summary: Asynchronous Python client library for the Habitica API
5
- Project-URL: Documentation, https://tr4nt0r.github.io/habiticalib/
6
- Project-URL: Source, https://github.com/tr4nt0r/habiticalib
7
- Author-email: Manfred Dennerlein Rodelo <manfred@dennerlein.name>
8
- License: MIT License
9
- License-File: LICENSE
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python :: 3 :: Only
13
- Requires-Python: >=3.12
14
- Requires-Dist: aiohttp~=3.9
15
- Requires-Dist: mashumaro~=3.13
16
- Requires-Dist: orjson~=3.10
17
- Requires-Dist: pillow~=10.4
18
- Description-Content-Type: text/markdown
19
-
20
- # Habiticalib
21
-
22
- <p align="center">
23
- <em>Modern asynchronous Python client library for the Habitica API</em>
24
- </p>
25
-
26
- [![build](https://github.com/tr4nt0r/habiticalib/workflows/Build/badge.svg)](https://github.com/tr4nt0r/habiticalib/actions)
27
- [![codecov](https://codecov.io/gh/tr4nt0r/habiticalib/branch/master/graph/badge.svg)](https://codecov.io/gh/tr4nt0r/habiticalib)
28
- [![PyPI version](https://badge.fury.io/py/habiticalib.svg)](https://badge.fury.io/py/habiticalib)
29
-
30
- ---
31
-
32
- **Documentation**: <a href="https://tr4nt0r.github.io/habiticalib/" target="_blank">https://tr4nt0r.github.io/habiticalib/</a>
33
-
34
- **Source Code**: <a href="https://github.com/tr4nt0r/habiticalib" target="_blank">https://github.com/tr4nt0r/habiticalib</a>
35
-
36
- ---
37
-
38
- ## Development
39
-
40
- ### Setup environment
41
-
42
- We use [Hatch](https://hatch.pypa.io/latest/install/) to manage the development environment and production build. Ensure it's installed on your system.
43
-
44
- ### Run unit tests
45
-
46
- You can run all the tests with:
47
-
48
- ```bash
49
- hatch run test
50
- ```
51
-
52
- ### Format the code
53
-
54
- Execute the following command to apply linting and check typing:
55
-
56
- ```bash
57
- hatch run lint
58
- ```
59
-
60
- ### Publish a new version
61
-
62
- You can bump the version, create a commit and associated tag with one command:
63
-
64
- ```bash
65
- hatch version patch
66
- ```
67
-
68
- ```bash
69
- hatch version minor
70
- ```
71
-
72
- ```bash
73
- hatch version major
74
- ```
75
-
76
- Your default Git text editor will open so you can add information about the release.
77
-
78
- When you push the tag on GitHub, the workflow will automatically publish it on PyPi and a GitHub release will be created as draft.
79
-
80
- ## Serve the documentation
81
-
82
- You can serve the Mkdocs documentation with:
83
-
84
- ```bash
85
- hatch run docs-serve
86
- ```
87
-
88
- It'll automatically watch for changes in your code.
89
-
90
- ## License
91
-
92
- This project is licensed under the terms of the MIT license.
@@ -1,11 +0,0 @@
1
- habiticalib/__init__.py,sha256=k4L3Ik4KZ7U_mtIseyqGHG0TdBE7iwxgQfazr2zli5U,301
2
- habiticalib/const.py,sha256=HeIl1dPAeAxt1V-9PkPCt60ThAJJWkUKpCzMEEAR-ik,584
3
- habiticalib/exceptions.py,sha256=V7QAPQM4HbGxq7-BYDSdG9Zi2u9F2w3tX8XwualJbWM,601
4
- habiticalib/helpers.py,sha256=1PEq8tjOgotMnGRUcRcFcLCEa7jFWUORD9ZPHEVH2sU,3527
5
- habiticalib/lib.py,sha256=S0wLsSCSSoEtq-nlx9Wy7TsqI20iLBYUZhrDOkq6Z3o,53766
6
- habiticalib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- habiticalib/types.py,sha256=r_5pCRdfZRv7whryAgVOUnqv4qUGfokgq9GqdkdK_y8,32549
8
- habiticalib-0.1.0a1.dist-info/METADATA,sha256=t7hoV7_9JMYFVUqZgjLN9qh3RFuz7CFaPPL24zB0TlY,2471
9
- habiticalib-0.1.0a1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
10
- habiticalib-0.1.0a1.dist-info/licenses/LICENSE,sha256=oIinIOSJ49l1iVIRI3XGXFWt6SF7a83kEFBAY8ORwNI,1084
11
- habiticalib-0.1.0a1.dist-info/RECORD,,