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.
@@ -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