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/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()