hackerrank 2026.9.7__tar.gz → 2026.9.16__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 (77) hide show
  1. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/autofix.yml +1 -1
  2. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/ci.yml +2 -2
  3. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/lint.yml +1 -1
  4. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/publish-site.yml +1 -1
  5. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/release.yml +4 -4
  6. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.vale.ini +2 -1
  7. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/CHANGELOG.rst +25 -26
  8. hackerrank-2026.9.16/PKG-INFO +100 -0
  9. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/README.rst +11 -12
  10. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/conftest.py +58 -44
  11. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/contributing.rst +5 -9
  12. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/index.rst +11 -12
  13. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/openapi-spec.rst +1 -2
  14. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/unreleased.rst +2 -3
  15. hackerrank-2026.9.16/openapi.json +1 -0
  16. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/prek.toml +55 -40
  17. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/pyproject.toml +45 -24
  18. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/spelling_private_dict.txt +1 -0
  19. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/_dict_types.py +28 -17
  20. hackerrank-2026.9.16/src/hackerrank/_responses.py +106 -0
  21. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/_retries.py +42 -25
  22. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/async_client.py +296 -100
  23. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/client.py +315 -119
  24. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/exceptions.py +3 -0
  25. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/transports.py +21 -30
  26. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/types.py +19 -10
  27. hackerrank-2026.9.16/src/hackerrank.egg-info/PKG-INFO +100 -0
  28. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/SOURCES.txt +3 -0
  29. hackerrank-2026.9.16/src/hackerrank.egg-info/requires.txt +3 -0
  30. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/scm_file_list.json +3 -0
  31. hackerrank-2026.9.16/src/hackerrank.egg-info/scm_version.json +8 -0
  32. hackerrank-2026.9.16/styles/snapper/LongProseLine.yml +11 -0
  33. hackerrank-2026.9.16/styles/snapper/SemanticLineBreaks.yml +11 -0
  34. hackerrank-2026.9.16/tests/conftest.py +252 -0
  35. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/openapi_helpers.py +17 -27
  36. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_async_client.py +22 -14
  37. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_client.py +111 -43
  38. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_coverage_gaps.py +98 -112
  39. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_documented_request_params.py +30 -62
  40. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_endpoints.py +199 -94
  41. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_newsfragments.py +3 -3
  42. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_openapi.py +53 -14
  43. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_reject_bare_string_lists.py +3 -3
  44. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_required_update_bodies.py +7 -7
  45. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_response_shapes.py +14 -14
  46. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_retries.py +44 -34
  47. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_retry_safety_docs.py +5 -3
  48. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_types.py +19 -3
  49. hackerrank-2026.9.7/PKG-INFO +0 -152
  50. hackerrank-2026.9.7/openapi.json +0 -1
  51. hackerrank-2026.9.7/src/hackerrank.egg-info/PKG-INFO +0 -152
  52. hackerrank-2026.9.7/src/hackerrank.egg-info/requires.txt +0 -56
  53. hackerrank-2026.9.7/src/hackerrank.egg-info/scm_version.json +0 -8
  54. hackerrank-2026.9.7/tests/conftest.py +0 -417
  55. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.git_archival.txt +0 -0
  56. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.gitattributes +0 -0
  57. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/dependabot.yml +0 -0
  58. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/dependabot-merge.yml +0 -0
  59. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.gitignore +0 -0
  60. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.prettierrc +0 -0
  61. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/LICENSE +0 -0
  62. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/__init__.py +0 -0
  63. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/api-reference.rst +0 -0
  64. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/changelog.rst +0 -0
  65. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/conf.py +0 -0
  66. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/release-process.rst +0 -0
  67. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/towncrier_template.rst.jinja +0 -0
  68. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/newsfragments/.gitkeep +0 -0
  69. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/setup.cfg +0 -0
  70. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/__init__.py +0 -0
  71. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/py.typed +0 -0
  72. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/dependency_links.txt +0 -0
  73. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/not-zip-safe +0 -0
  74. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/top_level.txt +0 -0
  75. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/__init__.py +0 -0
  76. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_candidate_search.py +0 -0
  77. {hackerrank-2026.9.7 → hackerrank-2026.9.16}/zizmor.yml +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
  persist-credentials: false
22
22
 
23
23
  - name: Install uv
24
- uses: astral-sh/setup-uv@v9.0.0
24
+ uses: astral-sh/setup-uv@v10.1.0
25
25
 
