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.
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/autofix.yml +1 -1
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/ci.yml +2 -2
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/lint.yml +1 -1
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/publish-site.yml +1 -1
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/release.yml +4 -4
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.vale.ini +2 -1
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/CHANGELOG.rst +25 -26
- hackerrank-2026.9.16/PKG-INFO +100 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/README.rst +11 -12
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/conftest.py +58 -44
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/contributing.rst +5 -9
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/index.rst +11 -12
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/openapi-spec.rst +1 -2
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/unreleased.rst +2 -3
- hackerrank-2026.9.16/openapi.json +1 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/prek.toml +55 -40
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/pyproject.toml +45 -24
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/spelling_private_dict.txt +1 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/_dict_types.py +28 -17
- hackerrank-2026.9.16/src/hackerrank/_responses.py +106 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/_retries.py +42 -25
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/async_client.py +296 -100
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/client.py +315 -119
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/exceptions.py +3 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/transports.py +21 -30
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/types.py +19 -10
- hackerrank-2026.9.16/src/hackerrank.egg-info/PKG-INFO +100 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/SOURCES.txt +3 -0
- hackerrank-2026.9.16/src/hackerrank.egg-info/requires.txt +3 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/scm_file_list.json +3 -0
- hackerrank-2026.9.16/src/hackerrank.egg-info/scm_version.json +8 -0
- hackerrank-2026.9.16/styles/snapper/LongProseLine.yml +11 -0
- hackerrank-2026.9.16/styles/snapper/SemanticLineBreaks.yml +11 -0
- hackerrank-2026.9.16/tests/conftest.py +252 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/openapi_helpers.py +17 -27
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_async_client.py +22 -14
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_client.py +111 -43
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_coverage_gaps.py +98 -112
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_documented_request_params.py +30 -62
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_endpoints.py +199 -94
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_newsfragments.py +3 -3
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_openapi.py +53 -14
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_reject_bare_string_lists.py +3 -3
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_required_update_bodies.py +7 -7
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_response_shapes.py +14 -14
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_retries.py +44 -34
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_retry_safety_docs.py +5 -3
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_types.py +19 -3
- hackerrank-2026.9.7/PKG-INFO +0 -152
- hackerrank-2026.9.7/openapi.json +0 -1
- hackerrank-2026.9.7/src/hackerrank.egg-info/PKG-INFO +0 -152
- hackerrank-2026.9.7/src/hackerrank.egg-info/requires.txt +0 -56
- hackerrank-2026.9.7/src/hackerrank.egg-info/scm_version.json +0 -8
- hackerrank-2026.9.7/tests/conftest.py +0 -417
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.git_archival.txt +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.gitattributes +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/dependabot.yml +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.github/workflows/dependabot-merge.yml +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.gitignore +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/.prettierrc +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/LICENSE +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/__init__.py +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/api-reference.rst +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/changelog.rst +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/conf.py +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/source/release-process.rst +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/docs/towncrier_template.rst.jinja +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/newsfragments/.gitkeep +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/setup.cfg +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/__init__.py +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank/py.typed +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/dependency_links.txt +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/not-zip-safe +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/src/hackerrank.egg-info/top_level.txt +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/__init__.py +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/tests/test_candidate_search.py +0 -0
- {hackerrank-2026.9.7 → hackerrank-2026.9.16}/zizmor.yml +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@
|
|
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 --
|
|
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
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
fetch-depth: 0
|
|
34
34
|
|
|
35
35
|
- name: Install uv
|
|
36
|
-
uses: astral-sh/setup-uv@
|
|
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 --
|
|
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 --
|
|
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 --
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
45
|
-
HTTPX2 transport explicitly and pass it to the client.
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
HTTPX2 only requires selecting the new transport
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
35
|
-
"""Return ``value`` as a
|
|
36
|
-
|
|
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(
|
|
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
|
|
95
|
-
props =
|
|
100
|
+
result = dict(schema)
|
|
101
|
+
props = _as_json_object(value=result.get("properties"))
|
|
96
102
|
if props is not None:
|
|
97
|
-
required_names: list[
|
|
98
|
-
existing_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,
|
|
109
|
+
fixed_props: dict[str, _JSONValue] = {}
|
|
104
110
|
for prop_name, prop_schema_raw in props.items():
|
|
105
|
-
prop_schema =
|
|
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 =
|
|
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(
|
|
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
|
|
131
|
-
params =
|
|
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[
|
|
135
|
-
body_param: dict[str,
|
|
142
|
+
kept: list[_JSONValue] = []
|
|
143
|
+
body_param: dict[str, _JSONValue] | None = None
|
|
136
144
|
for param_raw in params:
|
|
137
|
-
param =
|
|
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:
|
|
146
|
-
schema_dict =
|
|
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(
|
|
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
|
|
161
|
-
raw_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,
|
|
175
|
+
cleaned_paths: dict[str, dict[str, _JSONValue]] = {}
|
|
166
176
|
for raw_key, raw_value_obj in raw_paths.items():
|
|
167
|
-
raw_value =
|
|
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
|
|
181
|
+
merged = dict(cleaned_paths.get(cleaned, {}))
|
|
172
182
|
for op_key, op_val_obj in raw_value.items():
|
|
173
|
-
op_val =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 --
|
|
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
|
|
30
|
+
_ = sys.stdout.write(interview.url)
|
|
32
31
|
|
|
33
32
|
HTTPX2 transports
|
|
34
33
|
-----------------
|
|
35
34
|
|
|
36
|
-
HTTPX remains the default client family.
|
|
37
|
-
HTTPX2 transport explicitly and pass it to the client.
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
HTTPX2 only requires selecting the new transport
|
|
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
|
-
|
|
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::
|