Habiticalib 0.1.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/__init__.py +71 -0
- habiticalib/const.py +16 -0
- habiticalib/exceptions.py +50 -0
- habiticalib/helpers.py +138 -0
- habiticalib/lib.py +1558 -0
- habiticalib/py.typed +0 -0
- habiticalib/types.py +1224 -0
- habiticalib-0.1.0.dist-info/METADATA +97 -0
- habiticalib-0.1.0.dist-info/RECORD +11 -0
- habiticalib-0.1.0.dist-info/WHEEL +4 -0
- habiticalib-0.1.0.dist-info/licenses/LICENSE +22 -0
habiticalib/lib.py
ADDED
@@ -0,0 +1,1558 @@
|
|
1
|
+
"""Modern asynchronous Python client library for the Habitica API."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
from http import HTTPStatus
|
7
|
+
from io import BytesIO
|
8
|
+
import logging
|
9
|
+
from typing import IO, TYPE_CHECKING, Self
|
10
|
+
|
11
|
+
from aiohttp import ClientError, ClientResponseError, ClientSession
|
12
|
+
from PIL import Image
|
13
|
+
from yarl import URL
|
14
|
+
|
15
|
+
from .const import ASSETS_URL, BACKER_ONLY_GEAR, DEFAULT_URL
|
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
|
+
)
|
29
|
+
from .types import (
|
30
|
+
Attributes,
|
31
|
+
Direction,
|
32
|
+
HabiticaClass,
|
33
|
+
HabiticaClassSystemResponse,
|
34
|
+
HabiticaErrorResponse,
|
35
|
+
HabiticaLoginResponse,
|
36
|
+
HabiticaResponse,
|
37
|
+
HabiticaScoreResponse,
|
38
|
+
HabiticaStatsResponse,
|
39
|
+
HabiticaTagResponse,
|
40
|
+
HabiticaTagsResponse,
|
41
|
+
HabiticaTaskOrderResponse,
|
42
|
+
HabiticaTaskResponse,
|
43
|
+
HabiticaTasksResponse,
|
44
|
+
HabiticaUserExport,
|
45
|
+
HabiticaUserResponse,
|
46
|
+
Language,
|
47
|
+
Skill,
|
48
|
+
Task,
|
49
|
+
TaskFilter,
|
50
|
+
UserStyles,
|
51
|
+
)
|
52
|
+
|
53
|
+
if TYPE_CHECKING:
|
54
|
+
from datetime import datetime
|
55
|
+
from uuid import UUID
|
56
|
+
|
57
|
+
_LOGGER = logging.getLogger(__package__)
|
58
|
+
|
59
|
+
|
60
|
+
class Habitica:
|
61
|
+
"""Modern asynchronous Python client library for the Habitica API."""
|
62
|
+
|
63
|
+
_close_session: bool = False
|
64
|
+
_cache_size = 32
|
65
|
+
|
66
|
+
def __init__(
|
67
|
+
self,
|
68
|
+
session: ClientSession | None = None,
|
69
|
+
api_user: str | None = None,
|
70
|
+
api_key: str | None = None,
|
71
|
+
url: str | None = None,
|
72
|
+
x_client: str | None = None,
|
73
|
+
) -> None:
|
74
|
+
"""Initialize the Habitica API client."""
|
75
|
+
client_headers = {"X-CLIENT": get_x_client(x_client)}
|
76
|
+
user_agent = {"User-Agent": get_user_agent()}
|
77
|
+
self._headers: dict[str, str] = {}
|
78
|
+
|
79
|
+
if session:
|
80
|
+
self._session = session
|
81
|
+
if "User-Agent" not in session.headers:
|
82
|
+
self._headers.update(user_agent)
|
83
|
+
self._headers.update(client_headers)
|
84
|
+
else:
|
85
|
+
self._session = ClientSession(
|
86
|
+
headers={**user_agent, **client_headers},
|
87
|
+
)
|
88
|
+
self._close_session = True
|
89
|
+
|
90
|
+
if api_user and api_key:
|
91
|
+
self._headers.update(
|
92
|
+
{
|
93
|
+
"X-API-USER": api_user,
|
94
|
+
"X-API-KEY": api_key,
|
95
|
+
}
|
96
|
+
)
|
97
|
+
elif api_user or api_key:
|
98
|
+
msg = "Both 'api_user' and 'api_key' must be provided together."
|
99
|
+
raise ValueError(msg)
|
100
|
+
|
101
|
+
self.url = URL(url if url else DEFAULT_URL)
|
102
|
+
|
103
|
+
self._assets_cache: dict[str, IO[bytes]] = {}
|
104
|
+
self._cache_order: list[str] = []
|
105
|
+
|
106
|
+
async def _request(self, method: str, url: URL, **kwargs) -> str:
|
107
|
+
"""Handle API request."""
|
108
|
+
async with self._session.request(
|
109
|
+
method,
|
110
|
+
url,
|
111
|
+
headers=self._headers,
|
112
|
+
**kwargs,
|
113
|
+
) as r:
|
114
|
+
if r.status == HTTPStatus.UNAUTHORIZED:
|
115
|
+
raise NotAuthorizedError(
|
116
|
+
HabiticaErrorResponse.from_json(await r.text()), r.headers
|
117
|
+
)
|
118
|
+
if r.status == HTTPStatus.NOT_FOUND:
|
119
|
+
raise NotFoundError(
|
120
|
+
HabiticaErrorResponse.from_json(await r.text()), r.headers
|
121
|
+
)
|
122
|
+
if r.status == HTTPStatus.BAD_REQUEST:
|
123
|
+
raise BadRequestError(
|
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
|
129
|
+
)
|
130
|
+
r.raise_for_status()
|
131
|
+
return await r.text()
|
132
|
+
|
133
|
+
async def __aenter__(self) -> Self:
|
134
|
+
"""Async enter."""
|
135
|
+
return self
|
136
|
+
|
137
|
+
async def __aexit__(self, *exc_info: object) -> None:
|
138
|
+
"""Async exit."""
|
139
|
+
if self._close_session:
|
140
|
+
await self._session.close()
|
141
|
+
|
142
|
+
async def login(
|
143
|
+
self,
|
144
|
+
username: str,
|
145
|
+
password: str,
|
146
|
+
) -> HabiticaLoginResponse:
|
147
|
+
"""Log in a user using their email or username and password.
|
148
|
+
|
149
|
+
This method sends a POST request to the Habitica API to authenticate
|
150
|
+
a user. Upon successful authentication, it updates the headers with
|
151
|
+
the user's API credentials for future requests.
|
152
|
+
|
153
|
+
Parameters
|
154
|
+
----------
|
155
|
+
username : str
|
156
|
+
The user's email or username used for logging in.
|
157
|
+
password : str
|
158
|
+
The user's password for authentication.
|
159
|
+
|
160
|
+
Returns
|
161
|
+
-------
|
162
|
+
HabiticaLoginResponse
|
163
|
+
An object containing the user's authentication details, including
|
164
|
+
user ID and API token.
|
165
|
+
|
166
|
+
Raises
|
167
|
+
------
|
168
|
+
NotAuthorizedError
|
169
|
+
If the login request fails due to incorrect username or password (HTTP 401).
|
170
|
+
aiohttp.ClientResponseError
|
171
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
172
|
+
aiohttp.ClientConnectionError
|
173
|
+
If the connection to the API fails.
|
174
|
+
aiohttp.ClientError
|
175
|
+
For any other exceptions raised by aiohttp during the request.
|
176
|
+
TimeoutError
|
177
|
+
If the connection times out.
|
178
|
+
|
179
|
+
Examples
|
180
|
+
--------
|
181
|
+
>>> response = await habitica.login("username_or_email", "password")
|
182
|
+
>>> response.data.id
|
183
|
+
'user-id'
|
184
|
+
>>> response.data.apiToken
|
185
|
+
'api-token'
|
186
|
+
"""
|
187
|
+
url = self.url / "api/v3/user/auth/local/login"
|
188
|
+
data = {
|
189
|
+
"username": username,
|
190
|
+
"password": password,
|
191
|
+
}
|
192
|
+
|
193
|
+
response = HabiticaLoginResponse.from_json(
|
194
|
+
await self._request("post", url=url, data=data),
|
195
|
+
)
|
196
|
+
self._headers.update(
|
197
|
+
{
|
198
|
+
"X-API-USER": str(response.data.id),
|
199
|
+
"X-API-KEY": str(response.data.apiToken),
|
200
|
+
},
|
201
|
+
)
|
202
|
+
return response
|
203
|
+
|
204
|
+
async def get_user(
|
205
|
+
self,
|
206
|
+
user_fields: str | list[str] | None = None,
|
207
|
+
*,
|
208
|
+
anonymized: bool = False,
|
209
|
+
) -> HabiticaUserResponse:
|
210
|
+
"""Get the authenticated user's profile.
|
211
|
+
|
212
|
+
Parameters
|
213
|
+
----------
|
214
|
+
user_fields : str | list[str] | None, optional
|
215
|
+
A string or a list of fields to include in the response.
|
216
|
+
If provided as a list, the fields will be joined with commas.
|
217
|
+
If None, the full user profile document is returned. Default is None.
|
218
|
+
anonymized : bool
|
219
|
+
When True, returns the user's data without: Authentication information,
|
220
|
+
New Messages/Invitations/Inbox, Profile, Purchased information,
|
221
|
+
Contributor information, Special items, Webhooks, Notifications.
|
222
|
+
(default is False)
|
223
|
+
|
224
|
+
Returns
|
225
|
+
-------
|
226
|
+
HabiticaUserResponse
|
227
|
+
A response object containing the result of the API call.
|
228
|
+
|
229
|
+
Raises
|
230
|
+
------
|
231
|
+
NotAuthorizedError
|
232
|
+
If the API request is unauthorized (HTTP 401).
|
233
|
+
aiohttp.ClientResponseError
|
234
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
235
|
+
aiohttp.ClientConnectionError
|
236
|
+
If the connection to the API fails.
|
237
|
+
aiohttp.ClientError
|
238
|
+
For any other exceptions raised by aiohttp during the request.
|
239
|
+
TimeoutError
|
240
|
+
If the connection times out.
|
241
|
+
|
242
|
+
Examples
|
243
|
+
--------
|
244
|
+
>>> response = await habitica.get_user(user_fields="achievements,items.mounts")
|
245
|
+
>>> response.data # Access the returned data from the response
|
246
|
+
"""
|
247
|
+
url = self.url / "api/v3/user"
|
248
|
+
params = {}
|
249
|
+
|
250
|
+
if user_fields:
|
251
|
+
params = {"userFields": join_fields(user_fields)}
|
252
|
+
if anonymized:
|
253
|
+
url = url / "anonymized"
|
254
|
+
|
255
|
+
return HabiticaUserResponse.from_json(
|
256
|
+
await self._request("get", url=url, params=params),
|
257
|
+
)
|
258
|
+
|
259
|
+
async def get_tasks(
|
260
|
+
self,
|
261
|
+
task_type: TaskFilter | None = None,
|
262
|
+
due_date: datetime | None = None,
|
263
|
+
) -> HabiticaResponse:
|
264
|
+
"""Get the authenticated user's tasks.
|
265
|
+
|
266
|
+
Parameters
|
267
|
+
----------
|
268
|
+
task_type : TaskFilter | None
|
269
|
+
The type of task to retrieve, defined in TaskFilter enum.
|
270
|
+
If `None`, all task types will be retrieved (default is None).
|
271
|
+
|
272
|
+
due_date : datetime | None
|
273
|
+
Optional date to use for computing the nextDue field for each returned task.
|
274
|
+
|
275
|
+
Returns
|
276
|
+
-------
|
277
|
+
HabiticaResponse
|
278
|
+
A response object containing the user's tasks, parsed from the JSON
|
279
|
+
response.
|
280
|
+
|
281
|
+
Raises
|
282
|
+
------
|
283
|
+
NotAuthorizedError
|
284
|
+
If the API request is unauthorized (HTTP 401).
|
285
|
+
aiohttp.ClientResponseError
|
286
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
287
|
+
aiohttp.ClientConnectionError
|
288
|
+
If the connection to the API fails.
|
289
|
+
aiohttp.ClientError
|
290
|
+
For any other exceptions raised by aiohttp during the request.
|
291
|
+
TimeoutError
|
292
|
+
If the connection times out.
|
293
|
+
|
294
|
+
Examples
|
295
|
+
--------
|
296
|
+
Retrieve all tasks:
|
297
|
+
|
298
|
+
>>> await habitica.get_tasks()
|
299
|
+
|
300
|
+
Retrieve only todos:
|
301
|
+
|
302
|
+
>>> await habitica.get_tasks(TaskType.HABITS)
|
303
|
+
|
304
|
+
Retrieve todos with a specific due date:
|
305
|
+
|
306
|
+
>>> await habitica.get_tasks(TaskType.HABITS, due_date=datetime(2024, 10, 15))
|
307
|
+
"""
|
308
|
+
url = self.url / "api/v3/tasks/user"
|
309
|
+
params = {}
|
310
|
+
|
311
|
+
if task_type:
|
312
|
+
params.update({"type": task_type.value})
|
313
|
+
if due_date:
|
314
|
+
params.update({"dueDate": due_date.isoformat()})
|
315
|
+
return HabiticaTasksResponse.from_json(
|
316
|
+
await self._request("get", url=url, params=params),
|
317
|
+
)
|
318
|
+
|
319
|
+
async def get_task(self, task_id: UUID) -> HabiticaTaskResponse:
|
320
|
+
"""Retrieve a specific task from the Habitica API.
|
321
|
+
|
322
|
+
This method sends a request to the Habitica API to retrieve a specific task
|
323
|
+
identified by the given `task_id`.
|
324
|
+
|
325
|
+
Parameters
|
326
|
+
----------
|
327
|
+
task_id : UUID
|
328
|
+
The UUID of the task to retrieve.
|
329
|
+
|
330
|
+
Returns
|
331
|
+
-------
|
332
|
+
HabiticaTaskResponse
|
333
|
+
A response object containing the data for the specified task.
|
334
|
+
|
335
|
+
Raises
|
336
|
+
------
|
337
|
+
aiohttp.ClientResponseError
|
338
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
339
|
+
aiohttp.ClientConnectionError
|
340
|
+
If the connection to the API fails.
|
341
|
+
aiohttp.ClientError
|
342
|
+
For any other exceptions raised by aiohttp during the request.
|
343
|
+
TimeoutError
|
344
|
+
If the connection times out.
|
345
|
+
|
346
|
+
Examples
|
347
|
+
--------
|
348
|
+
>>> task_id = UUID("12345678-1234-5678-1234-567812345678")
|
349
|
+
>>> task_response = await habitica.get_task(task_id)
|
350
|
+
>>> print(task_response.data) # Displays the retrieved task information
|
351
|
+
"""
|
352
|
+
url = self.url / "api/v3/tasks" / str(task_id)
|
353
|
+
|
354
|
+
return HabiticaTaskResponse.from_json(
|
355
|
+
await self._request("get", url=url),
|
356
|
+
)
|
357
|
+
|
358
|
+
async def create_task(self, task: Task) -> HabiticaTaskResponse:
|
359
|
+
"""Create a new task in the Habitica API.
|
360
|
+
|
361
|
+
This method sends a request to the Habitica API to create a new task
|
362
|
+
with the specified attributes defined in the `task` object.
|
363
|
+
|
364
|
+
Parameters
|
365
|
+
----------
|
366
|
+
task : Task
|
367
|
+
An instance of the `Task` dataclass containing the attributes for the new task.
|
368
|
+
|
369
|
+
Returns
|
370
|
+
-------
|
371
|
+
HabiticaTaskResponse
|
372
|
+
A response object containing the data for the newly created task.
|
373
|
+
|
374
|
+
Raises
|
375
|
+
------
|
376
|
+
aiohttp.ClientResponseError
|
377
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
378
|
+
aiohttp.ClientConnectionError
|
379
|
+
If the connection to the API fails.
|
380
|
+
aiohttp.ClientError
|
381
|
+
For any other exceptions raised by aiohttp during the request.
|
382
|
+
TimeoutError
|
383
|
+
If the connection times out.
|
384
|
+
|
385
|
+
Examples
|
386
|
+
--------
|
387
|
+
>>> new_task = Task(text="New Task", type=TaskType.TODO ...)
|
388
|
+
>>> create_response = await habitica.create_task(new_task)
|
389
|
+
>>> print(create_response.data) # Displays the created task information
|
390
|
+
"""
|
391
|
+
url = self.url / "api/v3/tasks/user"
|
392
|
+
|
393
|
+
json = deserialize_task(task)
|
394
|
+
|
395
|
+
return HabiticaTaskResponse.from_json(
|
396
|
+
await self._request("post", url=url, json=json),
|
397
|
+
)
|
398
|
+
|
399
|
+
async def update_task(self, task_id: UUID, task: Task) -> HabiticaTaskResponse:
|
400
|
+
"""Update an existing task in the Habitica API.
|
401
|
+
|
402
|
+
This method sends a request to the Habitica API to update the attributes
|
403
|
+
of a specific task identified by the given `task_id` with the specified
|
404
|
+
attributes defined in the `task` object.
|
405
|
+
|
406
|
+
Parameters
|
407
|
+
----------
|
408
|
+
task_id : UUID
|
409
|
+
The UUID of the task to update.
|
410
|
+
task : Task
|
411
|
+
An instance of the `Task` dataclass containing the updated attributes for the task.
|
412
|
+
|
413
|
+
Returns
|
414
|
+
-------
|
415
|
+
HabiticaTaskResponse
|
416
|
+
A response object containing the data of the updated task.
|
417
|
+
|
418
|
+
Raises
|
419
|
+
------
|
420
|
+
aiohttp.ClientResponseError
|
421
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
422
|
+
aiohttp.ClientConnectionError
|
423
|
+
If the connection to the API fails.
|
424
|
+
aiohttp.ClientError
|
425
|
+
For any other exceptions raised by aiohttp during the request.
|
426
|
+
TimeoutError
|
427
|
+
If the connection times out.
|
428
|
+
|
429
|
+
Examples
|
430
|
+
--------
|
431
|
+
>>> task_id = UUID("12345678-1234-5678-1234-567812345678")
|
432
|
+
>>> updated_task = Task(text="Updated Task", ...)
|
433
|
+
>>> update_response = await habitica.update_task(task_id, updated_task)
|
434
|
+
>>> print(update_response.data) # Displays the updated task information
|
435
|
+
"""
|
436
|
+
url = self.url / "api/v3/tasks" / str(task_id)
|
437
|
+
|
438
|
+
json = deserialize_task(task)
|
439
|
+
|
440
|
+
return HabiticaTaskResponse.from_json(
|
441
|
+
await self._request("put", url=url, json=json),
|
442
|
+
)
|
443
|
+
|
444
|
+
async def delete_task(self, task_id: UUID) -> HabiticaResponse:
|
445
|
+
"""Delete a specific task.
|
446
|
+
|
447
|
+
This method sends a request to the Habitica API to delete a specific task
|
448
|
+
identified by the given `task_id`.
|
449
|
+
|
450
|
+
Parameters
|
451
|
+
----------
|
452
|
+
task_id : UUID
|
453
|
+
The UUID of the task to delete.
|
454
|
+
|
455
|
+
Returns
|
456
|
+
-------
|
457
|
+
HabiticaTaskResponse
|
458
|
+
A response object containing the data for the deleted task.
|
459
|
+
|
460
|
+
Raises
|
461
|
+
------
|
462
|
+
aiohttp.ClientResponseError
|
463
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
464
|
+
aiohttp.ClientConnectionError
|
465
|
+
If the connection to the API fails.
|
466
|
+
aiohttp.ClientError
|
467
|
+
For any other exceptions raised by aiohttp during the request.
|
468
|
+
TimeoutError
|
469
|
+
If the connection times out.
|
470
|
+
|
471
|
+
Examples
|
472
|
+
--------
|
473
|
+
>>> task_id = UUID("12345678-1234-5678-1234-567812345678")
|
474
|
+
>>> delete_response = await habitica.delete_task(task_id)
|
475
|
+
>>> print(delete_response.success) # True if successfully deleted
|
476
|
+
"""
|
477
|
+
url = self.url / "api/v3/tasks" / str(task_id)
|
478
|
+
|
479
|
+
return HabiticaResponse.from_json(
|
480
|
+
await self._request("delete", url=url),
|
481
|
+
)
|
482
|
+
|
483
|
+
async def reorder_task(self, task_id: UUID, to: int) -> HabiticaTaskOrderResponse:
|
484
|
+
"""Reorder a user's tasks.
|
485
|
+
|
486
|
+
This method sends a request to the Habitica API to reorder a specific task,
|
487
|
+
identified by the given `task_id`, to a new position specified by `to`.
|
488
|
+
|
489
|
+
Parameters
|
490
|
+
----------
|
491
|
+
task_id : UUID
|
492
|
+
The UUID of the task to reorder.
|
493
|
+
to : int
|
494
|
+
The new position to move the task to. Use 0 to move the task to the top,
|
495
|
+
and -1 to move it to the bottom of the list.
|
496
|
+
|
497
|
+
Returns
|
498
|
+
-------
|
499
|
+
HabiticaTaskOrderResponse
|
500
|
+
A response object containing a list of task IDs in the new sort order.
|
501
|
+
|
502
|
+
Raises
|
503
|
+
------
|
504
|
+
aiohttp.ClientResponseError
|
505
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
506
|
+
aiohttp.ClientConnectionError
|
507
|
+
If the connection to the API fails.
|
508
|
+
aiohttp.ClientError
|
509
|
+
For any other exceptions raised by aiohttp during the request.
|
510
|
+
TimeoutError
|
511
|
+
If the connection times out.
|
512
|
+
|
513
|
+
Examples
|
514
|
+
--------
|
515
|
+
>>> task_id = UUID("12345678-1234-5678-1234-567812345678")
|
516
|
+
>>> reorder_response = await habitica.reorder_task(task_id, 2)
|
517
|
+
>>> print(reorder_response.data) # Displays a list of task IDs in the new order
|
518
|
+
"""
|
519
|
+
url = self.url / "api/v3/tasks" / str(task_id) / "move/to" / str(to)
|
520
|
+
|
521
|
+
return HabiticaTaskOrderResponse.from_json(
|
522
|
+
await self._request("post", url=url),
|
523
|
+
)
|
524
|
+
|
525
|
+
async def get_user_export(self) -> HabiticaUserExport:
|
526
|
+
"""Export the user's data from Habitica.
|
527
|
+
|
528
|
+
Notes
|
529
|
+
-----
|
530
|
+
This endpoint is part of Habitica's private API and intended for use
|
531
|
+
on the website only. It may change at any time without notice, and
|
532
|
+
backward compatibility is not guaranteed.
|
533
|
+
|
534
|
+
Returns
|
535
|
+
-------
|
536
|
+
HabiticaUserExportResponse:
|
537
|
+
The user's exported data, containing
|
538
|
+
information such as tasks, settings, and profile details.
|
539
|
+
|
540
|
+
Raises
|
541
|
+
------
|
542
|
+
NotAuthorizedError
|
543
|
+
If the API request is unauthorized (HTTP 401).
|
544
|
+
aiohttp.ClientResponseError
|
545
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
546
|
+
aiohttp.ClientConnectionError
|
547
|
+
If the connection to the API fails.
|
548
|
+
aiohttp.ClientError
|
549
|
+
For any other exceptions raised by aiohttp during the request.
|
550
|
+
TimeoutError
|
551
|
+
If the connection times out.
|
552
|
+
"""
|
553
|
+
url = self.url / "export/userdata.json"
|
554
|
+
|
555
|
+
return HabiticaUserExport.from_json(
|
556
|
+
await self._request("get", url=url),
|
557
|
+
)
|
558
|
+
|
559
|
+
async def get_content(
|
560
|
+
self,
|
561
|
+
language: Language | None = None,
|
562
|
+
) -> HabiticaResponse:
|
563
|
+
"""
|
564
|
+
Fetch game content from the Habitica API.
|
565
|
+
|
566
|
+
This method retrieves the game content, which includes information
|
567
|
+
such as available equipment, pets, mounts, and other game elements.
|
568
|
+
|
569
|
+
Parameters
|
570
|
+
----------
|
571
|
+
language : Language | None
|
572
|
+
Optional language code to specify the localization of the content,
|
573
|
+
possible values are defined in the `Language` enum.
|
574
|
+
If not provided, it defaults to Language.EN or the authenticated
|
575
|
+
user's language.
|
576
|
+
|
577
|
+
Available languages include: BG, CS, DA, DE, EN, EN_PIRATE, EN_GB,
|
578
|
+
ES, ES_419, FR, HE, HU, ID, IT, JA, NL, PL, PT, PT_BR, RO, RU, SK,
|
579
|
+
SR, SV, UK, ZH, ZH_TW.
|
580
|
+
|
581
|
+
|
582
|
+
Returns
|
583
|
+
-------
|
584
|
+
HabiticaResponse:
|
585
|
+
A response object containing the game content in JSON format.
|
586
|
+
|
587
|
+
Raises
|
588
|
+
------
|
589
|
+
aiohttp.ClientResponseError
|
590
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
591
|
+
aiohttp.ClientConnectionError
|
592
|
+
If the connection to the API fails.
|
593
|
+
aiohttp.ClientError
|
594
|
+
For any other exceptions raised by aiohttp during the request.
|
595
|
+
TimeoutError
|
596
|
+
If the connection times out.
|
597
|
+
"""
|
598
|
+
url = self.url / "api/v3/content"
|
599
|
+
params = {}
|
600
|
+
|
601
|
+
if language:
|
602
|
+
params.update({"language": language.value})
|
603
|
+
|
604
|
+
return HabiticaResponse.from_json(
|
605
|
+
await self._request("get", url=url, params=params),
|
606
|
+
)
|
607
|
+
|
608
|
+
async def run_cron(self) -> HabiticaResponse:
|
609
|
+
"""Run the Habitica cron.
|
610
|
+
|
611
|
+
This method triggers the cron process, which applies the daily reset for the authenticated user.
|
612
|
+
It assumes that the user has already confirmed their activity for the previous day
|
613
|
+
(i.e., checked off any Dailies they completed). The cron will immediately apply
|
614
|
+
damage for incomplete Dailies that were due and handle other daily resets.
|
615
|
+
|
616
|
+
Returns
|
617
|
+
-------
|
618
|
+
HabiticaResponse
|
619
|
+
A response containing an empty data object.
|
620
|
+
|
621
|
+
Raises
|
622
|
+
------
|
623
|
+
NotAuthorizedError
|
624
|
+
If the API request is unauthorized (HTTP 401).
|
625
|
+
aiohttp.ClientResponseError
|
626
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
627
|
+
aiohttp.ClientConnectionError
|
628
|
+
If the connection to the API fails.
|
629
|
+
aiohttp.ClientError
|
630
|
+
For any other exceptions raised by aiohttp during the request.
|
631
|
+
TimeoutError
|
632
|
+
If the connection times out.
|
633
|
+
"""
|
634
|
+
url = self.url / "api/v3/cron"
|
635
|
+
return HabiticaResponse.from_json(await self._request("post", url=url))
|
636
|
+
|
637
|
+
async def allocate_single_stat_point(
|
638
|
+
self,
|
639
|
+
stat: Attributes = Attributes.STR,
|
640
|
+
) -> HabiticaStatsResponse:
|
641
|
+
"""Allocate a single stat point to the specified attribute.
|
642
|
+
|
643
|
+
If no stat is specified, the default is 'str' (strength).
|
644
|
+
If the user does not have any stat points to allocate,
|
645
|
+
a NotAuthorized error is returned.
|
646
|
+
|
647
|
+
Parameters
|
648
|
+
----------
|
649
|
+
stat : Attributes, optional
|
650
|
+
The stat to increase, either 'str', 'con', 'int', or 'per'. Defaults to 'str'.
|
651
|
+
|
652
|
+
Returns
|
653
|
+
-------
|
654
|
+
HabiticaAllocatStatPointsResponse
|
655
|
+
A response containing the updated user stats, including points, buffs, and training data.
|
656
|
+
|
657
|
+
Raises
|
658
|
+
------
|
659
|
+
NotAuthorizedError
|
660
|
+
If the user does not have enough stat points to allocate.
|
661
|
+
aiohttp.ClientResponseError
|
662
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
663
|
+
aiohttp.ClientConnectionError
|
664
|
+
If the connection to the API fails.
|
665
|
+
aiohttp.ClientError
|
666
|
+
For any other exceptions raised by aiohttp during the request.
|
667
|
+
TimeoutError
|
668
|
+
If the connection times out.
|
669
|
+
|
670
|
+
Examples
|
671
|
+
--------
|
672
|
+
Allocate a single stat point to Intelligence:
|
673
|
+
>>> await habitica.allocate_single_stat_point(stat=Attributes.INT)
|
674
|
+
|
675
|
+
Allocate a single stat point to Strength (default):
|
676
|
+
>>> await habitica.allocate_single_stat_point()
|
677
|
+
"""
|
678
|
+
url = self.url / "api/v3/user/allocate"
|
679
|
+
params = {"stat": stat}
|
680
|
+
|
681
|
+
return HabiticaStatsResponse.from_json(
|
682
|
+
await self._request("post", url=url, params=params),
|
683
|
+
)
|
684
|
+
|
685
|
+
async def allocate_stat_points(self) -> HabiticaStatsResponse:
|
686
|
+
"""Allocate all available stat points using the user's chosen automatic allocation method.
|
687
|
+
|
688
|
+
This method uses the user's configured allocation strategy to distribute any unassigned
|
689
|
+
stat points. If the user has no specific method defined, all points are allocated to
|
690
|
+
Strength (STR). If there are no points to allocate, the method will still return a
|
691
|
+
success response. The response includes updated user stats, including health, mana,
|
692
|
+
experience, and gold.
|
693
|
+
|
694
|
+
Returns
|
695
|
+
-------
|
696
|
+
HabiticaAllocatStatPointsResponse
|
697
|
+
A response containing the updated user stats, including points, buffs, and training data.
|
698
|
+
|
699
|
+
Raises
|
700
|
+
------
|
701
|
+
NotAuthorizedError
|
702
|
+
If the user does not have enough stat points to allocate.
|
703
|
+
aiohttp.ClientResponseError
|
704
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
705
|
+
aiohttp.ClientConnectionError
|
706
|
+
If the connection to the API fails.
|
707
|
+
aiohttp.ClientError
|
708
|
+
For any other exceptions raised by aiohttp during the request.
|
709
|
+
TimeoutError
|
710
|
+
If the connection times out.
|
711
|
+
"""
|
712
|
+
url = self.url / "api/v3/user/allocate-now"
|
713
|
+
|
714
|
+
return HabiticaStatsResponse.from_json(
|
715
|
+
await self._request("post", url=url),
|
716
|
+
)
|
717
|
+
|
718
|
+
async def allocate_bulk_stat_points(
|
719
|
+
self,
|
720
|
+
int_points: int = 0,
|
721
|
+
str_points: int = 0,
|
722
|
+
con_points: int = 0,
|
723
|
+
per_points: int = 0,
|
724
|
+
) -> HabiticaStatsResponse:
|
725
|
+
"""Allocate multiple stat points manually to different attributes.
|
726
|
+
|
727
|
+
This method allows the user to manually allocate their available stat points to the
|
728
|
+
desired attributes. The number of points to allocate for each attribute must be provided
|
729
|
+
as parameters. The request will fail if the user does not have enough available points.
|
730
|
+
|
731
|
+
Parameters
|
732
|
+
----------
|
733
|
+
int_points : int, optional
|
734
|
+
The number of points to allocate to Intelligence (default is 0).
|
735
|
+
str_points : int, optional
|
736
|
+
The number of points to allocate to Strength (default is 0).
|
737
|
+
con_points : int, optional
|
738
|
+
The number of points to allocate to Constitution (default is 0).
|
739
|
+
per_points : int, optional
|
740
|
+
The number of points to allocate to Perception (default is 0).
|
741
|
+
|
742
|
+
Returns
|
743
|
+
-------
|
744
|
+
HabiticaAllocatStatPointsResponse
|
745
|
+
A response containing the updated user stats, including points, buffs, and training data.
|
746
|
+
|
747
|
+
Raises
|
748
|
+
------
|
749
|
+
NotAuthorizedError
|
750
|
+
If the user does not have enough stat points to allocate.
|
751
|
+
aiohttp.ClientResponseError
|
752
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
753
|
+
aiohttp.ClientConnectionError
|
754
|
+
If the connection to the API fails.
|
755
|
+
aiohttp.ClientError
|
756
|
+
For any other exceptions raised by aiohttp during the request.
|
757
|
+
TimeoutError
|
758
|
+
If the connection times out.
|
759
|
+
|
760
|
+
Examples
|
761
|
+
--------
|
762
|
+
Allocate 2 points to INT and 1 point to STR:
|
763
|
+
>>> await allocate_bulk_stat_points(int_points=2, str_points=1)
|
764
|
+
"""
|
765
|
+
url = self.url / "api/v3/user/allocate-bulk"
|
766
|
+
json = {
|
767
|
+
"stats": {
|
768
|
+
"int": int_points,
|
769
|
+
"str": str_points,
|
770
|
+
"con": con_points,
|
771
|
+
"per": per_points,
|
772
|
+
},
|
773
|
+
}
|
774
|
+
|
775
|
+
return HabiticaStatsResponse.from_json(
|
776
|
+
await self._request("post", url=url, json=json),
|
777
|
+
)
|
778
|
+
|
779
|
+
async def buy_health_potion(self) -> HabiticaStatsResponse:
|
780
|
+
"""Purchase a health potion for the authenticated user.
|
781
|
+
|
782
|
+
If the user has enough gold and their health is not already full,
|
783
|
+
this method allows them to buy a health potion to restore health.
|
784
|
+
The user's current stats will be returned upon a successful purchase.
|
785
|
+
|
786
|
+
Returns
|
787
|
+
-------
|
788
|
+
HabiticaStatResponse
|
789
|
+
A response object containing the user's updated stats and a success message.
|
790
|
+
|
791
|
+
Raises
|
792
|
+
------
|
793
|
+
NotAuthorizedError
|
794
|
+
If the user does not have enough gold or if the user's health is already at maximum.
|
795
|
+
aiohttp.ClientResponseError
|
796
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
797
|
+
aiohttp.ClientConnectionError
|
798
|
+
If the connection to the API fails.
|
799
|
+
aiohttp.ClientError
|
800
|
+
For any other exceptions raised by aiohttp during the request.
|
801
|
+
TimeoutError
|
802
|
+
If the connection times out.
|
803
|
+
"""
|
804
|
+
url = self.url / "api/v3/user/buy-health-potion"
|
805
|
+
|
806
|
+
return HabiticaStatsResponse.from_json(
|
807
|
+
await self._request("post", url=url),
|
808
|
+
)
|
809
|
+
|
810
|
+
async def cast_skill(
|
811
|
+
self,
|
812
|
+
skill: Skill,
|
813
|
+
target_id: UUID | None = None,
|
814
|
+
) -> HabiticaUserResponse:
|
815
|
+
"""Cast a skill (spell) in Habitica, optionally targeting a specific user, task or party.
|
816
|
+
|
817
|
+
Parameters
|
818
|
+
----------
|
819
|
+
skill : Skill
|
820
|
+
The skill (or spell) to be cast. This should be a valid `Skill` enum value.
|
821
|
+
target_id : UUID, optional
|
822
|
+
The unique identifier of the target for the skill. If the skill does not require a target,
|
823
|
+
this can be omitted.
|
824
|
+
|
825
|
+
Returns
|
826
|
+
-------
|
827
|
+
HabiticaStatResponse
|
828
|
+
A response object containing the user's updated stats and a success message.
|
829
|
+
|
830
|
+
Raises
|
831
|
+
------
|
832
|
+
NotAuthorizedError
|
833
|
+
If the user does not have enough mana.
|
834
|
+
NotFoundError
|
835
|
+
The specified task, party or user could not be found
|
836
|
+
aiohttp.ClientResponseError
|
837
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
838
|
+
aiohttp.ClientConnectionError
|
839
|
+
If the connection to the API fails.
|
840
|
+
aiohttp.ClientError
|
841
|
+
For any other exceptions raised by aiohttp during the request.
|
842
|
+
TimeoutError
|
843
|
+
If the connection times out.
|
844
|
+
"""
|
845
|
+
url = self.url / "api/v3/user/class/cast" / skill
|
846
|
+
params = {}
|
847
|
+
|
848
|
+
if target_id:
|
849
|
+
params.update({"targetId": str(target_id)})
|
850
|
+
return HabiticaUserResponse.from_json(
|
851
|
+
await self._request("post", url=url, json=params),
|
852
|
+
)
|
853
|
+
|
854
|
+
async def toggle_sleep(
|
855
|
+
self,
|
856
|
+
) -> HabiticaResponse:
|
857
|
+
"""Toggles the user's sleep mode in Habitica.
|
858
|
+
|
859
|
+
Returns
|
860
|
+
-------
|
861
|
+
HabiticaResponse
|
862
|
+
A response object containing the result of the sleep mode toggle,
|
863
|
+
and the new sleep state (True if sleeping, False if not).
|
864
|
+
|
865
|
+
Raises
|
866
|
+
------
|
867
|
+
aiohttp.ClientResponseError
|
868
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
869
|
+
aiohttp.ClientConnectionError
|
870
|
+
If the connection to the API fails.
|
871
|
+
aiohttp.ClientError
|
872
|
+
For any other exceptions raised by aiohttp during the request.
|
873
|
+
TimeoutError
|
874
|
+
If the connection times out.
|
875
|
+
"""
|
876
|
+
url = self.url / "api/v3/user/sleep"
|
877
|
+
|
878
|
+
return HabiticaResponse.from_json(await self._request("post", url=url))
|
879
|
+
|
880
|
+
async def revive(
|
881
|
+
self,
|
882
|
+
) -> HabiticaResponse:
|
883
|
+
"""Revive user from death.
|
884
|
+
|
885
|
+
Raises
|
886
|
+
------
|
887
|
+
NotAuthorizedError
|
888
|
+
If the player is not dead and therefore cannot be revived.
|
889
|
+
aiohttp.ClientResponseError
|
890
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
891
|
+
aiohttp.ClientConnectionError
|
892
|
+
If the connection to the API fails.
|
893
|
+
aiohttp.ClientError
|
894
|
+
For any other exceptions raised by aiohttp during the request.
|
895
|
+
TimeoutError
|
896
|
+
If the connection times out.
|
897
|
+
"""
|
898
|
+
url = self.url / "api/v3/user/revive"
|
899
|
+
|
900
|
+
return HabiticaResponse.from_json(await self._request("post", url=url))
|
901
|
+
|
902
|
+
async def change_class(self, Class: HabiticaClass) -> HabiticaClassSystemResponse: # noqa: N803
|
903
|
+
"""Change the user's class in Habitica.
|
904
|
+
|
905
|
+
This method sends a request to the Habitica API to change the user's class
|
906
|
+
(e.g., warrior, mage, rogue, healer) to the specified class.
|
907
|
+
|
908
|
+
Parameters
|
909
|
+
----------
|
910
|
+
Class : Class
|
911
|
+
An instance of the `Class` enum representing the new class to assign to the user.
|
912
|
+
|
913
|
+
Returns
|
914
|
+
-------
|
915
|
+
HabiticaClassSystemResponse
|
916
|
+
A response object containing stats, flags, items and preferences.
|
917
|
+
|
918
|
+
Raises
|
919
|
+
------
|
920
|
+
NotAuthorizedError
|
921
|
+
If the player cannot change class at this time (e.g., conditions not met).
|
922
|
+
aiohttp.ClientResponseError
|
923
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
924
|
+
aiohttp.ClientConnectionError
|
925
|
+
If the connection to the API fails.
|
926
|
+
aiohttp.ClientError
|
927
|
+
For any other exceptions raised by aiohttp during the request.
|
928
|
+
TimeoutError
|
929
|
+
If the connection times out.
|
930
|
+
|
931
|
+
Examples
|
932
|
+
--------
|
933
|
+
>>> new_class = HabiticaClass.WARRIOR
|
934
|
+
>>> change_response = await habitica.change_class(new_class)
|
935
|
+
>>> print(change_response.data.stats) # Displays the user's stats after class change
|
936
|
+
"""
|
937
|
+
url = self.url / "api/v3/user/change-class"
|
938
|
+
params = {"class": Class.value}
|
939
|
+
|
940
|
+
return HabiticaClassSystemResponse.from_json(
|
941
|
+
await self._request("post", url=url, params=params)
|
942
|
+
)
|
943
|
+
|
944
|
+
async def disable_classes(self) -> HabiticaClassSystemResponse:
|
945
|
+
"""Disable the class system for the user in Habitica.
|
946
|
+
|
947
|
+
This method sends a request to the Habitica API to disable the class system for the user.
|
948
|
+
|
949
|
+
Returns
|
950
|
+
-------
|
951
|
+
HabiticaClassSystemResponse
|
952
|
+
A response object containing stats, flags, and preferences.
|
953
|
+
|
954
|
+
Raises
|
955
|
+
------
|
956
|
+
aiohttp.ClientResponseError
|
957
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
958
|
+
aiohttp.ClientConnectionError
|
959
|
+
If the connection to the API fails.
|
960
|
+
aiohttp.ClientError
|
961
|
+
For any other exceptions raised by aiohttp during the request.
|
962
|
+
TimeoutError
|
963
|
+
If the connection times out.
|
964
|
+
|
965
|
+
Examples
|
966
|
+
--------
|
967
|
+
>>> disable_response = await habitica.disable_classes()
|
968
|
+
>>> print(disable_response.data.stats) # Displays the user's stats after disabling the class system
|
969
|
+
"""
|
970
|
+
url = self.url / "api/v3/user/disable-classes"
|
971
|
+
|
972
|
+
return HabiticaClassSystemResponse.from_json(
|
973
|
+
await self._request("post", url=url)
|
974
|
+
)
|
975
|
+
|
976
|
+
async def delete_completed_todos(self) -> HabiticaResponse:
|
977
|
+
"""Delete all completed to-dos from the user's task list.
|
978
|
+
|
979
|
+
This method sends a request to the Habitica API to delete all completed to-dos
|
980
|
+
from the user's task list.
|
981
|
+
|
982
|
+
Returns
|
983
|
+
-------
|
984
|
+
HabiticaResponse
|
985
|
+
A response object containing an empty data object.
|
986
|
+
|
987
|
+
Raises
|
988
|
+
------
|
989
|
+
aiohttp.ClientResponseError
|
990
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
991
|
+
aiohttp.ClientConnectionError
|
992
|
+
If the connection to the API fails.
|
993
|
+
aiohttp.ClientError
|
994
|
+
For any other exceptions raised by aiohttp during the request.
|
995
|
+
TimeoutError
|
996
|
+
If the connection times out.
|
997
|
+
|
998
|
+
Examples
|
999
|
+
--------
|
1000
|
+
>>> delete_response = await habitica.delete_completed_todos()
|
1001
|
+
>>> print(delete_response.success) # True if successfully cleared completed to-dos
|
1002
|
+
"""
|
1003
|
+
url = self.url / "api/v3/tasks/clearCompletedTodos"
|
1004
|
+
|
1005
|
+
return HabiticaClassSystemResponse.from_json(
|
1006
|
+
await self._request("post", url=url)
|
1007
|
+
)
|
1008
|
+
|
1009
|
+
async def update_score(
|
1010
|
+
self,
|
1011
|
+
task_id: UUID | str,
|
1012
|
+
direction: Direction,
|
1013
|
+
) -> HabiticaScoreResponse:
|
1014
|
+
"""Submit a score update for a task in Habitica.
|
1015
|
+
|
1016
|
+
This method allows scoring a task based on its type:
|
1017
|
+
- For Dailies and To-Dos: Marks the task as complete or incomplete.
|
1018
|
+
- For Habits: Increases the positive or negative habit score.
|
1019
|
+
- For Rewards: Buys the reward
|
1020
|
+
|
1021
|
+
Parameters
|
1022
|
+
----------
|
1023
|
+
task_id : UUID | str
|
1024
|
+
The ID of the task or an alias (e.g., a slug) associated with the task.
|
1025
|
+
direction : Direction
|
1026
|
+
The direction to score the task, either `Direction.UP` to increase or complete the task,
|
1027
|
+
or `Direction.DOWN` to decrease or uncomplete it..
|
1028
|
+
|
1029
|
+
Returns
|
1030
|
+
-------
|
1031
|
+
HabiticaScoreResponse
|
1032
|
+
A response object that contains the updated stats and item drop.
|
1033
|
+
|
1034
|
+
Raises
|
1035
|
+
------
|
1036
|
+
aiohttp.ClientResponseError
|
1037
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
1038
|
+
aiohttp.ClientConnectionError
|
1039
|
+
If the connection to the API fails.
|
1040
|
+
aiohttp.ClientError
|
1041
|
+
For any other exceptions raised by aiohttp during the request.
|
1042
|
+
TimeoutError
|
1043
|
+
If the connection times out.
|
1044
|
+
"""
|
1045
|
+
url = self.url / "api/v3/tasks" / str(task_id) / "score" / direction.value
|
1046
|
+
|
1047
|
+
return HabiticaScoreResponse.from_json(
|
1048
|
+
await self._request("post", url=url),
|
1049
|
+
)
|
1050
|
+
|
1051
|
+
async def get_tags(self) -> HabiticaTagsResponse:
|
1052
|
+
"""Retrieve a user's tags from the Habitica API.
|
1053
|
+
|
1054
|
+
This method sends a POST request to the Habitica API to fetch the tags
|
1055
|
+
associated with the user's account.
|
1056
|
+
|
1057
|
+
Returns
|
1058
|
+
-------
|
1059
|
+
HabiticaTagsResponse
|
1060
|
+
A response object containing the user's tags.
|
1061
|
+
|
1062
|
+
Raises
|
1063
|
+
------
|
1064
|
+
aiohttp.ClientResponseError
|
1065
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
1066
|
+
aiohttp.ClientConnectionError
|
1067
|
+
If the connection to the API fails.
|
1068
|
+
aiohttp.ClientError
|
1069
|
+
For any other exceptions raised by aiohttp during the request.
|
1070
|
+
TimeoutError
|
1071
|
+
If the connection times out.
|
1072
|
+
|
1073
|
+
Examples
|
1074
|
+
--------
|
1075
|
+
>>> tags_response = await habitica.get_tags()
|
1076
|
+
>>> print(tags_response.data)
|
1077
|
+
"""
|
1078
|
+
url = self.url / "api/v3/tags"
|
1079
|
+
|
1080
|
+
return HabiticaTagsResponse.from_json(
|
1081
|
+
await self._request("get", url=url),
|
1082
|
+
)
|
1083
|
+
|
1084
|
+
async def get_tag(self, tag_id: UUID) -> HabiticaTagResponse:
|
1085
|
+
"""Retrieve a specific tag from the Habitica API.
|
1086
|
+
|
1087
|
+
This method sends a request to the Habitica API to retrieve a specific tag
|
1088
|
+
identified by the given `tag_id`.
|
1089
|
+
|
1090
|
+
Parameters
|
1091
|
+
----------
|
1092
|
+
tag_id : UUID
|
1093
|
+
The UUID of the tag to retrieve.
|
1094
|
+
|
1095
|
+
Returns
|
1096
|
+
-------
|
1097
|
+
HabiticaTagResponse
|
1098
|
+
A response object containing the data for the specified tag.
|
1099
|
+
|
1100
|
+
Raises
|
1101
|
+
------
|
1102
|
+
aiohttp.ClientResponseError
|
1103
|
+
For other HTTP-related errors raised by aiohttp, such as HTTP 400 or 500.
|
1104
|
+
aiohttp.ClientConnectionError
|
1105
|
+
If the connection to the API fails.
|
1106
|
+
aiohttp.ClientError
|
1107
|
+
For any other exceptions raised by aiohttp during the request.
|
1108
|
+
TimeoutError
|
1109
|
+
If the connection times out.
|
1110
|
+
|
1111
|
+
Examples
|
1112
|
+
--------
|
1113
|
+
>>> tag_response = await habitica.get_tag()
|
1114
|
+
>>> print(tag_response.data)
|
1115
|
+
"""
|
1116
|
+
url = self.url / "api/v3/tags" / str(tag_id)
|
1117
|
+
|
1118
|
+
return HabiticaTagResponse.from_json(
|
1119
|
+
await self._request("get", url=url),
|
1120
|
+
)
|
1121
|
+
|
1122
|
+
async def delete_tag(self, tag_id: UUID) -> HabiticaResponse:
|
1123
|
+
"""Delete a user's tag from the Habitica API.
|
1124
|
+
|
1125
|
+
This method sends a request to the Habitica API to delete a specific
|
1126
|
+
tag identified by the given `tag_id`.
|
1127
|
+
|
1128
|
+
Parameters
|
1129
|
+
----------
|
1130
|
+
tag_id : UUID
|
1131
|
+
The UUID of the tag to delete.
|
1132
|
+
|
1133
|
+
Returns
|
1134
|
+
-------
|
1135
|
+
HabiticaTagResponse
|
1136
|
+
A response object with an empty data object.
|
1137
|
+
|
1138
|
+
Raises
|
1139
|
+
------
|
1140
|
+
aiohttp.ClientResponseError
|
1141
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
1142
|
+
aiohttp.ClientConnectionError
|
1143
|
+
If the connection to the API fails.
|
1144
|
+
aiohttp.ClientError
|
1145
|
+
For any other exceptions raised by aiohttp during the request.
|
1146
|
+
TimeoutError
|
1147
|
+
If the connection times out.
|
1148
|
+
|
1149
|
+
Examples
|
1150
|
+
--------
|
1151
|
+
>>> tag_id = UUID("12345678-1234-5678-1234-567812345678")
|
1152
|
+
>>> delete_response = await habitica.delete_tag(tag_id)
|
1153
|
+
>>> print(delete_response.success) # True if successfully deleted
|
1154
|
+
"""
|
1155
|
+
url = self.url / "api/v3/tags" / str(tag_id)
|
1156
|
+
|
1157
|
+
return HabiticaTagResponse.from_json(
|
1158
|
+
await self._request("delete", url=url),
|
1159
|
+
)
|
1160
|
+
|
1161
|
+
async def create_tag(self, name: str) -> HabiticaTagResponse:
|
1162
|
+
"""Create a new tag in the Habitica API.
|
1163
|
+
|
1164
|
+
This method sends a request to the Habitica API to create a new tag
|
1165
|
+
with the specified `name`.
|
1166
|
+
|
1167
|
+
Parameters
|
1168
|
+
----------
|
1169
|
+
name : str
|
1170
|
+
The name to assign to the new tag.
|
1171
|
+
|
1172
|
+
Returns
|
1173
|
+
-------
|
1174
|
+
HabiticaTagResponse
|
1175
|
+
A response object containing a dictionary with the newly
|
1176
|
+
created tag information, including the ID of the new tag.
|
1177
|
+
|
1178
|
+
Raises
|
1179
|
+
------
|
1180
|
+
aiohttp.ClientResponseError
|
1181
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
1182
|
+
aiohttp.ClientConnectionError
|
1183
|
+
If the connection to the API fails.
|
1184
|
+
aiohttp.ClientError
|
1185
|
+
For any other exceptions raised by aiohttp during the request.
|
1186
|
+
TimeoutError
|
1187
|
+
If the connection times out.
|
1188
|
+
|
1189
|
+
Examples
|
1190
|
+
--------
|
1191
|
+
>>> new_tag_response = await habitica.create_tag("New Tag Name")
|
1192
|
+
>>> print(new_tag_response.data.id) # Displays the id of the new tag
|
1193
|
+
"""
|
1194
|
+
url = self.url / "api/v3/tags"
|
1195
|
+
json = {"name": name}
|
1196
|
+
return HabiticaTagResponse.from_json(
|
1197
|
+
await self._request("post", url=url, json=json),
|
1198
|
+
)
|
1199
|
+
|
1200
|
+
async def update_tag(self, tag_id: UUID, name: str) -> HabiticaTagResponse:
|
1201
|
+
"""Update a user's tag in the Habitica API.
|
1202
|
+
|
1203
|
+
This method sends a request to the Habitica API to update the name of a
|
1204
|
+
specific tag, identified by the given `tag_id`.
|
1205
|
+
|
1206
|
+
Parameters
|
1207
|
+
----------
|
1208
|
+
tag_id : UUID
|
1209
|
+
The UUID of the tag to update.
|
1210
|
+
name : str
|
1211
|
+
The new name to assign to the tag.
|
1212
|
+
|
1213
|
+
Returns
|
1214
|
+
-------
|
1215
|
+
HabiticaTagResponse
|
1216
|
+
A response object containing a dictionary with the updated tag information.
|
1217
|
+
|
1218
|
+
Raises
|
1219
|
+
------
|
1220
|
+
aiohttp.ClientResponseError
|
1221
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
1222
|
+
aiohttp.ClientConnectionError
|
1223
|
+
If the connection to the API fails.
|
1224
|
+
aiohttp.ClientError
|
1225
|
+
For any other exceptions raised by aiohttp during the request.
|
1226
|
+
TimeoutError
|
1227
|
+
If the connection times out.
|
1228
|
+
|
1229
|
+
Examples
|
1230
|
+
--------
|
1231
|
+
>>> tag_id = UUID("12345678-1234-5678-1234-567812345678")
|
1232
|
+
>>> update_response = await habitica.update_tag(tag_id, "New Tag Name")
|
1233
|
+
>>> print(update_response.data) # Displays the updated tag information
|
1234
|
+
"""
|
1235
|
+
url = self.url / "api/v3/tags" / str(tag_id)
|
1236
|
+
json = {"name": name}
|
1237
|
+
return HabiticaTagResponse.from_json(
|
1238
|
+
await self._request("put", url=url, json=json),
|
1239
|
+
)
|
1240
|
+
|
1241
|
+
async def reorder_tag(self, tag_id: UUID, to: int) -> HabiticaResponse:
|
1242
|
+
"""Reorder a user's tag in the Habitica API.
|
1243
|
+
|
1244
|
+
This method sends a request to the Habitica API to reorder a specific tag,
|
1245
|
+
identified by the given `tag_id`, to a new position specified by `to`.
|
1246
|
+
|
1247
|
+
Parameters
|
1248
|
+
----------
|
1249
|
+
tag_id : UUID
|
1250
|
+
The UUID of the tag to reorder.
|
1251
|
+
to : int
|
1252
|
+
The new position to move the tag to (starting from 0).
|
1253
|
+
|
1254
|
+
Returns
|
1255
|
+
-------
|
1256
|
+
HabiticaResponse
|
1257
|
+
A response object containing an empty data object.
|
1258
|
+
|
1259
|
+
Raises
|
1260
|
+
------
|
1261
|
+
aiohttp.ClientResponseError
|
1262
|
+
For HTTP-related errors, such as HTTP 400 or 500 response status.
|
1263
|
+
aiohttp.ClientConnectionError
|
1264
|
+
If the connection to the API fails.
|
1265
|
+
aiohttp.ClientError
|
1266
|
+
For any other exceptions raised by aiohttp during the request.
|
1267
|
+
TimeoutError
|
1268
|
+
If the connection times out.
|
1269
|
+
|
1270
|
+
Examples
|
1271
|
+
--------
|
1272
|
+
>>> tag_id = UUID("12345678-1234-5678-1234-567812345678")
|
1273
|
+
>>> reorder_response = await habitica.reorder_tag(tag_id, 2)
|
1274
|
+
>>> print(reorder_response.success) # True if reorder is successful
|
1275
|
+
"""
|
1276
|
+
url = self.url / "api/v3/reorder-tags"
|
1277
|
+
json = {"tagId": str(tag_id), "to": to}
|
1278
|
+
|
1279
|
+
return HabiticaResponse.from_json(
|
1280
|
+
await self._request("post", url=url, json=json),
|
1281
|
+
)
|
1282
|
+
|
1283
|
+
def _cache_asset(self, asset: str, asset_data: IO[bytes]) -> None:
|
1284
|
+
"""Cache an asset and maintain the cache size limit by removing older entries.
|
1285
|
+
|
1286
|
+
This method stores the given asset in the in-memory cache. If the cache exceeds the
|
1287
|
+
specified limit (`self._cache_size`), the oldest cached asset is removed.
|
1288
|
+
The cache is updated with the new asset, and its order of insertion is tracked.
|
1289
|
+
|
1290
|
+
Parameters
|
1291
|
+
----------
|
1292
|
+
asset : str
|
1293
|
+
The identifier or name of the asset to be cached.
|
1294
|
+
asset_data : IO[bytes]
|
1295
|
+
The asset data as an I/O stream of bytes to be cached.
|
1296
|
+
|
1297
|
+
Notes
|
1298
|
+
-----
|
1299
|
+
If `self._cache_size` is zero or `None`, the caching operation is skipped. When
|
1300
|
+
the cache limit is exceeded, the least recently added asset is evicted to
|
1301
|
+
make space for the new asset.
|
1302
|
+
"""
|
1303
|
+
if not self._cache_size:
|
1304
|
+
return
|
1305
|
+
if len(self._cache_order) > self._cache_size:
|
1306
|
+
del self._assets_cache[self._cache_order.pop(0)]
|
1307
|
+
self._assets_cache[asset] = asset_data
|
1308
|
+
self._cache_order.append(asset)
|
1309
|
+
|
1310
|
+
async def paste_image(
|
1311
|
+
self,
|
1312
|
+
image: Image.Image,
|
1313
|
+
asset: str,
|
1314
|
+
position: tuple[int, int],
|
1315
|
+
) -> None:
|
1316
|
+
"""Fetch asset and paste it onto the base image at specified position.
|
1317
|
+
|
1318
|
+
Parameters
|
1319
|
+
----------
|
1320
|
+
image : Image
|
1321
|
+
The base image onto which the asset will be pasted.
|
1322
|
+
asset : str
|
1323
|
+
The name of the image asset to fetch (e.g., "hair_bangs_1_red.png").
|
1324
|
+
If no file extension is provided, `.png` will be added by default.
|
1325
|
+
position : tuple of int
|
1326
|
+
The (x, y) position coordinates where the asset will be pasted on the base image.
|
1327
|
+
|
1328
|
+
Returns
|
1329
|
+
-------
|
1330
|
+
None
|
1331
|
+
"""
|
1332
|
+
url = URL(ASSETS_URL) / f"{asset}"
|
1333
|
+
if not url.suffix:
|
1334
|
+
url = url.with_suffix(".png")
|
1335
|
+
try:
|
1336
|
+
if not (asset_data := self._assets_cache.get(asset)):
|
1337
|
+
async with self._session.get(url) as r:
|
1338
|
+
r.raise_for_status()
|
1339
|
+
asset_data = BytesIO(await r.read())
|
1340
|
+
self._cache_asset(asset, asset_data)
|
1341
|
+
except ClientResponseError as e:
|
1342
|
+
_LOGGER.exception(
|
1343
|
+
"Failed to load %s.png due to error [%s]: %s",
|
1344
|
+
asset,
|
1345
|
+
e.status,
|
1346
|
+
e.message,
|
1347
|
+
)
|
1348
|
+
except ClientError:
|
1349
|
+
_LOGGER.exception(
|
1350
|
+
"Failed to load %s.png due to a request error",
|
1351
|
+
asset,
|
1352
|
+
)
|
1353
|
+
else:
|
1354
|
+
fetched_image = Image.open(asset_data).convert("RGBA")
|
1355
|
+
image.paste(fetched_image, position, fetched_image)
|
1356
|
+
|
1357
|
+
async def generate_avatar( # noqa: PLR0912, PLR0915
|
1358
|
+
self,
|
1359
|
+
fp: str | IO[bytes],
|
1360
|
+
user_styles: UserStyles | None = None,
|
1361
|
+
fmt: str | None = None,
|
1362
|
+
) -> UserStyles:
|
1363
|
+
"""Generate an avatar image based on the provided user styles or fetched user data.
|
1364
|
+
|
1365
|
+
If no `user_styles` object is provided, the method retrieves user preferences, items, and stats
|
1366
|
+
for the authenticated user and builds the avatar accordingly. The base image is initialized
|
1367
|
+
as a transparent RGBA image of size (141, 147). A mount offset is applied based on the user's
|
1368
|
+
current mount status.
|
1369
|
+
|
1370
|
+
Note:
|
1371
|
+
Animated avatars are not supported, animated gear and mounts will
|
1372
|
+
be pasted without animation, showing only the first sprite.
|
1373
|
+
|
1374
|
+
|
1375
|
+
Parameters
|
1376
|
+
----------
|
1377
|
+
fp : str or IO[bytes]
|
1378
|
+
The file path or a bytes buffer to store or modify the avatar image.
|
1379
|
+
user_styles : UserStyles, optional
|
1380
|
+
The user style preferences, items, and stats. If not provided, the method will fetch
|
1381
|
+
this data.
|
1382
|
+
fmt : str
|
1383
|
+
If a file object is used instead of a filename, the format
|
1384
|
+
must be speciefied (e.g. "png").
|
1385
|
+
|
1386
|
+
Returns
|
1387
|
+
-------
|
1388
|
+
UserStyles
|
1389
|
+
The user styles used to generate the avatar.
|
1390
|
+
|
1391
|
+
Examples
|
1392
|
+
--------
|
1393
|
+
Using a bytes buffer:
|
1394
|
+
>>> avatar = BytesIO()
|
1395
|
+
>>> await habitica generate_avatar(avatar, fmt='png')
|
1396
|
+
|
1397
|
+
Using a file path:
|
1398
|
+
>>> await habitica.generate_avatar("/path/to/image/avatar.png")
|
1399
|
+
"""
|
1400
|
+
if not user_styles:
|
1401
|
+
user_styles = extract_user_styles(
|
1402
|
+
await self.get_user(user_fields=["preferences", "items", "stats"]),
|
1403
|
+
)
|
1404
|
+
preferences = user_styles.preferences
|
1405
|
+
items = user_styles.items
|
1406
|
+
stats = user_styles.stats
|
1407
|
+
mount_offset_y = 0 if items.currentMount else 24
|
1408
|
+
|
1409
|
+
# Initializing the base image
|
1410
|
+
image = Image.new("RGBA", (141, 147), (255, 0, 0, 0))
|
1411
|
+
|
1412
|
+
async def paste_gear(gear_type: str) -> None:
|
1413
|
+
"""Fetch and paste gear from equipped or costume gear sets."""
|
1414
|
+
gear_set = (
|
1415
|
+
items.gear.costume if preferences.costume else items.gear.equipped
|
1416
|
+
)
|
1417
|
+
gear = getattr(gear_set, gear_type)
|
1418
|
+
if gear and gear != f"{gear_type}_base_0":
|
1419
|
+
# 2019 Kickstarter gear doesn't follow name conventions
|
1420
|
+
if special_ks2019 := BACKER_ONLY_GEAR.get(gear):
|
1421
|
+
gear = special_ks2019
|
1422
|
+
# armor has slim and broad size options
|
1423
|
+
elif gear_type == "armor":
|
1424
|
+
gear = f"{preferences.size}_{gear}"
|
1425
|
+
await self.paste_image(image, gear, (24, mount_offset_y))
|
1426
|
+
|
1427
|
+
# fetch and paste the background
|
1428
|
+
if preferences.background:
|
1429
|
+
await self.paste_image(
|
1430
|
+
image,
|
1431
|
+
f"background_{preferences.background}",
|
1432
|
+
(0, 0),
|
1433
|
+
)
|
1434
|
+
|
1435
|
+
# Fetch and paste the mount body
|
1436
|
+
if items.currentMount:
|
1437
|
+
await self.paste_image(
|
1438
|
+
image,
|
1439
|
+
f"Mount_Body_{items.currentMount}",
|
1440
|
+
(24, 18),
|
1441
|
+
)
|
1442
|
+
|
1443
|
+
# Fetch and paste avatars for visual buffs
|
1444
|
+
if (
|
1445
|
+
stats.buffs.seafoam
|
1446
|
+
or stats.buffs.shinySeed
|
1447
|
+
or stats.buffs.snowball
|
1448
|
+
or stats.buffs.spookySparkles
|
1449
|
+
):
|
1450
|
+
if stats.buffs.spookySparkles:
|
1451
|
+
await self.paste_image(image, "ghost", (24, mount_offset_y))
|
1452
|
+
if stats.buffs.shinySeed:
|
1453
|
+
await self.paste_image(
|
1454
|
+
image,
|
1455
|
+
f"avatar_snowball_{stats.Class}",
|
1456
|
+
(24, mount_offset_y),
|
1457
|
+
)
|
1458
|
+
if stats.buffs.shinySeed:
|
1459
|
+
await self.paste_image(
|
1460
|
+
image,
|
1461
|
+
f"avatar_floral_{stats.Class}",
|
1462
|
+
(24, mount_offset_y),
|
1463
|
+
)
|
1464
|
+
if stats.buffs.seafoam:
|
1465
|
+
await self.paste_image(
|
1466
|
+
image,
|
1467
|
+
"seafoam_star",
|
1468
|
+
(24, mount_offset_y),
|
1469
|
+
)
|
1470
|
+
|
1471
|
+
# Fetch and paste the hairflower
|
1472
|
+
if preferences.hair.flower:
|
1473
|
+
await self.paste_image(
|
1474
|
+
image,
|
1475
|
+
f"hair_flower_{preferences.hair.flower}",
|
1476
|
+
(24, mount_offset_y),
|
1477
|
+
)
|
1478
|
+
|
1479
|
+
else:
|
1480
|
+
# Fetch and paste the chair
|
1481
|
+
if preferences.chair and preferences.chair != "none":
|
1482
|
+
await self.paste_image(
|
1483
|
+
image,
|
1484
|
+
f"chair_{preferences.chair}",
|
1485
|
+
(24, 0),
|
1486
|
+
)
|
1487
|
+
|
1488
|
+
# Fetch and paste the back accessory
|
1489
|
+
await paste_gear("back")
|
1490
|
+
|
1491
|
+
# Fetch and paste the skin
|
1492
|
+
await self.paste_image(
|
1493
|
+
image,
|
1494
|
+
f"skin_{preferences.skin}{"_sleep" if preferences.sleep else ""}",
|
1495
|
+
(24, mount_offset_y),
|
1496
|
+
)
|
1497
|
+
|
1498
|
+
# Fetch and paste the shirt
|
1499
|
+
await self.paste_image(
|
1500
|
+
image,
|
1501
|
+
f"{preferences.size}_shirt_{preferences.shirt}",
|
1502
|
+
(24, mount_offset_y),
|
1503
|
+
)
|
1504
|
+
|
1505
|
+
# Fetch and paste the head base
|
1506
|
+
await self.paste_image(image, "head_0", (24, mount_offset_y))
|
1507
|
+
|
1508
|
+
# Fetch and paste the armor if not the base armor
|
1509
|
+
await paste_gear("armor")
|
1510
|
+
|
1511
|
+
# Fetch and paste the hair elements
|
1512
|
+
for hair_type in ("bangs", "base", "mustache", "beard"):
|
1513
|
+
if style := getattr(preferences.hair, hair_type, 0):
|
1514
|
+
await self.paste_image(
|
1515
|
+
image,
|
1516
|
+
f"hair_{hair_type}_{style}_{preferences.hair.color}",
|
1517
|
+
(24, mount_offset_y),
|
1518
|
+
)
|
1519
|
+
|
1520
|
+
# Fetch and paste body accessory, eyewear, headgear and head accessory
|
1521
|
+
for gear in ("body", "eyewear", "head", "headAccessory"):
|
1522
|
+
await paste_gear(gear)
|
1523
|
+
|
1524
|
+
# Fetch and paste the hairflower
|
1525
|
+
if preferences.hair.flower:
|
1526
|
+
await self.paste_image(
|
1527
|
+
image,
|
1528
|
+
f"hair_flower_{preferences.hair.flower}",
|
1529
|
+
(24, mount_offset_y),
|
1530
|
+
)
|
1531
|
+
|
1532
|
+
# Fetch and paste the shield
|
1533
|
+
await paste_gear("shield")
|
1534
|
+
# Fetch and paste the weapon
|
1535
|
+
await paste_gear("weapon")
|
1536
|
+
|
1537
|
+
# Fetch and paste the zzz
|
1538
|
+
if preferences.sleep:
|
1539
|
+
await self.paste_image(image, "zzz", (24, mount_offset_y))
|
1540
|
+
|
1541
|
+
# Fetch and paste the mount head
|
1542
|
+
if items.currentMount:
|
1543
|
+
await self.paste_image(
|
1544
|
+
image,
|
1545
|
+
f"Mount_Head_{items.currentMount}",
|
1546
|
+
(24, 18),
|
1547
|
+
)
|
1548
|
+
# Fetch and paste the pet
|
1549
|
+
if items.currentPet:
|
1550
|
+
await self.paste_image(image, f"Pet-{items.currentPet}", (0, 48))
|
1551
|
+
|
1552
|
+
if isinstance(fp, str):
|
1553
|
+
loop = asyncio.get_running_loop()
|
1554
|
+
loop.run_in_executor(None, image.save, fp)
|
1555
|
+
else:
|
1556
|
+
image.save(fp, fmt)
|
1557
|
+
|
1558
|
+
return user_styles
|