26
26
  - name: Run fixers
27
27
  uses: j178/prek-action@v3.0.0
@@ -28,7 +28,7 @@ jobs:
28
28
  persist-credentials: false
29
29
 
30
30
  - name: Install uv
31
- uses: astral-sh/setup-uv@v7
31
+ uses: astral-sh/setup-uv@v10.1.0
32
32
  with:
33
33
  enable-cache: true
34
34
  cache-dependency-glob: '**/pyproject.toml'
@@ -37,7 +37,7 @@ jobs:
37
37
  run: |
38
38
  # We run tests against "." and not the tests directory as we test the README
39
39
  # and documentation.
40
- uv run --extra=dev --python=${{ matrix.python-version }} pytest -s -vvv --cov-fail-under 100 --cov=src/ --cov=tests/ .
40
+ uv run --group=dev --python=${{ matrix.python-version }} pytest -s -vvv --cov-fail-under 100 --cov=src/ --cov=tests/ .
41
41
 
42
42
  completion-ci:
43
43
  needs: build
@@ -29,7 +29,7 @@ jobs:
29
29
  persist-credentials: false
30
30
 
31
31
  - name: Install uv
32
- uses: astral-sh/setup-uv@v7
32
+ uses: astral-sh/setup-uv@v10.1.0
33
33
  with:
34
34
  enable-cache: true
35
35
  cache-dependency-glob: '**/pyproject.toml'
@@ -21,7 +21,7 @@ jobs:
21
21
  uses: sphinx-notes/pages@v3
22
22
  with:
23
23
  documentation_path: docs/source
24
- pyproject_extras: dev
24
+ pyproject_group: dev
25
25
  python_version: '3.13'
26
26
  sphinx_build_options: -W
27
27
  cache: true
@@ -33,7 +33,7 @@ jobs:
33
33
  fetch-depth: 0
34
34
 
35
35
  - name: Install uv
36
- uses: astral-sh/setup-uv@v7
36
+ uses: astral-sh/setup-uv@v10.1.0
37
37
  with:
38
38
  enable-cache: true
39
39
  cache-dependency-glob: '**/pyproject.toml'
@@ -53,7 +53,7 @@ jobs:
53
53
  - name: Generate the GitHub release notes
54
54
  env:
55
55
  RELEASE: ${{ steps.calver.outputs.release }}
56
- run: uv run --extra=release towncrier build --draft --version "$RELEASE" >
56
+ run: uv run --group=release towncrier build --draft --version "$RELEASE" >
57
57
  release-notes.md
58
58
 
59
59
  # Assemble the same fragments into CHANGELOG.rst under a new
@@ -61,7 +61,7 @@ jobs:
61
61
  - name: Update the changelog
62
62
  env:
63
63
  RELEASE: ${{ steps.calver.outputs.release }}
64
- run: uv run --extra=release towncrier build --yes --version "$RELEASE"
64
+ run: uv run --group=release towncrier build --yes --version "$RELEASE"
65
65
 
66
66
  - uses: stefanzweifel/git-auto-commit-action@v7
67
67
  id: commit
@@ -95,7 +95,7 @@ jobs:
95
95
  git fetch --tags
96
96
  git checkout "$NEW_TAG"
97
97
  uv build --sdist --wheel --out-dir dist/
