hackerrank 2026.8.28__tar.gz → 2026.9.7__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 (70) hide show
  1. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/CHANGELOG.rst +6 -0
  2. {hackerrank-2026.8.28/src/hackerrank.egg-info → hackerrank-2026.9.7}/PKG-INFO +41 -12
  3. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/README.rst +27 -0
  4. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/source/conf.py +2 -0
  5. hackerrank-2026.9.7/docs/source/index.rst +75 -0
  6. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/pyproject.toml +22 -13
  7. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/spelling_private_dict.txt +1 -0
  8. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/async_client.py +7 -2
  9. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/client.py +4 -2
  10. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/transports.py +172 -2
  11. {hackerrank-2026.8.28 → hackerrank-2026.9.7/src/hackerrank.egg-info}/PKG-INFO +41 -12
  12. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank.egg-info/requires.txt +13 -11
  13. hackerrank-2026.9.7/src/hackerrank.egg-info/scm_version.json +8 -0
  14. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_async_client.py +72 -0
  15. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_client.py +65 -0
  16. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_retries.py +47 -0
  17. hackerrank-2026.8.28/docs/source/index.rst +0 -48
  18. hackerrank-2026.8.28/src/hackerrank.egg-info/scm_version.json +0 -8
  19. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.git_archival.txt +0 -0
  20. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.gitattributes +0 -0
  21. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.github/dependabot.yml +0 -0
  22. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.github/workflows/autofix.yml +0 -0
  23. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.github/workflows/ci.yml +0 -0
  24. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.github/workflows/dependabot-merge.yml +0 -0
  25. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.github/workflows/lint.yml +0 -0
  26. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.github/workflows/publish-site.yml +0 -0
  27. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.github/workflows/release.yml +0 -0
  28. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.gitignore +0 -0
  29. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.prettierrc +0 -0
  30. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/.vale.ini +0 -0
  31. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/LICENSE +0 -0
  32. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/conftest.py +0 -0
  33. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/source/__init__.py +0 -0
  34. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/source/api-reference.rst +0 -0
  35. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/source/changelog.rst +0 -0
  36. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/source/contributing.rst +0 -0
  37. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/source/openapi-spec.rst +0 -0
  38. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/source/release-process.rst +0 -0
  39. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/source/unreleased.rst +0 -0
  40. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/docs/towncrier_template.rst.jinja +0 -0
  41. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/newsfragments/.gitkeep +0 -0
  42. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/openapi.json +0 -0
  43. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/prek.toml +0 -0
  44. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/setup.cfg +0 -0
  45. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/__init__.py +0 -0
  46. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/_dict_types.py +0 -0
  47. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/_retries.py +0 -0
  48. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/exceptions.py +0 -0
  49. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/py.typed +0 -0
  50. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank/types.py +0 -0
  51. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank.egg-info/SOURCES.txt +0 -0
  52. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank.egg-info/dependency_links.txt +0 -0
  53. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank.egg-info/not-zip-safe +0 -0
  54. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank.egg-info/scm_file_list.json +0 -0
  55. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/src/hackerrank.egg-info/top_level.txt +0 -0
  56. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/__init__.py +0 -0
  57. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/conftest.py +0 -0
  58. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/openapi_helpers.py +0 -0
  59. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_candidate_search.py +0 -0
  60. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_coverage_gaps.py +0 -0
  61. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_documented_request_params.py +0 -0
  62. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_endpoints.py +0 -0
  63. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_newsfragments.py +0 -0
  64. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_openapi.py +0 -0
  65. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_reject_bare_string_lists.py +0 -0
  66. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_required_update_bodies.py +0 -0
  67. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_response_shapes.py +0 -0
  68. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_retry_safety_docs.py +0 -0
  69. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/tests/test_types.py +0 -0
  70. {hackerrank-2026.8.28 → hackerrank-2026.9.7}/zizmor.yml +0 -0
@@ -3,6 +3,12 @@ Changelog
3
3
 
4
4
  .. towncrier release notes start
5
5
 
6
+ 2026.09.07
7
+ ----------
8
+
9
+ - Add synchronous and asynchronous HTTPX2 transports while retaining HTTPX as
10
+ the default client family.
11
+
6
12
  2026.08.28
7
13
  ----------
