hackerrank 2026.5.12.2__py2.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.
- hackerrank/__init__.py +1 -0
- hackerrank/_dict_types.py +324 -0
- hackerrank/async_client.py +1044 -0
- hackerrank/client.py +2446 -0
- hackerrank/exceptions.py +129 -0
- hackerrank/py.typed +0 -0
- hackerrank/transports.py +263 -0
- hackerrank/types.py +1012 -0
- hackerrank-2026.5.12.2.dist-info/METADATA +117 -0
- hackerrank-2026.5.12.2.dist-info/RECORD +13 -0
- hackerrank-2026.5.12.2.dist-info/WHEEL +6 -0
- hackerrank-2026.5.12.2.dist-info/licenses/LICENSE +21 -0
- hackerrank-2026.5.12.2.dist-info/top_level.txt +1 -0
hackerrank/exceptions.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Custom exception hierarchy for the HackerRank API."""
|
|
2
|
+
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from hackerrank.transports import TransportResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HackerRankError(Exception):
|
|
10
|
+
"""Base exception for all HackerRank API errors.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
response: The full transport response for debugging.
|
|
14
|
+
status_code: The HTTP status code.
|
|
15
|
+
content: The response body.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_registry: ClassVar[dict[int, type["HackerRankError"]]] = {}
|
|
19
|
+
|
|
20
|
+
def __init_subclass__(
|
|
21
|
+
cls,
|
|
22
|
+
*,
|
|
23
|
+
status_code: HTTPStatus | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Register subclass for a specific HTTP status code.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
status_code: The HTTP status code to map.
|
|
29
|
+
"""
|
|
30
|
+
super().__init_subclass__()
|
|
31
|
+
if status_code is not None:
|
|
32
|
+
HackerRankError._registry[status_code.value] = cls
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
response: TransportResponse,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Create a new HackerRank error.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
response: The transport response that caused
|
|
43
|
+
the error.
|
|
44
|
+
"""
|
|
45
|
+
message = f"HTTP {response.status_code}"
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.response: TransportResponse = response
|
|
48
|
+
self.status_code: int = response.status_code
|
|
49
|
+
self.content: bytes = response.content
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_response(
|
|
53
|
+
cls,
|
|
54
|
+
*,
|
|
55
|
+
response: TransportResponse,
|
|
56
|
+
) -> "HackerRankError":
|
|
57
|
+
"""Create the appropriate exception for a response.
|
|
58
|
+
|
|
59
|
+
Uses the registry to find a specific exception class
|
|
60
|
+
for the response's status code, falling back to
|
|
61
|
+
``HackerRankError``.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
response: The transport response.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The appropriate exception instance.
|
|
68
|
+
"""
|
|
69
|
+
exc_cls = cls._registry.get(
|
|
70
|
+
response.status_code,
|
|
71
|
+
HackerRankError,
|
|
72
|
+
)
|
|
73
|
+
return exc_cls(response=response)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BadRequestError(
|
|
77
|
+
HackerRankError,
|
|
78
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
79
|
+
):
|
|
80
|
+
"""Raised for 400 Bad Request responses."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AuthenticationError(
|
|
84
|
+
HackerRankError,
|
|
85
|
+
status_code=HTTPStatus.UNAUTHORIZED,
|
|
86
|
+
):
|
|
87
|
+
"""Raised for 401 Unauthorized responses."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ForbiddenError(
|
|
91
|
+
HackerRankError,
|
|
92
|
+
status_code=HTTPStatus.FORBIDDEN,
|
|
93
|
+
):
|
|
94
|
+
"""Raised for 403 Forbidden responses."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class NotFoundError(
|
|
98
|
+
HackerRankError,
|
|
99
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
100
|
+
):
|
|
101
|
+
"""Raised for 404 Not Found responses."""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ConflictError(
|
|
105
|
+
HackerRankError,
|
|
106
|
+
status_code=HTTPStatus.CONFLICT,
|
|
107
|
+
):
|
|
108
|
+
"""Raised for 409 Conflict responses."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class UnprocessableEntityError(
|
|
112
|
+
HackerRankError,
|
|
113
|
+
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
|
|
114
|
+
):
|
|
115
|
+
"""Raised for 422 Unprocessable Entity responses."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RateLimitError(
|
|
119
|
+
HackerRankError,
|
|
120
|
+
status_code=HTTPStatus.TOO_MANY_REQUESTS,
|
|
121
|
+
):
|
|
122
|
+
"""Raised for 429 Too Many Requests responses."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ServerError(
|
|
126
|
+
HackerRankError,
|
|
127
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
128
|
+
):
|
|
129
|
+
"""Raised for 500 Internal Server Error responses."""
|
hackerrank/py.typed
ADDED
|
File without changes
|
hackerrank/transports.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Transport abstractions for the HackerRank for Work API."""
|
|
2
|
+
|
|
3
|
+
import json as json_module
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Any, Protocol, Self, runtime_checkable
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from beartype import beartype
|
|
12
|
+
|
|
13
|
+
from hackerrank.types import JSONValue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HTTPStatusError(Exception):
|
|
17
|
+
"""Raised when an HTTP response has an error status code."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
status_code: int,
|
|
23
|
+
content: bytes,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Create a new HTTP status error.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
status_code: The HTTP status code.
|
|
29
|
+
content: The response body.
|
|
30
|
+
"""
|
|
31
|
+
message = f"HTTP {status_code}"
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.status_code = status_code
|
|
34
|
+
self.content = content
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@beartype
|
|
38
|
+
@dataclass(frozen=True, kw_only=True)
|
|
39
|
+
class TransportResponse:
|
|
40
|
+
"""A response from a transport."""
|
|
41
|
+
|
|
42
|
+
status_code: int
|
|
43
|
+
headers: dict[str, str]
|
|
44
|
+
content: bytes
|
|
45
|
+
|
|
46
|
+
def json(self) -> Any: # noqa: ANN401
|
|
47
|
+
"""Parse the response body as JSON.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The parsed JSON data.
|
|
51
|
+
"""
|
|
52
|
+
return json_module.loads(s=self.content)
|
|
53
|
+
|
|
54
|
+
def raise_for_status(self) -> None:
|
|
55
|
+
"""Raise an error if the response has an error status.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
HTTPStatusError: If the status code is 400 or above.
|
|
59
|
+
"""
|
|
60
|
+
if self.status_code >= HTTPStatus.BAD_REQUEST:
|
|
61
|
+
raise HTTPStatusError(
|
|
62
|
+
status_code=self.status_code,
|
|
63
|
+
content=self.content,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@runtime_checkable
|
|
68
|
+
class Transport(Protocol):
|
|
69
|
+
"""Protocol for HTTP transports.
|
|
70
|
+
|
|
71
|
+
A transport is a callable that makes an HTTP request and
|
|
72
|
+
returns a ``TransportResponse``.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __call__(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
method: str,
|
|
79
|
+
url: str,
|
|
80
|
+
headers: dict[str, str],
|
|
81
|
+
params: dict[str, str | int] | None = None,
|
|
82
|
+
json: Mapping[str, JSONValue] | None = None,
|
|
83
|
+
) -> TransportResponse:
|
|
84
|
+
"""Make an HTTP request.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
method: The HTTP method (e.g. ``GET``, ``POST``).
|
|
88
|
+
url: The full URL to request.
|
|
89
|
+
headers: Headers to send with the request.
|
|
90
|
+
params: Query parameters.
|
|
91
|
+
json: A JSON-serialisable body. When provided the
|
|
92
|
+
``Content-Type`` header is set to
|
|
93
|
+
``application/json``.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A ``TransportResponse`` populated from the HTTP
|
|
97
|
+
response.
|
|
98
|
+
"""
|
|
99
|
+
... # pylint: disable=unnecessary-ellipsis
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@beartype
|
|
103
|
+
class HTTPXTransport:
|
|
104
|
+
"""HTTP transport using the ``httpx`` library.
|
|
105
|
+
|
|
106
|
+
This is the default transport. It uses a shared
|
|
107
|
+
``httpx.Client`` for connection pooling.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self) -> None:
|
|
111
|
+
"""Create a new HTTPX transport."""
|
|
112
|
+
self._client = httpx.Client()
|
|
113
|
+
|
|
114
|
+
def close(self) -> None:
|
|
115
|
+
"""Close the underlying HTTP client."""
|
|
116
|
+
self._client.close()
|
|
117
|
+
|
|
118
|
+
def __enter__(self) -> Self:
|
|
119
|
+
"""Enter the context manager.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
This transport instance.
|
|
123
|
+
"""
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def __exit__(
|
|
127
|
+
self,
|
|
128
|
+
_exc_type: type[BaseException] | None,
|
|
129
|
+
_exc_val: BaseException | None,
|
|
130
|
+
_exc_tb: TracebackType | None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Exit the context manager and close the client."""
|
|
133
|
+
self.close()
|
|
134
|
+
|
|
135
|
+
def __call__(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
method: str,
|
|
139
|
+
url: str,
|
|
140
|
+
headers: dict[str, str],
|
|
141
|
+
params: dict[str, str | int] | None = None,
|
|
142
|
+
json: Mapping[str, JSONValue] | None = None,
|
|
143
|
+
) -> TransportResponse:
|
|
144
|
+
"""Make an HTTP request using ``httpx``.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
method: The HTTP method.
|
|
148
|
+
url: The full URL.
|
|
149
|
+
headers: Request headers.
|
|
150
|
+
params: Query parameters.
|
|
151
|
+
json: A JSON-serialisable body.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A ``TransportResponse`` populated from the httpx
|
|
155
|
+
response.
|
|
156
|
+
"""
|
|
157
|
+
response = self._client.request(
|
|
158
|
+
method=method,
|
|
159
|
+
url=url,
|
|
160
|
+
headers=headers,
|
|
161
|
+
params=params,
|
|
162
|
+
json=json,
|
|
163
|
+
)
|
|
164
|
+
return TransportResponse(
|
|
165
|
+
status_code=response.status_code,
|
|
166
|
+
headers=dict(response.headers),
|
|
167
|
+
content=response.content,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@runtime_checkable
|
|
172
|
+
class AsyncTransport(Protocol):
|
|
173
|
+
"""Protocol for async HTTP transports."""
|
|
174
|
+
|
|
175
|
+
async def __call__(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
method: str,
|
|
179
|
+
url: str,
|
|
180
|
+
headers: dict[str, str],
|
|
181
|
+
params: dict[str, str | int] | None = None,
|
|
182
|
+
json: Mapping[str, JSONValue] | None = None,
|
|
183
|
+
) -> TransportResponse:
|
|
184
|
+
"""Make an async HTTP request.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
method: The HTTP method.
|
|
188
|
+
url: The full URL to request.
|
|
189
|
+
headers: Headers to send with the request.
|
|
190
|
+
params: Query parameters.
|
|
191
|
+
json: A JSON-serialisable body.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A ``TransportResponse`` populated from the HTTP
|
|
195
|
+
response.
|
|
196
|
+
"""
|
|
197
|
+
... # pylint: disable=unnecessary-ellipsis
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@beartype
|
|
201
|
+
class AsyncHTTPXTransport:
|
|
202
|
+
"""Async HTTP transport using the ``httpx`` library."""
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
"""Create a new async HTTPX transport."""
|
|
206
|
+
self._client = httpx.AsyncClient()
|
|
207
|
+
|
|
208
|
+
async def aclose(self) -> None:
|
|
209
|
+
"""Close the underlying async HTTP client."""
|
|
210
|
+
await self._client.aclose()
|
|
211
|
+
|
|
212
|
+
async def __aenter__(self) -> Self:
|
|
213
|
+
"""Enter the async context manager.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
This transport instance.
|
|
217
|
+
"""
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
async def __aexit__(
|
|
221
|
+
self,
|
|
222
|
+
_exc_type: type[BaseException] | None,
|
|
223
|
+
_exc_val: BaseException | None,
|
|
224
|
+
_exc_tb: TracebackType | None,
|
|
225
|
+
/,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Exit the async context manager and close."""
|
|
228
|
+
await self.aclose()
|
|
229
|
+
|
|
230
|
+
async def __call__(
|
|
231
|
+
self,
|
|
232
|
+
*,
|
|
233
|
+
method: str,
|
|
234
|
+
url: str,
|
|
235
|
+
headers: dict[str, str],
|
|
236
|
+
params: dict[str, str | int] | None = None,
|
|
237
|
+
json: Mapping[str, JSONValue] | None = None,
|
|
238
|
+
) -> TransportResponse:
|
|
239
|
+
"""Make an async HTTP request using ``httpx``.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
method: The HTTP method.
|
|
243
|
+
url: The full URL.
|
|
244
|
+
headers: Request headers.
|
|
245
|
+
params: Query parameters.
|
|
246
|
+
json: A JSON-serialisable body.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
A ``TransportResponse`` populated from the httpx
|
|
250
|
+
response.
|
|
251
|
+
"""
|
|
252
|
+
response = await self._client.request(
|
|
253
|
+
method=method,
|
|
254
|
+
url=url,
|
|
255
|
+
headers=headers,
|
|
256
|
+
params=params,
|
|
257
|
+
json=json,
|
|
258
|
+
)
|
|
259
|
+
return TransportResponse(
|
|
260
|
+
status_code=response.status_code,
|
|
261
|
+
headers=dict(response.headers),
|
|
262
|
+
content=response.content,
|
|
263
|
+
)
|