hackerrank 2026.7.19__tar.gz → 2026.7.30__tar.gz

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.
Files changed (56) hide show
  1. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/CHANGELOG.rst +7 -0
  2. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/PKG-INFO +9 -9
  3. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/pyproject.toml +10 -8
  4. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank/_dict_types.py +2 -2
  5. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank/async_client.py +19 -23
  6. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank/client.py +19 -23
  7. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank/types.py +9 -5
  8. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank.egg-info/PKG-INFO +9 -9
  9. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank.egg-info/requires.txt +8 -8
  10. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank.egg-info/scm_file_list.json +31 -31
  11. hackerrank-2026.7.30/src/hackerrank.egg-info/scm_version.json +8 -0
  12. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/tests/conftest.py +13 -1
  13. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/tests/test_endpoints.py +157 -22
  14. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/tests/test_types.py +7 -2
  15. hackerrank-2026.7.19/src/hackerrank.egg-info/scm_version.json +0 -8
  16. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.git_archival.txt +0 -0
  17. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.gitattributes +0 -0
  18. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.github/dependabot.yml +0 -0
  19. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.github/workflows/ci.yml +0 -0
  20. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.github/workflows/dependabot-merge.yml +0 -0
  21. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.github/workflows/lint.yml +0 -0
  22. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.github/workflows/publish-site.yml +0 -0
  23. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.github/workflows/release.yml +0 -0
  24. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.gitignore +0 -0
  25. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.pre-commit-config.yaml +0 -0
  26. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/.prettierrc +0 -0
  27. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/LICENSE +0 -0
  28. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/README.rst +0 -0
  29. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/conftest.py +0 -0
  30. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/__init__.py +0 -0
  31. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/api-reference.rst +0 -0
  32. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/changelog.rst +0 -0
  33. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/conf.py +0 -0
  34. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/contributing.rst +0 -0
  35. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/index.rst +0 -0
  36. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/openapi-spec.rst +0 -0
  37. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/release-process.rst +0 -0
  38. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/source/unreleased.rst +0 -0
  39. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/docs/towncrier_template.rst.jinja +0 -0
  40. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/newsfragments/.gitkeep +0 -0
  41. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/openapi.json +0 -0
  42. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/setup.cfg +0 -0
  43. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/spelling_private_dict.txt +0 -0
  44. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank/__init__.py +0 -0
  45. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank/exceptions.py +0 -0
  46. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank/py.typed +0 -0
  47. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank/transports.py +0 -0
  48. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank.egg-info/SOURCES.txt +0 -0
  49. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank.egg-info/dependency_links.txt +0 -0
  50. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank.egg-info/not-zip-safe +0 -0
  51. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/src/hackerrank.egg-info/top_level.txt +0 -0
  52. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/tests/__init__.py +0 -0
  53. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/tests/test_async_client.py +0 -0
  54. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/tests/test_client.py +0 -0
  55. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/tests/test_coverage_gaps.py +0 -0
  56. {hackerrank-2026.7.19 → hackerrank-2026.7.30}/zizmor.yml +0 -0
@@ -3,6 +3,13 @@ Changelog
3
3
 
4
4
  .. towncrier release notes start
5
5
 
6
+ 2026.07.30
7
+ ----------
8
+
9
+ - Align interview-template create and update requests with the current API,
10
+ return updated templates, and parse live string template IDs and integer
11
+ question IDs.
12
+
6
13
  2026.07.19
7
14
  ----------
8
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hackerrank
3
- Version: 2026.7.19
3
+ Version: 2026.7.30
4
4
  Summary: Interact with the HackerRank for Work API.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -25,19 +25,19 @@ Requires-Dist: beartype==0.22.9; extra == "dev"
25
25
  Requires-Dist: check-manifest==0.51; extra == "dev"
26
26
  Requires-Dist: deptry==0.25.1; extra == "dev"
27
27
  Requires-Dist: doc8==2.0.0; extra == "dev"
28
- Requires-Dist: doccmd==2026.5.25; extra == "dev"
28
+ Requires-Dist: doccmd==2026.7.19; extra == "dev"
29
29
  Requires-Dist: furo==2025.12.19; extra == "dev"
30
30
  Requires-Dist: interrogate==1.7.0; extra == "dev"
31
31
  Requires-Dist: mypy[faster-cache]==2.3.0; extra == "dev"
32
- Requires-Dist: mypy-strict-kwargs==2026.5.20.1; extra == "dev"
32
+ Requires-Dist: mypy-strict-kwargs==2026.7.19.1; extra == "dev"
33
33
  Requires-Dist: openapi-mock==2026.3.2; extra == "dev"
34
- Requires-Dist: prek==0.4.10; extra == "dev"
34
+ Requires-Dist: prek==0.4.11; extra == "dev"
35
35
  Requires-Dist: pydocstringformatter==1.0.0; extra == "dev"
36
36
  Requires-Dist: pydocstyle==6.3; extra == "dev"
