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/__init__.py
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
"""Modern asynchronous Python client library for the Habitica API."""
|
2
|
+
|
3
|
+
from .const import ASSETS_URL, DEFAULT_URL, __version__
|
4
|
+
from .exceptions import (
|
5
|
+
BadRequestError,
|
6
|
+
HabiticaException,
|
7
|
+
NotAuthorizedError,
|
8
|
+
NotFoundError,
|
9
|
+
TooManyRequestsError,
|
10
|
+
)
|
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
|
+
)
|
37
|
+
|
38
|
+
__all__ = [
|
39
|
+
"__version__",
|
40
|
+
"ASSETS_URL",
|
41
|
+
"Attributes",
|
42
|
+
"BadRequestError",
|
43
|
+
"DEFAULT_URL",
|
44
|
+
"Direction",
|
45
|
+
"Frequency",
|
46
|
+
"Habitica",
|
47
|
+
"HabiticaClass",
|
48
|
+
"HabiticaClassSystemResponse",
|
49
|
+
"HabiticaErrorResponse",
|
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",
|
63
|
+
"NotAuthorizedError",
|
64
|
+
"NotFoundError",
|
65
|
+
"Skill",
|
66
|
+
"Task",
|
67
|
+
"TaskFilter",
|
68
|
+
"TaskType",
|
69
|
+
"TooManyRequestsError",
|
70
|
+
"UserStyles",
|
71
|
+
]
|
habiticalib/const.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
"""Constants for Habiticalib."""
|
2
|
+
|
3
|
+
__version__ = "0.1.0"
|
4
|
+
|
5
|
+
DEFAULT_URL = "https://habitica.com/"
|
6
|
+
ASSETS_URL = "https://habitica-assets.s3.amazonaws.com/mobileApp/images/"
|
7
|
+
|
8
|
+
DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
|
9
|
+
|
10
|
+
BACKER_ONLY_GEAR = {
|
11
|
+
"armor_special_ks2019": "BackerOnly-Equip-MythicGryphonArmor.gif",
|
12
|
+
"eyewear_special_ks2019": "BackerOnly-Equip-MythicGryphonVisor.gif",
|
13
|
+
"head_special_ks2019": "BackerOnly-Equip-MythicGryphonHelm.gif",
|
14
|
+
"shield_special_ks2019": "BackerOnly-Equip-MythicGryphonShield.gif",
|
15
|
+
"weapon_special_ks2019": "BackerOnly-Equip-MythicGryphonGlaive.gif",
|
16
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
"""Exceptions for Habiticalib."""
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Self
|
5
|
+
|
6
|
+
from multidict import CIMultiDictProxy
|
7
|
+
|
8
|
+
from habiticalib.types import HabiticaErrorResponse
|
9
|
+
|
10
|
+
|
11
|
+
class HabiticaException(Exception): # noqa: N818
|
12
|
+
"""Base class for Habitica errors."""
|
13
|
+
|
14
|
+
def __init__(
|
15
|
+
self: Self,
|
16
|
+
error: HabiticaErrorResponse,
|
17
|
+
headers: CIMultiDictProxy,
|
18
|
+
) -> None:
|
19
|
+
"""Initialize the Exception."""
|
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)))
|
33
|
+
|
34
|
+
super().__init__(error.message)
|
35
|
+
|
36
|
+
|
37
|
+
class NotAuthorizedError(HabiticaException):
|
38
|
+
"""NotAuthorized error."""
|
39
|
+
|
40
|
+
|
41
|
+
class NotFoundError(HabiticaException):
|
42
|
+
"""NotFound error."""
|
43
|
+
|
44
|
+
|
45
|
+
class BadRequestError(HabiticaException):
|
46
|
+
"""BadRequest error."""
|
47
|
+
|
48
|
+
|
49
|
+
class TooManyRequestsError(HabiticaException):
|
50
|
+
"""TooManyRequests error."""
|
habiticalib/helpers.py
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
"""Helper functions for Habiticalib."""
|
2
|
+
|
3
|
+
from dataclasses import asdict, is_dataclass
|
4
|
+
from datetime import date, datetime
|
5
|
+
from enum import Enum
|
6
|
+
import platform
|
7
|
+
from typing import Any
|
8
|
+
import uuid
|
9
|
+
|
10
|
+
import aiohttp
|
11
|
+
|
12
|
+
from .const import DEVELOPER_ID, __version__
|
13
|
+
from .types import HabiticaUserResponse, UserData, UserStyles
|
14
|
+
|
15
|
+
|
16
|
+
def join_fields(user_fields: list[str] | str) -> str:
|
17
|
+
"""Join user fields into a comma-separated string.
|
18
|
+
|
19
|
+
This method takes a list of user fields or a single string and
|
20
|
+
returns a string. If a list is provided, it joins the elements
|
21
|
+
with a comma separator. If a string is provided, it returns
|
22
|
+
the string as-is.
|
23
|
+
|
24
|
+
Parameters
|
25
|
+
----------
|
26
|
+
user_fields : list of str or str
|
27
|
+
A list of user field strings or a single user field string.
|
28
|
+
|
29
|
+
Returns
|
30
|
+
-------
|
31
|
+
str
|
32
|
+
A comma-separated string of user fields if `user_fields` is a list,
|
33
|
+
or the original string if `user_fields` is a single string.
|
34
|
+
"""
|
35
|
+
return ",".join(user_fields) if isinstance(user_fields, list) else str(user_fields)
|
36
|
+
|
37
|
+
|
38
|
+
def get_user_agent() -> str:
|
39
|
+
"""Generate User-Agent string.
|
40
|
+
|
41
|
+
The User-Agent string contains details about the operating system,
|
42
|
+
its version, architecture, the habiticalib version, aiohttp version,
|
43
|
+
and Python version.
|
44
|
+
|
45
|
+
Returns
|
46
|
+
-------
|
47
|
+
str
|
48
|
+
A User-Agent string with OS details, library versions, and a project URL.
|
49
|
+
|
50
|
+
Examples
|
51
|
+
--------
|
52
|
+
>>> client.get_user_agent()
|
53
|
+
'Habiticalib/0.0.0 (Windows 11 (10.0.22000); 64bit)
|
54
|
+
aiohttp/3.10.9 Python/3.12.7 +https://github.com/tr4nt0r/habiticalib)'
|
55
|
+
"""
|
56
|
+
os_name = platform.system()
|
57
|
+
os_version = platform.version()
|
58
|
+
os_release = platform.release()
|
59
|
+
arch, _ = platform.architecture()
|
60
|
+
os_info = f"{os_name} {os_release} ({os_version}); {arch}"
|
61
|
+
|
62
|
+
return (
|
63
|
+
f"Habiticalib/{__version__} ({os_info}) "
|
64
|
+
f"aiohttp/{aiohttp.__version__} Python/{platform.python_version()} "
|
65
|
+
" +https://github.com/tr4nt0r/habiticalib)"
|
66
|
+
)
|
67
|
+
|
68
|
+
|
69
|
+
def get_x_client(x_client: str | None = None) -> str:
|
70
|
+
"""Generate the x-client header string.
|
71
|
+
|
72
|
+
If a valid `x_client` is provided, it validates the User ID (UUID format).
|
73
|
+
If no `x_client` is provided, it generates a default x-client header string.
|
74
|
+
|
75
|
+
Parameters
|
76
|
+
----------
|
77
|
+
x_client : str or None, optional
|
78
|
+
A client identifier containing a UUID (first 36 characters) or None. If
|
79
|
+
None, a default x-client header will be generated.
|
80
|
+
|
81
|
+
Returns
|
82
|
+
-------
|
83
|
+
str
|
84
|
+
The validated x-client header string or habiticalib's default x-client header.
|
85
|
+
|
86
|
+
Raises
|
87
|
+
------
|
88
|
+
ValueError
|
89
|
+
If the provided `x_client` is not in a valid UUID format.
|
90
|
+
|
91
|
+
Examples
|
92
|
+
--------
|
93
|
+
>>> get_x_client("123e4567-e89b-12d3-a456-426614174000 - MyHabiticaApp")
|
94
|
+
'123e4567-e89b-12d3-a456-426614174000 - MyHabiticaApp'
|
95
|
+
|
96
|
+
>>> get_x_client()
|
97
|
+
'4c4ca53f-c059-4ffa-966e-9d29dd405daf - Habiticalib/0.0.0'
|
98
|
+
"""
|
99
|
+
if x_client:
|
100
|
+
try:
|
101
|
+
uuid.UUID(x_client[:36])
|
102
|
+
except ValueError as e:
|
103
|
+
msg = (
|
104
|
+
"Invalid User ID provided in x-client. Expected a valid "
|
105
|
+
"UUID format. Please ensure the User ID is correct"
|
106
|
+
)
|
107
|
+
raise ValueError(msg) from e
|
108
|
+
|
109
|
+
return x_client
|
110
|
+
|
111
|
+
return f"{DEVELOPER_ID} - Habiticalib/{__version__}"
|
112
|
+
|
113
|
+
|
114
|
+
def extract_user_styles(user_data: HabiticaUserResponse) -> UserStyles:
|
115
|
+
"""Extract user styles from a user data object."""
|
116
|
+
data: UserData = user_data.data
|
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
|