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