37
37
  Requires-Dist: pygments==2.20.0; extra == "dev"
38
38
  Requires-Dist: pylint[spelling]==4.0.6; extra == "dev"
39
39
  Requires-Dist: pylint-per-file-ignores==3.2.1; extra == "dev"
40
- Requires-Dist: pyproject-fmt==2.25.3; extra == "dev"
40
+ Requires-Dist: pyproject-fmt==2.26.0; extra == "dev"
41
41
  Requires-Dist: pyrefly==1.1.1; extra == "dev"
42
42
  Requires-Dist: pyright==1.1.411; extra == "dev"
43
43
  Requires-Dist: pyroma==5.0.1; extra == "dev"
@@ -47,7 +47,7 @@ Requires-Dist: pytest-beartype-tests==2026.4.26; extra == "dev"
47
47
  Requires-Dist: pytest-cov==7.1.0; extra == "dev"
48
48
  Requires-Dist: pyyaml==6.0.3; extra == "dev"
49
49
  Requires-Dist: respx==0.23.1; extra == "dev"
50
- Requires-Dist: ruff==0.15.22; extra == "dev"
50
+ Requires-Dist: ruff==0.16.0; extra == "dev"
51
51
  Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
52
52
  Requires-Dist: shfmt-py==4.0.0; extra == "dev"
53
53
  Requires-Dist: sphinx==9.1.0; extra == "dev"
@@ -57,13 +57,13 @@ Requires-Dist: sphinx-pyproject==0.3.0; extra == "dev"
57
57
  Requires-Dist: sphinx-substitution-extensions==2026.6.17; extra == "dev"
58
58
  Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
59
59
  Requires-Dist: sphinxcontrib-towncrier==0.5.0a0; extra == "dev"
60
- Requires-Dist: strict-kwargs==2026.6.8.post1; extra == "dev"
60
+ Requires-Dist: strict-kwargs==2026.7.24; extra == "dev"
61
61
  Requires-Dist: sybil==10.1.0; extra == "dev"
62
62
  Requires-Dist: towncrier==25.8.0; extra == "dev"
63
- Requires-Dist: ty==0.0.60; extra == "dev"
63
+ Requires-Dist: ty==0.0.65; extra == "dev"
64
64
  Requires-Dist: vulture==2.16; extra == "dev"
65
65
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
66
- Requires-Dist: zizmor==1.27.0; extra == "dev"
66
+ Requires-Dist: zizmor==1.28.0; extra == "dev"
67
67
  Provides-Extra: release
68
68
  Requires-Dist: check-wheel-contents==0.6.3; extra == "release"
69
69
  Requires-Dist: towncrier==25.8.0; extra == "release"