8
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hackerrank
3
- Version: 2026.8.28
3
+ Version: 2026.9.7
4
4
  Summary: Interact with the HackerRank for Work API.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -19,26 +19,27 @@ Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
20
  Requires-Dist: beartype>=0.22.9
21
21
  Requires-Dist: httpx>=0.28.0
22
+ Requires-Dist: httpx2>=2.12.0
22
23
  Provides-Extra: dev
23
24
  Requires-Dist: actionlint-py==1.7.12.24; extra == "dev"
24
25
  Requires-Dist: beartype==0.22.9; extra == "dev"
25
26
  Requires-Dist: check-manifest==0.51; extra == "dev"
26
27
  Requires-Dist: deptry==0.25.1; extra == "dev"
27
28
  Requires-Dist: doc8==2.0.0; extra == "dev"
28
- Requires-Dist: doccmd==2026.8.16; extra == "dev"
29
+ Requires-Dist: doccmd==2026.9.1; extra == "dev"
29
30
  Requires-Dist: furo==2025.12.19; extra == "dev"
30
31
  Requires-Dist: interrogate==1.7.0; extra == "dev"
31
32
  Requires-Dist: mypy[faster-cache]==2.3.1; extra == "dev"
32
33
  Requires-Dist: mypy-strict-kwargs==2026.8.25.1; extra == "dev"
33
- Requires-Dist: no-defaults==2.1.0; extra == "dev"
34
+ Requires-Dist: no-defaults==2026.9.1; extra == "dev"
34
35
  Requires-Dist: openapi-mock==2026.8.16; extra == "dev"
35
- Requires-Dist: prek==0.4.14; extra == "dev"
36
+ Requires-Dist: prek==0.5.2; extra == "dev"
36
37
  Requires-Dist: pydocstringformatter==1.0.0; extra == "dev"
37
38
  Requires-Dist: pydocstyle==6.3; extra == "dev"
38
39
  Requires-Dist: pygments==2.21.0; extra == "dev"
39
- Requires-Dist: pylint[spelling]==4.0.7; extra == "dev"
40
+ Requires-Dist: pylint[spelling]==4.0.8; extra == "dev"
40
41
  Requires-Dist: pylint-per-file-ignores==3.2.1; extra == "dev"
41
- Requires-Dist: pyproject-fmt==2.28.1; extra == "dev"
42
+ Requires-Dist: pyproject-fmt==2.29.3; extra == "dev"
42
43
  Requires-Dist: pyrefly==1.2.0; extra == "dev"
43
44
  Requires-Dist: pyright==1.1.411; extra == "dev"
44
45
  Requires-Dist: pyroma==5.0.1; extra == "dev"
@@ -46,11 +47,12 @@ Requires-Dist: pytest==9.1.1; extra == "dev"
46
47
  Requires-Dist: pytest-asyncio==1.4.0; extra == "dev"
47
48
  Requires-Dist: pytest-beartype-tests==2026.8.16; extra == "dev"
48
49
  Requires-Dist: pytest-cov==7.1.0; extra == "dev"
50
+ Requires-Dist: pytest-httpx2==1.0.0; extra == "dev"
49
51
  Requires-Dist: pyyaml==6.0.3; extra == "dev"
50
52
  Requires-Dist: respx==0.23.1; extra == "dev"
51
- Requires-Dist: ruff==0.16.4; extra == "dev"
53
+ Requires-Dist: ruff==0.16.6; extra == "dev"
52
54
  Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
53
- Requires-Dist: shfmt-py==4.0.0; extra == "dev"
55
+ Requires-Dist: shfmt-py==4.1.0; extra == "dev"
54
56
  Requires-Dist: sphinx==9.1.0; extra == "dev"
55
57
  Requires-Dist: sphinx-copybutton==0.5.2; extra == "dev"
56
58
  Requires-Dist: sphinx-lint==1.0.2; extra == "dev"
@@ -58,14 +60,14 @@ Requires-Dist: sphinx-pyproject==0.3.0; extra == "dev"
58
60
  Requires-Dist: sphinx-substitution-extensions==2026.8.13.1; extra == "dev"
59
61
  Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
60
62
  Requires-Dist: sphinxcontrib-towncrier==0.5.0a0; extra == "dev"
