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/client.py
ADDED
|
@@ -0,0 +1,2446 @@
|
|
|
1
|
+
"""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
|
+
HTTPXTransport,
|
|
13
|
+
Transport,
|
|
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
|
+
@beartype
|
|
57
|
+
class _Namespace:
|
|
58
|
+
"""Base class providing shared request logic."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
transport: Transport,
|
|
64
|
+
base_url: str,
|
|
65
|
+
headers: dict[str, str],
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Create a new namespace.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
transport: The HTTP transport.
|
|
71
|
+
base_url: The base URL for the API.
|
|
72
|
+
headers: Headers to send with every request.
|
|
73
|
+
"""
|
|
74
|
+
self.transport = transport
|
|
75
|
+
self.base_url = base_url
|
|
76
|
+
self.headers = headers
|
|
77
|
+
|
|
78
|
+
def _request(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
method: str,
|
|
82
|
+
url: str,
|
|
83
|
+
params: dict[str, str | int] | None = None,
|
|
84
|
+
json: Mapping[str, JSONValue] | None = None,
|
|
85
|
+
) -> TransportResponse:
|
|
86
|
+
"""Make an HTTP request.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
method: The HTTP method.
|
|
90
|
+
url: The URL path.
|
|
91
|
+
params: Query parameters.
|
|
92
|
+
json: JSON-serialisable body.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The transport response.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
HackerRankError: If the response has an error
|
|
99
|
+
status code.
|
|
100
|
+
"""
|
|
101
|
+
response = self.transport(
|
|
102
|
+
method=method,
|
|
103
|
+
url=self.base_url + url,
|
|
104
|
+
headers=self.headers,
|
|
105
|
+
params=params,
|
|
106
|
+
json=json,
|
|
107
|
+
)
|
|
108
|
+
if response.status_code >= HTTPStatus.BAD_REQUEST:
|
|
109
|
+
raise HackerRankError.from_response(response=response)
|
|
110
|
+
return response
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _coerce_int(value: object, /) -> int:
|
|
114
|
+
"""Coerce ``value`` to an ``int``, defaulting to ``0``.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
value: A value that may be ``int``, ``str`` or
|
|
118
|
+
``None``.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The integer value, or ``0`` if conversion is not
|
|
122
|
+
possible.
|
|
123
|
+
"""
|
|
124
|
+
if isinstance(value, bool):
|
|
125
|
+
return int(value)
|
|
126
|
+
if isinstance(value, int):
|
|
127
|
+
return value
|
|
128
|
+
if isinstance(value, str) and value:
|
|
129
|
+
try:
|
|
130
|
+
return int(value)
|
|
131
|
+
except ValueError:
|
|
132
|
+
return 0
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _coerce_str(value: object, /) -> str:
|
|
137
|
+
"""Coerce ``value`` to a ``str``, defaulting to ``""``.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
value: A value that may be a ``str`` or ``None``.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The string value, or ``""`` if ``None``.
|
|
144
|
+
"""
|
|
145
|
+
if isinstance(value, str):
|
|
146
|
+
return value
|
|
147
|
+
return ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _make_page[T](
|
|
151
|
+
items: list[T],
|
|
152
|
+
metadata: Mapping[str, JSONValue],
|
|
153
|
+
/,
|
|
154
|
+
) -> Page[T]:
|
|
155
|
+
"""Wrap ``items`` and ``metadata`` into a ``Page``.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
items: The items in the page.
|
|
159
|
+
metadata: The raw response metadata.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
A populated ``Page`` instance.
|
|
163
|
+
"""
|
|
164
|
+
return Page(
|
|
165
|
+
items,
|
|
166
|
+
page_total=_coerce_int(metadata.get("page_total")),
|
|
167
|
+
offset=_coerce_int(metadata.get("offset")),
|
|
168
|
+
previous=_coerce_str(metadata.get("previous")),
|
|
169
|
+
next_=_coerce_str(metadata.get("next")),
|
|
170
|
+
first=_coerce_str(metadata.get("first")),
|
|
171
|
+
last=_coerce_str(metadata.get("last")),
|
|
172
|
+
total=_coerce_int(metadata.get("total")),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _list_params(
|
|
177
|
+
*,
|
|
178
|
+
limit: int | None,
|
|
179
|
+
offset: int | None,
|
|
180
|
+
extra: dict[str, str | int] | None = None,
|
|
181
|
+
) -> dict[str, str | int]:
|
|
182
|
+
"""Build query parameters for a list call.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
limit: Maximum number of items to return.
|
|
186
|
+
offset: Offset to start from.
|
|
187
|
+
extra: Any extra parameters to include.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
The query parameter mapping.
|
|
191
|
+
"""
|
|
192
|
+
params: dict[str, str | int] = {}
|
|
193
|
+
if limit is not None:
|
|
194
|
+
params["limit"] = limit
|
|
195
|
+
if offset is not None:
|
|
196
|
+
params["offset"] = offset
|
|
197
|
+
if extra:
|
|
198
|
+
params.update(extra)
|
|
199
|
+
return params
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@beartype
|
|
203
|
+
class InterviewsNamespace(_Namespace):
|
|
204
|
+
"""Namespace for interview operations."""
|
|
205
|
+
|
|
206
|
+
def list(
|
|
207
|
+
self,
|
|
208
|
+
*,
|
|
209
|
+
limit: int | None = None,
|
|
210
|
+
offset: int | None = None,
|
|
211
|
+
created_at: str | None = None,
|
|
212
|
+
updated_at: str | None = None,
|
|
213
|
+
ended_at: str | None = None,
|
|
214
|
+
) -> Page[Interview]:
|
|
215
|
+
"""List interviews.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
limit: Number of records to fetch.
|
|
219
|
+
offset: Offset of records.
|
|
220
|
+
created_at: ``from..to`` filter for creation time.
|
|
221
|
+
updated_at: ``from..to`` filter for update time.
|
|
222
|
+
ended_at: ``from..to`` filter for ending time.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
A page of interviews.
|
|
226
|
+
"""
|
|
227
|
+
extra: dict[str, str | int] = {}
|
|
228
|
+
if created_at is not None:
|
|
229
|
+
extra["created_at"] = created_at
|
|
230
|
+
if updated_at is not None:
|
|
231
|
+
extra["updated_at"] = updated_at
|
|
232
|
+
if ended_at is not None:
|
|
233
|
+
extra["ended_at"] = ended_at
|
|
234
|
+
response = self._request(
|
|
235
|
+
method="GET",
|
|
236
|
+
url=f"{_API_V3}/interviews",
|
|
237
|
+
params=_list_params(
|
|
238
|
+
limit=limit,
|
|
239
|
+
offset=offset,
|
|
240
|
+
extra=extra,
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
payload = response.json()
|
|
244
|
+
raw_items = list(payload.get("data", []))
|
|
245
|
+
items: list[Interview] = [
|
|
246
|
+
Interview.from_dict(data=item) for item in raw_items
|
|
247
|
+
]
|
|
248
|
+
return _make_page(items, payload)
|
|
249
|
+
|
|
250
|
+
def create(
|
|
251
|
+
self,
|
|
252
|
+
*,
|
|
253
|
+
title: str | None = None,
|
|
254
|
+
from_: str | None = None,
|
|
255
|
+
to: str | None = None,
|
|
256
|
+
notes: str | None = None,
|
|
257
|
+
resume_url: str | None = None,
|
|
258
|
+
interviewers: Sequence[str] | None = None,
|
|
259
|
+
result_url: str | None = None,
|
|
260
|
+
candidate: Mapping[str, JSONValue] | None = None,
|
|
261
|
+
send_email: bool | None = None,
|
|
262
|
+
metadata: Mapping[str, JSONValue] | None = None,
|
|
263
|
+
interview_template_id: int | None = None,
|
|
264
|
+
) -> Interview:
|
|
265
|
+
"""Create an interview.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
title: Title of the interview.
|
|
269
|
+
from_: Scheduled start time.
|
|
270
|
+
to: Scheduled end time.
|
|
271
|
+
notes: Private notes.
|
|
272
|
+
resume_url: URL to the candidate resume.
|
|
273
|
+
interviewers: Emails of interviewers.
|
|
274
|
+
result_url: URL invoked when the interview ends.
|
|
275
|
+
candidate: Candidate details.
|
|
276
|
+
send_email: Whether to send an email invite.
|
|
277
|
+
metadata: Arbitrary metadata.
|
|
278
|
+
interview_template_id: Template to apply.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
The created interview.
|
|
282
|
+
"""
|
|
283
|
+
body = _drop_none(
|
|
284
|
+
{
|
|
285
|
+
"title": title,
|
|
286
|
+
"from": from_,
|
|
287
|
+
"to": to,
|
|
288
|
+
"notes": notes,
|
|
289
|
+
"resume_url": resume_url,
|
|
290
|
+
"interviewers": (
|
|
291
|
+
list(interviewers) if interviewers is not None else None
|
|
292
|
+
),
|
|
293
|
+
"result_url": result_url,
|
|
294
|
+
"candidate": candidate,
|
|
295
|
+
"send_email": send_email,
|
|
296
|
+
"metadata": metadata,
|
|
297
|
+
"interview_template_id": interview_template_id,
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
response = self._request(
|
|
301
|
+
method="POST",
|
|
302
|
+
url=f"{_API_V3}/interviews",
|
|
303
|
+
json=body,
|
|
304
|
+
)
|
|
305
|
+
return Interview.from_dict(data=response.json())
|
|
306
|
+
|
|
307
|
+
def get(self, *, interview_id: str) -> Interview:
|
|
308
|
+
"""Retrieve an interview.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
interview_id: The id of the interview.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
The interview.
|
|
315
|
+
"""
|
|
316
|
+
response = self._request(
|
|
317
|
+
method="GET",
|
|
318
|
+
url=f"{_API_V3}/interviews/{interview_id}",
|
|
319
|
+
)
|
|
320
|
+
return Interview.from_dict(data=response.json())
|
|
321
|
+
|
|
322
|
+
def update(
|
|
323
|
+
self,
|
|
324
|
+
*,
|
|
325
|
+
interview_id: str,
|
|
326
|
+
title: str | None = None,
|
|
327
|
+
from_: str | None = None,
|
|
328
|
+
to: str | None = None,
|
|
329
|
+
notes: str | None = None,
|
|
330
|
+
resume_url: str | None = None,
|
|
331
|
+
result_url: str | None = None,
|
|
332
|
+
candidate: Mapping[str, JSONValue] | None = None,
|
|
333
|
+
send_email: bool | None = None,
|
|
334
|
+
metadata: Mapping[str, JSONValue] | None = None,
|
|
335
|
+
interview_template_id: int | None = None,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Update an interview.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
interview_id: The id of the interview.
|
|
341
|
+
title: New title.
|
|
342
|
+
from_: New start time.
|
|
343
|
+
to: New end time.
|
|
344
|
+
notes: New private notes.
|
|
345
|
+
resume_url: New resume URL.
|
|
346
|
+
result_url: New result URL.
|
|
347
|
+
candidate: New candidate details.
|
|
348
|
+
send_email: Whether to send an email.
|
|
349
|
+
metadata: New metadata.
|
|
350
|
+
interview_template_id: New template id.
|
|
351
|
+
"""
|
|
352
|
+
body = _drop_none(
|
|
353
|
+
{
|
|
354
|
+
"title": title,
|
|
355
|
+
"from": from_,
|
|
356
|
+
"to": to,
|
|
357
|
+
"notes": notes,
|
|
358
|
+
"resume_url": resume_url,
|
|
359
|
+
"result_url": result_url,
|
|
360
|
+
"candidate": candidate,
|
|
361
|
+
"send_email": send_email,
|
|
362
|
+
"metadata": metadata,
|
|
363
|
+
"interview_template_id": interview_template_id,
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
self._request(
|
|
367
|
+
method="PUT",
|
|
368
|
+
url=f"{_API_V3}/interviews/{interview_id}",
|
|
369
|
+
json=body,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def delete(self, *, interview_id: str) -> None:
|
|
373
|
+
"""Delete an interview.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
interview_id: The id of the interview.
|
|
377
|
+
"""
|
|
378
|
+
self._request(
|
|
379
|
+
method="DELETE",
|
|
380
|
+
url=f"{_API_V3}/interviews/{interview_id}",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def get_transcript(
|
|
384
|
+
self,
|
|
385
|
+
*,
|
|
386
|
+
interview_id: str,
|
|
387
|
+
) -> InterviewTranscript:
|
|
388
|
+
"""Retrieve the transcript of an interview.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
interview_id: The id of the interview.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
The transcript.
|
|
395
|
+
"""
|
|
396
|
+
response = self._request(
|
|
397
|
+
method="GET",
|
|
398
|
+
url=(f"{_API_V3}/interviews/{interview_id}/transcript"),
|
|
399
|
+
)
|
|
400
|
+
return InterviewTranscript.from_dict(data=response.json())
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@beartype
|
|
404
|
+
class InterviewTemplatesNamespace(_Namespace):
|
|
405
|
+
"""Namespace for interview-template operations."""
|
|
406
|
+
|
|
407
|
+
def list(
|
|
408
|
+
self,
|
|
409
|
+
*,
|
|
410
|
+
limit: int | None = None,
|
|
411
|
+
offset: int | None = None,
|
|
412
|
+
) -> Page[InterviewTemplate]:
|
|
413
|
+
"""List interview templates.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
limit: Number of records to fetch.
|
|
417
|
+
offset: Offset of records.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
A page of interview templates.
|
|
421
|
+
"""
|
|
422
|
+
response = self._request(
|
|
423
|
+
method="GET",
|
|
424
|
+
url=f"{_API_V3}/interview_templates",
|
|
425
|
+
params=_list_params(limit=limit, offset=offset),
|
|
426
|
+
)
|
|
427
|
+
payload = response.json()
|
|
428
|
+
raw_items = list(payload.get("data", []))
|
|
429
|
+
items: list[InterviewTemplate] = [
|
|
430
|
+
InterviewTemplate.from_dict(data=item) for item in raw_items
|
|
431
|
+
]
|
|
432
|
+
return _make_page(items, payload)
|
|
433
|
+
|
|
434
|
+
def create(
|
|
435
|
+
self,
|
|
436
|
+
*,
|
|
437
|
+
name: str,
|
|
438
|
+
roles: Sequence[str] | None = None,
|
|
439
|
+
team_share: int | None = None,
|
|
440
|
+
questions: Sequence[str] | None = None,
|
|
441
|
+
scorecard: int | None = None,
|
|
442
|
+
) -> InterviewTemplate:
|
|
443
|
+
"""Create an interview template.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
name: The template name.
|
|
447
|
+
roles: Roles for the template.
|
|
448
|
+
team_share: Team-share flag.
|
|
449
|
+
questions: Associated question ids.
|
|
450
|
+
scorecard: Scorecard id.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
The created interview template.
|
|
454
|
+
"""
|
|
455
|
+
body = _drop_none(
|
|
456
|
+
{
|
|
457
|
+
"name": name,
|
|
458
|
+
"roles": list(roles) if roles is not None else None,
|
|
459
|
+
"team_share": team_share,
|
|
460
|
+
"questions": (
|
|
461
|
+
list(questions) if questions is not None else None
|
|
462
|
+
),
|
|
463
|
+
"scorecard": scorecard,
|
|
464
|
+
},
|
|
465
|
+
)
|
|
466
|
+
response = self._request(
|
|
467
|
+
method="POST",
|
|
468
|
+
url=f"{_API_V3}/interview_templates",
|
|
469
|
+
json=body,
|
|
470
|
+
)
|
|
471
|
+
return InterviewTemplate.from_dict(data=response.json())
|
|
472
|
+
|
|
473
|
+
def get(self, *, template_id: int | str) -> InterviewTemplate:
|
|
474
|
+
"""Retrieve an interview template.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
template_id: The id of the template.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
The template.
|
|
481
|
+
"""
|
|
482
|
+
response = self._request(
|
|
483
|
+
method="GET",
|
|
484
|
+
url=(f"{_API_V3}/interview_templates/{template_id}"),
|
|
485
|
+
)
|
|
486
|
+
return InterviewTemplate.from_dict(data=response.json())
|
|
487
|
+
|
|
488
|
+
def update(
|
|
489
|
+
self,
|
|
490
|
+
*,
|
|
491
|
+
template_id: int | str,
|
|
492
|
+
name: str | None = None,
|
|
493
|
+
roles: Sequence[str] | None = None,
|
|
494
|
+
team_share: int | None = None,
|
|
495
|
+
questions: Sequence[str] | None = None,
|
|
496
|
+
scorecard: int | None = None,
|
|
497
|
+
) -> None:
|
|
498
|
+
"""Update an interview template.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
template_id: The id of the template.
|
|
502
|
+
name: New name.
|
|
503
|
+
roles: New roles.
|
|
504
|
+
team_share: New team-share flag.
|
|
505
|
+
questions: New question ids.
|
|
506
|
+
scorecard: New scorecard id.
|
|
507
|
+
"""
|
|
508
|
+
body = _drop_none(
|
|
509
|
+
{
|
|
510
|
+
"name": name,
|
|
511
|
+
"roles": list(roles) if roles is not None else None,
|
|
512
|
+
"team_share": team_share,
|
|
513
|
+
"questions": (
|
|
514
|
+
list(questions) if questions is not None else None
|
|
515
|
+
),
|
|
516
|
+
"scorecard": scorecard,
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
self._request(
|
|
520
|
+
method="PUT",
|
|
521
|
+
url=(f"{_API_V3}/interview_templates/{template_id}"),
|
|
522
|
+
json=body,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
def delete(self, *, template_id: int | str) -> None:
|
|
526
|
+
"""Delete an interview template.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
template_id: The id of the template.
|
|
530
|
+
"""
|
|
531
|
+
self._request(
|
|
532
|
+
method="DELETE",
|
|
533
|
+
url=(f"{_API_V3}/interview_templates/{template_id}"),
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@beartype
|
|
538
|
+
class QuestionsNamespace(_Namespace):
|
|
539
|
+
"""Namespace for question operations."""
|
|
540
|
+
|
|
541
|
+
def list(
|
|
542
|
+
self,
|
|
543
|
+
*,
|
|
544
|
+
limit: int | None = None,
|
|
545
|
+
offset: int | None = None,
|
|
546
|
+
) -> Page[Question]:
|
|
547
|
+
"""List questions.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
limit: Number of records to fetch.
|
|
551
|
+
offset: Offset of records.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
A page of questions.
|
|
555
|
+
"""
|
|
556
|
+
response = self._request(
|
|
557
|
+
method="GET",
|
|
558
|
+
url=f"{_API_V3}/questions",
|
|
559
|
+
params=_list_params(limit=limit, offset=offset),
|
|
560
|
+
)
|
|
561
|
+
payload = response.json()
|
|
562
|
+
raw_items = list(payload.get("data", []))
|
|
563
|
+
items: list[Question] = [
|
|
564
|
+
Question.from_dict(data=item) for item in raw_items
|
|
565
|
+
]
|
|
566
|
+
return _make_page(items, payload)
|
|
567
|
+
|
|
568
|
+
def create(
|
|
569
|
+
self,
|
|
570
|
+
*,
|
|
571
|
+
name: str,
|
|
572
|
+
type: str, # noqa: A002 # pylint: disable=redefined-builtin
|
|
573
|
+
internal_notes: str | None = None,
|
|
574
|
+
languages: Sequence[str] | None = None,
|
|
575
|
+
problem_statement: str | None = None,
|
|
576
|
+
recommended_duration: int | None = None,
|
|
577
|
+
tags: Sequence[str] | None = None,
|
|
578
|
+
options: Sequence[str] | None = None,
|
|
579
|
+
answer: int | Sequence[int] | None = None,
|
|
580
|
+
) -> Question:
|
|
581
|
+
"""Create a question.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
name: Question name.
|
|
585
|
+
type: Question type (``code``, ``mcq``, ...).
|
|
586
|
+
internal_notes: Private notes.
|
|
587
|
+
languages: Supported languages.
|
|
588
|
+
problem_statement: Problem statement.
|
|
589
|
+
recommended_duration: Recommended duration.
|
|
590
|
+
tags: Tags.
|
|
591
|
+
options: MCQ options.
|
|
592
|
+
answer: Correct MCQ answer.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
The created question.
|
|
596
|
+
"""
|
|
597
|
+
body = _drop_none(
|
|
598
|
+
{
|
|
599
|
+
"name": name,
|
|
600
|
+
"type": type,
|
|
601
|
+
"internal_notes": internal_notes,
|
|
602
|
+
"languages": (
|
|
603
|
+
list(languages) if languages is not None else None
|
|
604
|
+
),
|
|
605
|
+
"problem_statement": problem_statement,
|
|
606
|
+
"recommended_duration": recommended_duration,
|
|
607
|
+
"tags": list(tags) if tags is not None else None,
|
|
608
|
+
"options": (list(options) if options is not None else None),
|
|
609
|
+
"answer": (
|
|
610
|
+
list(answer)
|
|
611
|
+
if isinstance(answer, Sequence)
|
|
612
|
+
and not isinstance(answer, str)
|
|
613
|
+
else answer
|
|
614
|
+
),
|
|
615
|
+
},
|
|
616
|
+
)
|
|
617
|
+
response = self._request(
|
|
618
|
+
method="POST",
|
|
619
|
+
url=f"{_API_V3}/questions",
|
|
620
|
+
json=body,
|
|
621
|
+
)
|
|
622
|
+
return Question.from_dict(data=response.json())
|
|
623
|
+
|
|
624
|
+
def get(self, *, question_id: str) -> Question:
|
|
625
|
+
"""Retrieve a question.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
question_id: The id of the question.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
The question.
|
|
632
|
+
"""
|
|
633
|
+
response = self._request(
|
|
634
|
+
method="GET",
|
|
635
|
+
url=f"{_API_V3}/questions/{question_id}",
|
|
636
|
+
)
|
|
637
|
+
return Question.from_dict(data=response.json())
|
|
638
|
+
|
|
639
|
+
def update(
|
|
640
|
+
self,
|
|
641
|
+
*,
|
|
642
|
+
question_id: str,
|
|
643
|
+
name: str | None = None,
|
|
644
|
+
type: str | None = None, # noqa: A002 # pylint: disable=redefined-builtin
|
|
645
|
+
internal_notes: str | None = None,
|
|
646
|
+
languages: Sequence[str] | None = None,
|
|
647
|
+
problem_statement: str | None = None,
|
|
648
|
+
recommended_duration: int | None = None,
|
|
649
|
+
tags: Sequence[str] | None = None,
|
|
650
|
+
options: Sequence[str] | None = None,
|
|
651
|
+
answer: int | Sequence[int] | None = None,
|
|
652
|
+
) -> None:
|
|
653
|
+
"""Update a question.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
question_id: The id of the question.
|
|
657
|
+
name: New name.
|
|
658
|
+
type: New question type.
|
|
659
|
+
internal_notes: New internal notes.
|
|
660
|
+
languages: New supported languages.
|
|
661
|
+
problem_statement: New problem statement.
|
|
662
|
+
recommended_duration: New recommended duration.
|
|
663
|
+
tags: New tags.
|
|
664
|
+
options: New MCQ options.
|
|
665
|
+
answer: New MCQ answer.
|
|
666
|
+
"""
|
|
667
|
+
body = _drop_none(
|
|
668
|
+
{
|
|
669
|
+
"name": name,
|
|
670
|
+
"type": type,
|
|
671
|
+
"internal_notes": internal_notes,
|
|
672
|
+
"languages": (
|
|
673
|
+
list(languages) if languages is not None else None
|
|
674
|
+
),
|
|
675
|
+
"problem_statement": problem_statement,
|
|
676
|
+
"recommended_duration": recommended_duration,
|
|
677
|
+
"tags": list(tags) if tags is not None else None,
|
|
678
|
+
"options": (list(options) if options is not None else None),
|
|
679
|
+
"answer": (
|
|
680
|
+
list(answer)
|
|
681
|
+
if isinstance(answer, Sequence)
|
|
682
|
+
and not isinstance(answer, str)
|
|
683
|
+
else answer
|
|
684
|
+
),
|
|
685
|
+
},
|
|
686
|
+
)
|
|
687
|
+
self._request(
|
|
688
|
+
method="PUT",
|
|
689
|
+
url=f"{_API_V3}/questions/{question_id}",
|
|
690
|
+
json=body,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
def update_codestubs(
|
|
694
|
+
self,
|
|
695
|
+
*,
|
|
696
|
+
question_id: str,
|
|
697
|
+
codestubs: Mapping[str, JSONValue],
|
|
698
|
+
) -> None:
|
|
699
|
+
"""Update custom code-stubs for a question.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
question_id: The id of the question.
|
|
703
|
+
codestubs: A mapping describing the code-stubs.
|
|
704
|
+
"""
|
|
705
|
+
self._request(
|
|
706
|
+
method="PUT",
|
|
707
|
+
url=(f"{_API_V3}/questions/{question_id}/custom_codestubs"),
|
|
708
|
+
json=codestubs,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
def generate_codestubs(
|
|
712
|
+
self,
|
|
713
|
+
*,
|
|
714
|
+
question_id: str,
|
|
715
|
+
body: Mapping[str, JSONValue] | None = None,
|
|
716
|
+
) -> dict[str, JSONValue]:
|
|
717
|
+
"""Generate code-stubs for a question.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
question_id: The id of the question.
|
|
721
|
+
body: An optional request body.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
The raw API response.
|
|
725
|
+
"""
|
|
726
|
+
response = self._request(
|
|
727
|
+
method="PUT",
|
|
728
|
+
url=(f"{_API_V3}/questions/{question_id}/generate"),
|
|
729
|
+
json=body if body is not None else {},
|
|
730
|
+
)
|
|
731
|
+
result: dict[str, JSONValue] = dict(response.json())
|
|
732
|
+
return result
|
|
733
|
+
|
|
734
|
+
def add_testcase(
|
|
735
|
+
self,
|
|
736
|
+
*,
|
|
737
|
+
question_id: str,
|
|
738
|
+
body: Mapping[str, JSONValue],
|
|
739
|
+
) -> dict[str, JSONValue]:
|
|
740
|
+
"""Add a test case to a question.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
question_id: The id of the question.
|
|
744
|
+
body: The test-case payload.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
The raw API response.
|
|
748
|
+
"""
|
|
749
|
+
response = self._request(
|
|
750
|
+
method="POST",
|
|
751
|
+
url=(f"{_API_V3}/questions/{question_id}/testcases"),
|
|
752
|
+
json=body,
|
|
753
|
+
)
|
|
754
|
+
result: dict[str, JSONValue] = dict(response.json())
|
|
755
|
+
return result
|
|
756
|
+
|
|
757
|
+
def update_testcase(
|
|
758
|
+
self,
|
|
759
|
+
*,
|
|
760
|
+
question_id: str,
|
|
761
|
+
testcase_id: str,
|
|
762
|
+
body: Mapping[str, JSONValue],
|
|
763
|
+
) -> None:
|
|
764
|
+
"""Update an existing test case.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
question_id: The id of the question.
|
|
768
|
+
testcase_id: The id of the test case.
|
|
769
|
+
body: The test-case payload.
|
|
770
|
+
"""
|
|
771
|
+
self._request(
|
|
772
|
+
method="PUT",
|
|
773
|
+
url=(f"{_API_V3}/questions/{question_id}/testcases/{testcase_id}"),
|
|
774
|
+
json=body,
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
def delete_testcase(
|
|
778
|
+
self,
|
|
779
|
+
*,
|
|
780
|
+
question_id: str,
|
|
781
|
+
testcase_id: str,
|
|
782
|
+
) -> None:
|
|
783
|
+
"""Delete a single test case.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
question_id: The id of the question.
|
|
787
|
+
testcase_id: The id of the test case.
|
|
788
|
+
"""
|
|
789
|
+
self._request(
|
|
790
|
+
method="DELETE",
|
|
791
|
+
url=(f"{_API_V3}/questions/{question_id}/testcases/{testcase_id}"),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
def delete_all_testcases(self, *, question_id: str) -> None:
|
|
795
|
+
"""Delete every test case on a question.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
question_id: The id of the question.
|
|
799
|
+
"""
|
|
800
|
+
self._request(
|
|
801
|
+
method="DELETE",
|
|
802
|
+
url=(f"{_API_V3}/questions/{question_id}/testcases/delete_all"),
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@beartype
|
|
807
|
+
class TestCandidatesNamespace(_Namespace):
|
|
808
|
+
"""Namespace for the candidates of a test."""
|
|
809
|
+
|
|
810
|
+
def list(
|
|
811
|
+
self,
|
|
812
|
+
*,
|
|
813
|
+
test_id: str,
|
|
814
|
+
limit: int | None = None,
|
|
815
|
+
offset: int | None = None,
|
|
816
|
+
) -> Page[TestCandidate]:
|
|
817
|
+
"""List candidates for a test.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
test_id: The id of the test.
|
|
821
|
+
limit: Number of records to fetch.
|
|
822
|
+
offset: Offset of records.
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
A page of candidates.
|
|
826
|
+
"""
|
|
827
|
+
response = self._request(
|
|
828
|
+
method="GET",
|
|
829
|
+
url=f"{_API_V3}/tests/{test_id}/candidates",
|
|
830
|
+
params=_list_params(limit=limit, offset=offset),
|
|
831
|
+
)
|
|
832
|
+
payload = response.json()
|
|
833
|
+
raw_items = list(payload.get("data", []))
|
|
834
|
+
items: list[TestCandidate] = [
|
|
835
|
+
TestCandidate.from_dict(data=item) for item in raw_items
|
|
836
|
+
]
|
|
837
|
+
return _make_page(items, payload)
|
|
838
|
+
|
|
839
|
+
def search(
|
|
840
|
+
self,
|
|
841
|
+
*,
|
|
842
|
+
test_id: str,
|
|
843
|
+
search: str,
|
|
844
|
+
limit: int | None = None,
|
|
845
|
+
offset: int | None = None,
|
|
846
|
+
) -> Page[TestCandidate]:
|
|
847
|
+
"""Search candidates within a test.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
test_id: The id of the test.
|
|
851
|
+
search: Search query.
|
|
852
|
+
limit: Number of records to fetch.
|
|
853
|
+
offset: Offset of records.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
A page of candidates.
|
|
857
|
+
"""
|
|
858
|
+
params = _list_params(
|
|
859
|
+
limit=limit,
|
|
860
|
+
offset=offset,
|
|
861
|
+
extra={"search": search},
|
|
862
|
+
)
|
|
863
|
+
response = self._request(
|
|
864
|
+
method="GET",
|
|
865
|
+
url=f"{_API_V3}/tests/{test_id}/candidates/search",
|
|
866
|
+
params=params,
|
|
867
|
+
)
|
|
868
|
+
payload = response.json()
|
|
869
|
+
raw_items = list(payload.get("data", []))
|
|
870
|
+
items: list[TestCandidate] = [
|
|
871
|
+
TestCandidate.from_dict(data=item) for item in raw_items
|
|
872
|
+
]
|
|
873
|
+
return _make_page(items, payload)
|
|
874
|
+
|
|
875
|
+
def invite(
|
|
876
|
+
self,
|
|
877
|
+
*,
|
|
878
|
+
test_id: str,
|
|
879
|
+
email: str,
|
|
880
|
+
full_name: str | None = None,
|
|
881
|
+
send_email: bool | None = None,
|
|
882
|
+
evaluator_email: str | None = None,
|
|
883
|
+
test_result_url: str | None = None,
|
|
884
|
+
test_finish_url: str | None = None,
|
|
885
|
+
tags: Sequence[str] | None = None,
|
|
886
|
+
invite_valid_from: str | None = None,
|
|
887
|
+
invite_valid_to: str | None = None,
|
|
888
|
+
force: bool | None = None,
|
|
889
|
+
force_reattempt: bool | None = None,
|
|
890
|
+
accommodations: Mapping[str, JSONValue] | None = None,
|
|
891
|
+
invite_metadata: Mapping[str, JSONValue] | None = None,
|
|
892
|
+
webhook_authentication: Mapping[str, JSONValue] | None = None,
|
|
893
|
+
accept_result_updates: bool | None = None,
|
|
894
|
+
subject: str | None = None,
|
|
895
|
+
message: str | None = None,
|
|
896
|
+
template: str | None = None,
|
|
897
|
+
) -> TestCandidate:
|
|
898
|
+
"""Invite a candidate to a test.
|
|
899
|
+
|
|
900
|
+
Args:
|
|
901
|
+
test_id: The id of the test.
|
|
902
|
+
email: Candidate email address.
|
|
903
|
+
full_name: Candidate full name.
|
|
904
|
+
send_email: Whether to send the invitation email.
|
|
905
|
+
evaluator_email: Evaluator email.
|
|
906
|
+
test_result_url: URL to which results are posted.
|
|
907
|
+
test_finish_url: URL the candidate sees on finish.
|
|
908
|
+
tags: Tags to set on the candidate.
|
|
909
|
+
invite_valid_from: Invitation start time.
|
|
910
|
+
invite_valid_to: Invitation end time.
|
|
911
|
+
force: Force inviting even if previously invited.
|
|
912
|
+
force_reattempt: Allow re-attempt.
|
|
913
|
+
accommodations: Accommodation settings.
|
|
914
|
+
invite_metadata: Arbitrary metadata.
|
|
915
|
+
webhook_authentication: Webhook auth config.
|
|
916
|
+
accept_result_updates: Accept result updates flag.
|
|
917
|
+
subject: Custom email subject.
|
|
918
|
+
message: Custom email message.
|
|
919
|
+
template: Email template id.
|
|
920
|
+
|
|
921
|
+
Returns:
|
|
922
|
+
The created candidate.
|
|
923
|
+
"""
|
|
924
|
+
body = _drop_none(
|
|
925
|
+
{
|
|
926
|
+
"email": email,
|
|
927
|
+
"full_name": full_name,
|
|
928
|
+
"send_email": send_email,
|
|
929
|
+
"evaluator_email": evaluator_email,
|
|
930
|
+
"test_result_url": test_result_url,
|
|
931
|
+
"test_finish_url": test_finish_url,
|
|
932
|
+
"tags": list(tags) if tags is not None else None,
|
|
933
|
+
"invite_valid_from": invite_valid_from,
|
|
934
|
+
"invite_valid_to": invite_valid_to,
|
|
935
|
+
"force": force,
|
|
936
|
+
"force_reattempt": force_reattempt,
|
|
937
|
+
"accommodations": accommodations,
|
|
938
|
+
"invite_metadata": invite_metadata,
|
|
939
|
+
"webhook_authentication": webhook_authentication,
|
|
940
|
+
"accept_result_updates": accept_result_updates,
|
|
941
|
+
"subject": subject,
|
|
942
|
+
"message": message,
|
|
943
|
+
"template": template,
|
|
944
|
+
},
|
|
945
|
+
)
|
|
946
|
+
response = self._request(
|
|
947
|
+
method="POST",
|
|
948
|
+
url=f"{_API_V3}/tests/{test_id}/candidates",
|
|
949
|
+
json=body,
|
|
950
|
+
)
|
|
951
|
+
return TestCandidate.from_dict(data=response.json())
|
|
952
|
+
|
|
953
|
+
def get(
|
|
954
|
+
self,
|
|
955
|
+
*,
|
|
956
|
+
test_id: str,
|
|
957
|
+
candidate_id: str,
|
|
958
|
+
additional_fields: str | None = None,
|
|
959
|
+
) -> TestCandidate:
|
|
960
|
+
"""Retrieve a single candidate.
|
|
961
|
+
|
|
962
|
+
Args:
|
|
963
|
+
test_id: The id of the test.
|
|
964
|
+
candidate_id: The id of the candidate.
|
|
965
|
+
additional_fields: Comma-separated extra fields.
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
The candidate.
|
|
969
|
+
"""
|
|
970
|
+
params: dict[str, str | int] = {}
|
|
971
|
+
if additional_fields is not None:
|
|
972
|
+
params["additional_fields"] = additional_fields
|
|
973
|
+
response = self._request(
|
|
974
|
+
method="GET",
|
|
975
|
+
url=(f"{_API_V3}/tests/{test_id}/candidates/{candidate_id}"),
|
|
976
|
+
params=params or None,
|
|
977
|
+
)
|
|
978
|
+
return TestCandidate.from_dict(data=response.json())
|
|
979
|
+
|
|
980
|
+
def update(
|
|
981
|
+
self,
|
|
982
|
+
*,
|
|
983
|
+
test_id: str,
|
|
984
|
+
candidate_id: str,
|
|
985
|
+
full_name: str | None = None,
|
|
986
|
+
ats_state: int | None = None,
|
|
987
|
+
invite_valid_from: str | None = None,
|
|
988
|
+
invite_valid_to: str | None = None,
|
|
989
|
+
invite_metadata: Mapping[str, JSONValue] | None = None,
|
|
990
|
+
evaluator_email: str | None = None,
|
|
991
|
+
test_finish_url: str | None = None,
|
|
992
|
+
test_result_url: str | None = None,
|
|
993
|
+
webhook_authentication: Mapping[str, JSONValue] | None = None,
|
|
994
|
+
accept_result_updates: bool | None = None,
|
|
995
|
+
tags: Sequence[str] | None = None,
|
|
996
|
+
accommodations: Mapping[str, JSONValue] | None = None,
|
|
997
|
+
) -> None:
|
|
998
|
+
"""Update a candidate.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
test_id: The id of the test.
|
|
1002
|
+
candidate_id: The id of the candidate.
|
|
1003
|
+
full_name: New full name.
|
|
1004
|
+
ats_state: New ATS state.
|
|
1005
|
+
invite_valid_from: New invitation start time.
|
|
1006
|
+
invite_valid_to: New invitation end time.
|
|
1007
|
+
invite_metadata: New invite metadata.
|
|
1008
|
+
evaluator_email: New evaluator email.
|
|
1009
|
+
test_finish_url: New finish URL.
|
|
1010
|
+
test_result_url: New result URL.
|
|
1011
|
+
webhook_authentication: New webhook auth.
|
|
1012
|
+
accept_result_updates: New flag value.
|
|
1013
|
+
tags: New tags.
|
|
1014
|
+
accommodations: New accommodations.
|
|
1015
|
+
"""
|
|
1016
|
+
body = _drop_none(
|
|
1017
|
+
{
|
|
1018
|
+
"full_name": full_name,
|
|
1019
|
+
"ats_state": ats_state,
|
|
1020
|
+
"invite_valid_from": invite_valid_from,
|
|
1021
|
+
"invite_valid_to": invite_valid_to,
|
|
1022
|
+
"invite_metadata": invite_metadata,
|
|
1023
|
+
"evaluator_email": evaluator_email,
|
|
1024
|
+
"test_finish_url": test_finish_url,
|
|
1025
|
+
"test_result_url": test_result_url,
|
|
1026
|
+
"webhook_authentication": webhook_authentication,
|
|
1027
|
+
"accept_result_updates": accept_result_updates,
|
|
1028
|
+
"tags": list(tags) if tags is not None else None,
|
|
1029
|
+
"accommodations": accommodations,
|
|
1030
|
+
},
|
|
1031
|
+
)
|
|
1032
|
+
self._request(
|
|
1033
|
+
method="PUT",
|
|
1034
|
+
url=(f"{_API_V3}/tests/{test_id}/candidates/{candidate_id}"),
|
|
1035
|
+
json=body,
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
def cancel_invite(
|
|
1039
|
+
self,
|
|
1040
|
+
*,
|
|
1041
|
+
test_id: str,
|
|
1042
|
+
candidate_id: str,
|
|
1043
|
+
) -> None:
|
|
1044
|
+
"""Cancel a candidate's invitation.
|
|
1045
|
+
|
|
1046
|
+
Args:
|
|
1047
|
+
test_id: The id of the test.
|
|
1048
|
+
candidate_id: The id of the candidate.
|
|
1049
|
+
"""
|
|
1050
|
+
self._request(
|
|
1051
|
+
method="DELETE",
|
|
1052
|
+
url=(
|
|
1053
|
+
f"{_API_V3}/tests/{test_id}/candidates/{candidate_id}/invite"
|
|
1054
|
+
),
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
def delete_report(
|
|
1058
|
+
self,
|
|
1059
|
+
*,
|
|
1060
|
+
test_id: str,
|
|
1061
|
+
candidate_id: str,
|
|
1062
|
+
) -> None:
|
|
1063
|
+
"""Delete the report for a candidate.
|
|
1064
|
+
|
|
1065
|
+
Args:
|
|
1066
|
+
test_id: The id of the test.
|
|
1067
|
+
candidate_id: The id of the candidate.
|
|
1068
|
+
"""
|
|
1069
|
+
self._request(
|
|
1070
|
+
method="DELETE",
|
|
1071
|
+
url=(
|
|
1072
|
+
f"{_API_V3}/tests/{test_id}/candidates/{candidate_id}/report"
|
|
1073
|
+
),
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
def get_report_pdf(
|
|
1077
|
+
self,
|
|
1078
|
+
*,
|
|
1079
|
+
test_id: str,
|
|
1080
|
+
candidate_id: str,
|
|
1081
|
+
format_: str = "url",
|
|
1082
|
+
) -> dict[str, JSONValue]:
|
|
1083
|
+
"""Retrieve the PDF report for a candidate.
|
|
1084
|
+
|
|
1085
|
+
Args:
|
|
1086
|
+
test_id: The id of the test.
|
|
1087
|
+
candidate_id: The id of the candidate.
|
|
1088
|
+
format_: Format of the PDF (e.g. ``"url"``).
|
|
1089
|
+
|
|
1090
|
+
Returns:
|
|
1091
|
+
The raw API response. When ``format_`` is
|
|
1092
|
+
``"url"`` the response typically contains a URL
|
|
1093
|
+
for downloading the PDF.
|
|
1094
|
+
"""
|
|
1095
|
+
response = self._request(
|
|
1096
|
+
method="GET",
|
|
1097
|
+
url=(f"{_API_V3}/tests/{test_id}/candidates/{candidate_id}/pdf"),
|
|
1098
|
+
params={"format": format_},
|
|
1099
|
+
)
|
|
1100
|
+
result: dict[str, JSONValue] = dict(response.json())
|
|
1101
|
+
return result
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
@beartype
|
|
1105
|
+
class TestsNamespace(_Namespace):
|
|
1106
|
+
"""Namespace for test operations."""
|
|
1107
|
+
|
|
1108
|
+
def __init__(
|
|
1109
|
+
self,
|
|
1110
|
+
*,
|
|
1111
|
+
transport: Transport,
|
|
1112
|
+
base_url: str,
|
|
1113
|
+
headers: dict[str, str],
|
|
1114
|
+
) -> None:
|
|
1115
|
+
"""Create the namespace.
|
|
1116
|
+
|
|
1117
|
+
Args:
|
|
1118
|
+
transport: The HTTP transport.
|
|
1119
|
+
base_url: The base URL for the API.
|
|
1120
|
+
headers: Headers to send with every request.
|
|
1121
|
+
"""
|
|
1122
|
+
super().__init__(
|
|
1123
|
+
transport=transport,
|
|
1124
|
+
base_url=base_url,
|
|
1125
|
+
headers=headers,
|
|
1126
|
+
)
|
|
1127
|
+
self.candidates: TestCandidatesNamespace = TestCandidatesNamespace(
|
|
1128
|
+
transport=transport,
|
|
1129
|
+
base_url=base_url,
|
|
1130
|
+
headers=headers,
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
def list(
|
|
1134
|
+
self,
|
|
1135
|
+
*,
|
|
1136
|
+
limit: int | None = None,
|
|
1137
|
+
offset: int | None = None,
|
|
1138
|
+
) -> Page[Test]:
|
|
1139
|
+
"""List tests.
|
|
1140
|
+
|
|
1141
|
+
Args:
|
|
1142
|
+
limit: Number of records to fetch.
|
|
1143
|
+
offset: Offset of records.
|
|
1144
|
+
|
|
1145
|
+
Returns:
|
|
1146
|
+
A page of tests.
|
|
1147
|
+
"""
|
|
1148
|
+
response = self._request(
|
|
1149
|
+
method="GET",
|
|
1150
|
+
url=f"{_API_V3}/tests",
|
|
1151
|
+
params=_list_params(limit=limit, offset=offset),
|
|
1152
|
+
)
|
|
1153
|
+
payload = response.json()
|
|
1154
|
+
raw_items = list(payload.get("data", []))
|
|
1155
|
+
items: list[Test] = [Test.from_dict(data=item) for item in raw_items]
|
|
1156
|
+
return _make_page(items, payload)
|
|
1157
|
+
|
|
1158
|
+
def create(
|
|
1159
|
+
self,
|
|
1160
|
+
*,
|
|
1161
|
+
name: str,
|
|
1162
|
+
starttime: str | None = None,
|
|
1163
|
+
endtime: str | None = None,
|
|
1164
|
+
duration: int | None = None,
|
|
1165
|
+
instructions: str | None = None,
|
|
1166
|
+
locked: bool | None = None,
|
|
1167
|
+
draft: bool | None = None,
|
|
1168
|
+
languages: Sequence[str] | None = None,
|
|
1169
|
+
candidate_details: Sequence[str] | None = None,
|
|
1170
|
+
custom_acknowledge_text: str | None = None,
|
|
1171
|
+
cutoff_score: int | None = None,
|
|
1172
|
+
master_password: str | None = None,
|
|
1173
|
+
hide_compile_test: bool | None = None,
|
|
1174
|
+
tags: Sequence[str] | None = None,
|
|
1175
|
+
role_ids: Sequence[str] | None = None,
|
|
1176
|
+
experience: Sequence[str] | None = None,
|
|
1177
|
+
questions: Sequence[str] | None = None,
|
|
1178
|
+
mcq_incorrect_score: int | None = None,
|
|
1179
|
+
mcq_correct_score: int | None = None,
|
|
1180
|
+
shuffle_questions: bool | None = None,
|
|
1181
|
+
test_admins: Sequence[str] | None = None,
|
|
1182
|
+
hide_template: bool | None = None,
|
|
1183
|
+
enable_acknowledgement: bool | None = None,
|
|
1184
|
+
enable_proctoring: bool | None = None,
|
|
1185
|
+
enable_advanced_proctoring: bool | None = None,
|
|
1186
|
+
enable_secure_assessment_mode: bool | None = None,
|
|
1187
|
+
enable_ml_plagiarism_analysis: bool | None = None,
|
|
1188
|
+
enable_photo_identification: bool | None = None,
|
|
1189
|
+
ide_config: str | None = None,
|
|
1190
|
+
) -> Test:
|
|
1191
|
+
"""Create a test.
|
|
1192
|
+
|
|
1193
|
+
Args:
|
|
1194
|
+
name: The name of the test.
|
|
1195
|
+
starttime: Test start time.
|
|
1196
|
+
endtime: Test end time.
|
|
1197
|
+
duration: Test duration in minutes.
|
|
1198
|
+
instructions: Test instructions.
|
|
1199
|
+
locked: Whether the test is locked.
|
|
1200
|
+
draft: Whether the test is a draft.
|
|
1201
|
+
languages: Allowed programming languages.
|
|
1202
|
+
candidate_details: Candidate detail fields.
|
|
1203
|
+
custom_acknowledge_text: Custom acknowledge text.
|
|
1204
|
+
cutoff_score: The cut-off score.
|
|
1205
|
+
master_password: Master password for the test.
|
|
1206
|
+
hide_compile_test: Hide compile-test option.
|
|
1207
|
+
tags: Tags applied to the test.
|
|
1208
|
+
role_ids: Role ids associated with the test.
|
|
1209
|
+
experience: Experience-level metadata.
|
|
1210
|
+
questions: Question ids to include.
|
|
1211
|
+
mcq_incorrect_score: Score for incorrect MCQ.
|
|
1212
|
+
mcq_correct_score: Score for correct MCQ.
|
|
1213
|
+
shuffle_questions: Whether to shuffle.
|
|
1214
|
+
test_admins: Test admin user ids.
|
|
1215
|
+
hide_template: Hide the template flag.
|
|
1216
|
+
enable_acknowledgement: Enable acknowledgement.
|
|
1217
|
+
enable_proctoring: Enable proctoring.
|
|
1218
|
+
enable_advanced_proctoring: Advanced proctoring.
|
|
1219
|
+
enable_secure_assessment_mode: Secure mode flag.
|
|
1220
|
+
enable_ml_plagiarism_analysis: ML plagiarism.
|
|
1221
|
+
enable_photo_identification: Photo ID flag.
|
|
1222
|
+
ide_config: IDE configuration.
|
|
1223
|
+
|
|
1224
|
+
Returns:
|
|
1225
|
+
The created test.
|
|
1226
|
+
"""
|
|
1227
|
+
body = _drop_none(
|
|
1228
|
+
{
|
|
1229
|
+
"name": name,
|
|
1230
|
+
"starttime": starttime,
|
|
1231
|
+
"endtime": endtime,
|
|
1232
|
+
"duration": duration,
|
|
1233
|
+
"instructions": instructions,
|
|
1234
|
+
"locked": locked,
|
|
1235
|
+
"draft": draft,
|
|
1236
|
+
"languages": (
|
|
1237
|
+
list(languages) if languages is not None else None
|
|
1238
|
+
),
|
|
1239
|
+
"candidate_details": (
|
|
1240
|
+
list(candidate_details)
|
|
1241
|
+
if candidate_details is not None
|
|
1242
|
+
else None
|
|
1243
|
+
),
|
|
1244
|
+
"custom_acknowledge_text": custom_acknowledge_text,
|
|
1245
|
+
"cutoff_score": cutoff_score,
|
|
1246
|
+
"master_password": master_password,
|
|
1247
|
+
"hide_compile_test": hide_compile_test,
|
|
1248
|
+
"tags": list(tags) if tags is not None else None,
|
|
1249
|
+
"role_ids": (list(role_ids) if role_ids is not None else None),
|
|
1250
|
+
"experience": (
|
|
1251
|
+
list(experience) if experience is not None else None
|
|
1252
|
+
),
|
|
1253
|
+
"questions": (
|
|
1254
|
+
list(questions) if questions is not None else None
|
|
1255
|
+
),
|
|
1256
|
+
"mcq_incorrect_score": mcq_incorrect_score,
|
|
1257
|
+
"mcq_correct_score": mcq_correct_score,
|
|
1258
|
+
"shuffle_questions": shuffle_questions,
|
|
1259
|
+
"test_admins": (
|
|
1260
|
+
list(test_admins) if test_admins is not None else None
|
|
1261
|
+
),
|
|
1262
|
+
"hide_template": hide_template,
|
|
1263
|
+
"enable_acknowledgement": enable_acknowledgement,
|
|
1264
|
+
"enable_proctoring": enable_proctoring,
|
|
1265
|
+
"enable_advanced_proctoring": (enable_advanced_proctoring),
|
|
1266
|
+
"enable_secure_assessment_mode": (
|
|
1267
|
+
enable_secure_assessment_mode
|
|
1268
|
+
),
|
|
1269
|
+
"enable_ml_plagiarism_analysis": (
|
|
1270
|
+
enable_ml_plagiarism_analysis
|
|
1271
|
+
),
|
|
1272
|
+
"enable_photo_identification": (enable_photo_identification),
|
|
1273
|
+
"ide_config": ide_config,
|
|
1274
|
+
},
|
|
1275
|
+
)
|
|
1276
|
+
response = self._request(
|
|
1277
|
+
method="POST",
|
|
1278
|
+
url=f"{_API_V3}/tests",
|
|
1279
|
+
json=body,
|
|
1280
|
+
)
|
|
1281
|
+
return Test.from_dict(data=response.json())
|
|
1282
|
+
|
|
1283
|
+
def get(
|
|
1284
|
+
self,
|
|
1285
|
+
*,
|
|
1286
|
+
test_id: str,
|
|
1287
|
+
additional_fields: str | None = None,
|
|
1288
|
+
) -> Test:
|
|
1289
|
+
"""Retrieve a test.
|
|
1290
|
+
|
|
1291
|
+
Args:
|
|
1292
|
+
test_id: The id of the test.
|
|
1293
|
+
additional_fields: Comma-separated extra fields.
|
|
1294
|
+
|
|
1295
|
+
Returns:
|
|
1296
|
+
The test.
|
|
1297
|
+
"""
|
|
1298
|
+
params: dict[str, str | int] = {}
|
|
1299
|
+
if additional_fields is not None:
|
|
1300
|
+
params["additional_fields"] = additional_fields
|
|
1301
|
+
response = self._request(
|
|
1302
|
+
method="GET",
|
|
1303
|
+
url=f"{_API_V3}/tests/{test_id}",
|
|
1304
|
+
params=params or None,
|
|
1305
|
+
)
|
|
1306
|
+
return Test.from_dict(data=response.json())
|
|
1307
|
+
|
|
1308
|
+
def update(
|
|
1309
|
+
self,
|
|
1310
|
+
*,
|
|
1311
|
+
test_id: str,
|
|
1312
|
+
body: Mapping[str, JSONValue],
|
|
1313
|
+
) -> None:
|
|
1314
|
+
"""Update a test using a raw payload.
|
|
1315
|
+
|
|
1316
|
+
Args:
|
|
1317
|
+
test_id: The id of the test.
|
|
1318
|
+
body: A mapping of fields to update. See the
|
|
1319
|
+
HackerRank API documentation for the
|
|
1320
|
+
supported fields.
|
|
1321
|
+
"""
|
|
1322
|
+
self._request(
|
|
1323
|
+
method="PUT",
|
|
1324
|
+
url=f"{_API_V3}/tests/{test_id}",
|
|
1325
|
+
json=body,
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
def delete(self, *, test_id: str) -> None:
|
|
1329
|
+
"""Delete a test.
|
|
1330
|
+
|
|
1331
|
+
Args:
|
|
1332
|
+
test_id: The id of the test.
|
|
1333
|
+
"""
|
|
1334
|
+
self._request(
|
|
1335
|
+
method="DELETE",
|
|
1336
|
+
url=f"{_API_V3}/tests/{test_id}",
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
def archive(self, *, test_id: str) -> None:
|
|
1340
|
+
"""Archive a test.
|
|
1341
|
+
|
|
1342
|
+
Args:
|
|
1343
|
+
test_id: The id of the test.
|
|
1344
|
+
"""
|
|
1345
|
+
self._request(
|
|
1346
|
+
method="POST",
|
|
1347
|
+
url=f"{_API_V3}/tests/{test_id}/archive",
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
def list_inviters(
|
|
1351
|
+
self,
|
|
1352
|
+
*,
|
|
1353
|
+
test_id: str,
|
|
1354
|
+
limit: int | None = None,
|
|
1355
|
+
offset: int | None = None,
|
|
1356
|
+
) -> Page[Inviter]:
|
|
1357
|
+
"""List inviters for a test.
|
|
1358
|
+
|
|
1359
|
+
Args:
|
|
1360
|
+
test_id: The id of the test.
|
|
1361
|
+
limit: Number of records to fetch.
|
|
1362
|
+
offset: Offset of records.
|
|
1363
|
+
|
|
1364
|
+
Returns:
|
|
1365
|
+
A page of inviters.
|
|
1366
|
+
"""
|
|
1367
|
+
response = self._request(
|
|
1368
|
+
method="GET",
|
|
1369
|
+
url=f"{_API_V3}/tests/{test_id}/inviters",
|
|
1370
|
+
params=_list_params(limit=limit, offset=offset),
|
|
1371
|
+
)
|
|
1372
|
+
payload = response.json()
|
|
1373
|
+
raw_items = list(payload.get("data", []))
|
|
1374
|
+
items: list[Inviter] = [
|
|
1375
|
+
Inviter.from_dict(data=item) for item in raw_items
|
|
1376
|
+
]
|
|
1377
|
+
return _make_page(items, payload)
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
@beartype
|
|
1381
|
+
class TemplatesNamespace(_Namespace):
|
|
1382
|
+
"""Namespace for invite-email templates."""
|
|
1383
|
+
|
|
1384
|
+
def list(
|
|
1385
|
+
self,
|
|
1386
|
+
*,
|
|
1387
|
+
limit: int | None = None,
|
|
1388
|
+
offset: int | None = None,
|
|
1389
|
+
) -> Page[Template]:
|
|
1390
|
+
"""List invite templates.
|
|
1391
|
+
|
|
1392
|
+
Args:
|
|
1393
|
+
limit: Number of records to fetch.
|
|
1394
|
+
offset: Offset of records.
|
|
1395
|
+
|
|
1396
|
+
Returns:
|
|
1397
|
+
A page of templates.
|
|
1398
|
+
"""
|
|
1399
|
+
response = self._request(
|
|
1400
|
+
method="GET",
|
|
1401
|
+
url=f"{_API_V3}/templates",
|
|
1402
|
+
params=_list_params(limit=limit, offset=offset),
|
|
1403
|
+
)
|
|
1404
|
+
payload = response.json()
|
|
1405
|
+
raw_items = list(payload.get("data", []))
|
|
1406
|
+
items: list[Template] = [
|
|
1407
|
+
Template.from_dict(data=item) for item in raw_items
|
|
1408
|
+
]
|
|
1409
|
+
return _make_page(items, payload)
|
|
1410
|
+
|
|
1411
|
+
def get(self, *, template_id: str) -> Template:
|
|
1412
|
+
"""Retrieve an invite template.
|
|
1413
|
+
|
|
1414
|
+
Args:
|
|
1415
|
+
template_id: The id of the template.
|
|
1416
|
+
|
|
1417
|
+
Returns:
|
|
1418
|
+
The template.
|
|
1419
|
+
"""
|
|
1420
|
+
response = self._request(
|
|
1421
|
+
method="GET",
|
|
1422
|
+
url=f"{_API_V3}/templates/{template_id}",
|
|
1423
|
+
)
|
|
1424
|
+
return Template.from_dict(data=response.json())
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
@beartype
|
|
1428
|
+
class UsersNamespace(_Namespace):
|
|
1429
|
+
"""Namespace for user operations."""
|
|
1430
|
+
|
|
1431
|
+
def list(
|
|
1432
|
+
self,
|
|
1433
|
+
*,
|
|
1434
|
+
limit: int | None = None,
|
|
1435
|
+
offset: int | None = None,
|
|
1436
|
+
) -> Page[User]:
|
|
1437
|
+
"""List users.
|
|
1438
|
+
|
|
1439
|
+
Args:
|
|
1440
|
+
limit: Number of records to fetch.
|
|
1441
|
+
offset: Offset of records.
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
A page of users.
|
|
1445
|
+
"""
|
|
1446
|
+
response = self._request(
|
|
1447
|
+
method="GET",
|
|
1448
|
+
url=f"{_API_V3}/users",
|
|
1449
|
+
params=_list_params(limit=limit, offset=offset),
|
|
1450
|
+
)
|
|
1451
|
+
payload = response.json()
|
|
1452
|
+
raw_items = list(payload.get("data", []))
|
|
1453
|
+
items: list[User] = [User.from_dict(data=item) for item in raw_items]
|
|
1454
|
+
return _make_page(items, payload)
|
|
1455
|
+
|
|
1456
|
+
def search(
|
|
1457
|
+
self,
|
|
1458
|
+
*,
|
|
1459
|
+
search: str,
|
|
1460
|
+
limit: int | None = None,
|
|
1461
|
+
offset: int | None = None,
|
|
1462
|
+
) -> Page[User]:
|
|
1463
|
+
"""Search users.
|
|
1464
|
+
|
|
1465
|
+
Args:
|
|
1466
|
+
search: Search query.
|
|
1467
|
+
limit: Number of records to fetch.
|
|
1468
|
+
offset: Offset of records.
|
|
1469
|
+
|
|
1470
|
+
Returns:
|
|
1471
|
+
A page of users.
|
|
1472
|
+
"""
|
|
1473
|
+
params = _list_params(
|
|
1474
|
+
limit=limit,
|
|
1475
|
+
offset=offset,
|
|
1476
|
+
extra={"search": search},
|
|
1477
|
+
)
|
|
1478
|
+
response = self._request(
|
|
1479
|
+
method="GET",
|
|
1480
|
+
url=f"{_API_V3}/users/search",
|
|
1481
|
+
params=params,
|
|
1482
|
+
)
|
|
1483
|
+
payload = response.json()
|
|
1484
|
+
raw_items = list(payload.get("data", []))
|
|
1485
|
+
items: list[User] = [User.from_dict(data=item) for item in raw_items]
|
|
1486
|
+
return _make_page(items, payload)
|
|
1487
|
+
|
|
1488
|
+
def create(
|
|
1489
|
+
self,
|
|
1490
|
+
*,
|
|
1491
|
+
email: str,
|
|
1492
|
+
firstname: str | None = None,
|
|
1493
|
+
lastname: str | None = None,
|
|
1494
|
+
country: str | None = None,
|
|
1495
|
+
role: str | None = None,
|
|
1496
|
+
send_email: bool | None = None,
|
|
1497
|
+
phone: str | None = None,
|
|
1498
|
+
questions_permission: int | None = None,
|
|
1499
|
+
tests_permission: int | None = None,
|
|
1500
|
+
interviews_permission: int | None = None,
|
|
1501
|
+
candidates_permission: int | None = None,
|
|
1502
|
+
shared_questions_permission: int | None = None,
|
|
1503
|
+
shared_tests_permission: int | None = None,
|
|
1504
|
+
shared_interviews_permission: int | None = None,
|
|
1505
|
+
shared_candidates_permission: int | None = None,
|
|
1506
|
+
company_admin: bool | None = None,
|
|
1507
|
+
team_admin: bool | None = None,
|
|
1508
|
+
teams: Sequence[str] | None = None,
|
|
1509
|
+
) -> User:
|
|
1510
|
+
"""Create a user.
|
|
1511
|
+
|
|
1512
|
+
Args:
|
|
1513
|
+
email: Email address.
|
|
1514
|
+
firstname: First name.
|
|
1515
|
+
lastname: Last name.
|
|
1516
|
+
country: Country.
|
|
1517
|
+
role: Role.
|
|
1518
|
+
send_email: Send invite email flag.
|
|
1519
|
+
phone: Phone number.
|
|
1520
|
+
questions_permission: Permission level.
|
|
1521
|
+
tests_permission: Permission level.
|
|
1522
|
+
interviews_permission: Permission level.
|
|
1523
|
+
candidates_permission: Permission level.
|
|
1524
|
+
shared_questions_permission: Permission level.
|
|
1525
|
+
shared_tests_permission: Permission level.
|
|
1526
|
+
shared_interviews_permission: Permission level.
|
|
1527
|
+
shared_candidates_permission: Permission level.
|
|
1528
|
+
company_admin: Company admin flag.
|
|
1529
|
+
team_admin: Team admin flag.
|
|
1530
|
+
teams: Team ids the user belongs to.
|
|
1531
|
+
|
|
1532
|
+
Returns:
|
|
1533
|
+
The created user.
|
|
1534
|
+
"""
|
|
1535
|
+
body = _drop_none(
|
|
1536
|
+
{
|
|
1537
|
+
"email": email,
|
|
1538
|
+
"firstname": firstname,
|
|
1539
|
+
"lastname": lastname,
|
|
1540
|
+
"country": country,
|
|
1541
|
+
"role": role,
|
|
1542
|
+
"send_email": send_email,
|
|
1543
|
+
"phone": phone,
|
|
1544
|
+
"questions_permission": questions_permission,
|
|
1545
|
+
"tests_permission": tests_permission,
|
|
1546
|
+
"interviews_permission": interviews_permission,
|
|
1547
|
+
"candidates_permission": candidates_permission,
|
|
1548
|
+
"shared_questions_permission": (shared_questions_permission),
|
|
1549
|
+
"shared_tests_permission": (shared_tests_permission),
|
|
1550
|
+
"shared_interviews_permission": (shared_interviews_permission),
|
|
1551
|
+
"shared_candidates_permission": (shared_candidates_permission),
|
|
1552
|
+
"company_admin": company_admin,
|
|
1553
|
+
"team_admin": team_admin,
|
|
1554
|
+
"teams": (
|
|
1555
|
+
[{"id": t} for t in teams] if teams is not None else None
|
|
1556
|
+
),
|
|
1557
|
+
},
|
|
1558
|
+
)
|
|
1559
|
+
response = self._request(
|
|
1560
|
+
method="POST",
|
|
1561
|
+
url=f"{_API_V3}/users",
|
|
1562
|
+
json=body,
|
|
1563
|
+
)
|
|
1564
|
+
return User.from_dict(data=response.json())
|
|
1565
|
+
|
|
1566
|
+
def get(self, *, user_id: str) -> User:
|
|
1567
|
+
"""Retrieve a user.
|
|
1568
|
+
|
|
1569
|
+
Args:
|
|
1570
|
+
user_id: The id of the user.
|
|
1571
|
+
|
|
1572
|
+
Returns:
|
|
1573
|
+
The user.
|
|
1574
|
+
"""
|
|
1575
|
+
response = self._request(
|
|
1576
|
+
method="GET",
|
|
1577
|
+
url=f"{_API_V3}/users/{user_id}",
|
|
1578
|
+
)
|
|
1579
|
+
return User.from_dict(data=response.json())
|
|
1580
|
+
|
|
1581
|
+
def update(
|
|
1582
|
+
self,
|
|
1583
|
+
*,
|
|
1584
|
+
user_id: str,
|
|
1585
|
+
body: Mapping[str, JSONValue],
|
|
1586
|
+
) -> None:
|
|
1587
|
+
"""Update a user.
|
|
1588
|
+
|
|
1589
|
+
Args:
|
|
1590
|
+
user_id: The id of the user.
|
|
1591
|
+
body: A mapping of fields to update. See the
|
|
1592
|
+
HackerRank API documentation for the
|
|
1593
|
+
supported fields.
|
|
1594
|
+
"""
|
|
1595
|
+
self._request(
|
|
1596
|
+
method="PUT",
|
|
1597
|
+
url=f"{_API_V3}/users/{user_id}",
|
|
1598
|
+
json=body,
|
|
1599
|
+
)
|
|
1600
|
+
|
|
1601
|
+
def delete(self, *, user_id: str) -> None:
|
|
1602
|
+
"""Lock (deactivate) a user.
|
|
1603
|
+
|
|
1604
|
+
Args:
|
|
1605
|
+
user_id: The id of the user.
|
|
1606
|
+
"""
|
|
1607
|
+
self._request(
|
|
1608
|
+
method="DELETE",
|
|
1609
|
+
url=f"{_API_V3}/users/{user_id}",
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
@beartype
|
|
1614
|
+
class TeamMembershipsNamespace(_Namespace):
|
|
1615
|
+
"""Namespace for user-team membership operations."""
|
|
1616
|
+
|
|
1617
|
+
def list(
|
|
1618
|
+
self,
|
|
1619
|
+
*,
|
|
1620
|
+
team_id: str,
|
|
1621
|
+
limit: int | None = None,
|
|
1622
|
+
offset: int | None = None,
|
|
1623
|
+
) -> Page[UserTeamMembership]:
|
|
1624
|
+
"""List memberships for a team.
|
|
1625
|
+
|
|
1626
|
+
Args:
|
|
1627
|
+
team_id: The id of the team.
|
|
1628
|
+
limit: Number of records to fetch.
|
|
1629
|
+
offset: Offset of records.
|
|
1630
|
+
|
|
1631
|
+
Returns:
|
|
1632
|
+
A page of memberships.
|
|
1633
|
+
"""
|
|
1634
|
+
response = self._request(
|
|
1635
|
+
method="GET",
|
|
1636
|
+
url=f"{_API_V3}/teams/{team_id}/users",
|
|
1637
|
+
params=_list_params(limit=limit, offset=offset),
|
|
1638
|
+
)
|
|
1639
|
+
payload = response.json()
|
|
1640
|
+
raw_items = list(payload.get("data", []))
|
|
1641
|
+
items: list[UserTeamMembership] = [
|
|
1642
|
+
UserTeamMembership.from_dict(data=item) for item in raw_items
|
|
1643
|
+
]
|
|
1644
|
+
return _make_page(items, payload)
|
|
1645
|
+
|
|
1646
|
+
def get(
|
|
1647
|
+
self,
|
|
1648
|
+
*,
|
|
1649
|
+
team_id: str,
|
|
1650
|
+
user_id: str,
|
|
1651
|
+
) -> UserTeamMembership:
|
|
1652
|
+
"""Retrieve a single membership.
|
|
1653
|
+
|
|
1654
|
+
Args:
|
|
1655
|
+
team_id: The id of the team.
|
|
1656
|
+
user_id: The id of the user.
|
|
1657
|
+
|
|
1658
|
+
Returns:
|
|
1659
|
+
The membership.
|
|
1660
|
+
"""
|
|
1661
|
+
response = self._request(
|
|
1662
|
+
method="GET",
|
|
1663
|
+
url=f"{_API_V3}/teams/{team_id}/users/{user_id}",
|
|
1664
|
+
)
|
|
1665
|
+
return UserTeamMembership.from_dict(data=response.json())
|
|
1666
|
+
|
|
1667
|
+
def create(
|
|
1668
|
+
self,
|
|
1669
|
+
*,
|
|
1670
|
+
team_id: str,
|
|
1671
|
+
user_id: str,
|
|
1672
|
+
license: str | None = None, # noqa: A002 # pylint: disable=redefined-builtin
|
|
1673
|
+
) -> UserTeamMembership:
|
|
1674
|
+
"""Add a user to a team.
|
|
1675
|
+
|
|
1676
|
+
Args:
|
|
1677
|
+
team_id: The id of the team.
|
|
1678
|
+
user_id: The id of the user.
|
|
1679
|
+
license: License kind to assign.
|
|
1680
|
+
|
|
1681
|
+
Returns:
|
|
1682
|
+
The created membership.
|
|
1683
|
+
"""
|
|
1684
|
+
params: dict[str, str | int] = {}
|
|
1685
|
+
if license is not None:
|
|
1686
|
+
params["license"] = license
|
|
1687
|
+
response = self._request(
|
|
1688
|
+
method="POST",
|
|
1689
|
+
url=f"{_API_V3}/teams/{team_id}/users/{user_id}",
|
|
1690
|
+
params=params or None,
|
|
1691
|
+
)
|
|
1692
|
+
return UserTeamMembership.from_dict(data=response.json())
|
|
1693
|
+
|
|
1694
|
+
def delete(
|
|
1695
|
+
self,
|
|
1696
|
+
*,
|
|
1697
|
+
team_id: str,
|
|
1698
|
+
user_id: str,
|
|
1699
|
+
) -> None:
|
|
1700
|
+
"""Remove a user from a team.
|
|
1701
|
+
|
|
1702
|
+
Args:
|
|
1703
|
+
team_id: The id of the team.
|
|
1704
|
+
user_id: The id of the user.
|
|
1705
|
+
"""
|
|
1706
|
+
self._request(
|
|
1707
|
+
method="DELETE",
|
|
1708
|
+
url=f"{_API_V3}/teams/{team_id}/users/{user_id}",
|
|
1709
|
+
)
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
@beartype
|
|
1713
|
+
class TeamsNamespace(_Namespace):
|
|
1714
|
+
"""Namespace for team operations."""
|
|
1715
|
+
|
|
1716
|
+
def __init__(
|
|
1717
|
+
self,
|
|
1718
|
+
*,
|
|
1719
|
+
transport: Transport,
|
|
1720
|
+
base_url: str,
|
|
1721
|
+
headers: dict[str, str],
|
|
1722
|
+
) -> None:
|
|
1723
|
+
"""Create the namespace.
|
|
1724
|
+
|
|
1725
|
+
Args:
|
|
1726
|
+
transport: The HTTP transport.
|
|
1727
|
+
base_url: The base URL for the API.
|
|
1728
|
+
headers: Headers to send with every request.
|
|
1729
|
+
"""
|
|
1730
|
+
super().__init__(
|
|
1731
|
+
transport=transport,
|
|
1732
|
+
base_url=base_url,
|
|
1733
|
+
headers=headers,
|
|
1734
|
+
)
|
|
1735
|
+
self.memberships: TeamMembershipsNamespace = TeamMembershipsNamespace(
|
|
1736
|
+
transport=transport,
|
|
1737
|
+
base_url=base_url,
|
|
1738
|
+
headers=headers,
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1741
|
+
def list(
|
|
1742
|
+
self,
|
|
1743
|
+
*,
|
|
1744
|
+
limit: int | None = None,
|
|
1745
|
+
offset: int | None = None,
|
|
1746
|
+
) -> Page[Team]:
|
|
1747
|
+
"""List teams.
|
|
1748
|
+
|
|
1749
|
+
Args:
|
|
1750
|
+
limit: Number of records to fetch.
|
|
1751
|
+
offset: Offset of records.
|
|
1752
|
+
|
|
1753
|
+
Returns:
|
|
1754
|
+
A page of teams.
|
|
1755
|
+
"""
|
|
1756
|
+
response = self._request(
|
|
1757
|
+
method="GET",
|
|
1758
|
+
url=f"{_API_V3}/teams",
|
|
1759
|
+
params=_list_params(limit=limit, offset=offset),
|
|
1760
|
+
)
|
|
1761
|
+
payload = response.json()
|
|
1762
|
+
raw_items = list(payload.get("data", []))
|
|
1763
|
+
items: list[Team] = [Team.from_dict(data=item) for item in raw_items]
|
|
1764
|
+
return _make_page(items, payload)
|
|
1765
|
+
|
|
1766
|
+
def create(
|
|
1767
|
+
self,
|
|
1768
|
+
*,
|
|
1769
|
+
name: str,
|
|
1770
|
+
recruiter_cap: int | None = None,
|
|
1771
|
+
developer_cap: int | None = None,
|
|
1772
|
+
invite_as: str | None = None,
|
|
1773
|
+
locations: Sequence[str] | None = None,
|
|
1774
|
+
departments: Sequence[str] | None = None,
|
|
1775
|
+
) -> Team:
|
|
1776
|
+
"""Create a team.
|
|
1777
|
+
|
|
1778
|
+
Args:
|
|
1779
|
+
name: Team name.
|
|
1780
|
+
recruiter_cap: Recruiter seat cap.
|
|
1781
|
+
developer_cap: Developer seat cap.
|
|
1782
|
+
invite_as: Default role for invites.
|
|
1783
|
+
locations: Allowed locations.
|
|
1784
|
+
departments: Allowed departments.
|
|
1785
|
+
|
|
1786
|
+
Returns:
|
|
1787
|
+
The created team.
|
|
1788
|
+
"""
|
|
1789
|
+
body = _drop_none(
|
|
1790
|
+
{
|
|
1791
|
+
"name": name,
|
|
1792
|
+
"recruiter_cap": recruiter_cap,
|
|
1793
|
+
"developer_cap": developer_cap,
|
|
1794
|
+
"invite_as": invite_as,
|
|
1795
|
+
"locations": (
|
|
1796
|
+
list(locations) if locations is not None else None
|
|
1797
|
+
),
|
|
1798
|
+
"departments": (
|
|
1799
|
+
list(departments) if departments is not None else None
|
|
1800
|
+
),
|
|
1801
|
+
},
|
|
1802
|
+
)
|
|
1803
|
+
response = self._request(
|
|
1804
|
+
method="POST",
|
|
1805
|
+
url=f"{_API_V3}/teams",
|
|
1806
|
+
json=body,
|
|
1807
|
+
)
|
|
1808
|
+
return Team.from_dict(data=response.json())
|
|
1809
|
+
|
|
1810
|
+
def get(self, *, team_id: str) -> Team:
|
|
1811
|
+
"""Retrieve a team.
|
|
1812
|
+
|
|
1813
|
+
Args:
|
|
1814
|
+
team_id: The id of the team.
|
|
1815
|
+
|
|
1816
|
+
Returns:
|
|
1817
|
+
The team.
|
|
1818
|
+
"""
|
|
1819
|
+
response = self._request(
|
|
1820
|
+
method="GET",
|
|
1821
|
+
url=f"{_API_V3}/teams/{team_id}",
|
|
1822
|
+
)
|
|
1823
|
+
return Team.from_dict(data=response.json())
|
|
1824
|
+
|
|
1825
|
+
def update(
|
|
1826
|
+
self,
|
|
1827
|
+
*,
|
|
1828
|
+
team_id: str,
|
|
1829
|
+
name: str | None = None,
|
|
1830
|
+
recruiter_cap: int | None = None,
|
|
1831
|
+
developer_cap: int | None = None,
|
|
1832
|
+
invite_as: str | None = None,
|
|
1833
|
+
locations: Sequence[str] | None = None,
|
|
1834
|
+
departments: Sequence[str] | None = None,
|
|
1835
|
+
) -> None:
|
|
1836
|
+
"""Update a team.
|
|
1837
|
+
|
|
1838
|
+
Args:
|
|
1839
|
+
team_id: The id of the team.
|
|
1840
|
+
name: New team name.
|
|
1841
|
+
recruiter_cap: New recruiter seat cap.
|
|
1842
|
+
developer_cap: New developer seat cap.
|
|
1843
|
+
invite_as: New default invite role.
|
|
1844
|
+
locations: New allowed locations.
|
|
1845
|
+
departments: New allowed departments.
|
|
1846
|
+
"""
|
|
1847
|
+
body = _drop_none(
|
|
1848
|
+
{
|
|
1849
|
+
"name": name,
|
|
1850
|
+
"recruiter_cap": recruiter_cap,
|
|
1851
|
+
"developer_cap": developer_cap,
|
|
1852
|
+
"invite_as": invite_as,
|
|
1853
|
+
"locations": (
|
|
1854
|
+
list(locations) if locations is not None else None
|
|
1855
|
+
),
|
|
1856
|
+
"departments": (
|
|
1857
|
+
list(departments) if departments is not None else None
|
|
1858
|
+
),
|
|
1859
|
+
},
|
|
1860
|
+
)
|
|
1861
|
+
self._request(
|
|
1862
|
+
method="PUT",
|
|
1863
|
+
url=f"{_API_V3}/teams/{team_id}",
|
|
1864
|
+
json=body,
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
def delete(self, *, team_id: str) -> None:
|
|
1868
|
+
"""Delete a team.
|
|
1869
|
+
|
|
1870
|
+
Args:
|
|
1871
|
+
team_id: The id of the team.
|
|
1872
|
+
"""
|
|
1873
|
+
self._request(
|
|
1874
|
+
method="DELETE",
|
|
1875
|
+
url=f"{_API_V3}/teams/{team_id}",
|
|
1876
|
+
)
|
|
1877
|
+
|
|
1878
|
+
|
|
1879
|
+
@beartype
|
|
1880
|
+
class AuditLogsNamespace(_Namespace):
|
|
1881
|
+
"""Namespace for audit-log operations."""
|
|
1882
|
+
|
|
1883
|
+
def list(
|
|
1884
|
+
self,
|
|
1885
|
+
*,
|
|
1886
|
+
limit: int | None = None,
|
|
1887
|
+
offset: int | None = None,
|
|
1888
|
+
user_id: str | None = None,
|
|
1889
|
+
) -> Page[AuditLog]:
|
|
1890
|
+
"""List audit logs.
|
|
1891
|
+
|
|
1892
|
+
Args:
|
|
1893
|
+
limit: Number of records to fetch.
|
|
1894
|
+
offset: Offset of records.
|
|
1895
|
+
user_id: Filter to a specific user.
|
|
1896
|
+
|
|
1897
|
+
Returns:
|
|
1898
|
+
A page of audit logs.
|
|
1899
|
+
"""
|
|
1900
|
+
extra: dict[str, str | int] = {}
|
|
1901
|
+
if user_id is not None:
|
|
1902
|
+
extra["user_id"] = user_id
|
|
1903
|
+
response = self._request(
|
|
1904
|
+
method="GET",
|
|
1905
|
+
url=f"{_API_V3}/audit_log",
|
|
1906
|
+
params=_list_params(
|
|
1907
|
+
limit=limit,
|
|
1908
|
+
offset=offset,
|
|
1909
|
+
extra=extra,
|
|
1910
|
+
),
|
|
1911
|
+
)
|
|
1912
|
+
payload = response.json()
|
|
1913
|
+
raw_items = list(payload.get("data", []))
|
|
1914
|
+
items: list[AuditLog] = [
|
|
1915
|
+
AuditLog.from_dict(data=item) for item in raw_items
|
|
1916
|
+
]
|
|
1917
|
+
return _make_page(items, payload)
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
@beartype
|
|
1921
|
+
class ATSCodePairNamespace(_Namespace):
|
|
1922
|
+
"""Namespace for ATS Codepair operations."""
|
|
1923
|
+
|
|
1924
|
+
def invite(
|
|
1925
|
+
self,
|
|
1926
|
+
*,
|
|
1927
|
+
title: str | None = None,
|
|
1928
|
+
requisition_id: str | None = None,
|
|
1929
|
+
candidate_id: str | None = None,
|
|
1930
|
+
candidate: Mapping[str, JSONValue] | None = None,
|
|
1931
|
+
send_email: bool | None = None,
|
|
1932
|
+
interview_metadata: Mapping[str, JSONValue] | None = None,
|
|
1933
|
+
) -> ATSCodePair:
|
|
1934
|
+
"""Invite a candidate to an ATS Codepair interview.
|
|
1935
|
+
|
|
1936
|
+
Args:
|
|
1937
|
+
title: The interview title.
|
|
1938
|
+
requisition_id: The ATS requisition id.
|
|
1939
|
+
candidate_id: The ATS candidate id.
|
|
1940
|
+
candidate: Candidate information.
|
|
1941
|
+
send_email: Whether to send an email.
|
|
1942
|
+
interview_metadata: Arbitrary metadata.
|
|
1943
|
+
|
|
1944
|
+
Returns:
|
|
1945
|
+
The ATS Codepair result.
|
|
1946
|
+
"""
|
|
1947
|
+
body = _drop_none(
|
|
1948
|
+
{
|
|
1949
|
+
"title": title,
|
|
1950
|
+
"requisition_id": requisition_id,
|
|
1951
|
+
"candidate_id": candidate_id,
|
|
1952
|
+
"candidate": candidate,
|
|
1953
|
+
"send_email": send_email,
|
|
1954
|
+
"interview_metadata": interview_metadata,
|
|
1955
|
+
},
|
|
1956
|
+
)
|
|
1957
|
+
response = self._request(
|
|
1958
|
+
method="POST",
|
|
1959
|
+
url=f"{_API_V3}/ats/codepair",
|
|
1960
|
+
json=body,
|
|
1961
|
+
)
|
|
1962
|
+
return ATSCodePair.from_dict(data=response.json())
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
@beartype
|
|
1966
|
+
class ATSCodeScreenNamespace(_Namespace):
|
|
1967
|
+
"""Namespace for ATS CodeScreen operations."""
|
|
1968
|
+
|
|
1969
|
+
def invite(
|
|
1970
|
+
self,
|
|
1971
|
+
*,
|
|
1972
|
+
test_id: str,
|
|
1973
|
+
email: str,
|
|
1974
|
+
requisition_id: str | None = None,
|
|
1975
|
+
candidate_id: str | None = None,
|
|
1976
|
+
send_email: bool | None = None,
|
|
1977
|
+
test_result_url: str | None = None,
|
|
1978
|
+
webhook_authentication: Mapping[str, JSONValue] | None = None,
|
|
1979
|
+
accept_result_updates: bool | None = None,
|
|
1980
|
+
force: bool | None = None,
|
|
1981
|
+
force_reattempt_after: int | None = None,
|
|
1982
|
+
accommodations: Mapping[str, JSONValue] | None = None,
|
|
1983
|
+
) -> ATSCodeScreen:
|
|
1984
|
+
"""Invite a candidate to a CodeScreen test.
|
|
1985
|
+
|
|
1986
|
+
Args:
|
|
1987
|
+
test_id: The test id.
|
|
1988
|
+
email: Candidate email.
|
|
1989
|
+
requisition_id: ATS requisition id.
|
|
1990
|
+
candidate_id: ATS candidate id.
|
|
1991
|
+
send_email: Whether to send an email.
|
|
1992
|
+
test_result_url: URL for posting results.
|
|
1993
|
+
webhook_authentication: Webhook auth config.
|
|
1994
|
+
accept_result_updates: Accept-updates flag.
|
|
1995
|
+
force: Force inviting flag.
|
|
1996
|
+
force_reattempt_after: Seconds before re-attempt.
|
|
1997
|
+
accommodations: Accommodation settings.
|
|
1998
|
+
|
|
1999
|
+
Returns:
|
|
2000
|
+
The ATS CodeScreen result.
|
|
2001
|
+
"""
|
|
2002
|
+
body = _drop_none(
|
|
2003
|
+
{
|
|
2004
|
+
"test_id": test_id,
|
|
2005
|
+
"email": email,
|
|
2006
|
+
"requisition_id": requisition_id,
|
|
2007
|
+
"candidate_id": candidate_id,
|
|
2008
|
+
"send_email": send_email,
|
|
2009
|
+
"test_result_url": test_result_url,
|
|
2010
|
+
"webhook_authentication": webhook_authentication,
|
|
2011
|
+
"accept_result_updates": accept_result_updates,
|
|
2012
|
+
"force": force,
|
|
2013
|
+
"force_reattempt_after": force_reattempt_after,
|
|
2014
|
+
"accommodations": accommodations,
|
|
2015
|
+
},
|
|
2016
|
+
)
|
|
2017
|
+
response = self._request(
|
|
2018
|
+
method="POST",
|
|
2019
|
+
url=f"{_API_V3}/ats/codescreen",
|
|
2020
|
+
json=body,
|
|
2021
|
+
)
|
|
2022
|
+
return ATSCodeScreen.from_dict(data=response.json())
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
@beartype
|
|
2026
|
+
class ATSNamespace(_Namespace):
|
|
2027
|
+
"""Namespace for ATS operations."""
|
|
2028
|
+
|
|
2029
|
+
def __init__(
|
|
2030
|
+
self,
|
|
2031
|
+
*,
|
|
2032
|
+
transport: Transport,
|
|
2033
|
+
base_url: str,
|
|
2034
|
+
headers: dict[str, str],
|
|
2035
|
+
) -> None:
|
|
2036
|
+
"""Create the ATS namespace.
|
|
2037
|
+
|
|
2038
|
+
Args:
|
|
2039
|
+
transport: The HTTP transport.
|
|
2040
|
+
base_url: The base URL for the API.
|
|
2041
|
+
headers: Headers to send with every request.
|
|
2042
|
+
"""
|
|
2043
|
+
super().__init__(
|
|
2044
|
+
transport=transport,
|
|
2045
|
+
base_url=base_url,
|
|
2046
|
+
headers=headers,
|
|
2047
|
+
)
|
|
2048
|
+
self.codepair: ATSCodePairNamespace = ATSCodePairNamespace(
|
|
2049
|
+
transport=transport,
|
|
2050
|
+
base_url=base_url,
|
|
2051
|
+
headers=headers,
|
|
2052
|
+
)
|
|
2053
|
+
self.codescreen: ATSCodeScreenNamespace = ATSCodeScreenNamespace(
|
|
2054
|
+
transport=transport,
|
|
2055
|
+
base_url=base_url,
|
|
2056
|
+
headers=headers,
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
def _make_scim_page[T](
|
|
2061
|
+
items: list[T],
|
|
2062
|
+
payload: Mapping[str, JSONValue],
|
|
2063
|
+
/,
|
|
2064
|
+
) -> SCIMPage[T]:
|
|
2065
|
+
"""Wrap items and a SCIM payload into a ``SCIMPage``.
|
|
2066
|
+
|
|
2067
|
+
Args:
|
|
2068
|
+
items: The SCIM items in the page.
|
|
2069
|
+
payload: The raw SCIM response payload.
|
|
2070
|
+
|
|
2071
|
+
Returns:
|
|
2072
|
+
The populated ``SCIMPage`` instance.
|
|
2073
|
+
"""
|
|
2074
|
+
schemas_raw = payload.get("schemas")
|
|
2075
|
+
schemas: list[str] = (
|
|
2076
|
+
list(cast("list[str]", schemas_raw))
|
|
2077
|
+
if isinstance(schemas_raw, list)
|
|
2078
|
+
else []
|
|
2079
|
+
)
|
|
2080
|
+
start_index = _coerce_int(payload.get("startIndex")) or 1
|
|
2081
|
+
return SCIMPage(
|
|
2082
|
+
items,
|
|
2083
|
+
schemas=schemas,
|
|
2084
|
+
start_index=start_index,
|
|
2085
|
+
items_per_page=_coerce_int(payload.get("itemsPerPage")),
|
|
2086
|
+
total_results=_coerce_int(payload.get("totalResults")),
|
|
2087
|
+
)
|
|
2088
|
+
|
|
2089
|
+
|
|
2090
|
+
@beartype
|
|
2091
|
+
class SCIMUsersNamespace(_Namespace):
|
|
2092
|
+
"""Namespace for SCIM v2 user operations."""
|
|
2093
|
+
|
|
2094
|
+
def list(
|
|
2095
|
+
self,
|
|
2096
|
+
*,
|
|
2097
|
+
limit: int | None = None,
|
|
2098
|
+
offset: int | None = None,
|
|
2099
|
+
) -> SCIMPage[SCIMUser]:
|
|
2100
|
+
"""List SCIM users.
|
|
2101
|
+
|
|
2102
|
+
Args:
|
|
2103
|
+
limit: Number of records to fetch.
|
|
2104
|
+
offset: Offset of records.
|
|
2105
|
+
|
|
2106
|
+
Returns:
|
|
2107
|
+
A page of SCIM users.
|
|
2108
|
+
"""
|
|
2109
|
+
response = self._request(
|
|
2110
|
+
method="GET",
|
|
2111
|
+
url="/Users",
|
|
2112
|
+
params=_list_params(limit=limit, offset=offset),
|
|
2113
|
+
)
|
|
2114
|
+
payload = response.json()
|
|
2115
|
+
raw = list(payload.get("Resources", []))
|
|
2116
|
+
items: list[SCIMUser] = [SCIMUser.from_dict(data=item) for item in raw]
|
|
2117
|
+
return _make_scim_page(items, payload)
|
|
2118
|
+
|
|
2119
|
+
def create(
|
|
2120
|
+
self,
|
|
2121
|
+
*,
|
|
2122
|
+
body: Mapping[str, JSONValue],
|
|
2123
|
+
) -> SCIMUser:
|
|
2124
|
+
"""Create a SCIM user.
|
|
2125
|
+
|
|
2126
|
+
Args:
|
|
2127
|
+
body: The SCIM user payload.
|
|
2128
|
+
|
|
2129
|
+
Returns:
|
|
2130
|
+
The created SCIM user.
|
|
2131
|
+
"""
|
|
2132
|
+
response = self._request(
|
|
2133
|
+
method="POST",
|
|
2134
|
+
url="/Users",
|
|
2135
|
+
json=body,
|
|
2136
|
+
)
|
|
2137
|
+
return SCIMUser.from_dict(data=response.json())
|
|
2138
|
+
|
|
2139
|
+
def get(self, *, scim_user_id: str) -> SCIMUser:
|
|
2140
|
+
"""Retrieve a SCIM user.
|
|
2141
|
+
|
|
2142
|
+
Args:
|
|
2143
|
+
scim_user_id: The id of the SCIM user.
|
|
2144
|
+
|
|
2145
|
+
Returns:
|
|
2146
|
+
The SCIM user.
|
|
2147
|
+
"""
|
|
2148
|
+
response = self._request(
|
|
2149
|
+
method="GET",
|
|
2150
|
+
url=f"/Users/{scim_user_id}",
|
|
2151
|
+
)
|
|
2152
|
+
return SCIMUser.from_dict(data=response.json())
|
|
2153
|
+
|
|
2154
|
+
def replace(
|
|
2155
|
+
self,
|
|
2156
|
+
*,
|
|
2157
|
+
scim_user_id: str,
|
|
2158
|
+
body: Mapping[str, JSONValue],
|
|
2159
|
+
) -> SCIMUser:
|
|
2160
|
+
"""Replace a SCIM user (PUT).
|
|
2161
|
+
|
|
2162
|
+
Args:
|
|
2163
|
+
scim_user_id: The id of the SCIM user.
|
|
2164
|
+
body: The full SCIM user payload.
|
|
2165
|
+
|
|
2166
|
+
Returns:
|
|
2167
|
+
The updated SCIM user.
|
|
2168
|
+
"""
|
|
2169
|
+
response = self._request(
|
|
2170
|
+
method="PUT",
|
|
2171
|
+
url=f"/Users/{scim_user_id}",
|
|
2172
|
+
json=body,
|
|
2173
|
+
)
|
|
2174
|
+
return SCIMUser.from_dict(data=response.json())
|
|
2175
|
+
|
|
2176
|
+
def patch(
|
|
2177
|
+
self,
|
|
2178
|
+
*,
|
|
2179
|
+
scim_user_id: str,
|
|
2180
|
+
operations: Sequence[Mapping[str, JSONValue]],
|
|
2181
|
+
) -> SCIMUser:
|
|
2182
|
+
"""Patch a SCIM user.
|
|
2183
|
+
|
|
2184
|
+
Args:
|
|
2185
|
+
scim_user_id: The id of the SCIM user.
|
|
2186
|
+
operations: The SCIM patch operations.
|
|
2187
|
+
|
|
2188
|
+
Returns:
|
|
2189
|
+
The updated SCIM user.
|
|
2190
|
+
"""
|
|
2191
|
+
body: dict[str, JSONValue] = {
|
|
2192
|
+
"operations": [dict(op) for op in operations],
|
|
2193
|
+
}
|
|
2194
|
+
response = self._request(
|
|
2195
|
+
method="PATCH",
|
|
2196
|
+
url=f"/Users/{scim_user_id}",
|
|
2197
|
+
json=body,
|
|
2198
|
+
)
|
|
2199
|
+
return SCIMUser.from_dict(data=response.json())
|
|
2200
|
+
|
|
2201
|
+
def delete(self, *, scim_user_id: str) -> None:
|
|
2202
|
+
"""Lock a SCIM user.
|
|
2203
|
+
|
|
2204
|
+
Args:
|
|
2205
|
+
scim_user_id: The id of the SCIM user.
|
|
2206
|
+
"""
|
|
2207
|
+
self._request(
|
|
2208
|
+
method="DELETE",
|
|
2209
|
+
url=f"/Users/{scim_user_id}",
|
|
2210
|
+
)
|
|
2211
|
+
|
|
2212
|
+
|
|
2213
|
+
@beartype
|
|
2214
|
+
class SCIMGroupsNamespace(_Namespace):
|
|
2215
|
+
"""Namespace for SCIM v2 group (team) operations."""
|
|
2216
|
+
|
|
2217
|
+
def list(
|
|
2218
|
+
self,
|
|
2219
|
+
*,
|
|
2220
|
+
limit: int | None = None,
|
|
2221
|
+
offset: int | None = None,
|
|
2222
|
+
) -> SCIMPage[SCIMTeam]:
|
|
2223
|
+
"""List SCIM groups.
|
|
2224
|
+
|
|
2225
|
+
Args:
|
|
2226
|
+
limit: Number of records to fetch.
|
|
2227
|
+
offset: Offset of records.
|
|
2228
|
+
|
|
2229
|
+
Returns:
|
|
2230
|
+
A page of SCIM teams.
|
|
2231
|
+
"""
|
|
2232
|
+
response = self._request(
|
|
2233
|
+
method="GET",
|
|
2234
|
+
url="/Groups",
|
|
2235
|
+
params=_list_params(limit=limit, offset=offset),
|
|
2236
|
+
)
|
|
2237
|
+
payload = response.json()
|
|
2238
|
+
raw = list(payload.get("Resources", []))
|
|
2239
|
+
items: list[SCIMTeam] = [SCIMTeam.from_dict(data=item) for item in raw]
|
|
2240
|
+
return _make_scim_page(items, payload)
|
|
2241
|
+
|
|
2242
|
+
def create(self, *, body: Mapping[str, JSONValue]) -> SCIMTeam:
|
|
2243
|
+
"""Create a SCIM group.
|
|
2244
|
+
|
|
2245
|
+
Args:
|
|
2246
|
+
body: The SCIM group payload.
|
|
2247
|
+
|
|
2248
|
+
Returns:
|
|
2249
|
+
The created SCIM team.
|
|
2250
|
+
"""
|
|
2251
|
+
response = self._request(
|
|
2252
|
+
method="POST",
|
|
2253
|
+
url="/Groups",
|
|
2254
|
+
json=body,
|
|
2255
|
+
)
|
|
2256
|
+
return SCIMTeam.from_dict(data=response.json())
|
|
2257
|
+
|
|
2258
|
+
def get(self, *, scim_group_id: str) -> SCIMTeam:
|
|
2259
|
+
"""Retrieve a SCIM group.
|
|
2260
|
+
|
|
2261
|
+
Args:
|
|
2262
|
+
scim_group_id: The id of the SCIM group.
|
|
2263
|
+
|
|
2264
|
+
Returns:
|
|
2265
|
+
The SCIM team.
|
|
2266
|
+
"""
|
|
2267
|
+
response = self._request(
|
|
2268
|
+
method="GET",
|
|
2269
|
+
url=f"/Groups/{scim_group_id}",
|
|
2270
|
+
)
|
|
2271
|
+
return SCIMTeam.from_dict(data=response.json())
|
|
2272
|
+
|
|
2273
|
+
def patch(
|
|
2274
|
+
self,
|
|
2275
|
+
*,
|
|
2276
|
+
scim_group_id: str,
|
|
2277
|
+
operations: Sequence[Mapping[str, JSONValue]],
|
|
2278
|
+
) -> SCIMTeam:
|
|
2279
|
+
"""Patch a SCIM group.
|
|
2280
|
+
|
|
2281
|
+
Args:
|
|
2282
|
+
scim_group_id: The id of the SCIM group.
|
|
2283
|
+
operations: The SCIM patch operations.
|
|
2284
|
+
|
|
2285
|
+
Returns:
|
|
2286
|
+
The updated SCIM team.
|
|
2287
|
+
"""
|
|
2288
|
+
body: dict[str, JSONValue] = {
|
|
2289
|
+
"operations": [dict(op) for op in operations],
|
|
2290
|
+
}
|
|
2291
|
+
response = self._request(
|
|
2292
|
+
method="PATCH",
|
|
2293
|
+
url=f"/Groups/{scim_group_id}",
|
|
2294
|
+
json=body,
|
|
2295
|
+
)
|
|
2296
|
+
return SCIMTeam.from_dict(data=response.json())
|
|
2297
|
+
|
|
2298
|
+
def delete(self, *, scim_group_id: str) -> None:
|
|
2299
|
+
"""Deprovision a SCIM group.
|
|
2300
|
+
|
|
2301
|
+
Args:
|
|
2302
|
+
scim_group_id: The id of the SCIM group.
|
|
2303
|
+
"""
|
|
2304
|
+
self._request(
|
|
2305
|
+
method="DELETE",
|
|
2306
|
+
url=f"/Groups/{scim_group_id}",
|
|
2307
|
+
)
|
|
2308
|
+
|
|
2309
|
+
|
|
2310
|
+
@beartype
|
|
2311
|
+
class SCIMNamespace(_Namespace):
|
|
2312
|
+
"""Namespace for SCIM v2 operations."""
|
|
2313
|
+
|
|
2314
|
+
def __init__(
|
|
2315
|
+
self,
|
|
2316
|
+
*,
|
|
2317
|
+
transport: Transport,
|
|
2318
|
+
base_url: str,
|
|
2319
|
+
headers: dict[str, str],
|
|
2320
|
+
) -> None:
|
|
2321
|
+
"""Create the SCIM namespace.
|
|
2322
|
+
|
|
2323
|
+
Args:
|
|
2324
|
+
transport: The HTTP transport.
|
|
2325
|
+
base_url: The base URL for the API.
|
|
2326
|
+
headers: Headers to send with every request.
|
|
2327
|
+
"""
|
|
2328
|
+
super().__init__(
|
|
2329
|
+
transport=transport,
|
|
2330
|
+
base_url=base_url,
|
|
2331
|
+
headers=headers,
|
|
2332
|
+
)
|
|
2333
|
+
self.users: SCIMUsersNamespace = SCIMUsersNamespace(
|
|
2334
|
+
transport=transport,
|
|
2335
|
+
base_url=base_url,
|
|
2336
|
+
headers=headers,
|
|
2337
|
+
)
|
|
2338
|
+
self.groups: SCIMGroupsNamespace = SCIMGroupsNamespace(
|
|
2339
|
+
transport=transport,
|
|
2340
|
+
base_url=base_url,
|
|
2341
|
+
headers=headers,
|
|
2342
|
+
)
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
@beartype
|
|
2346
|
+
class HackerRank:
|
|
2347
|
+
"""A client for the HackerRank for Work API."""
|
|
2348
|
+
|
|
2349
|
+
def __init__(
|
|
2350
|
+
self,
|
|
2351
|
+
*,
|
|
2352
|
+
api_key: str,
|
|
2353
|
+
base_url: str = "https://www.hackerrank.com",
|
|
2354
|
+
transport: Transport | None = None,
|
|
2355
|
+
) -> None:
|
|
2356
|
+
"""Create a new HackerRank client.
|
|
2357
|
+
|
|
2358
|
+
Args:
|
|
2359
|
+
api_key: The API key for authentication.
|
|
2360
|
+
base_url: The base URL for the API.
|
|
2361
|
+
transport: The HTTP transport. Defaults to
|
|
2362
|
+
``HTTPXTransport()``.
|
|
2363
|
+
"""
|
|
2364
|
+
self.base_url = base_url
|
|
2365
|
+
resolved_transport = transport or HTTPXTransport()
|
|
2366
|
+
headers = {
|
|
2367
|
+
"Authorization": f"Bearer {api_key}",
|
|
2368
|
+
"Accept": "application/json",
|
|
2369
|
+
}
|
|
2370
|
+
self.interviews: InterviewsNamespace = InterviewsNamespace(
|
|
2371
|
+
transport=resolved_transport,
|
|
2372
|
+
base_url=base_url,
|
|
2373
|
+
headers=headers,
|
|
2374
|
+
)
|
|
2375
|
+
self.interview_templates: InterviewTemplatesNamespace = (
|
|
2376
|
+
InterviewTemplatesNamespace(
|
|
2377
|
+
transport=resolved_transport,
|
|
2378
|
+
base_url=base_url,
|
|
2379
|
+
headers=headers,
|
|
2380
|
+
)
|
|
2381
|
+
)
|
|
2382
|
+
self.questions: QuestionsNamespace = QuestionsNamespace(
|
|
2383
|
+
transport=resolved_transport,
|
|
2384
|
+
base_url=base_url,
|
|
2385
|
+
headers=headers,
|
|
2386
|
+
)
|
|
2387
|
+
self.tests: TestsNamespace = TestsNamespace(
|
|
2388
|
+
transport=resolved_transport,
|
|
2389
|
+
base_url=base_url,
|
|
2390
|
+
headers=headers,
|
|
2391
|
+
)
|
|
2392
|
+
self.templates: TemplatesNamespace = TemplatesNamespace(
|
|
2393
|
+
transport=resolved_transport,
|
|
2394
|
+
base_url=base_url,
|
|
2395
|
+
headers=headers,
|
|
2396
|
+
)
|
|
2397
|
+
self.users: UsersNamespace = UsersNamespace(
|
|
2398
|
+
transport=resolved_transport,
|
|
2399
|
+
base_url=base_url,
|
|
2400
|
+
headers=headers,
|
|
2401
|
+
)
|
|
2402
|
+
self.teams: TeamsNamespace = TeamsNamespace(
|
|
2403
|
+
transport=resolved_transport,
|
|
2404
|
+
base_url=base_url,
|
|
2405
|
+
headers=headers,
|
|
2406
|
+
)
|
|
2407
|
+
self.audit_logs: AuditLogsNamespace = AuditLogsNamespace(
|
|
2408
|
+
transport=resolved_transport,
|
|
2409
|
+
base_url=base_url,
|
|
2410
|
+
headers=headers,
|
|
2411
|
+
)
|
|
2412
|
+
self.ats: ATSNamespace = ATSNamespace(
|
|
2413
|
+
transport=resolved_transport,
|
|
2414
|
+
base_url=base_url,
|
|
2415
|
+
headers=headers,
|
|
2416
|
+
)
|
|
2417
|
+
self.scim: SCIMNamespace = SCIMNamespace(
|
|
2418
|
+
transport=resolved_transport,
|
|
2419
|
+
base_url=base_url,
|
|
2420
|
+
headers=headers,
|
|
2421
|
+
)
|
|
2422
|
+
if isinstance(resolved_transport, HTTPXTransport):
|
|
2423
|
+
self._close = resolved_transport.close
|
|
2424
|
+
else:
|
|
2425
|
+
self._close = lambda: None
|
|
2426
|
+
|
|
2427
|
+
def close(self) -> None:
|
|
2428
|
+
"""Close the underlying transport if it supports closing."""
|
|
2429
|
+
self._close()
|
|
2430
|
+
|
|
2431
|
+
def __enter__(self) -> Self:
|
|
2432
|
+
"""Enter the context manager.
|
|
2433
|
+
|
|
2434
|
+
Returns:
|
|
2435
|
+
This client instance.
|
|
2436
|
+
"""
|
|
2437
|
+
return self
|
|
2438
|
+
|
|
2439
|
+
def __exit__(
|
|
2440
|
+
self,
|
|
2441
|
+
_exc_type: type[BaseException] | None,
|
|
2442
|
+
_exc_val: BaseException | None,
|
|
2443
|
+
_exc_tb: TracebackType | None,
|
|
2444
|
+
) -> None:
|
|
2445
|
+
"""Exit the context manager and close the transport."""
|
|
2446
|
+
self.close()
|