@@ -41,19 +41,19 @@ optional-dependencies.dev = [
41
41
  "check-manifest==0.51",
42
42
  "deptry==0.25.1",
43
43
  "doc8==2.0.0",
44
- "doccmd==2026.5.25",
44
+ "doccmd==2026.7.19",
45
45
  "furo==2025.12.19",
46
46
  "interrogate==1.7.0",
47
47
  "mypy[faster-cache]==2.3.0",
48
- "mypy-strict-kwargs==2026.5.20.1",
48
+ "mypy-strict-kwargs==2026.7.19.1",
49
49
  "openapi-mock==2026.3.2",
50
- "prek==0.4.10",
50
+ "prek==0.4.11",
51
51
  "pydocstringformatter==1.0.0",
52
52
  "pydocstyle==6.3",
53
53
  "pygments==2.20.0",
54
54
  "pylint[spelling]==4.0.6",
55
55
  "pylint-per-file-ignores==3.2.1",
56
- "pyproject-fmt==2.25.3",
56
+ "pyproject-fmt==2.26.0",
57
57
  "pyrefly==1.1.1",
58
58
  "pyright==1.1.411",
59
59
  "pyroma==5.0.1",
@@ -63,7 +63,7 @@ optional-dependencies.dev = [
63
63
  "pytest-cov==7.1.0",
64
64
  "pyyaml==6.0.3",
65
65
  "respx==0.23.1",
66
- "ruff==0.15.22",
66
+ "ruff==0.16.0",
67
67
  # We add shellcheck-py not only for shell scripts and shell code blocks,
68
68
  # but also because having it installed means that ``actionlint-py`` will
69
69
  # use it to lint shell commands in GitHub workflow files.
@@ -78,13 +78,13 @@ optional-dependencies.dev = [
78
78
  # ``sphinxcontrib-towncrier`` renders unreleased news fragments
79
79
  # into docs/source/unreleased.rst during Sphinx builds.
80
80
  "sphinxcontrib-towncrier==0.5.0a0",
81
- "strict-kwargs==2026.6.8.post1",
81
+ "strict-kwargs==2026.7.24",
82
82
  "sybil==10.1.0",
83
83
  "towncrier==25.8.0",
84
- "ty==0.0.60",
84
+ "ty==0.0.65",
85
85
  "vulture==2.16",
86
86
  "yamlfix==1.19.1",
87
- "zizmor==1.27.0",
87
+ "zizmor==1.28.0",
88
88
  ]
89
89
  optional-dependencies.release = [ "check-wheel-contents==0.6.3", "towncrier==25.8.0" ]
90
90
  urls.Documentation = "https://adamtheturtle.github.io/hackerrank/"
@@ -118,6 +118,8 @@ lint.select = [
118
118
  lint.ignore = [
119
119
  # Ruff warns that this conflicts with the formatter.
120
120
  "COM812",
121
+ # Copyright headers are not required.
122
+ "CPY001",
121
123
  # Allow our chosen docstring line-style - pydocstringformatter handles formatting
122
124
  # but doesn't enforce D205 (blank line after summary) or D212 (summary on first line).
123
125
  "D205",
@@ -47,14 +47,14 @@ class InterviewTranscriptDict(TypedDict):
47
47
  class InterviewTemplateDict(TypedDict):
48
48
  """A HackerRank interview template."""
49
49
 
50
- id: int
50
+ id: int | str
51
51
  name: str
52
52
  created_at: NotRequired[str]
53
53
  status: NotRequired[int]
54
54
  user: NotRequired[int]
55
55
  roles: NotRequired[list[str]]
56
56
  team_share: NotRequired[int]
57
- questions: NotRequired[list[str]]
57
+ questions: NotRequired[list[int | str]]
58
58
  scorecard: NotRequired[int]
59
59
  import_template: NotRequired[bool]
60
60
  editor_access: NotRequired[bool]
@@ -510,19 +510,17 @@ class AsyncInterviewTemplatesNamespace(_AsyncNamespace):
510
510
  self,
511
511
  *,
512
512
  name: str,
513
- roles: Sequence[str] | None = None,
513
+ role_id: str | None = None,
514
514
  team_share: int | None = None,
515
- questions: Sequence[str] | None = None,
516
- scorecard: int | None = None,
515
+ question_ids: Sequence[int] | None = None,
517
516
  ) -> InterviewTemplate:
518
517
  """Create an interview template.
519
518
 
520
519
  Args:
521
520
  name: The template name.
522
- roles: Roles for the template.
521
+ role_id: Role unique id for the template.
523
522
  team_share: Team-share flag.
524
- questions: Associated question ids.
525
- scorecard: Scorecard id.
523
+ question_ids: Question ids to add to the template.
526
524
 
527
525
  Returns:
528
526
  The created interview template.
@@ -530,12 +528,11 @@ class AsyncInterviewTemplatesNamespace(_AsyncNamespace):
530
528
  body = _drop_none(
531
529
  {
532
530
  "name": name,
533
- "roles": list(roles) if roles is not None else None,
531
+ "role_id": role_id,
534
532
  "team_share": team_share,
535
- "questions": (
536
- list(questions) if questions is not None else None
533
+ "question_ids": (
534
+ list(question_ids) if question_ids is not None else None
537
535
  ),
538
- "scorecard": scorecard,
539
536
  },
540
537
  )
541
538
  response = await self._request(
@@ -569,37 +566,36 @@ class AsyncInterviewTemplatesNamespace(_AsyncNamespace):
569
566
  *,
570
567
  template_id: int | str,
571
568
  name: str | None = None,
572
- roles: Sequence[str] | None = None,
569
+ role_id: str | None = None,
573
570
  team_share: int | None = None,
574
- questions: Sequence[str] | None = None,
575
- scorecard: int | None = None,
576
- ) -> None:
571
+ scorecard_id: int | None = None,
572
+ ) -> InterviewTemplate:
577
573
  """Update an interview template.
578
574
 
579
575
  Args:
580
576
  template_id: The id of the template.
581
577
  name: New name.
582
- roles: New roles.
578
+ role_id: New role unique id.
583
579
  team_share: New team-share flag.
584
- questions: New question ids.
585
- scorecard: New scorecard id.
580
+ scorecard_id: New scorecard id.
581
+
582
+ Returns:
583
+ The updated interview template.
586
584
  """
587
585
  body = _drop_none(
588
586
  {
589
587
  "name": name,
590
- "roles": list(roles) if roles is not None else None,
588
+ "role_id": role_id,
591
589
  "team_share": team_share,
592
- "questions": (
593
- list(questions) if questions is not None else None
594
- ),
595
- "scorecard": scorecard,
590
+ "scorecard_id": scorecard_id,
596
591
  },
597
592
  )
598
- await self._request(
593
+ response = await self._request(
599
594
  method="PUT",
600
595
  url=(f"{_API_V3}/interview_templates/{template_id}"),
601
596
  json=body,
602
597
  )
598
+ return InterviewTemplate.from_dict(data=response.json())
603
599
 
604
600
  async def delete(self, *, template_id: int | str) -> None:
605
601
  """Delete an interview template.
@@ -497,19 +497,17 @@ class InterviewTemplatesNamespace(_Namespace):
497
497
  self,
498
498
  *,
499
499
  name: str,
500
- roles: Sequence[str] | None = None,
500
+ role_id: str | None = None,
501
501
  team_share: int | None = None,
502
- questions: Sequence[str] | None = None,
503
- scorecard: int | None = None,
502
+ question_ids: Sequence[int] | None = None,
504
503
  ) -> InterviewTemplate:
505
504
  """Create an interview template.
506
505
 
507
506
  Args:
508
507
  name: The template name.
509
- roles: Roles for the template.
508
+ role_id: Role unique id for the template.
510
509
  team_share: Team-share flag.
511
- questions: Associated question ids.
512
- scorecard: Scorecard id.
510
+ question_ids: Question ids to add to the template.
513
511
 
514
512
  Returns:
515
513
  The created interview template.
@@ -517,12 +515,11 @@ class InterviewTemplatesNamespace(_Namespace):
517
515
  body = _drop_none(
518
516
  {
519
517
  "name": name,
520
- "roles": list(roles) if roles is not None else None,
518
+ "role_id": role_id,
521
519
  "team_share": team_share,
522
- "questions": (
523
- list(questions) if questions is not None else None
520
+ "question_ids": (
521
+ list(question_ids) if question_ids is not None else None
524
522
  ),
525
- "scorecard": scorecard,
526
523
  },
527
524
  )
528
525
  response = self._request(
@@ -552,37 +549,36 @@ class InterviewTemplatesNamespace(_Namespace):
552
549
  *,
553
550
  template_id: int | str,
554
551
  name: str | None = None,
555
- roles: Sequence[str] | None = None,
552
+ role_id: str | None = None,
556
553
  team_share: int | None = None,
557
- questions: Sequence[str] | None = None,
558
- scorecard: int | None = None,
559
- ) -> None:
554
+ scorecard_id: int | None = None,
555
+ ) -> InterviewTemplate:
560
556
  """Update an interview template.
561
557
 
562
558
  Args:
563
559
  template_id: The id of the template.
564
560
  name: New name.
565
- roles: New roles.
561
+ role_id: New role unique id.
566
562
  team_share: New team-share flag.
567
- questions: New question ids.
568
- scorecard: New scorecard id.
563
+ scorecard_id: New scorecard id.
564
+
565
+ Returns:
566
+ The updated interview template.
569
567
  """
570
568
  body = _drop_none(
571
569
  {
572
570
  "name": name,
573
- "roles": list(roles) if roles is not None else None,
571
+ "role_id": role_id,
574
572
  "team_share": team_share,
575
- "questions": (
576
- list(questions) if questions is not None else None
577
- ),
578
- "scorecard": scorecard,
573
+ "scorecard_id": scorecard_id,
579
574
  },
580
575
  )
581
- self._request(
576
+ response = self._request(
582
577
  method="PUT",
583
578
  url=(f"{_API_V3}/interview_templates/{template_id}"),
584
579
  json=body,
585
580
  )
581
+ return InterviewTemplate.from_dict(data=response.json())
586
582
 
587
583
  def delete(self, *, template_id: int | str) -> None:
588
584
  """Delete an interview template.
@@ -30,7 +30,7 @@ from hackerrank._dict_types import (
30
30
  )
31
31
 
32
32
  type JSONValue = (
33
- str | int | float | bool | None | Sequence[Any] | Mapping[str, Any]
33
+ str | int | float | bool | Sequence[Any] | Mapping[str, Any] | None
34
34
  )
35
35
  """A JSON-compatible value.
36
36
 
@@ -278,14 +278,14 @@ class InterviewTranscript:
278
278
  class InterviewTemplate:
279
279
  """A HackerRank interview template."""
280
280
 
281
- id: int
281
+ id: str
282
282
  name: str
283
283
  created_at: str | None = None
284
284
  status: int | None = None
285
285
  user: int | None = None
286
286
  roles: list[str] | None = None
287
287
  team_share: int | None = None
288
- questions: list[str] | None = None
288
+ questions: list[int] | None = None
289
289
  scorecard: int | None = None
290
290
  import_template: bool | None = None
291
291
  editor_access: bool | None = None
@@ -301,14 +301,18 @@ class InterviewTemplate:
301
301
  A new instance.
302
302
  """
303
303
  return cls(
304
- id=data["id"],
304
+ id=str(object=data["id"]),
305
305
  name=data["name"],
306
306
  created_at=data.get("created_at"),
307
307
  status=data.get("status"),
308
308
  user=data.get("user"),
309
309
  roles=data.get("roles"),
310
310
  team_share=data.get("team_share"),
311
- questions=data.get("questions"),
311
+ questions=(
312
+ [int(question_id) for question_id in data["questions"]]
313
+ if "questions" in data
314
+ else None
315
+ ),
312
316
  scorecard=data.get("scorecard"),
313
317
  import_template=data.get("import_template"),
314
318
  editor_access=data.get("editor_access"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hackerrank
3
- Version: 2026.7.19
3
+ Version: 2026.7.30
4
4
  Summary: Interact with the HackerRank for Work API.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -25,19 +25,19 @@ Requires-Dist: beartype==0.22.9; extra == "dev"
25
25
  Requires-Dist: check-manifest==0.51; extra == "dev"
26
26
  Requires-Dist: deptry==0.25.1; extra == "dev"
27
27
  Requires-Dist: doc8==2.0.0; extra == "dev"
28
- Requires-Dist: doccmd==2026.5.25; extra == "dev"
28
+ Requires-Dist: doccmd==2026.7.19; extra == "dev"
29
29
  Requires-Dist: furo==2025.12.19; extra == "dev"
30
30
  Requires-Dist: interrogate==1.7.0; extra == "dev"
31
31
  Requires-Dist: mypy[faster-cache]==2.3.0; extra == "dev"
32
- Requires-Dist: mypy-strict-kwargs==2026.5.20.1; extra == "dev"
32
+ Requires-Dist: mypy-strict-kwargs==2026.7.19.1; extra == "dev"
33
33
  Requires-Dist: openapi-mock==2026.3.2; extra == "dev"
34
- Requires-Dist: prek==0.4.10; extra == "dev"
34
+ Requires-Dist: prek==0.4.11; extra == "dev"
35
35
  Requires-Dist: pydocstringformatter==1.0.0; extra == "dev"
36
36
  Requires-Dist: pydocstyle==6.3; extra == "dev"
37
37
  Requires-Dist: pygments==2.20.0; extra == "dev"
38
38
  Requires-Dist: pylint[spelling]==4.0.6; extra == "dev"
39
39
  Requires-Dist: pylint-per-file-ignores==3.2.1; extra == "dev"
40
- Requires-Dist: pyproject-fmt==2.25.3; extra == "dev"
40
+ Requires-Dist: pyproject-fmt==2.26.0; extra == "dev"
41
41
  Requires-Dist: pyrefly==1.1.1; extra == "dev"
42
42
  Requires-Dist: pyright==1.1.411; extra == "dev"
43
43
  Requires-Dist: pyroma==5.0.1; extra == "dev"
@@ -47,7 +47,7 @@ Requires-Dist: pytest-beartype-tests==2026.4.26; extra == "dev"
47
47
  Requires-Dist: pytest-cov==7.1.0; extra == "dev"
48
48
  Requires-Dist: pyyaml==6.0.3; extra == "dev"
49
49
  Requires-Dist: respx==0.23.1; extra == "dev"
50
- Requires-Dist: ruff==0.15.22; extra == "dev"
50
+ Requires-Dist: ruff==0.16.0; extra == "dev"
51
51
  Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
52
52
  Requires-Dist: shfmt-py==4.0.0; extra == "dev"
53
53
  Requires-Dist: sphinx==9.1.0; extra == "dev"
@@ -57,13 +57,13 @@ Requires-Dist: sphinx-pyproject==0.3.0; extra == "dev"
57
57
  Requires-Dist: sphinx-substitution-extensions==2026.6.17; extra == "dev"
58
58
  Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
59
59
  Requires-Dist: sphinxcontrib-towncrier==0.5.0a0; extra == "dev"
60
- Requires-Dist: strict-kwargs==2026.6.8.post1; extra == "dev"
60
+ Requires-Dist: strict-kwargs==2026.7.24; extra == "dev"
61
61
  Requires-Dist: sybil==10.1.0; extra == "dev"
62
62
  Requires-Dist: towncrier==25.8.0; extra == "dev"
63
- Requires-Dist: ty==0.0.60; extra == "dev"
63
+ Requires-Dist: ty==0.0.65; extra == "dev"
64
64
  Requires-Dist: vulture==2.16; extra == "dev"
65
65
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
66
- Requires-Dist: zizmor==1.27.0; extra == "dev"
66
+ Requires-Dist: zizmor==1.28.0; extra == "dev"
67
67
  Provides-Extra: release
68
68
  Requires-Dist: check-wheel-contents==0.6.3; extra == "release"
69
69
  Requires-Dist: towncrier==25.8.0; extra == "release"
@@ -7,19 +7,19 @@ beartype==0.22.9
7
7
  check-manifest==0.51
8
8
  deptry==0.25.1
9
9
  doc8==2.0.0
10
- doccmd==2026.5.25
10
+ doccmd==2026.7.19
11
11
  furo==2025.12.19
12
12
  interrogate==1.7.0
13
13
  mypy[faster-cache]==2.3.0
14
- mypy-strict-kwargs==2026.5.20.1
14
+ mypy-strict-kwargs==2026.7.19.1
15
15
  openapi-mock==2026.3.2
16
- prek==0.4.10
16
+ prek==0.4.11
17
17
  pydocstringformatter==1.0.0
18
18
  pydocstyle==6.3
19
19
  pygments==2.20.0
20
20
  pylint[spelling]==4.0.6
21
21
  pylint-per-file-ignores==3.2.1
22
- pyproject-fmt==2.25.3
22
+ pyproject-fmt==2.26.0
23
23
  pyrefly==1.1.1
24
24
  pyright==1.1.411
25
25
  pyroma==5.0.1
@@ -29,7 +29,7 @@ pytest-beartype-tests==2026.4.26
29
29
  pytest-cov==7.1.0
30
30
  pyyaml==6.0.3
31
31
  respx==0.23.1
32
- ruff==0.15.22
32
+ ruff==0.16.0
33
33
  shellcheck-py==0.11.0.1
34
34
  shfmt-py==4.0.0
35
35
  sphinx==9.1.0
@@ -39,13 +39,13 @@ sphinx-pyproject==0.3.0
39
39
  sphinx-substitution-extensions==2026.6.17
40
40
  sphinxcontrib-spelling==8.0.2
41
41
  sphinxcontrib-towncrier==0.5.0a0
42
- strict-kwargs==2026.6.8.post1
42
+ strict-kwargs==2026.7.24
43
43
  sybil==10.1.0
44
44
  towncrier==25.8.0
45
- ty==0.0.60
45
+ ty==0.0.65
46
46
  vulture==2.16
47
47
  yamlfix==1.19.1
48
- zizmor==1.27.0
48
+ zizmor==1.28.0
49
49
 
50
50
  [release]
51
51
  check-wheel-contents==0.6.3
@@ -1,49 +1,49 @@
1
1
  {
2
2
  "files": [
3
- ".gitignore",
4
- ".git_archival.txt",
5
- "conftest.py",
3
+ "openapi.json",
4
+ ".gitattributes",
5
+ ".pre-commit-config.yaml",
6
6
  "zizmor.yml",
7
- "spelling_private_dict.txt",
7
+ "README.rst",
8
8
  "CHANGELOG.rst",
9
- ".prettierrc",
10
- ".gitattributes",
9
+ ".git_archival.txt",
11
10
  "pyproject.toml",
12
- ".pre-commit-config.yaml",
13
11
  "LICENSE",
14
- "openapi.json",
15
- "README.rst",
16
- "src/hackerrank/client.py",
17
- "src/hackerrank/transports.py",
18
- "src/hackerrank/types.py",
19
- "src/hackerrank/exceptions.py",
20
- "src/hackerrank/_dict_types.py",
21
- "src/hackerrank/async_client.py",
22
- "src/hackerrank/py.typed",
23
- "src/hackerrank/__init__.py",
12
+ "spelling_private_dict.txt",
13
+ "conftest.py",
14
+ ".prettierrc",
15
+ ".gitignore",
24
16
  ".github/dependabot.yml",
25
- ".github/workflows/publish-site.yml",
17
+ ".github/workflows/dependabot-merge.yml",
18
+ ".github/workflows/lint.yml",
26
19
  ".github/workflows/ci.yml",
20
+ ".github/workflows/publish-site.yml",
27
21
  ".github/workflows/release.yml",
28
- ".github/workflows/lint.yml",
29
- ".github/workflows/dependabot-merge.yml",
30
- "newsfragments/.gitkeep",
31
- "tests/conftest.py",
32
- "tests/test_types.py",
33
- "tests/test_endpoints.py",
34
- "tests/test_client.py",
35
22
  "tests/test_async_client.py",
36
- "tests/test_coverage_gaps.py",
37
23
  "tests/__init__.py",
24
+ "tests/test_client.py",
25
+ "tests/test_types.py",
26
+ "tests/test_coverage_gaps.py",
27
+ "tests/test_endpoints.py",
28
+ "tests/conftest.py",
29
+ "src/hackerrank/types.py",
30
+ "src/hackerrank/async_client.py",
31
+ "src/hackerrank/transports.py",
32
+ "src/hackerrank/py.typed",
33
+ "src/hackerrank/__init__.py",
34
+ "src/hackerrank/_dict_types.py",
35
+ "src/hackerrank/client.py",
36
+ "src/hackerrank/exceptions.py",
38
37
  "docs/towncrier_template.rst.jinja",
38
+ "docs/source/index.rst",
39
+ "docs/source/contributing.rst",
39
40
  "docs/source/conf.py",
41
+ "docs/source/changelog.rst",
42
+ "docs/source/__init__.py",
40
43
  "docs/source/release-process.rst",
41
44
  "docs/source/unreleased.rst",
42
- "docs/source/index.rst",
43
- "docs/source/changelog.rst",
44
- "docs/source/contributing.rst",
45
- "docs/source/openapi-spec.rst",
46
45
  "docs/source/api-reference.rst",
47
- "docs/source/__init__.py"
46
+ "docs/source/openapi-spec.rst",
47
+ "newsfragments/.gitkeep"
48
48
  ]
49
49
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2026.7.30",
3
+ "distance": 0,
4
+ "node": "g2bf78d9fa737747a32510a32544a1c4dd7b1dc96",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-30"
8
+ }
@@ -42,7 +42,11 @@ _INTERVIEW_OBJ = {
42
42
  "status": "scheduled",
43
43
  "url": "https://example.com/iv1",
44
44
  }
45
- _INTERVIEW_TEMPLATE_OBJ = {"id": 1, "name": "T"}
45
+ _INTERVIEW_TEMPLATE_OBJ = {
46
+ "id": "template-1",
47
+ "name": "T",
48
+ "questions": [2687118],
49
+ }
46
50
  _ENVIRONMENT_OBJ = {
47
51
  "id": 92,
48
52
  "name": "PERN",
@@ -248,6 +252,14 @@ def _response_for( # noqa: C901, PLR0911, PLR0912 # pylint: disable=too-comple
248
252
  json=_SCIM_TEAM_OBJ,
249
253
  )
250
254
  if method == "PUT":
255
+ if re.fullmatch(
256
+ pattern=r"/x/api/v3/interview_templates/[^/]+",
257
+ string=path,
258
+ ):
259
+ return httpx.Response(
260
+ status_code=200,
261
+ json=_INTERVIEW_TEMPLATE_OBJ,
262
+ )
251
263
  if re.fullmatch(
252
264
  pattern=r"/x/api/v3/questions/[^/]+/(custom_codestubs|generate)",
253
265
  string=path,
@@ -8,10 +8,14 @@ populated. Responses are stubbed via the ``stub_router`` fixture in
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import json
12
+
13
+ import httpx
11
14
  import pytest
15
+ import respx
12
16
 
13
- from hackerrank.async_client import AsyncHackerRank # noqa: TC001
14
- from hackerrank.client import HackerRank # noqa: TC001
17
+ from hackerrank.async_client import AsyncHackerRank
18
+ from hackerrank.client import HackerRank
15
19
 
16
20
 
17
21
  class TestSyncEndpoints:
@@ -68,22 +72,84 @@ class TestSyncEndpoints:
68
72
  sync_client.interview_templates.create(name="t")
69
73
  sync_client.interview_templates.create(
70
74
  name="t",
71
- roles=["dev"],
75
+ role_id="dev",
72
76
  team_share=1,
73
- questions=["q1"],
74
- scorecard=2,
77
+ question_ids=[2687118],
75
78
  )
76
- sync_client.interview_templates.get(template_id=1)
77
- sync_client.interview_templates.update(template_id=1)
79
+ sync_client.interview_templates.get(template_id="template-1")
80
+ sync_client.interview_templates.update(template_id="template-1")
78
81
  sync_client.interview_templates.update(
79
- template_id=1,
82
+ template_id="template-1",
80
83
  name="t",
81
- roles=["dev"],
84
+ role_id="dev",
82
85
  team_share=1,
83
- questions=["q1"],
84
- scorecard=2,
86
+ scorecard_id=2,
85
87
  )
86
- sync_client.interview_templates.delete(template_id=1)
88
+ sync_client.interview_templates.delete(template_id="template-1")
89
+
90
+ @staticmethod
91
+ def test_interview_template_contract() -> None:
92
+ """Use the current request and response fields for templates."""
93
+ requests: list[httpx.Request] = []
94
+
95
+ def template_response(request: httpx.Request) -> httpx.Response:
96
+ """Record a request and return a live-shaped response."""
97
+ requests.append(request)
98
+ return httpx.Response(
99
+ status_code=201 if request.method == "POST" else 200,
100
+ json={
101
+ "id": "template-1",
102
+ "name": "Backend interview",
103
+ "questions": [2687118],
104
+ },
105
+ )
106
+
107
+ with respx.mock(assert_all_called=True) as router:
108
+ router.post(
109
+ url=(
110
+ "https://www.hackerrank.com/x/api/v3/interview_templates"
111
+ ),
112
+ ).mock(side_effect=template_response)
113
+ router.put(
114
+ url=(
115
+ "https://www.hackerrank.com/x/api/v3/"
116
+ "interview_templates/template-1"
117
+ ),
118
+ ).mock(side_effect=template_response)
119
+ with HackerRank(api_key="test-key") as client:
120
+ created = client.interview_templates.create(
121
+ name="Backend interview",
122
+ role_id="backend",
123
+ team_share=2,
124
+ question_ids=[2687118, 2687119],
125
+ )
126
+ updated = client.interview_templates.update(
127
+ template_id=created.id,
128
+ name="Senior backend interview",
129
+ role_id="senior-backend",
130
+ team_share=3,
131
+ scorecard_id=98765,
132
+ )
133
+
134
+ create_request = requests[0]
135
+ assert json.loads(s=create_request.content) == {
136
+ "name": "Backend interview",
137
+ "role_id": "backend",
138
+ "team_share": 2,
139
+ "question_ids": [2687118, 2687119],
140
+ }
141
+ assert created.id == "template-1"
142
+ assert created.questions == [2687118]
143
+
144
+ update_request = requests[1]
145
+ assert json.loads(s=update_request.content) == {
146
+ "name": "Senior backend interview",
147
+ "role_id": "senior-backend",
148
+ "team_share": 3,
149
+ "scorecard_id": 98765,
150
+ }
151
+ assert updated.id == "template-1"
152
+ assert updated.questions == [2687118]
87
153
 
88
154
  @staticmethod
89
155
  def test_questions(sync_client: HackerRank) -> None:
@@ -526,22 +592,91 @@ class TestAsyncEndpoints:
526
592
  await async_client.interview_templates.create(name="t")
527
593
  await async_client.interview_templates.create(
528
594
  name="t",
529
- roles=["dev"],
595
+ role_id="dev",
530
596
  team_share=1,
531
- questions=["q1"],
532
- scorecard=2,
597
+ question_ids=[2687118],
598
+ )
599
+ await async_client.interview_templates.get(
600
+ template_id="template-1",
533
601
  )
534
- await async_client.interview_templates.get(template_id=1)
535
- await async_client.interview_templates.update(template_id=1)
536
602
  await async_client.interview_templates.update(
537
- template_id=1,
603
+ template_id="template-1",
604
+ )
605
+ await async_client.interview_templates.update(
606
+ template_id="template-1",
538
607
  name="t",
539
- roles=["dev"],
608
+ role_id="dev",
540
609
  team_share=1,
541
- questions=["q1"],
542
- scorecard=2,
610
+ scorecard_id=2,
543
611
  )
544
- await async_client.interview_templates.delete(template_id=1)
612
+ await async_client.interview_templates.delete(
613
+ template_id="template-1",
614
+ )
615
+
616
+ @staticmethod
617
+ @pytest.mark.asyncio
618
+ async def test_interview_template_contract() -> None:
619
+ """Use current template fields in the async client."""
620
+ requests: list[httpx.Request] = []
621
+
622
+ def template_response(request: httpx.Request) -> httpx.Response:
623
+ """Record a request and return a live-shaped response."""
624
+ requests.append(request)
625
+ return httpx.Response(
626
+ status_code=201 if request.method == "POST" else 200,
627
+ json={
628
+ "id": "template-1",
629
+ "name": "Backend interview",
630
+ "questions": [2687118],
631
+ },
632
+ )
633
+
634
+ with respx.mock(assert_all_called=True) as router:
635
+ router.post(
636
+ url=(
637
+ "https://www.hackerrank.com/x/api/v3/interview_templates"
638
+ ),
639
+ ).mock(side_effect=template_response)
640
+ router.put(
641
+ url=(
642
+ "https://www.hackerrank.com/x/api/v3/"
643
+ "interview_templates/template-1"
644
+ ),
645
+ ).mock(side_effect=template_response)
646
+ async with AsyncHackerRank(api_key="test-key") as client:
647
+ created = await client.interview_templates.create(
648
+ name="Backend interview",
649
+ role_id="backend",
650
+ team_share=2,
651
+ question_ids=[2687118, 2687119],
652
+ )
653
+ updated = await client.interview_templates.update(
654
+ template_id=created.id,
655
+ name="Senior backend interview",
656
+ role_id="senior-backend",
657
+ team_share=3,
658
+ scorecard_id=98765,
659
+ )
660
+
661
+ create_request = requests[0]
662
+ assert json.loads(s=create_request.content) == {
663
+ "name": "Backend interview",
664
+ "role_id": "backend",
665
+ "team_share": 2,
666
+ "question_ids": [2687118, 2687119],
667
+ }
668
+ assert created.id == "template-1"
669
+ assert created.questions == [2687118]
670
+
671
+ update_request = requests[1]
672
+ assert json.loads(s=update_request.content) == {
673
+ "name": "Senior backend interview",
674
+ "role_id": "senior-backend",
675
+ "team_share": 3,
676
+ "scorecard_id": 98765,
677
+ }
678
+ assert updated.id == "template-1"
679
+ assert updated.questions == [2687118]
545
680
 
546
681
  @staticmethod
547
682
  @pytest.mark.asyncio
@@ -203,9 +203,14 @@ class TestFromDict:
203
203
  def test_interview_template_from_dict() -> None:
204
204
  """``InterviewTemplate.from_dict`` populates the dataclass."""
205
205
  template = InterviewTemplate.from_dict(
206
- data={"id": 1, "name": "Standard"},
206
+ data={
207
+ "id": "template-1",
208
+ "name": "Standard",
209
+ "questions": [2687118, "2687119"],
210
+ },
207
211
  )
208
- assert template.id == 1
212
+ assert template.id == "template-1"
213
+ assert template.questions == [2687118, 2687119]
209
214
 
210
215
  @staticmethod
211
216
  def test_interview_transcript_from_dict() -> None:
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2026.7.19",
3
- "distance": 0,
4
- "node": "g505c1e5827e6e5c65c67ab5d0742d2a51f668c7c",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-07-19"
8
- }
File without changes
File without changes