61
- Requires-Dist: strict-kwargs==2026.8.24; extra == "dev"
63
+ Requires-Dist: strict-kwargs==2026.8.28.post2; extra == "dev"
62
64
  Requires-Dist: sybil==10.1.0; extra == "dev"
63
65
  Requires-Dist: towncrier==25.8.0; extra == "dev"
64
- Requires-Dist: ty==0.0.74; extra == "dev"
65
- Requires-Dist: vale==3.18.0.0; extra == "dev"
66
+ Requires-Dist: ty==0.0.78; extra == "dev"
67
+ Requires-Dist: vale==3.20.0.0; extra == "dev"
66
68
  Requires-Dist: vulture==2.16; extra == "dev"
67
69
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
68
- Requires-Dist: zizmor==1.29.0; extra == "dev"
70
+ Requires-Dist: zizmor==1.30.0; extra == "dev"
69
71
  Provides-Extra: release
70
72
  Requires-Dist: check-wheel-contents==0.6.3; extra == "release"
71
73
  Requires-Dist: towncrier==25.8.0; extra == "release"
@@ -111,6 +113,33 @@ pass it as ``api_key``:
111
113
 
112
114
  .. _HackerRank for Work tokens page: https://www.hackerrank.com/work/settings/token
113
115
 
116
+ HTTPX2 transports
117
+ -----------------
118
+
119
+ HTTPX remains the default client family. To opt in to HTTPX2, construct the
120
+ HTTPX2 transport explicitly and pass it to the client. The standard
121
+ ``hackerrank`` installation includes both client families.
122
+
123
+ .. code-block:: python
124
+
125
+ """Configure HackerRank to use HTTPX2."""
126
+
127
+ import httpx2
128
+
129
+ from hackerrank.client import HackerRank
130
+ from hackerrank.transports import HTTPX2Transport
131
+
132
+ transport = HTTPX2Transport(timeout=httpx2.Timeout(timeout=10))
133
+ with HackerRank(api_key="your-api-key", transport=transport) as client:
134
+ tests = client.tests.list()
135
+
136
+ Use ``AsyncHTTPX2Transport`` with ``AsyncHackerRank`` for asynchronous calls.
137
+ The ``timeout`` argument accepts an HTTPX2 timeout object or a number of
138
+ seconds; do not pass an equivalent ``httpx`` object across the package
139
+ boundary. Existing HTTPX users do not need to change anything. Migrating to
140
+ HTTPX2 only requires selecting the new transport; HackerRank client methods
141
+ and returned models are unchanged.
142
+
114
143
  Full Documentation
115
144
  ------------------
116
145
 
@@ -38,6 +38,33 @@ pass it as ``api_key``:
38
38
 
39
39
  .. _HackerRank for Work tokens page: https://www.hackerrank.com/work/settings/token
40
40
 
41
+ HTTPX2 transports
42
+ -----------------
43
+
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.
47
+
48
+ .. code-block:: python
49
+
50
+ """Configure HackerRank to use HTTPX2."""
51
+
52
+ import httpx2
53
+
54
+ from hackerrank.client import HackerRank
55
+ from hackerrank.transports import HTTPX2Transport
56
+
57
+ transport = HTTPX2Transport(timeout=httpx2.Timeout(timeout=10))
58
+ with HackerRank(api_key="your-api-key", transport=transport) as client:
59
+ tests = client.tests.list()
60
+
61
+ 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.
67
+
41
68
  Full Documentation
42
69
  ------------------
43
70
 
@@ -82,6 +82,8 @@ nitpick_ignore: list[tuple[str, str]] = [
82
82
  # ``httpx`` does not publish a Sphinx object inventory, so its
83
83
  # types cannot be cross-referenced.
84
84
  ("py:class", "httpx.Timeout"),
85
+ # ``httpx2`` does not expose this class in its Sphinx inventory.
86
+ ("py:class", "httpx2.Timeout"),
85
87
  ]