98
- uv run --extra=release check-wheel-contents dist/*.whl
98
+ uv run --group=release check-wheel-contents dist/*.whl
99
99
 
100
100
  - name: Publish distribution to PyPI
101
101
  # We use PyPI trusted publishing rather than a PyPI API token.
@@ -4,7 +4,8 @@ MinAlertLevel = error
4
4
  Packages = https://github.com/tbhb/vale-ai-tells/releases/download/v1.29.0/ai-tells.zip
5
5
 
6
6
  [*.{rst,md}]
7
- BasedOnStyles = ai-tells
7
+ BasedOnStyles = ai-tells, snapper
8
+ snapper.SemanticLineBreaks = error
8
9
 
9
10
  # These rules misclassify established technical, example, or release-note prose
10
11
  # in this repository. All other ai-tells rules remain enforced.
@@ -3,16 +3,27 @@ Changelog
3
3
 
4
4
  .. towncrier release notes start
5
5
 
6
+ 2026.09.16
7
+ ----------
8
+
9
+ - Validate interview API response shapes before constructing client models.
10
+
11
+ - Type shared page fixtures and captured request bodies as JSON values.
12
+
13
+ - Restored the missing ``beartype`` import so the client modules load successfully.
14
+
15
+ - Add ``interview_templates.add_questions`` and ``interview_templates.remove_question`` to both clients, matching the newly documented HackerRank endpoints, and refresh ``openapi.json`` from the served spec.
16
+
6
17
  2026.09.07
7
18
  ----------
8
19
 
9
- - Add synchronous and asynchronous HTTPX2 transports while retaining HTTPX as
10
- the default client family.
20
+ - Add synchronous and asynchronous HTTPX2 transports while retaining HTTPX as the default client family.
11
21
 
12
22
  2026.08.28
13
23
  ----------
14
24
 
15
- - Every method which changes state now documents whether it is safe to retry, and why. ``tests.archive`` is now treated as safe to repeat, so it is retried when ``retries`` is set.
25
+ - Every method which changes state now documents whether it is safe to retry, and why.
26
+ ``tests.archive`` is now treated as safe to repeat, so it is retried when ``retries`` is set.
16
27
 
17
28
  2026.08.27
18
29
  ----------
@@ -24,31 +35,20 @@ Changelog
24
35
  2026.08.22
25
36
  ----------
26
37
 
27
- - Accept documented object-form interviewers and candidate_details, preserve
28
- interview schedule / live fields, and return invite and update response
29
- bodies from sync and async clients.
38
+ - Accept documented object-form interviewers and candidate_details, preserve interview schedule / live fields, and return invite and update response bodies from sync and async clients.
30
39
 
31
- - Add ``client.candidates.search`` (sync and async) for the global
32
- candidate-search endpoint, with typed attempt and candidate results.
40
+ - Add ``client.candidates.search`` (sync and async) for the global candidate-search endpoint, with typed attempt and candidate results.
33
41
 
34
- - Expose documented list filters and body fields for interviews,
35
- questions, templates, and candidate invites on sync and async
36
- clients.
42
+ - Expose documented list filters and body fields for interviews, questions, templates, and candidate invites on sync and async clients.
37
43
 
38
- - Reject bare ``str`` values for list-like API fields (``interviewers``,
39
- ``languages``, ``tags``, and related parameters) so they are no longer
40
- split into character arrays in request bodies.
44
+ - Reject bare ``str`` values for list-like API fields (``interviewers``, ``languages``, ``tags``, and related parameters) so they are no longer split into character arrays in request bodies.
41
45
 
42
- - Refresh ``openapi.json`` from the live HackerRank schema and add a
43
- semantic drift check that ignores dynamic example timestamps.
46
+ - Refresh ``openapi.json`` from the live HackerRank schema and add a semantic drift check that ignores dynamic example timestamps.
44
47
 
45
- - Add ``client.interview_templates.explicit_sharing_roles`` with
46
- ``update_access`` and ``remove_access`` for sharing an interview template with
47
- users, teams or the whole company.
48
+ - Add ``client.interview_templates.explicit_sharing_roles`` with ``update_access`` and ``remove_access`` for sharing an interview template with users, teams or the whole company.
48
49
 
49
- Remove the ``team_share`` argument from interview-template ``create`` and
50
- ``update``. HackerRank ignores the parameter and has removed it from the API
51
- documentation; use ``explicit_sharing_roles`` instead.
50
+ Remove the ``team_share`` argument from interview-template ``create`` and ``update``.
51
+ HackerRank ignores the parameter and has removed it from the API documentation; use ``explicit_sharing_roles`` instead.
52
52
 
53
53
  - SCIM user and group PATCH methods now parse the documented message acknowledgement instead of treating the response as a full resource.
54
54
 
@@ -72,14 +72,13 @@ No significant changes.
72
72
  2026.07.30
73
73
  ----------
74
74
 
75
- - Align interview-template create and update requests with the current API,
76
- return updated templates, and parse live string template IDs and integer
77
- question IDs.
75
+ - Align interview-template create and update requests with the current API, return updated templates, and parse live string template IDs and integer question IDs.
78
76
 
79
77
  2026.07.19
80
78
  ----------
81
79
 
82
- - SCIM v2 requests are now sent to ``https://services.hackerrank.com/scim/v2`` instead of the v3 API host, so ``client.scim`` works out of the box. A new ``scim_base_url`` argument on ``HackerRank`` and ``AsyncHackerRank`` allows overriding it.
80
+ - SCIM v2 requests are now sent to ``https://services.hackerrank.com/scim/v2`` instead of the v3 API host, so ``client.scim`` works out of the box.
81
+ A new ``scim_base_url`` argument on ``HackerRank`` and ``AsyncHackerRank`` allows overriding it.
83
82
 
84
83
  - Bring the async ``AsyncHackerRank`` client to full parity with the synchronous ``HackerRank`` client, adding all missing operations and aligning method shapes (``ats.codepair``/``ats.codescreen``, ``teams.memberships`` and ``scim.users``/``scim.groups`` sub-namespaces).
85
84
 
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: hackerrank
3
+ Version: 2026.9.16
4
+ Summary: Interact with the HackerRank for Work API.
5
+ Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://adamtheturtle.github.io/hackerrank/
8
+ Project-URL: Source, https://github.com/adamtheturtle/hackerrank
9
+ Keywords: client,hackerrank,interview
10
+ Classifier: Development Status :: 1 - Planning
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Operating System :: POSIX
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.13
18
+ Description-Content-Type: text/x-rst
19
+ License-File: LICENSE
20
+ Requires-Dist: beartype>=0.22.9
21
+ Requires-Dist: httpx>=0.28.0
22
+ Requires-Dist: httpx2>=2.12.0
23
+ Dynamic: license-file
24
+
25
+ |Build Status| |PyPI|
26
+
27
+ hackerrank
28
+ ==========
29
+
30
+ Python library for the `HackerRank for Work API`_.
31
+
32
+ .. _HackerRank for Work API: https://www.hackerrank.com/work/apidocs
33
+
34
+ Installation
35
+ ------------
36
+
37
+ .. code-block:: shell
38
+
39
+ pip install hackerrank
40
+
41
+ This is tested on Python |minimum-python-version|\+.
42
+
43
+ Getting Started
44
+ ---------------
45
+
46
+ Generate an API token from the `HackerRank for Work tokens page`_ and pass it as ``api_key``:
47
+
48
+ .. code-block:: python
49
+
50
+ """Example usage."""
51
+
52
+ import sys
53
+
54
+ from hackerrank.client import HackerRank
55
+
56
+ client = HackerRank(api_key="your-api-key")
57
+ for test in client.tests.list().data:
58
+ _ = sys.stdout.write(test.name)
59
+ interview = client.interviews.create(title="My Interview")
60
+ _ = sys.stdout.write(interview.url)
61
+
62
+ .. _HackerRank for Work tokens page: https://www.hackerrank.com/work/settings/token
63
+
64
+ HTTPX2 transports
65
+ -----------------
66
+
67
+ HTTPX remains the default client family.
68
+ To opt in to HTTPX2, construct the HTTPX2 transport explicitly and pass it to the client.
69
+ The standard ``hackerrank`` installation includes both client families.
70
+
71
+ .. code-block:: python
72
+
73
+ """Configure HackerRank to use HTTPX2."""
74
+
75
+ import httpx2
76
+
77
+ from hackerrank.client import HackerRank
78
+ from hackerrank.transports import HTTPX2Transport
79
+
80
+ transport = HTTPX2Transport(timeout=httpx2.Timeout(timeout=10))
81
+ with HackerRank(api_key="your-api-key", transport=transport) as client:
82
+ tests = client.tests.list()
83
+
84
+ Use ``AsyncHTTPX2Transport`` with ``AsyncHackerRank`` for asynchronous calls.
85
+ The ``timeout`` argument accepts an HTTPX2 timeout object or a number of seconds.
86
+ Pass only HTTPX2 timeout objects across the package boundary.
87
+ Existing HTTPX users do not need to change anything.
88
+ Migrating to HTTPX2 only requires selecting the new transport.
89
+ HackerRank client methods and returned models are unchanged.
90
+
91
+ Full Documentation
92
+ ------------------
93
+
94
+ See the `full documentation <https://adamtheturtle.github.io/hackerrank/>`__.
95
+
96
+ .. |Build Status| image:: https://github.com/adamtheturtle/hackerrank/actions/workflows/ci.yml/badge.svg?branch=main
97
+ :target: https://github.com/adamtheturtle/hackerrank/actions
98
+ .. |PyPI| image:: https://badge.fury.io/py/hackerrank.svg
99
+ :target: https://badge.fury.io/py/hackerrank
100
+ .. |minimum-python-version| replace:: 3.13
@@ -19,8 +19,7 @@ This is tested on Python |minimum-python-version|\+.
19
19
  Getting Started
20
20
  ---------------
21
21
 
22
- Generate an API token from the `HackerRank for Work tokens page`_ and
23
- pass it as ``api_key``:
22
+ Generate an API token from the `HackerRank for Work tokens page`_ and pass it as ``api_key``:
24
23
 
25
24
  .. code-block:: python
26
25
 
@@ -32,18 +31,18 @@ pass it as ``api_key``:
32
31
 
33
32
  client = HackerRank(api_key="your-api-key")
34
33
  for test in client.tests.list().data:
35
- sys.stdout.write(test.name)
34
+ _ = sys.stdout.write(test.name)
36
35
  interview = client.interviews.create(title="My Interview")
37
- sys.stdout.write(interview.url or "")
36
+ _ = sys.stdout.write(interview.url)
38
37
 
39
38
  .. _HackerRank for Work tokens page: https://www.hackerrank.com/work/settings/token
40
39
 
41
40
  HTTPX2 transports
42
41
  -----------------
43
42
 
44
- HTTPX remains the default client family. To opt in to HTTPX2, construct the
45
- HTTPX2 transport explicitly and pass it to the client. The standard
46
- ``hackerrank`` installation includes both client families.
43
+ HTTPX remains the default client family.
44
+ To opt in to HTTPX2, construct the HTTPX2 transport explicitly and pass it to the client.
45
+ The standard ``hackerrank`` installation includes both client families.
47
46
 
48
47
  .. code-block:: python
49
48
 
@@ -59,11 +58,11 @@ HTTPX2 transport explicitly and pass it to the client. The standard
59
58
  tests = client.tests.list()
60
59
 
61
60
  Use ``AsyncHTTPX2Transport`` with ``AsyncHackerRank`` for asynchronous calls.
62
- The ``timeout`` argument accepts an HTTPX2 timeout object or a number of
63
- seconds; do not pass an equivalent ``httpx`` object across the package
64
- boundary. Existing HTTPX users do not need to change anything. Migrating to
65
- HTTPX2 only requires selecting the new transport; HackerRank client methods
66
- and returned models are unchanged.
61
+ The ``timeout`` argument accepts an HTTPX2 timeout object or a number of seconds.
62
+ Pass only HTTPX2 timeout objects across the package boundary.
63
+ Existing HTTPX users do not need to change anything.
64
+ Migrating to HTTPX2 only requires selecting the new transport.
65
+ HackerRank client methods and returned models are unchanged.
67
66
 
68
67
  Full Documentation
69
68
  ------------------
@@ -2,12 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING
7
6
 
8
7
  import pytest
9
8
  import respx
10
9
  from openapi_mock import add_openapi_to_respx
10
+ from pydantic import TypeAdapter, ValidationError
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from collections.abc import Generator
@@ -17,27 +17,31 @@ _HTTP_METHODS = frozenset(
17
17
  {"get", "post", "put", "delete", "patch", "head", "options", "trace"},
18
18
  )
19
19
 
20
+ type _JSONValue = (
21
+ bool | int | float | str | list[_JSONValue] | dict[str, _JSONValue] | None
22
+ )
20
23
 
21
- def _as_str_keyed_dict(*, value: object) -> dict[str, Any] | None:
22
- """Return ``value`` as a ``str``-keyed dict, or ``None``.
23
24
 
24
- Uses a JSON round-trip so static checkers see concrete ``Any``
25
- values rather than unknown dict items from ``isinstance`` narrowing.
26
- """
27
- if not isinstance(value, dict):
25
+ def _as_json_object(*, value: object) -> dict[str, _JSONValue] | None:
26
+ """Return ``value`` as a JSON object, or ``None``."""
27
+ try:
28
+ return TypeAdapter(type=dict[str, _JSONValue]).validate_python(
29
+ value,
30
+ strict=True,
31
+ )
32
+ except ValidationError:
28
33
  return None
29
- decoded: Any = json.loads(s=json.dumps(obj=value))
30
- typed: dict[str, Any] = decoded
31
- return typed
32
34
 
33
35
 
34
- def _as_object_list(*, value: object) -> list[object] | None:
35
- """Return ``value`` as a list of objects, or ``None``."""
36
- if not isinstance(value, list):
36
+ def _as_json_list(*, value: object) -> list[_JSONValue] | None:
37
+ """Return ``value`` as a JSON array, or ``None``."""
38
+ try:
39
+ return TypeAdapter(type=list[_JSONValue]).validate_python(
40
+ value,
41
+ strict=True,
42
+ )
43
+ except ValidationError:
37
44
  return None
38
- decoded: Any = json.loads(s=json.dumps(obj=value))
39
- typed: list[object] = decoded
40
- return typed
41
45
 
42
46
 
43
47
  def pytest_addoption(parser: pytest.Parser) -> None:
@@ -87,22 +91,24 @@ def pytest_collection_modifyitems(
87
91
  item.add_marker(marker=skip_network)
88
92
 
89
93
 
90
- def _fix_schema_required(*, schema: dict[str, Any]) -> dict[str, Any]:
94
+ def _fix_schema_required(
95
+ *, schema: dict[str, _JSONValue]
96
+ ) -> dict[str, _JSONValue]:
91
97
  """Normalize Swagger-style ``required`` flags for OpenAPI 3
92
98
  schemas.
93
99
  """
94
- result: dict[str, Any] = dict(schema)
95
- props = _as_str_keyed_dict(value=result.get("properties"))
100
+ result = dict(schema)
101
+ props = _as_json_object(value=result.get("properties"))
96
102
  if props is not None:
97
- required_names: list[str] = []
98
- existing_required = _as_object_list(value=result.get("required"))
103
+ required_names: list[_JSONValue] = []
104
+ existing_required = _as_json_list(value=result.get("required"))
99
105
  if existing_required is not None:
100
106
  required_names.extend(
101
107
  name for name in existing_required if isinstance(name, str)
102
108
  )
103
- fixed_props: dict[str, Any] = {}
109
+ fixed_props: dict[str, _JSONValue] = {}
104
110
  for prop_name, prop_schema_raw in props.items():
105
- prop_schema = _as_str_keyed_dict(value=prop_schema_raw)
111
+ prop_schema = _as_json_object(value=prop_schema_raw)
106
112
  if prop_schema is None:
107
113
  continue
108
114
  fixed_prop = _fix_schema_required(schema=prop_schema)
@@ -113,28 +119,30 @@ def _fix_schema_required(*, schema: dict[str, Any]) -> dict[str, Any]:
113
119
  required_names.append(prop_name)
114
120
  fixed_props[prop_name] = fixed_prop
115
121
  result["properties"] = fixed_props
116
- if required_names:
122
+ if bool(required_names):
117
123
  result["required"] = required_names
118
124
  elif "required" in result and not isinstance(result["required"], list):
119
- result.pop("required", None)
120
- items = _as_str_keyed_dict(value=result.get("items"))
125
+ _ = result.pop("required", None)
126
+ items = _as_json_object(value=result.get("items"))
121
127
  if items is not None:
122
128
  result["items"] = _fix_schema_required(schema=items)
123
129
  return result
124
130
 
125
131
 
126
- def _migrate_body_parameter(*, operation: dict[str, Any]) -> dict[str, Any]:
132
+ def _migrate_body_parameter(
133
+ *, operation: dict[str, _JSONValue]
134
+ ) -> dict[str, _JSONValue]:
127
135
  """Convert Swagger 2 ``in: body`` parameters to OpenAPI 3
128
136
  requestBody.
129
137
  """
130
- result: dict[str, Any] = dict(operation)
131
- params = _as_object_list(value=result.get("parameters"))
138
+ result = dict(operation)
139
+ params = _as_json_list(value=result.get("parameters"))
132
140
  if params is None:
133
141
  return result
134
- kept: list[object] = []
135
- body_param: dict[str, Any] | None = None
142
+ kept: list[_JSONValue] = []
143
+ body_param: dict[str, _JSONValue] | None = None
136
144
  for param_raw in params:
137
- param = _as_str_keyed_dict(value=param_raw)
145
+ param = _as_json_object(value=param_raw)
138
146
  if param is not None and param.get("in") == "body":
139
147
  body_param = param
140
148
  else:
@@ -142,8 +150,8 @@ def _migrate_body_parameter(*, operation: dict[str, Any]) -> dict[str, Any]:
142
150
  result["parameters"] = kept
143
151
  if body_param is not None and "requestBody" not in result:
144
152
  schema_raw = body_param.get("schema", {})
145
- schema: object = schema_raw
146
- schema_dict = _as_str_keyed_dict(value=schema_raw)
153
+ schema: _JSONValue = schema_raw
154
+ schema_dict = _as_json_object(value=schema_raw)
147
155
  if schema_dict is not None:
148
156
  schema = _fix_schema_required(schema=schema_dict)
149
157
  result["requestBody"] = {
@@ -153,30 +161,33 @@ def _migrate_body_parameter(*, operation: dict[str, Any]) -> dict[str, Any]:
153
161
  return result
154
162
 
155
163
 
156
- def _prepare_openapi_spec(*, spec: dict[str, object]) -> dict[str, object]:
164
+ def _prepare_openapi_spec(
165
+ *, spec: dict[str, _JSONValue]
166
+ ) -> dict[str, _JSONValue]:
157
167
  """Normalize the HackerRank OpenAPI document for mock route
158
168
  registration.
159
169
  """
160
- prepared: dict[str, object] = dict(spec)
161
- raw_paths = _as_str_keyed_dict(value=prepared.get("paths", {}))
170
+ prepared = dict(spec)
171
+ raw_paths = _as_json_object(value=prepared.get("paths", {}))
162
172
  if raw_paths is None:
163
173
  return prepared
164
174
 
165
- cleaned_paths: dict[str, dict[str, object]] = {}
175
+ cleaned_paths: dict[str, dict[str, _JSONValue]] = {}
166
176
  for raw_key, raw_value_obj in raw_paths.items():
167
- raw_value = _as_str_keyed_dict(value=raw_value_obj)
177
+ raw_value = _as_json_object(value=raw_value_obj)
168
178
  if raw_value is None:
169
179
  continue
170
180
  cleaned = raw_key.split(sep="?", maxsplit=1)[0]
171
- merged: dict[str, object] = dict(cleaned_paths.get(cleaned, {}))
181
+ merged = dict(cleaned_paths.get(cleaned, {}))
172
182
  for op_key, op_val_obj in raw_value.items():
173
- op_val = _as_str_keyed_dict(value=op_val_obj)
183
+ op_val = _as_json_object(value=op_val_obj)
174
184
  if op_key in _HTTP_METHODS and op_val is not None:
175
185
  merged[op_key] = _migrate_body_parameter(operation=op_val)
176
186
  else:
177
187
  merged[op_key] = op_val_obj
178
188
  cleaned_paths[cleaned] = merged
179
- prepared["paths"] = cleaned_paths
189
+ json_paths = dict[str, _JSONValue](cleaned_paths)
190
+ prepared["paths"] = json_paths
180
191
  return prepared
181
192
 
182
193
 
@@ -195,7 +206,10 @@ def fixture_mock_hackerrank_api(
195
206
  """
196
207
  openapi_spec_path = request.config.rootpath / "openapi.json"
197
208
  spec_text = openapi_spec_path.read_text(encoding="utf-8")
198
- openapi_spec: dict[str, object] = json.loads(s=spec_text)
209
+ openapi_spec = TypeAdapter(type=dict[str, _JSONValue]).validate_json(
210
+ spec_text,
211
+ strict=True,
212
+ )
199
213
  openapi_spec = _prepare_openapi_spec(spec=openapi_spec)
200
214
  with respx.mock(
201
215
  base_url=_BASE_URL,
@@ -12,7 +12,7 @@ Install Python dependencies in a virtual environment.
12
12
 
13
13
  .. code-block:: console
14
14
 
15
- $ pip install --editable '.[dev]'
15
+ $ pip install --editable . --group dev
16
16
 
17
17
  Spell checking requires ``enchant``.
18
18
  This can be installed on macOS, for example, with `Homebrew`_:
@@ -58,14 +58,10 @@ Run ``pytest``:
58
58
  Changelog entries
59
59
  -----------------
60
60
 
61
- Add one news fragment per user-facing change, as
62
- :file:`newsfragments/<issue-number>.change.rst`, containing a single
63
- sentence in the past or present tense.
61
+ Add one news fragment per user-facing change, as :file:`newsfragments/<issue-number>.change.rst`, containing a single sentence in the past or present tense.
64
62
 
65
- ``towncrier`` collects fragments by that exact filename pattern at
66
- release time and silently ignores anything else, including a fragment
67
- filed in a subdirectory, so a misnamed fragment never reaches the
68
- changelog. A test guards against this.
63
+ ``towncrier`` collects fragments by that exact filename pattern at release time and silently ignores anything else, including a fragment filed in a subdirectory, so a fragment with the wrong name never reaches the changelog.
64
+ A test guards against this.
69
65
 
70
66
  Documentation
71
67
  -------------
@@ -76,7 +72,7 @@ Run the following commands to build and view documentation locally:
76
72
 
77
73
  .. code-block:: console
78
74
 
79
- $ uv run --extra=dev sphinx-build -M html docs/source docs/build -W
75
+ $ uv run --group=dev sphinx-build -M html docs/source docs/build -W
80
76
  $ python -c 'import os, webbrowser; webbrowser.open("file://" + os.path.abspath("docs/build/html/index.html"))'
81
77
 
82
78
  Continuous integration
@@ -13,8 +13,7 @@ This is tested on Python |minimum-python-version|\+.
13
13
  Usage
14
14
  -----
15
15
 
16
- Generate an API token from the `HackerRank for Work tokens page`_ and pass
17
- it to :class:`hackerrank.client.HackerRank`:
16
+ Generate an API token from the `HackerRank for Work tokens page`_ and pass it to :class:`hackerrank.client.HackerRank`:
18
17
 
19
18
  .. code-block:: python
20
19
 
@@ -26,16 +25,16 @@ it to :class:`hackerrank.client.HackerRank`:
26
25
 
27
26
  client = HackerRank(api_key="your-api-key")
28
27
  for test in client.tests.list().data:
29
- sys.stdout.write(test.name)
28
+ _ = sys.stdout.write(test.name)
30
29
  interview = client.interviews.create(title="My Interview")
31
- sys.stdout.write(interview.url or "")
30
+ _ = sys.stdout.write(interview.url)
32
31
 
33
32
  HTTPX2 transports
34
33
  -----------------
35
34
 
36
- HTTPX remains the default client family. To opt in to HTTPX2, construct the
37
- HTTPX2 transport explicitly and pass it to the client. The standard
38
- ``hackerrank`` installation includes both client families.
35
+ HTTPX remains the default client family.
36
+ To opt in to HTTPX2, construct the HTTPX2 transport explicitly and pass it to the client.
37
+ The standard ``hackerrank`` installation includes both client families.
39
38
 
40
39
  .. code-block:: python
41
40
 
@@ -51,11 +50,11 @@ HTTPX2 transport explicitly and pass it to the client. The standard
51
50
  tests = client.tests.list()
52
51
 
53
52
  Use ``AsyncHTTPX2Transport`` with ``AsyncHackerRank`` for asynchronous calls.
54
- The ``timeout`` argument accepts an HTTPX2 timeout object or a number of
55
- seconds; do not pass an equivalent ``httpx`` object across the package
56
- boundary. Existing HTTPX users do not need to change anything. Migrating to
57
- HTTPX2 only requires selecting the new transport; HackerRank client methods
58
- and returned models are unchanged.
53
+ The ``timeout`` argument accepts an HTTPX2 timeout object or a number of seconds.
54
+ Pass only HTTPX2 timeout objects across the package boundary.
55
+ Existing HTTPX users do not need to change anything.
56
+ Migrating to HTTPX2 only requires selecting the new transport.
57
+ HackerRank client methods and returned models are unchanged.
59
58
 
60
59
  See the :doc:`api-reference` for full usage details.
61
60
 
@@ -1,8 +1,7 @@
1
1
  OpenAPI Spec
2
2
  ============
3
3
 
4
- The OpenAPI spec used to generate this library is the Swagger spec served at
5
- ``https://www.hackerrank.com/apidoc``.
4
+ The OpenAPI spec used to generate this library is the Swagger spec served at ``https://www.hackerrank.com/apidoc``.
6
5
  This is the spec that backs the `HackerRank for Work API documentation`_.
7
6
 
8
7
  .. _HackerRank for Work API documentation: https://www.hackerrank.com/work/apidocs
@@ -1,8 +1,7 @@
1
1
  Unreleased changes
2
2
  ==================
3
3
 
4
- Changes that have landed on the main branch but are not yet part of a
5
- tagged release. These entries are assembled into the
6
- :doc:`changelog` when the next release is published.
4
+ Changes that have landed on the main branch but are not yet part of a tagged release.
5
+ These entries are assembled into the :doc:`changelog` when the next release is published.
7
6
 
8
7
  .. towncrier-draft-entries::