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
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
"""Async HackerRank for Work API client."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Self, cast
|
|
7
|
+
|
|
8
|
+
from beartype import beartype
|
|
9
|
+
|
|
10
|
+
from hackerrank.exceptions import HackerRankError
|
|
11
|
+
from hackerrank.transports import (
|
|
12
|
+
AsyncHTTPXTransport,
|
|
13
|
+
AsyncTransport,
|
|
14
|
+
TransportResponse,
|
|
15
|
+
)
|
|
16
|
+
from hackerrank.types import (
|
|
17
|
+
ATSCodePair,
|
|
18
|
+
ATSCodeScreen,
|
|
19
|
+
AuditLog,
|
|
20
|
+
Interview,
|
|
21
|
+
InterviewTemplate,
|
|
22
|
+
InterviewTranscript,
|
|
23
|
+
Inviter,
|
|
24
|
+
JSONValue,
|
|
25
|
+
Page,
|
|
26
|
+
Question,
|
|
27
|
+
SCIMPage,
|
|
28
|
+
SCIMTeam,
|
|
29
|
+
SCIMUser,
|
|
30
|
+
Team,
|
|
31
|
+
Template,
|
|
32
|
+
Test,
|
|
33
|
+
TestCandidate,
|
|
34
|
+
User,
|
|
35
|
+
UserTeamMembership,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_API_V3 = "/x/api/v3"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _drop_none(
|
|
42
|
+
data: Mapping[str, JSONValue],
|
|
43
|
+
/,
|
|
44
|
+
) -> dict[str, JSONValue]:
|
|
45
|
+
"""Return a copy of ``data`` with ``None`` values removed.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data: The dictionary to filter.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A new dictionary without ``None`` values.
|
|
52
|
+
"""
|
|
53
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _coerce_int(value: object, /) -> int:
|
|
57
|
+
"""Coerce ``value`` to ``int``, defaulting to ``0``.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
value: A value that may be ``int``, ``str`` or
|
|
61
|
+
``None``.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The integer value, or ``0`` if conversion is not
|
|
65
|
+
possible.
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(value, bool):
|
|
68
|
+
return int(value)
|
|
69
|
+
if isinstance(value, int):
|
|
70
|
+
return value
|
|
71
|
+
if isinstance(value, str) and value:
|
|
72
|
+
try:
|
|
73
|
+
return int(value)
|
|
74
|
+
except ValueError:
|
|
75
|
+
return 0
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _coerce_str(value: object, /) -> str:
|
|
80
|
+
"""Coerce ``value`` to ``str``, defaulting to ``""``.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
value: A value that may be a ``str`` or ``None``.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The string value, or ``""`` if ``None``.
|
|
87
|
+
"""
|
|
88
|
+
if isinstance(value, str):
|
|
89
|
+
return value
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _make_page[T](
|
|
94
|
+
items: list[T],
|
|
95
|
+
metadata: Mapping[str, JSONValue],
|
|
96
|
+
/,
|
|
97
|
+
) -> Page[T]:
|
|
98
|
+
"""Wrap ``items`` and ``metadata`` into a ``Page``.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
items: The items in the page.
|
|
102
|
+
metadata: The raw response metadata.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A populated ``Page`` instance.
|
|
106
|
+
"""
|
|
107
|
+
return Page(
|
|
108
|
+
items,
|
|
109
|
+
page_total=_coerce_int(metadata.get("page_total")),
|
|
110
|
+
offset=_coerce_int(metadata.get("offset")),
|
|
111
|
+
previous=_coerce_str(metadata.get("previous")),
|
|
112
|
+
next_=_coerce_str(metadata.get("next")),
|
|
113
|
+
first=_coerce_str(metadata.get("first")),
|
|
114
|
+
last=_coerce_str(metadata.get("last")),
|
|
115
|
+
total=_coerce_int(metadata.get("total")),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _make_scim_page[T](
|
|
120
|
+
items: list[T],
|
|
121
|
+
payload: Mapping[str, JSONValue],
|
|
122
|
+
/,
|
|
123
|
+
) -> SCIMPage[T]:
|
|
124
|
+
"""Wrap items and a SCIM payload into a ``SCIMPage``.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
items: The SCIM items in the page.
|
|
128
|
+
payload: The raw SCIM response payload.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A populated ``SCIMPage`` instance.
|
|
132
|
+
"""
|
|
133
|
+
schemas_raw = payload.get("schemas")
|
|
134
|
+
schemas: list[str] = (
|
|
135
|
+
list(cast("list[str]", schemas_raw))
|
|
136
|
+
if isinstance(schemas_raw, list)
|
|
137
|
+
else []
|
|
138
|
+
)
|
|
139
|
+
start_index = _coerce_int(payload.get("startIndex")) or 1
|
|
140
|
+
return SCIMPage(
|
|
141
|
+
items,
|
|
142
|
+
schemas=schemas,
|
|
143
|
+
start_index=start_index,
|
|
144
|
+
items_per_page=_coerce_int(payload.get("itemsPerPage")),
|
|
145
|
+
total_results=_coerce_int(payload.get("totalResults")),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _list_params(
|
|
150
|
+
*,
|
|
151
|
+
limit: int | None,
|
|
152
|
+
offset: int | None,
|
|
153
|
+
extra: dict[str, str | int] | None = None,
|
|
154
|
+
) -> dict[str, str | int]:
|
|
155
|
+
"""Build query parameters for a list call.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
limit: Maximum number of items to return.
|
|
159
|
+
offset: Offset to start from.
|
|
160
|
+
extra: Any extra parameters to include.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The query parameter mapping.
|
|
164
|
+
"""
|
|
165
|
+
params: dict[str, str | int] = {}
|
|
166
|
+
if limit is not None:
|
|
167
|
+
params["limit"] = limit
|
|
168
|
+
if offset is not None:
|
|
169
|
+
params["offset"] = offset
|
|
170
|
+
if extra:
|
|
171
|
+
params.update(extra)
|
|
172
|
+
return params
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@beartype
|
|
176
|
+
class _AsyncNamespace:
|
|
177
|
+
"""Base class providing shared async request logic."""
|
|
178
|
+
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
*,
|
|
182
|
+
transport: AsyncTransport,
|
|
183
|
+
base_url: str,
|
|
184
|
+
headers: dict[str, str],
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Create a new async namespace.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
transport: The async HTTP transport.
|
|
190
|
+
base_url: The base URL for the API.
|
|
191
|
+
headers: Headers to send with every request.
|
|
192
|
+
"""
|
|
193
|
+
self.transport = transport
|
|
194
|
+
self.base_url = base_url
|
|
195
|
+
self.headers = headers
|
|
196
|
+
|
|
197
|
+
async def _request(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
method: str,
|
|
201
|
+
url: str,
|
|
202
|
+
params: dict[str, str | int] | None = None,
|
|
203
|
+
json: Mapping[str, JSONValue] | None = None,
|
|
204
|
+
) -> TransportResponse:
|
|
205
|
+
"""Make an async HTTP request.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
method: The HTTP method.
|
|
209
|
+
url: The URL path.
|
|
210
|
+
params: Query parameters.
|
|
211
|
+
json: JSON-serialisable body.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
The transport response.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
HackerRankError: If the response has an error
|
|
218
|
+
status code.
|
|
219
|
+
"""
|
|
220
|
+
response = await self.transport(
|
|
221
|
+
method=method,
|
|
222
|
+
url=self.base_url + url,
|
|
223
|
+
headers=self.headers,
|
|
224
|
+
params=params,
|
|
225
|
+
json=json,
|
|
226
|
+
)
|
|
227
|
+
if response.status_code >= HTTPStatus.BAD_REQUEST:
|
|
228
|
+
raise HackerRankError.from_response(response=response)
|
|
229
|
+
return response
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@beartype
|
|
233
|
+
class AsyncInterviewsNamespace(_AsyncNamespace):
|
|
234
|
+
"""Async namespace for interview operations."""
|
|
235
|
+
|
|
236
|
+
async def list(
|
|
237
|
+
self,
|
|
238
|
+
*,
|
|
239
|
+
limit: int | None = None,
|
|
240
|
+
offset: int | None = None,
|
|
241
|
+
) -> Page[Interview]:
|
|
242
|
+
"""List interviews.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
limit: Number of records to fetch.
|
|
246
|
+
offset: Offset of records.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
A page of interviews.
|
|
250
|
+
"""
|
|
251
|
+
response = await self._request(
|
|
252
|
+
method="GET",
|
|
253
|
+
url=f"{_API_V3}/interviews",
|
|
254
|
+
params=_list_params(limit=limit, offset=offset),
|
|
255
|
+
)
|
|
256
|
+
payload = response.json()
|
|
257
|
+
raw_items = list(payload.get("data", []))
|
|
258
|
+
items: list[Interview] = [
|
|
259
|
+
Interview.from_dict(data=item) for item in raw_items
|
|
260
|
+
]
|
|
261
|
+
return _make_page(items, payload)
|
|
262
|
+
|
|
263
|
+
async def create(
|
|
264
|
+
self,
|
|
265
|
+
*,
|
|
266
|
+
title: str | None = None,
|
|
267
|
+
from_: str | None = None,
|
|
268
|
+
to: str | None = None,
|
|
269
|
+
interview_template_id: int | None = None,
|
|
270
|
+
candidate: Mapping[str, JSONValue] | None = None,
|
|
271
|
+
send_email: bool | None = None,
|
|
272
|
+
metadata: Mapping[str, JSONValue] | None = None,
|
|
273
|
+
interviewers: Sequence[str] | None = None,
|
|
274
|
+
) -> Interview:
|
|
275
|
+
"""Create an interview.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
title: Title of the interview.
|
|
279
|
+
from_: Scheduled start time.
|
|
280
|
+
to: Scheduled end time.
|
|
281
|
+
interview_template_id: Template to apply.
|
|
282
|
+
candidate: Candidate details.
|
|
283
|
+
send_email: Whether to send an email invite.
|
|
284
|
+
metadata: Arbitrary metadata.
|
|
285
|
+
interviewers: Emails of interviewers.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
The created interview.
|
|
289
|
+
"""
|
|
290
|
+
body = _drop_none(
|
|
291
|
+
{
|
|
292
|
+
"title": title,
|
|
293
|
+
"from": from_,
|
|
294
|
+
"to": to,
|
|
295
|
+
"interview_template_id": interview_template_id,
|
|
296
|
+
"candidate": candidate,
|
|
297
|
+
"send_email": send_email,
|
|
298
|
+
"metadata": metadata,
|
|
299
|
+
"interviewers": (
|
|
300
|
+
list(interviewers) if interviewers is not None else None
|
|
301
|
+
),
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
response = await self._request(
|
|
305
|
+
method="POST",
|
|
306
|
+
url=f"{_API_V3}/interviews",
|
|
307
|
+
json=body,
|
|
308
|
+
)
|
|
309
|
+
return Interview.from_dict(data=response.json())
|
|
310
|
+
|
|
311
|
+
async def get(self, *, interview_id: str) -> Interview:
|
|
312
|
+
"""Retrieve an interview.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
interview_id: The id of the interview.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
The interview.
|
|
319
|
+
"""
|
|
320
|
+
response = await self._request(
|
|
321
|
+
method="GET",
|
|
322
|
+
url=f"{_API_V3}/interviews/{interview_id}",
|
|
323
|
+
)
|
|
324
|
+
return Interview.from_dict(data=response.json())
|
|
325
|
+
|
|
326
|
+
async def delete(self, *, interview_id: str) -> None:
|
|
327
|
+
"""Delete an interview.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
interview_id: The id of the interview.
|
|
331
|
+
"""
|
|
332
|
+
await self._request(
|
|
333
|
+
method="DELETE",
|
|
334
|
+
url=f"{_API_V3}/interviews/{interview_id}",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
async def get_transcript(
|
|
338
|
+
self,
|
|
339
|
+
*,
|
|
340
|
+
interview_id: str,
|
|
341
|
+
) -> InterviewTranscript:
|
|
342
|
+
"""Retrieve the transcript of an interview.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
interview_id: The id of the interview.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
The transcript.
|
|
349
|
+
"""
|
|
350
|
+
response = await self._request(
|
|
351
|
+
method="GET",
|
|
352
|
+
url=(f"{_API_V3}/interviews/{interview_id}/transcript"),
|
|
353
|
+
)
|
|
354
|
+
return InterviewTranscript.from_dict(data=response.json())
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@beartype
|
|
358
|
+
class AsyncInterviewTemplatesNamespace(_AsyncNamespace):
|
|
359
|
+
"""Async namespace for interview-template operations."""
|
|
360
|
+
|
|
361
|
+
async def list(
|
|
362
|
+
self,
|
|
363
|
+
*,
|
|
364
|
+
limit: int | None = None,
|
|
365
|
+
offset: int | None = None,
|
|
366
|
+
) -> Page[InterviewTemplate]:
|
|
367
|
+
"""List interview templates.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
limit: Number of records to fetch.
|
|
371
|
+
offset: Offset of records.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
A page of interview templates.
|
|
375
|
+
"""
|
|
376
|
+
response = await self._request(
|
|
377
|
+
method="GET",
|
|
378
|
+
url=f"{_API_V3}/interview_templates",
|
|
379
|
+
params=_list_params(limit=limit, offset=offset),
|
|
380
|
+
)
|
|
381
|
+
payload = response.json()
|
|
382
|
+
raw_items = list(payload.get("data", []))
|
|
383
|
+
items: list[InterviewTemplate] = [
|
|
384
|
+
InterviewTemplate.from_dict(data=item) for item in raw_items
|
|
385
|
+
]
|
|
386
|
+
return _make_page(items, payload)
|
|
387
|
+
|
|
388
|
+
async def get(
|
|
389
|
+
self,
|
|
390
|
+
*,
|
|
391
|
+
template_id: int | str,
|
|
392
|
+
) -> InterviewTemplate:
|
|
393
|
+
"""Retrieve an interview template.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
template_id: The id of the template.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
The template.
|
|
400
|
+
"""
|
|
401
|
+
response = await self._request(
|
|
402
|
+
method="GET",
|
|
403
|
+
url=(f"{_API_V3}/interview_templates/{template_id}"),
|
|
404
|
+
)
|
|
405
|
+
return InterviewTemplate.from_dict(data=response.json())
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@beartype
|
|
409
|
+
class AsyncQuestionsNamespace(_AsyncNamespace):
|
|
410
|
+
"""Async namespace for question operations."""
|
|
411
|
+
|
|
412
|
+
async def list(
|
|
413
|
+
self,
|
|
414
|
+
*,
|
|
415
|
+
limit: int | None = None,
|
|
416
|
+
offset: int | None = None,
|
|
417
|
+
) -> Page[Question]:
|
|
418
|
+
"""List questions.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
limit: Number of records to fetch.
|
|
422
|
+
offset: Offset of records.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
A page of questions.
|
|
426
|
+
"""
|
|
427
|
+
response = await self._request(
|
|
428
|
+
method="GET",
|
|
429
|
+
url=f"{_API_V3}/questions",
|
|
430
|
+
params=_list_params(limit=limit, offset=offset),
|
|
431
|
+
)
|
|
432
|
+
payload = response.json()
|
|
433
|
+
raw_items = list(payload.get("data", []))
|
|
434
|
+
items: list[Question] = [
|
|
435
|
+
Question.from_dict(data=item) for item in raw_items
|
|
436
|
+
]
|
|
437
|
+
return _make_page(items, payload)
|
|
438
|
+
|
|
439
|
+
async def get(self, *, question_id: str) -> Question:
|
|
440
|
+
"""Retrieve a question.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
question_id: The id of the question.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
The question.
|
|
447
|
+
"""
|
|
448
|
+
response = await self._request(
|
|
449
|
+
method="GET",
|
|
450
|
+
url=f"{_API_V3}/questions/{question_id}",
|
|
451
|
+
)
|
|
452
|
+
return Question.from_dict(data=response.json())
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@beartype
|
|
456
|
+
class AsyncTestCandidatesNamespace(_AsyncNamespace):
|
|
457
|
+
"""Async namespace for candidates of a test."""
|
|
458
|
+
|
|
459
|
+
async def list(
|
|
460
|
+
self,
|
|
461
|
+
*,
|
|
462
|
+
test_id: str,
|
|
463
|
+
limit: int | None = None,
|
|
464
|
+
offset: int | None = None,
|
|
465
|
+
) -> Page[TestCandidate]:
|
|
466
|
+
"""List candidates for a test.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
test_id: The id of the test.
|
|
470
|
+
limit: Number of records to fetch.
|
|
471
|
+
offset: Offset of records.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
A page of candidates.
|
|
475
|
+
"""
|
|
476
|
+
response = await self._request(
|
|
477
|
+
method="GET",
|
|
478
|
+
url=f"{_API_V3}/tests/{test_id}/candidates",
|
|
479
|
+
params=_list_params(limit=limit, offset=offset),
|
|
480
|
+
)
|
|
481
|
+
payload = response.json()
|
|
482
|
+
raw_items = list(payload.get("data", []))
|
|
483
|
+
items: list[TestCandidate] = [
|
|
484
|
+
TestCandidate.from_dict(data=item) for item in raw_items
|
|
485
|
+
]
|
|
486
|
+
return _make_page(items, payload)
|
|
487
|
+
|
|
488
|
+
async def invite(
|
|
489
|
+
self,
|
|
490
|
+
*,
|
|
491
|
+
test_id: str,
|
|
492
|
+
email: str,
|
|
493
|
+
full_name: str | None = None,
|
|
494
|
+
send_email: bool | None = None,
|
|
495
|
+
) -> TestCandidate:
|
|
496
|
+
"""Invite a candidate to a test.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
test_id: The id of the test.
|
|
500
|
+
email: Candidate email address.
|
|
501
|
+
full_name: Candidate full name.
|
|
502
|
+
send_email: Whether to send the invitation email.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
The created candidate.
|
|
506
|
+
"""
|
|
507
|
+
body = _drop_none(
|
|
508
|
+
{
|
|
509
|
+
"email": email,
|
|
510
|
+
"full_name": full_name,
|
|
511
|
+
"send_email": send_email,
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
response = await self._request(
|
|
515
|
+
method="POST",
|
|
516
|
+
url=f"{_API_V3}/tests/{test_id}/candidates",
|
|
517
|
+
json=body,
|
|
518
|
+
)
|
|
519
|
+
return TestCandidate.from_dict(data=response.json())
|
|
520
|
+
|
|
521
|
+
async def get(
|
|
522
|
+
self,
|
|
523
|
+
*,
|
|
524
|
+
test_id: str,
|
|
525
|
+
candidate_id: str,
|
|
526
|
+
) -> TestCandidate:
|
|
527
|
+
"""Retrieve a single candidate.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
test_id: The id of the test.
|
|
531
|
+
candidate_id: The id of the candidate.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
The candidate.
|
|
535
|
+
"""
|
|
536
|
+
response = await self._request(
|
|
537
|
+
method="GET",
|
|
538
|
+
url=(f"{_API_V3}/tests/{test_id}/candidates/{candidate_id}"),
|
|
539
|
+
)
|
|
540
|
+
return TestCandidate.from_dict(data=response.json())
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@beartype
|
|
544
|
+
class AsyncTestsNamespace(_AsyncNamespace):
|
|
545
|
+
"""Async namespace for test operations."""
|
|
546
|
+
|
|
547
|
+
def __init__(
|
|
548
|
+
self,
|
|
549
|
+
*,
|
|
550
|
+
transport: AsyncTransport,
|
|
551
|
+
base_url: str,
|
|
552
|
+
headers: dict[str, str],
|
|
553
|
+
) -> None:
|
|
554
|
+
"""Create the namespace.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
transport: The HTTP transport.
|
|
558
|
+
base_url: The base URL for the API.
|
|
559
|
+
headers: Headers to send with every request.
|
|
560
|
+
"""
|
|
561
|
+
super().__init__(
|
|
562
|
+
transport=transport,
|
|
563
|
+
base_url=base_url,
|
|
564
|
+
headers=headers,
|
|
565
|
+
)
|
|
566
|
+
self.candidates: AsyncTestCandidatesNamespace = (
|
|
567
|
+
AsyncTestCandidatesNamespace(
|
|
568
|
+
transport=transport,
|
|
569
|
+
base_url=base_url,
|
|
570
|
+
headers=headers,
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
async def list(
|
|
575
|
+
self,
|
|
576
|
+
*,
|
|
577
|
+
limit: int | None = None,
|
|
578
|
+
offset: int | None = None,
|
|
579
|
+
) -> Page[Test]:
|
|
580
|
+
"""List tests.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
limit: Number of records to fetch.
|
|
584
|
+
offset: Offset of records.
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
A page of tests.
|
|
588
|
+
"""
|
|
589
|
+
response = await self._request(
|
|
590
|
+
method="GET",
|
|
591
|
+
url=f"{_API_V3}/tests",
|
|
592
|
+
params=_list_params(limit=limit, offset=offset),
|
|
593
|
+
)
|
|
594
|
+
payload = response.json()
|
|
595
|
+
raw_items = list(payload.get("data", []))
|
|
596
|
+
items: list[Test] = [Test.from_dict(data=item) for item in raw_items]
|
|
597
|
+
return _make_page(items, payload)
|
|
598
|
+
|
|
599
|
+
async def get(
|
|
600
|
+
self,
|
|
601
|
+
*,
|
|
602
|
+
test_id: str,
|
|
603
|
+
) -> Test:
|
|
604
|
+
"""Retrieve a test.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
test_id: The id of the test.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
The test.
|
|
611
|
+
"""
|
|
612
|
+
response = await self._request(
|
|
613
|
+
method="GET",
|
|
614
|
+
url=f"{_API_V3}/tests/{test_id}",
|
|
615
|
+
)
|
|
616
|
+
return Test.from_dict(data=response.json())
|
|
617
|
+
|
|
618
|
+
async def list_inviters(
|
|
619
|
+
self,
|
|
620
|
+
*,
|
|
621
|
+
test_id: str,
|
|
622
|
+
limit: int | None = None,
|
|
623
|
+
offset: int | None = None,
|
|
624
|
+
) -> Page[Inviter]:
|
|
625
|
+
"""List inviters for a test.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
test_id: The id of the test.
|
|
629
|
+
limit: Number of records to fetch.
|
|
630
|
+
offset: Offset of records.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
A page of inviters.
|
|
634
|
+
"""
|
|
635
|
+
response = await self._request(
|
|
636
|
+
method="GET",
|
|
637
|
+
url=f"{_API_V3}/tests/{test_id}/inviters",
|
|
638
|
+
params=_list_params(limit=limit, offset=offset),
|
|
639
|
+
)
|
|
640
|
+
payload = response.json()
|
|
641
|
+
raw_items = list(payload.get("data", []))
|
|
642
|
+
items: list[Inviter] = [
|
|
643
|
+
Inviter.from_dict(data=item) for item in raw_items
|
|
644
|
+
]
|
|
645
|
+
return _make_page(items, payload)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@beartype
|
|
649
|
+
class AsyncTemplatesNamespace(_AsyncNamespace):
|
|
650
|
+
"""Async namespace for invite-email templates."""
|
|
651
|
+
|
|
652
|
+
async def list(
|
|
653
|
+
self,
|
|
654
|
+
*,
|
|
655
|
+
limit: int | None = None,
|
|
656
|
+
offset: int | None = None,
|
|
657
|
+
) -> Page[Template]:
|
|
658
|
+
"""List invite templates.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
limit: Number of records to fetch.
|
|
662
|
+
offset: Offset of records.
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
A page of templates.
|
|
666
|
+
"""
|
|
667
|
+
response = await self._request(
|
|
668
|
+
method="GET",
|
|
669
|
+
url=f"{_API_V3}/templates",
|
|
670
|
+
params=_list_params(limit=limit, offset=offset),
|
|
671
|
+
)
|
|
672
|
+
payload = response.json()
|
|
673
|
+
raw_items = list(payload.get("data", []))
|
|
674
|
+
items: list[Template] = [
|
|
675
|
+
Template.from_dict(data=item) for item in raw_items
|
|
676
|
+
]
|
|
677
|
+
return _make_page(items, payload)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@beartype
|
|
681
|
+
class AsyncUsersNamespace(_AsyncNamespace):
|
|
682
|
+
"""Async namespace for user operations."""
|
|
683
|
+
|
|
684
|
+
async def list(
|
|
685
|
+
self,
|
|
686
|
+
*,
|
|
687
|
+
limit: int | None = None,
|
|
688
|
+
offset: int | None = None,
|
|
689
|
+
) -> Page[User]:
|
|
690
|
+
"""List users.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
limit: Number of records to fetch.
|
|
694
|
+
offset: Offset of records.
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
A page of users.
|
|
698
|
+
"""
|
|
699
|
+
response = await self._request(
|
|
700
|
+
method="GET",
|
|
701
|
+
url=f"{_API_V3}/users",
|
|
702
|
+
params=_list_params(limit=limit, offset=offset),
|
|
703
|
+
)
|
|
704
|
+
payload = response.json()
|
|
705
|
+
raw_items = list(payload.get("data", []))
|
|
706
|
+
items: list[User] = [User.from_dict(data=item) for item in raw_items]
|
|
707
|
+
return _make_page(items, payload)
|
|
708
|
+
|
|
709
|
+
async def get(self, *, user_id: str) -> User:
|
|
710
|
+
"""Retrieve a user.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
user_id: The id of the user.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
The user.
|
|
717
|
+
"""
|
|
718
|
+
response = await self._request(
|
|
719
|
+
method="GET",
|
|
720
|
+
url=f"{_API_V3}/users/{user_id}",
|
|
721
|
+
)
|
|
722
|
+
return User.from_dict(data=response.json())
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
@beartype
|
|
726
|
+
class AsyncTeamsNamespace(_AsyncNamespace):
|
|
727
|
+
"""Async namespace for team operations."""
|
|
728
|
+
|
|
729
|
+
async def list(
|
|
730
|
+
self,
|
|
731
|
+
*,
|
|
732
|
+
limit: int | None = None,
|
|
733
|
+
offset: int | None = None,
|
|
734
|
+
) -> Page[Team]:
|
|
735
|
+
"""List teams.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
limit: Number of records to fetch.
|
|
739
|
+
offset: Offset of records.
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
A page of teams.
|
|
743
|
+
"""
|
|
744
|
+
response = await self._request(
|
|
745
|
+
method="GET",
|
|
746
|
+
url=f"{_API_V3}/teams",
|
|
747
|
+
params=_list_params(limit=limit, offset=offset),
|
|
748
|
+
)
|
|
749
|
+
payload = response.json()
|
|
750
|
+
raw_items = list(payload.get("data", []))
|
|
751
|
+
items: list[Team] = [Team.from_dict(data=item) for item in raw_items]
|
|
752
|
+
return _make_page(items, payload)
|
|
753
|
+
|
|
754
|
+
async def get(self, *, team_id: str) -> Team:
|
|
755
|
+
"""Retrieve a team.
|
|
756
|
+
|
|
757
|
+
Args:
|
|
758
|
+
team_id: The id of the team.
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
The team.
|
|
762
|
+
"""
|
|
763
|
+
response = await self._request(
|
|
764
|
+
method="GET",
|
|
765
|
+
url=f"{_API_V3}/teams/{team_id}",
|
|
766
|
+
)
|
|
767
|
+
return Team.from_dict(data=response.json())
|
|
768
|
+
|
|
769
|
+
async def list_members(
|
|
770
|
+
self,
|
|
771
|
+
*,
|
|
772
|
+
team_id: str,
|
|
773
|
+
limit: int | None = None,
|
|
774
|
+
offset: int | None = None,
|
|
775
|
+
) -> Page[UserTeamMembership]:
|
|
776
|
+
"""List memberships for a team.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
team_id: The id of the team.
|
|
780
|
+
limit: Number of records to fetch.
|
|
781
|
+
offset: Offset of records.
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
A page of memberships.
|
|
785
|
+
"""
|
|
786
|
+
response = await self._request(
|
|
787
|
+
method="GET",
|
|
788
|
+
url=f"{_API_V3}/teams/{team_id}/users",
|
|
789
|
+
params=_list_params(limit=limit, offset=offset),
|
|
790
|
+
)
|
|
791
|
+
payload = response.json()
|
|
792
|
+
raw_items = list(payload.get("data", []))
|
|
793
|
+
items: list[UserTeamMembership] = [
|
|
794
|
+
UserTeamMembership.from_dict(data=item) for item in raw_items
|
|
795
|
+
]
|
|
796
|
+
return _make_page(items, payload)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
@beartype
|
|
800
|
+
class AsyncAuditLogsNamespace(_AsyncNamespace):
|
|
801
|
+
"""Async namespace for audit-log operations."""
|
|
802
|
+
|
|
803
|
+
async def list(
|
|
804
|
+
self,
|
|
805
|
+
*,
|
|
806
|
+
limit: int | None = None,
|
|
807
|
+
offset: int | None = None,
|
|
808
|
+
user_id: str | None = None,
|
|
809
|
+
) -> Page[AuditLog]:
|
|
810
|
+
"""List audit logs.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
limit: Number of records to fetch.
|
|
814
|
+
offset: Offset of records.
|
|
815
|
+
user_id: Filter to a specific user.
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
A page of audit logs.
|
|
819
|
+
"""
|
|
820
|
+
extra: dict[str, str | int] = {}
|
|
821
|
+
if user_id is not None:
|
|
822
|
+
extra["user_id"] = user_id
|
|
823
|
+
response = await self._request(
|
|
824
|
+
method="GET",
|
|
825
|
+
url=f"{_API_V3}/audit_log",
|
|
826
|
+
params=_list_params(
|
|
827
|
+
limit=limit,
|
|
828
|
+
offset=offset,
|
|
829
|
+
extra=extra,
|
|
830
|
+
),
|
|
831
|
+
)
|
|
832
|
+
payload = response.json()
|
|
833
|
+
raw_items = list(payload.get("data", []))
|
|
834
|
+
items: list[AuditLog] = [
|
|
835
|
+
AuditLog.from_dict(data=item) for item in raw_items
|
|
836
|
+
]
|
|
837
|
+
return _make_page(items, payload)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@beartype
|
|
841
|
+
class AsyncATSNamespace(_AsyncNamespace):
|
|
842
|
+
"""Async namespace for ATS operations."""
|
|
843
|
+
|
|
844
|
+
async def codepair_invite(
|
|
845
|
+
self,
|
|
846
|
+
*,
|
|
847
|
+
body: Mapping[str, JSONValue],
|
|
848
|
+
) -> ATSCodePair:
|
|
849
|
+
"""Invite a candidate to an ATS Codepair interview.
|
|
850
|
+
|
|
851
|
+
Args:
|
|
852
|
+
body: The Codepair invite payload.
|
|
853
|
+
|
|
854
|
+
Returns:
|
|
855
|
+
The ATS Codepair result.
|
|
856
|
+
"""
|
|
857
|
+
response = await self._request(
|
|
858
|
+
method="POST",
|
|
859
|
+
url=f"{_API_V3}/ats/codepair",
|
|
860
|
+
json=body,
|
|
861
|
+
)
|
|
862
|
+
return ATSCodePair.from_dict(data=response.json())
|
|
863
|
+
|
|
864
|
+
async def codescreen_invite(
|
|
865
|
+
self,
|
|
866
|
+
*,
|
|
867
|
+
body: Mapping[str, JSONValue],
|
|
868
|
+
) -> ATSCodeScreen:
|
|
869
|
+
"""Invite a candidate to a CodeScreen test.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
body: The CodeScreen invite payload.
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
The ATS CodeScreen result.
|
|
876
|
+
"""
|
|
877
|
+
response = await self._request(
|
|
878
|
+
method="POST",
|
|
879
|
+
url=f"{_API_V3}/ats/codescreen",
|
|
880
|
+
json=body,
|
|
881
|
+
)
|
|
882
|
+
return ATSCodeScreen.from_dict(data=response.json())
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@beartype
|
|
886
|
+
class AsyncSCIMNamespace(_AsyncNamespace):
|
|
887
|
+
"""Async namespace for SCIM v2 operations."""
|
|
888
|
+
|
|
889
|
+
async def list_users(
|
|
890
|
+
self,
|
|
891
|
+
*,
|
|
892
|
+
limit: int | None = None,
|
|
893
|
+
offset: int | None = None,
|
|
894
|
+
) -> SCIMPage[SCIMUser]:
|
|
895
|
+
"""List SCIM users.
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
limit: Number of records to fetch.
|
|
899
|
+
offset: Offset of records.
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
A page of SCIM users.
|
|
903
|
+
"""
|
|
904
|
+
response = await self._request(
|
|
905
|
+
method="GET",
|
|
906
|
+
url="/Users",
|
|
907
|
+
params=_list_params(limit=limit, offset=offset),
|
|
908
|
+
)
|
|
909
|
+
payload = response.json()
|
|
910
|
+
raw = list(payload.get("Resources", []))
|
|
911
|
+
items: list[SCIMUser] = [SCIMUser.from_dict(data=item) for item in raw]
|
|
912
|
+
return _make_scim_page(items, payload)
|
|
913
|
+
|
|
914
|
+
async def list_groups(
|
|
915
|
+
self,
|
|
916
|
+
*,
|
|
917
|
+
limit: int | None = None,
|
|
918
|
+
offset: int | None = None,
|
|
919
|
+
) -> SCIMPage[SCIMTeam]:
|
|
920
|
+
"""List SCIM groups (teams).
|
|
921
|
+
|
|
922
|
+
Args:
|
|
923
|
+
limit: Number of records to fetch.
|
|
924
|
+
offset: Offset of records.
|
|
925
|
+
|
|
926
|
+
Returns:
|
|
927
|
+
A page of SCIM teams.
|
|
928
|
+
"""
|
|
929
|
+
response = await self._request(
|
|
930
|
+
method="GET",
|
|
931
|
+
url="/Groups",
|
|
932
|
+
params=_list_params(limit=limit, offset=offset),
|
|
933
|
+
)
|
|
934
|
+
payload = response.json()
|
|
935
|
+
raw = list(payload.get("Resources", []))
|
|
936
|
+
items: list[SCIMTeam] = [SCIMTeam.from_dict(data=item) for item in raw]
|
|
937
|
+
return _make_scim_page(items, payload)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
@beartype
|
|
941
|
+
class AsyncHackerRank:
|
|
942
|
+
"""An async client for the HackerRank for Work API."""
|
|
943
|
+
|
|
944
|
+
def __init__(
|
|
945
|
+
self,
|
|
946
|
+
*,
|
|
947
|
+
api_key: str,
|
|
948
|
+
base_url: str = "https://www.hackerrank.com",
|
|
949
|
+
transport: AsyncTransport | None = None,
|
|
950
|
+
) -> None:
|
|
951
|
+
"""Create a new async HackerRank client.
|
|
952
|
+
|
|
953
|
+
Args:
|
|
954
|
+
api_key: The API key for authentication.
|
|
955
|
+
base_url: The base URL for the API.
|
|
956
|
+
transport: The HTTP transport. Defaults to
|
|
957
|
+
``AsyncHTTPXTransport()``.
|
|
958
|
+
"""
|
|
959
|
+
self.base_url = base_url
|
|
960
|
+
resolved_transport = transport or AsyncHTTPXTransport()
|
|
961
|
+
headers = {
|
|
962
|
+
"Authorization": f"Bearer {api_key}",
|
|
963
|
+
"Accept": "application/json",
|
|
964
|
+
}
|
|
965
|
+
self.interviews: AsyncInterviewsNamespace = AsyncInterviewsNamespace(
|
|
966
|
+
transport=resolved_transport,
|
|
967
|
+
base_url=base_url,
|
|
968
|
+
headers=headers,
|
|
969
|
+
)
|
|
970
|
+
self.interview_templates: AsyncInterviewTemplatesNamespace = (
|
|
971
|
+
AsyncInterviewTemplatesNamespace(
|
|
972
|
+
transport=resolved_transport,
|
|
973
|
+
base_url=base_url,
|
|
974
|
+
headers=headers,
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
self.questions: AsyncQuestionsNamespace = AsyncQuestionsNamespace(
|
|
978
|
+
transport=resolved_transport,
|
|
979
|
+
base_url=base_url,
|
|
980
|
+
headers=headers,
|
|
981
|
+
)
|
|
982
|
+
self.tests: AsyncTestsNamespace = AsyncTestsNamespace(
|
|
983
|
+
transport=resolved_transport,
|
|
984
|
+
base_url=base_url,
|
|
985
|
+
headers=headers,
|
|
986
|
+
)
|
|
987
|
+
self.templates: AsyncTemplatesNamespace = AsyncTemplatesNamespace(
|
|
988
|
+
transport=resolved_transport,
|
|
989
|
+
base_url=base_url,
|
|
990
|
+
headers=headers,
|
|
991
|
+
)
|
|
992
|
+
self.users: AsyncUsersNamespace = AsyncUsersNamespace(
|
|
993
|
+
transport=resolved_transport,
|
|
994
|
+
base_url=base_url,
|
|
995
|
+
headers=headers,
|
|
996
|
+
)
|
|
997
|
+
self.teams: AsyncTeamsNamespace = AsyncTeamsNamespace(
|
|
998
|
+
transport=resolved_transport,
|
|
999
|
+
base_url=base_url,
|
|
1000
|
+
headers=headers,
|
|
1001
|
+
)
|
|
1002
|
+
self.audit_logs: AsyncAuditLogsNamespace = AsyncAuditLogsNamespace(
|
|
1003
|
+
transport=resolved_transport,
|
|
1004
|
+
base_url=base_url,
|
|
1005
|
+
headers=headers,
|
|
1006
|
+
)
|
|
1007
|
+
self.ats: AsyncATSNamespace = AsyncATSNamespace(
|
|
1008
|
+
transport=resolved_transport,
|
|
1009
|
+
base_url=base_url,
|
|
1010
|
+
headers=headers,
|
|
1011
|
+
)
|
|
1012
|
+
self.scim: AsyncSCIMNamespace = AsyncSCIMNamespace(
|
|
1013
|
+
transport=resolved_transport,
|
|
1014
|
+
base_url=base_url,
|
|
1015
|
+
headers=headers,
|
|
1016
|
+
)
|
|
1017
|
+
self._owned_transport = (
|
|
1018
|
+
resolved_transport
|
|
1019
|
+
if isinstance(resolved_transport, AsyncHTTPXTransport)
|
|
1020
|
+
else None
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
async def aclose(self) -> None:
|
|
1024
|
+
"""Close the underlying transport if it supports closing."""
|
|
1025
|
+
if self._owned_transport is not None:
|
|
1026
|
+
await self._owned_transport.aclose()
|
|
1027
|
+
|
|
1028
|
+
async def __aenter__(self) -> Self:
|
|
1029
|
+
"""Enter the async context manager.
|
|
1030
|
+
|
|
1031
|
+
Returns:
|
|
1032
|
+
This client instance.
|
|
1033
|
+
"""
|
|
1034
|
+
return self
|
|
1035
|
+
|
|
1036
|
+
async def __aexit__(
|
|
1037
|
+
self,
|
|
1038
|
+
_exc_type: type[BaseException] | None,
|
|
1039
|
+
_exc_val: BaseException | None,
|
|
1040
|
+
_exc_tb: TracebackType | None,
|
|
1041
|
+
/,
|
|
1042
|
+
) -> None:
|
|
1043
|
+
"""Exit the async context manager and close."""
|
|
1044
|
+
await self.aclose()
|