86
88
  nitpick_ignore_regex = [
87
89
  (r"py:class", r"hackerrank\._dict_types\..*"),
@@ -0,0 +1,75 @@
1
+ |project|
2
+ =========
3
+
4
+ Installation
5
+ ------------
6
+
7
+ .. code-block:: console
8
+
9
+ $ pip install hackerrank
10
+
11
+ This is tested on Python |minimum-python-version|\+.
12
+
13
+ Usage
14
+ -----
15
+
16
+ Generate an API token from the `HackerRank for Work tokens page`_ and pass
17
+ it to :class:`hackerrank.client.HackerRank`:
18
+
19
+ .. code-block:: python
20
+
21
+ """Example usage."""
22
+
23
+ import sys
24
+
25
+ from hackerrank.client import HackerRank
26
+
27
+ client = HackerRank(api_key="your-api-key")
28
+ for test in client.tests.list().data:
29
+ sys.stdout.write(test.name)
30
+ interview = client.interviews.create(title="My Interview")
31
+ sys.stdout.write(interview.url or "")
32
+
33
+ HTTPX2 transports
34
+ -----------------
35
+
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.
39
+
40
+ .. code-block:: python
41
+
42
+ """Configure HackerRank to use HTTPX2."""
43
+
44
+ import httpx2
45
+
46
+ from hackerrank.client import HackerRank
47
+ from hackerrank.transports import HTTPX2Transport
48
+
49
+ transport = HTTPX2Transport(timeout=httpx2.Timeout(timeout=10))
50
+ with HackerRank(api_key="your-api-key", transport=transport) as client:
51
+ tests = client.tests.list()
52
+
53
+ 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.
59
+
60
+ See the :doc:`api-reference` for full usage details.
61
+
62
+ .. _HackerRank for Work tokens page: https://www.hackerrank.com/work/settings/token
63
+
64
+ Reference
65
+ ---------
66
+
67
+ .. toctree::
68
+ :maxdepth: 3
69
+
70
+ api-reference
71
+ openapi-spec
72
+ contributing
73
+ release-process
74
+ unreleased
75
+ changelog
@@ -34,6 +34,7 @@ dynamic = [
34
34
  dependencies = [
35
35
  "beartype>=0.22.9",
36
36
  "httpx>=0.28.0",
37
+ "httpx2>=2.12.0",
37
38
  ]
38
39
  optional-dependencies.dev = [
39
40
  "actionlint-py==1.7.12.24",
@@ -41,20 +42,20 @@ optional-dependencies.dev = [
41
42
  "check-manifest==0.51",
42
43
  "deptry==0.25.1",
43
44
  "doc8==2.0.0",
44
- "doccmd==2026.8.16",
45
+ "doccmd==2026.9.1",
45
46
  "furo==2025.12.19",
46
47
  "interrogate==1.7.0",
47
48
  "mypy[faster-cache]==2.3.1",
48
49
  "mypy-strict-kwargs==2026.8.25.1",
49
- "no-defaults==2.1.0",
50
+ "no-defaults==2026.9.1",
50
51
  "openapi-mock==2026.8.16",
51
- "prek==0.4.14",
52
+ "prek==0.5.2",
52
53
  "pydocstringformatter==1.0.0",
53
54
  "pydocstyle==6.3",
54
55
  "pygments==2.21.0",
55
- "pylint[spelling]==4.0.7",
56
+ "pylint[spelling]==4.0.8",
56
57
  "pylint-per-file-ignores==3.2.1",
57
- "pyproject-fmt==2.28.1",
58
+ "pyproject-fmt==2.29.3",
58
59
  "pyrefly==1.2.0",
59
60
  "pyright==1.1.411",
60
61
  "pyroma==5.0.1",
@@ -62,14 +63,15 @@ optional-dependencies.dev = [
62
63
  "pytest-asyncio==1.4.0",
63
64
  "pytest-beartype-tests==2026.8.16",
64
65
  "pytest-cov==7.1.0",
66
+ "pytest-httpx2==1.0.0",
65
67
  "pyyaml==6.0.3",
66
68
  "respx==0.23.1",
67
- "ruff==0.16.4",
69
+ "ruff==0.16.6",
68
70
  # We add shellcheck-py not only for shell scripts and shell code blocks,
69
71
  # but also because having it installed means that ``actionlint-py`` will
70
72
  # use it to lint shell commands in GitHub workflow files.
71
73
  "shellcheck-py==0.11.0.1",
72
- "shfmt-py==4.0.0",
74
+ "shfmt-py==4.1.0",
73
75
  "sphinx==9.1.0",
74
76
  "sphinx-copybutton==0.5.2",
75
77
  "sphinx-lint==1.0.2",
@@ -79,14 +81,14 @@ optional-dependencies.dev = [
79
81
  # ``sphinxcontrib-towncrier`` renders unreleased news fragments
80
82
  # into docs/source/unreleased.rst during Sphinx builds.
81
83
  "sphinxcontrib-towncrier==0.5.0a0",
82
- "strict-kwargs==2026.8.24",
84
+ "strict-kwargs==2026.8.28.post2",
83
85
  "sybil==10.1.0",
84
86
  "towncrier==25.8.0",
85
- "ty==0.0.74",
86
- "vale==3.18.0.0",
87
+ "ty==0.0.78",
88
+ "vale==3.20.0.0",
87
89
  "vulture==2.16",
88
90
  "yamlfix==1.19.1",
89
- "zizmor==1.29.0",
91
+ "zizmor==1.30.0",
90
92
  ]
91
93
  optional-dependencies.release = [ "check-wheel-contents==0.6.3", "towncrier==25.8.0" ]
92
94
  urls.Documentation = "https://adamtheturtle.github.io/hackerrank/"
@@ -149,7 +151,9 @@ lint.unfixable = [
149
151
  "ERA001",
150
152
  ]
151
153
  lint.external = [ "NOD" ]
152
- lint.flake8-tidy-imports.banned-api."typing.cast".msg = "typing.cast is banned: use explicit type narrowing or a typed variable instead."
154
+ lint.flake8-tidy-imports.banned-api."typing.cast".msg = """\
155
+ typing.cast is banned: use explicit type narrowing or a typed variable instead.\
156
+ """
153
157
  lint.pydocstyle.convention = "google"
154
158
 
155
159
  [tool.pylint]
@@ -537,8 +541,8 @@ plugins = [
537
541
  ]
538
542
 
539
543
  [tool.pyrefly]
540
- errors.non-exhaustive-match = "error"
541
544
  search-path = [ "." ]
545
+ errors.non-exhaustive-match = "error"
542
546
 
543
547
  [tool.pyright]
544
548
  typeCheckingMode = "strict"
@@ -574,6 +578,11 @@ issue_format = "#{issue}"
574
578
  # version such as ``2026.05.18`` underlined with ``-`` matches every
575
579
  # pre-towncrier entry in CHANGELOG.rst.
576
580
  underlines = [ "-", "~", "^" ]
581
+ # Setting ``ignore`` makes ``towncrier build`` fail on any file in
582
+ # the fragment directory whose suffix is not a declared fragment
583
+ # type, instead of silently dropping it. Only the listed
584
+ # non-fragment files are allowed through.
585
+ ignore = [ ".gitkeep" ]
577
586
  type = [
578
587
  # A single, unnamed fragment type keeps the assembled output as one
579
588
  # flat bullet list, matching the historical changelog (which never
@@ -40,6 +40,7 @@ html
40
40
  http
41
41
  https
42
42
  httpx
43
+ httpx2
43
44
  invariance
44
45
  inviters
45
46
  io
@@ -8,6 +8,7 @@ from types import TracebackType
8
8
  from typing import Any, BinaryIO, Self
9
9
 
10
10
  import httpx
11
+ import httpx2
11
12
  from beartype import beartype
12
13
 
13
14
  from hackerrank._retries import (
@@ -18,6 +19,7 @@ from hackerrank._retries import (
18
19
  )
19
20
  from hackerrank.exceptions import HackerRankError
20
21
  from hackerrank.transports import (
22
+ AsyncHTTPX2Transport,
21
23
  AsyncHTTPXTransport,
22
24
  AsyncTransport,
23
25
  TransportResponse,
@@ -318,7 +320,7 @@ class _AsyncNamespace:
318
320
  json=json,
319
321
  files=files,
320
322
  )
321
- except httpx.TransportError as exc:
323
+ except (httpx.TransportError, httpx2.TransportError) as exc:
322
324
  if not (retriable and rewind_files(files=files)):
323
325
  raise
324
326
  headers = None
@@ -3290,7 +3292,10 @@ class AsyncHackerRank:
3290
3292
  )
3291
3293
  self._owned_transport = (
3292
3294
  resolved_transport
3293
- if isinstance(resolved_transport, AsyncHTTPXTransport)
3295
+ if isinstance(
3296
+ resolved_transport,
3297
+ (AsyncHTTPXTransport, AsyncHTTPX2Transport),
3298
+ )
3294
3299
  else None
3295
3300
  )
3296
3301
 
@@ -8,6 +8,7 @@ from types import TracebackType
8
8
  from typing import Any, BinaryIO, Self
9
9
 
10
10
  import httpx
11
+ import httpx2
11
12
  from beartype import beartype
12
13
 
13
14
  from hackerrank._retries import (
@@ -18,6 +19,7 @@ from hackerrank._retries import (
18
19
  )
19
20
  from hackerrank.exceptions import HackerRankError
20
21
  from hackerrank.transports import (
22
+ HTTPX2Transport,
21
23
  HTTPXTransport,
22
24
  Transport,
23
25
  TransportResponse,
@@ -140,7 +142,7 @@ class _Namespace:
140
142
  json=json,
141
143
  files=files,
142
144
  )
143
- except httpx.TransportError as exc:
145
+ except (httpx.TransportError, httpx2.TransportError) as exc:
144
146
  if not (retriable and rewind_files(files=files)):
145
147
  raise
146
148
  headers = None
@@ -3278,7 +3280,7 @@ class HackerRank:
3278
3280
  headers=headers,
3279
3281
  retries=retries,
3280
3282
  )
3281
- if isinstance(resolved_transport, HTTPXTransport):
3283
+ if isinstance(resolved_transport, (HTTPXTransport, HTTPX2Transport)):
3282
3284
  self._close = resolved_transport.close
3283
3285
  else:
3284
3286
  self._close = lambda: None
@@ -8,6 +8,7 @@ from types import TracebackType
8
8
  from typing import Any, Protocol, Self, runtime_checkable
9
9
 
10
10
  import httpx
11
+ import httpx2
11
12
  from beartype import beartype
12
13
 
13
14
  from hackerrank.types import JSONValue
@@ -15,8 +16,8 @@ from hackerrank.types import JSONValue
15
16
  DEFAULT_TIMEOUT_SECONDS = 60.0
16
17
  """The default timeout, in seconds, for the built-in transports.
17
18
 
18
- ``httpx`` defaults to 5 seconds, which is too short for endpoints
19
- which carry large payloads, such as project zip uploads.
19
+ The HTTP client libraries default to 5 seconds, which is too short for
20
+ endpoints which carry large payloads, such as project zip uploads.
20
21
  """
21
22
 
22
23
 
@@ -194,6 +195,92 @@ class HTTPXTransport:
194
195
  )
195
196
 
196
197
 
198
+ @beartype
199
+ class HTTPX2Transport:
200
+ """HTTP transport using the ``httpx2`` library.
201
+
202
+ This opt-in transport uses a shared ``httpx2.Client`` for connection
203
+ pooling. Configuration objects must come from ``httpx2``, not ``httpx``.
204
+ """
205
+
206
+ def __init__(
207
+ self,
208
+ *,
209
+ # ``int`` is redundant to a type checker, which reads ``float``
210
+ # as "int or float", but ``beartype`` checks the annotation
211
+ # literally at runtime and would reject ``timeout=60``.
212
+ timeout: httpx2.Timeout | float | int = DEFAULT_TIMEOUT_SECONDS, # noqa: PYI041
213
+ ) -> None:
214
+ """Create a new HTTPX2 transport.
215
+
216
+ Args:
217
+ timeout: The timeout to use for every request, either
218
+ a number of seconds or an ``httpx2.Timeout``.
219
+ Defaults to
220
+ :data:`DEFAULT_TIMEOUT_SECONDS` seconds.
221
+ """
222
+ self._client = httpx2.Client(timeout=timeout)
223
+
224
+ def close(self) -> None:
225
+ """Close the underlying HTTP client."""
226
+ self._client.close()
227
+
228
+ def __enter__(self) -> Self:
229
+ """Enter the context manager.
230
+
231
+ Returns:
232
+ This transport instance.
233
+ """
234
+ return self
235
+
236
+ def __exit__(
237
+ self,
238
+ _exc_type: type[BaseException] | None,
239
+ _exc_val: BaseException | None,
240
+ _exc_tb: TracebackType | None,
241
+ ) -> None:
242
+ """Exit the context manager and close the client."""
243
+ self.close()
244
+
245
+ def __call__(
246
+ self,
247
+ *,
248
+ method: str,
249
+ url: str,
250
+ headers: dict[str, str],
251
+ params: dict[str, str | int] | None,
252
+ json: Mapping[str, JSONValue] | None,
253
+ files: Mapping[str, Any] | None,
254
+ ) -> TransportResponse:
255
+ """Make an HTTP request using ``httpx2``.
256
+
257
+ Args:
258
+ method: The HTTP method.
259
+ url: The full URL.
260
+ headers: Request headers.
261
+ params: Query parameters.
262
+ json: A JSON-serialisable body.
263
+ files: Files to send as multipart form-data.
264
+
265
+ Returns:
266
+ A ``TransportResponse`` populated from the httpx2
267
+ response.
268
+ """
269
+ response = self._client.request(
270
+ method=method,
271
+ url=url,
272
+ headers=headers,
273
+ params=params,
274
+ json=json,
275
+ files=files,
276
+ )
277
+ return TransportResponse(
278
+ status_code=response.status_code,
279
+ headers=dict(response.headers),
280
+ content=response.content,
281
+ )
282
+
283
+
197
284
  @runtime_checkable
198
285
  class AsyncTransport(Protocol):
199
286
  """Protocol for async HTTP transports."""
@@ -306,3 +393,86 @@ class AsyncHTTPXTransport:
306
393
  headers=dict(response.headers),
307
394
  content=response.content,
308
395
  )
396
+
397
+
398
+ @beartype
399
+ class AsyncHTTPX2Transport:
400
+ """Async HTTP transport using the ``httpx2`` library."""
401
+
402
+ def __init__(
403
+ self,
404
+ *,
405
+ # ``int`` is redundant to a type checker, which reads ``float``
406
+ # as "int or float", but ``beartype`` checks the annotation
407
+ # literally at runtime and would reject ``timeout=60``.
408
+ timeout: httpx2.Timeout | float | int = DEFAULT_TIMEOUT_SECONDS, # noqa: PYI041
409
+ ) -> None:
410
+ """Create a new async HTTPX2 transport.
411
+
412
+ Args:
413
+ timeout: The timeout to use for every request, either
414
+ a number of seconds or an ``httpx2.Timeout``.
415
+ Defaults to
416
+ :data:`DEFAULT_TIMEOUT_SECONDS` seconds.
417
+ """
418
+ self._client = httpx2.AsyncClient(timeout=timeout)
419
+
420
+ async def aclose(self) -> None:
421
+ """Close the underlying async HTTP client."""
422
+ await self._client.aclose()
423
+
424
+ async def __aenter__(self) -> Self:
425
+ """Enter the async context manager.
426
+
427
+ Returns:
428
+ This transport instance.
429
+ """
430
+ return self
431
+
432
+ async def __aexit__(
433
+ self,
434
+ _exc_type: type[BaseException] | None,
435
+ _exc_val: BaseException | None,
436
+ _exc_tb: TracebackType | None,
437
+ /,
438
+ ) -> None:
439
+ """Exit the async context manager and close."""
440
+ await self.aclose()
441
+
442
+ async def __call__(
443
+ self,
444
+ *,
445
+ method: str,
446
+ url: str,
447
+ headers: dict[str, str],
448
+ params: dict[str, str | int] | None,
449
+ json: Mapping[str, JSONValue] | None,
450
+ files: Mapping[str, Any] | None,
451
+ ) -> TransportResponse:
452
+ """Make an async HTTP request using ``httpx2``.
453
+
454
+ Args:
455
+ method: The HTTP method.
456
+ url: The full URL.
457
+ headers: Request headers.
458
+ params: Query parameters.
459
+ json: A JSON-serialisable body.
460
+ files: Files to send as multipart form-data.
461
+
462
+ Returns:
463
+ A ``TransportResponse`` populated from the httpx2
464
+ response.
465
+ """
466
+ response = await self._client.request(
467
+ method=method,
468
+ url=url,
469
+ headers=headers,
470
+ params=params,
471
+ json=json,
472
+ files=files,
473
+ )
474
+ return TransportResponse(
475
+ status_code=response.status_code,
476
+ headers=dict(response.headers),
477
+ content=response.content,
478
+ )