hackerrank 2026.7.30__tar.gz → 2026.8.22__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 (75) hide show
  1. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.github/dependabot.yml +5 -0
  2. hackerrank-2026.8.22/.github/workflows/autofix.yml +37 -0
  3. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.github/workflows/lint.yml +6 -8
  4. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.gitignore +3 -0
  5. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.pre-commit-config.yaml +40 -41
  6. hackerrank-2026.8.22/.vale.ini +14 -0
  7. hackerrank-2026.8.22/CHANGELOG.rst +71 -0
  8. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/PKG-INFO +17 -15
  9. hackerrank-2026.8.22/conftest.py +209 -0
  10. hackerrank-2026.8.22/newsfragments/change/163.rst +1 -0
  11. hackerrank-2026.8.22/newsfragments/change/164.rst +1 -0
  12. hackerrank-2026.8.22/newsfragments/change/167.rst +1 -0
  13. hackerrank-2026.8.22/newsfragments/change/195.rst +1 -0
  14. hackerrank-2026.8.22/newsfragments/change/248.rst +1 -0
  15. hackerrank-2026.8.22/newsfragments/change/249.rst +1 -0
  16. hackerrank-2026.8.22/newsfragments/change/250.rst +1 -0
  17. hackerrank-2026.8.22/openapi.json +1 -0
  18. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/pyproject.toml +27 -14
  19. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/spelling_private_dict.txt +11 -0
  20. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank/_dict_types.py +87 -26
  21. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank/async_client.py +626 -203
  22. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank/client.py +608 -202
  23. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank/exceptions.py +12 -1
  24. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank/transports.py +14 -14
  25. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank/types.py +303 -10
  26. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank.egg-info/PKG-INFO +17 -15
  27. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank.egg-info/SOURCES.txt +16 -0
  28. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank.egg-info/requires.txt +16 -14
  29. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank.egg-info/scm_file_list.json +51 -35
  30. hackerrank-2026.8.22/src/hackerrank.egg-info/scm_version.json +8 -0
  31. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/tests/conftest.py +35 -6
  32. hackerrank-2026.8.22/tests/openapi_helpers.py +80 -0
  33. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/tests/test_async_client.py +51 -0
  34. hackerrank-2026.8.22/tests/test_candidate_search.py +156 -0
  35. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/tests/test_client.py +83 -3
  36. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/tests/test_coverage_gaps.py +224 -3
  37. hackerrank-2026.8.22/tests/test_documented_request_params.py +452 -0
  38. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/tests/test_endpoints.py +411 -43
  39. hackerrank-2026.8.22/tests/test_openapi.py +195 -0
  40. hackerrank-2026.8.22/tests/test_reject_bare_string_lists.py +393 -0
  41. hackerrank-2026.8.22/tests/test_required_update_bodies.py +285 -0
  42. hackerrank-2026.8.22/tests/test_response_shapes.py +384 -0
  43. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/tests/test_types.py +241 -1
  44. hackerrank-2026.7.30/CHANGELOG.rst +0 -37
  45. hackerrank-2026.7.30/conftest.py +0 -56
  46. hackerrank-2026.7.30/openapi.json +0 -1
  47. hackerrank-2026.7.30/src/hackerrank.egg-info/scm_version.json +0 -8
  48. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.git_archival.txt +0 -0
  49. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.gitattributes +0 -0
  50. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.github/workflows/ci.yml +0 -0
  51. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.github/workflows/dependabot-merge.yml +0 -0
  52. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.github/workflows/publish-site.yml +0 -0
  53. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.github/workflows/release.yml +0 -0
  54. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/.prettierrc +0 -0
  55. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/LICENSE +0 -0
  56. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/README.rst +0 -0
  57. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/__init__.py +0 -0
  58. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/api-reference.rst +0 -0
  59. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/changelog.rst +0 -0
  60. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/conf.py +0 -0
  61. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/contributing.rst +0 -0
  62. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/index.rst +0 -0
  63. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/openapi-spec.rst +0 -0
  64. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/release-process.rst +0 -0
  65. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/source/unreleased.rst +0 -0
  66. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/docs/towncrier_template.rst.jinja +0 -0
  67. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/newsfragments/.gitkeep +0 -0
  68. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/setup.cfg +0 -0
  69. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank/__init__.py +0 -0
  70. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank/py.typed +0 -0
  71. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank.egg-info/dependency_links.txt +0 -0
  72. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank.egg-info/not-zip-safe +0 -0
  73. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/src/hackerrank.egg-info/top_level.txt +0 -0
  74. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/tests/__init__.py +0 -0
  75. {hackerrank-2026.7.30 → hackerrank-2026.8.22}/zizmor.yml +0 -0
@@ -12,3 +12,8 @@ updates:
12
12
  schedule:
13
13
  interval: daily
14
14
  open-pull-requests-limit: 10
15
+
16
+ - package-ecosystem: pre-commit
17
+ directory: /
18
+ schedule:
19
+ interval: daily
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: autofix.ci
3
+
4
+ on:
5
+ pull_request:
6
+ branches: [main]
7
+ push:
8
+ branches: [main]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ autofix:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ with:
19
+ fetch-depth: 0
20
+ fetch-tags: true
21
+ persist-credentials: false
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v9.0.0
25
+
26
+ - name: Run fixers
27
+ uses: j178/prek-action@v3.0.0
28
+ with:
29
+ prek-version: 0.4.11
30
+ extra-args: >-
31
+ --all-files --hook-stage pre-commit --no-fail-fast --verbose
32
+ env:
33
+ UV_NO_CACHE: '1'
34
+ UV_PYTHON: 3.13
35
+
36
+ - uses: autofix-ci/action@v1.3.4
37
+ if: always()
@@ -35,17 +35,15 @@ jobs:
35
35
  cache-dependency-glob: '**/pyproject.toml'
36
36
 
37
37
  - name: Lint
38
- # Use bash to ensure the step fails if any command fails.
39
- # PowerShell does not fail on intermediate command failures by default.
40
- shell: bash
41
- run: uv run --extra=dev prek run --all-files --hook-stage ${{ matrix.hook-stage }}
42
- --verbose
38
+ uses: j178/prek-action@v3.0.0
39
+ with:
40
+ prek-version: 0.4.11
41
+ extra-args: >-
42
+ --all-files --hook-stage ${{ matrix.hook-stage }} --verbose
43
43
  env:
44
+ UV_NO_CACHE: '1'
44
45
  UV_PYTHON: ${{ matrix.python-version }}
45
46
 
46
- - uses: pre-commit-ci/lite-action@v1.1.0
47
- if: always()
48
-
49
47
  completion-lint:
50
48
  needs: build
51
49
  runs-on: ubuntu-latest
@@ -107,3 +107,6 @@ ENV/
107
107
  src/*/_setuptools_scm_version.txt
108
108
 
109
109
  uv.lock
110
+
111
+ # Vale styles downloaded by ``vale sync``
112
+ styles/
@@ -6,47 +6,6 @@ fail_fast: true
6
6
  # See https://pre-commit.com for more information
7
7
  # See https://pre-commit.com/hooks.html for more hooks
8
8
 
9
- ci:
10
- # We use system Python, with required dependencies specified in pyproject.toml.
11
- # We therefore cannot use those dependencies in pre-commit CI.
12
- skip:
13
- - actionlint
14
- - sphinx-lint
15
- - strict-kwargs-fix
16
- - check-manifest
17
- - deptry
18
- - doc8
19
- - pydocstringformatter
20
- - docs
21
- - interrogate
22
- - interrogate-docs
23
- - linkcheck
24
- - mypy
25
- - mypy-docs
26
- - pylint
27
- - pyproject-fmt-fix
28
- - pyright
29
- - pyright-docs
30
- - pyright-verifytypes
31
- - ty
32
- - ty-docs
33
- - pyroma
34
- - ruff-check-fix
35
- - ruff-check-fix-docs
36
- - ruff-format-fix
37
- - ruff-format-fix-docs
38
- - shellcheck
39
- - shellcheck-docs
40
- - shfmt
41
- - shfmt-docs
42
- - spelling
43
- - vulture
44
- - vulture-docs
45
- - yamlfix
46
- - zizmor
47
- - pyrefly
48
- - pyrefly-docs
49
-
50
9
  default_install_hook_types: [pre-commit, pre-push]
51
10
 
52
11
  repos:
@@ -309,6 +268,46 @@ repos:
309
268
  stages: [pre-commit]
310
269
  require_serial: true
311
270
 
271
+ - id: no-defaults
272
+ name: no-defaults
273
+ entry: uv run --extra=dev no-defaults
274
+ language: python
275
+ types_or: [python]
276
+ additional_dependencies:
277
+ - *uv_version
278
+ stages: [pre-commit]
279
+ require_serial: true
280
+
281
+ # Vale enforces prose style rules, such as banning em dashes, in
282
+ # reStructuredText and Markdown files.
283
+ # The rules come from the ``ai-tells`` package pinned in ``.vale.ini``,
284
+ # which ``vale sync`` downloads into the (gitignored) ``styles``
285
+ # directory.
286
+ # Vale needs ``rst2html`` from Docutils on the ``PATH`` to parse
287
+ # reStructuredText.
288
+ # ``vale sync`` also runs when ``.vale.ini`` changes, so a package
289
+ # bump takes effect without waiting for the next reStructuredText
290
+ # change.
291
+ - id: vale-sync
292
+ name: vale sync
293
+ entry: uv run --extra=dev vale sync
294
+ language: python
295
+ pass_filenames: false
296
+ files: (\.(rst|md)$|^\.vale\.ini$)
297
+ additional_dependencies:
298
+ - *uv_version
299
+ stages: [pre-commit]
300
+
301
+ - id: vale
302
+ name: vale
303
+ entry: uv run --extra=dev vale
304
+ language: python
305
+ types_or: [rst, markdown]
306
+ require_serial: true
307
+ additional_dependencies:
308
+ - *uv_version
309
+ stages: [pre-commit]
310
+
312
311
  - id: doc8
313
312
  name: doc8
314
313
  entry: uv run --extra=dev -m doc8
@@ -0,0 +1,14 @@
1
+ StylesPath = styles
2
+ MinAlertLevel = error
3
+
4
+ Packages = https://github.com/tbhb/vale-ai-tells/releases/download/v1.29.0/ai-tells.zip
5
+
6
+ [*.{rst,md}]
7
+ BasedOnStyles = ai-tells
8
+
9
+ # These rules misclassify established technical, example, or release-note prose
10
+ # in this repository. All other ai-tells rules remain enforced.
11
+ ai-tells.FigurativeLands = NO
12
+ ai-tells.FormalTransitions = NO
13
+ ai-tells.OverusedVocabulary = NO
14
+ ai-tells.VerbTricolon = NO
@@ -0,0 +1,71 @@
1
+ Changelog
2
+ =========
3
+
4
+ .. towncrier release notes start
5
+
6
+ 2026.08.22
7
+ ----------
8
+
9
+ - Accept documented object-form interviewers and candidate_details, preserve
10
+ interview schedule / live fields, and return invite and update response
11
+ bodies from sync and async clients.
12
+
13
+ - Add ``client.candidates.search`` (sync and async) for the global
14
+ candidate-search endpoint, with typed attempt and candidate results.
15
+
16
+ - Expose documented list filters and body fields for interviews,
17
+ questions, templates, and candidate invites on sync and async
18
+ clients.
19
+
20
+ - Reject bare ``str`` values for list-like API fields (``interviewers``,
21
+ ``languages``, ``tags``, and related parameters) so they are no longer
22
+ split into character arrays in request bodies.
23
+
24
+ - Refresh ``openapi.json`` from the live HackerRank schema and add a
25
+ semantic drift check that ignores dynamic example timestamps.
26
+
27
+ - Add ``client.interview_templates.explicit_sharing_roles`` with
28
+ ``update_access`` and ``remove_access`` for sharing an interview template with
29
+ users, teams or the whole company.
30
+
31
+ Remove the ``team_share`` argument from interview-template ``create`` and
32
+ ``update``. HackerRank ignores the parameter and has removed it from the API
33
+ documentation; use ``explicit_sharing_roles`` instead.
34
+
35
+ 2026.08.16
36
+ ----------
37
+
38
+ No significant changes.
39
+
40
+ 2026.07.30
41
+ ----------
42
+
43
+ - Align interview-template create and update requests with the current API,
44
+ return updated templates, and parse live string template IDs and integer
45
+ question IDs.
46
+
47
+ 2026.07.19
48
+ ----------
49
+
50
+ - 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.
51
+
52
+ - 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).
53
+
54
+ 2026.07.08
55
+ ----------
56
+
57
+ No significant changes.
58
+
59
+ 2026.05.12.2
60
+ ------------
61
+
62
+
63
+ 2026.05.12.1
64
+ ------------
65
+
66
+
67
+ 2026.05.12
68
+ ----------
69
+
70
+
71
+ * Initial release.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hackerrank
3
- Version: 2026.7.30
3
+ Version: 2026.8.22
4
4
  Summary: Interact with the HackerRank for Work API.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -25,45 +25,47 @@ Requires-Dist: beartype==0.22.9; extra == "dev"
25
25
  Requires-Dist: check-manifest==0.51; extra == "dev"
26
26
  Requires-Dist: deptry==0.25.1; extra == "dev"
27
27
  Requires-Dist: doc8==2.0.0; extra == "dev"
28
- Requires-Dist: doccmd==2026.7.19; extra == "dev"
28
+ Requires-Dist: doccmd==2026.8.16; extra == "dev"
29
29
  Requires-Dist: furo==2025.12.19; extra == "dev"
30
30
  Requires-Dist: interrogate==1.7.0; extra == "dev"
31
- Requires-Dist: mypy[faster-cache]==2.3.0; extra == "dev"
31
+ Requires-Dist: mypy[faster-cache]==2.3.1; extra == "dev"
32
32
  Requires-Dist: mypy-strict-kwargs==2026.7.19.1; extra == "dev"
33
- Requires-Dist: openapi-mock==2026.3.2; extra == "dev"
34
- Requires-Dist: prek==0.4.11; extra == "dev"
33
+ Requires-Dist: no-defaults==2.1.0; extra == "dev"
34
+ Requires-Dist: openapi-mock==2026.8.16; extra == "dev"
35
+ Requires-Dist: prek==0.4.14; extra == "dev"
35
36
  Requires-Dist: pydocstringformatter==1.0.0; extra == "dev"
36
37
  Requires-Dist: pydocstyle==6.3; extra == "dev"
37
- Requires-Dist: pygments==2.20.0; extra == "dev"
38
- Requires-Dist: pylint[spelling]==4.0.6; extra == "dev"
38
+ Requires-Dist: pygments==2.21.0; extra == "dev"
39
+ Requires-Dist: pylint[spelling]==4.0.7; extra == "dev"
39
40
  Requires-Dist: pylint-per-file-ignores==3.2.1; extra == "dev"
40
- Requires-Dist: pyproject-fmt==2.26.0; extra == "dev"
41
- Requires-Dist: pyrefly==1.1.1; extra == "dev"
41
+ Requires-Dist: pyproject-fmt==2.28.0; extra == "dev"
42
+ Requires-Dist: pyrefly==1.2.0; extra == "dev"
42
43
  Requires-Dist: pyright==1.1.411; extra == "dev"
43
44
  Requires-Dist: pyroma==5.0.1; extra == "dev"
44
45
  Requires-Dist: pytest==9.1.1; extra == "dev"
45
46
  Requires-Dist: pytest-asyncio==1.4.0; extra == "dev"
46
- Requires-Dist: pytest-beartype-tests==2026.4.26; extra == "dev"
47
+ Requires-Dist: pytest-beartype-tests==2026.8.16; extra == "dev"
47
48
  Requires-Dist: pytest-cov==7.1.0; extra == "dev"
48
49
  Requires-Dist: pyyaml==6.0.3; extra == "dev"
49
50
  Requires-Dist: respx==0.23.1; extra == "dev"
50
- Requires-Dist: ruff==0.16.0; extra == "dev"
51
+ Requires-Dist: ruff==0.16.3; extra == "dev"
51
52
  Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
52
53
  Requires-Dist: shfmt-py==4.0.0; extra == "dev"
53
54
  Requires-Dist: sphinx==9.1.0; extra == "dev"
54
55
  Requires-Dist: sphinx-copybutton==0.5.2; extra == "dev"
55
56
  Requires-Dist: sphinx-lint==1.0.2; extra == "dev"
56
57
  Requires-Dist: sphinx-pyproject==0.3.0; extra == "dev"
57
- Requires-Dist: sphinx-substitution-extensions==2026.6.17; extra == "dev"
58
+ Requires-Dist: sphinx-substitution-extensions==2026.8.13.1; extra == "dev"
58
59
  Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
59
60
  Requires-Dist: sphinxcontrib-towncrier==0.5.0a0; extra == "dev"
60
- Requires-Dist: strict-kwargs==2026.7.24; extra == "dev"
61
+ Requires-Dist: strict-kwargs==2026.8.16; extra == "dev"
61
62
  Requires-Dist: sybil==10.1.0; extra == "dev"
62
63
  Requires-Dist: towncrier==25.8.0; extra == "dev"
63
- Requires-Dist: ty==0.0.65; extra == "dev"
64
+ Requires-Dist: ty==0.0.72; extra == "dev"
65
+ Requires-Dist: vale==3.13.0.0; extra == "dev"
64
66
  Requires-Dist: vulture==2.16; extra == "dev"
65
67
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
66
- Requires-Dist: zizmor==1.28.0; extra == "dev"
68
+ Requires-Dist: zizmor==1.29.0; extra == "dev"
67
69
  Provides-Extra: release
68
70
  Requires-Dist: check-wheel-contents==0.6.3; extra == "release"
69
71
  Requires-Dist: towncrier==25.8.0; extra == "release"
@@ -0,0 +1,209 @@
1
+ """Test configuration for hackerrank-api-python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ import pytest
9
+ import respx
10
+ from openapi_mock import add_openapi_to_respx
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Generator
14
+
15
+ _BASE_URL = "https://www.hackerrank.com"
16
+ _HTTP_METHODS = frozenset(
17
+ {"get", "post", "put", "delete", "patch", "head", "options", "trace"},
18
+ )
19
+
20
+
21
+ def _as_str_keyed_dict(*, value: object) -> dict[str, Any] | None:
22
+ """Return ``value`` as a ``str``-keyed dict, or ``None``.
23
+
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):
28
+ return None
29
+ decoded: Any = json.loads(s=json.dumps(obj=value))
30
+ typed: dict[str, Any] = decoded
31
+ return typed
32
+
33
+
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):
37
+ return None
38
+ decoded: Any = json.loads(s=json.dumps(obj=value))
39
+ typed: list[object] = decoded
40
+ return typed
41
+
42
+
43
+ def pytest_addoption(parser: pytest.Parser) -> None:
44
+ """Register command-line options used by this test suite.
45
+
46
+ Args:
47
+ parser: The pytest argument parser.
48
+ """
49
+ parser.addoption(
50
+ "--run-network",
51
+ action="store_true",
52
+ default=False,
53
+ help="Run tests marked with @pytest.mark.network.",
54
+ )
55
+
56
+
57
+ def pytest_configure(config: pytest.Config) -> None:
58
+ """Register custom markers.
59
+
60
+ Args:
61
+ config: The pytest config object.
62
+ """
63
+ config.addinivalue_line(
64
+ name="markers",
65
+ line=(
66
+ "network: tests that fetch live resources "
67
+ "(skipped without --run-network)"
68
+ ),
69
+ )
70
+
71
+
72
+ def pytest_collection_modifyitems(
73
+ config: pytest.Config,
74
+ items: list[pytest.Item],
75
+ ) -> None:
76
+ """Skip network tests unless ``--run-network`` is given.
77
+
78
+ Args:
79
+ config: The pytest config object.
80
+ items: Collected test items.
81
+ """
82
+ if config.getoption(name="--run-network"):
83
+ return
84
+ skip_network = pytest.mark.skip(reason="need --run-network option to run")
85
+ for item in items:
86
+ if "network" in item.keywords:
87
+ item.add_marker(marker=skip_network)
88
+
89
+
90
+ def _fix_schema_required(*, schema: dict[str, Any]) -> dict[str, Any]:
91
+ """Normalize Swagger-style ``required`` flags for OpenAPI 3
92
+ schemas.
93
+ """
94
+ result: dict[str, Any] = dict(schema)
95
+ props = _as_str_keyed_dict(value=result.get("properties"))
96
+ if props is not None:
97
+ required_names: list[str] = []
98
+ existing_required = _as_object_list(value=result.get("required"))
99
+ if existing_required is not None:
100
+ required_names.extend(
101
+ name for name in existing_required if isinstance(name, str)
102
+ )
103
+ fixed_props: dict[str, Any] = {}
104
+ for prop_name, prop_schema_raw in props.items():
105
+ prop_schema = _as_str_keyed_dict(value=prop_schema_raw)
106
+ if prop_schema is None:
107
+ continue
108
+ fixed_prop = _fix_schema_required(schema=prop_schema)
109
+ if (
110
+ fixed_prop.pop("required", None) is True
111
+ and prop_name not in required_names
112
+ ):
113
+ required_names.append(prop_name)
114
+ fixed_props[prop_name] = fixed_prop
115
+ result["properties"] = fixed_props
116
+ if required_names:
117
+ result["required"] = required_names
118
+ 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"))
121
+ if items is not None:
122
+ result["items"] = _fix_schema_required(schema=items)
123
+ return result
124
+
125
+
126
+ def _migrate_body_parameter(*, operation: dict[str, Any]) -> dict[str, Any]:
127
+ """Convert Swagger 2 ``in: body`` parameters to OpenAPI 3
128
+ requestBody.
129
+ """
130
+ result: dict[str, Any] = dict(operation)
131
+ params = _as_object_list(value=result.get("parameters"))
132
+ if params is None:
133
+ return result
134
+ kept: list[object] = []
135
+ body_param: dict[str, Any] | None = None
136
+ for param_raw in params:
137
+ param = _as_str_keyed_dict(value=param_raw)
138
+ if param is not None and param.get("in") == "body":
139
+ body_param = param
140
+ else:
141
+ kept.append(param_raw)
142
+ result["parameters"] = kept
143
+ if body_param is not None and "requestBody" not in result:
144
+ schema_raw = body_param.get("schema", {})
145
+ schema: object = schema_raw
146
+ schema_dict = _as_str_keyed_dict(value=schema_raw)
147
+ if schema_dict is not None:
148
+ schema = _fix_schema_required(schema=schema_dict)
149
+ result["requestBody"] = {
150
+ "required": bool(body_param.get("required", False)),
151
+ "content": {"application/json": {"schema": schema}},
152
+ }
153
+ return result
154
+
155
+
156
+ def _prepare_openapi_spec(*, spec: dict[str, object]) -> dict[str, object]:
157
+ """Normalize the HackerRank OpenAPI document for mock route
158
+ registration.
159
+ """
160
+ prepared: dict[str, object] = dict(spec)
161
+ raw_paths = _as_str_keyed_dict(value=prepared.get("paths", {}))
162
+ if raw_paths is None:
163
+ return prepared
164
+
165
+ cleaned_paths: dict[str, dict[str, object]] = {}
166
+ for raw_key, raw_value_obj in raw_paths.items():
167
+ raw_value = _as_str_keyed_dict(value=raw_value_obj)
168
+ if raw_value is None:
169
+ continue
170
+ cleaned = raw_key.split(sep="?", maxsplit=1)[0]
171
+ merged: dict[str, object] = dict(cleaned_paths.get(cleaned, {}))
172
+ for op_key, op_val_obj in raw_value.items():
173
+ op_val = _as_str_keyed_dict(value=op_val_obj)
174
+ if op_key in _HTTP_METHODS and op_val is not None:
175
+ merged[op_key] = _migrate_body_parameter(operation=op_val)
176
+ else:
177
+ merged[op_key] = op_val_obj
178
+ cleaned_paths[cleaned] = merged
179
+ prepared["paths"] = cleaned_paths
180
+ return prepared
181
+
182
+
183
+ @pytest.fixture(name="mock_hackerrank_api")
184
+ def fixture_mock_hackerrank_api(
185
+ request: pytest.FixtureRequest,
186
+ ) -> Generator[respx.MockRouter]:
187
+ """Provide a respx mock router backed by the OpenAPI spec.
188
+
189
+ Args:
190
+ request: The pytest request, used to locate the
191
+ ``openapi.json`` file in the project root.
192
+
193
+ Yields:
194
+ The configured respx mock router.
195
+ """
196
+ openapi_spec_path = request.config.rootpath / "openapi.json"
197
+ spec_text = openapi_spec_path.read_text(encoding="utf-8")
198
+ openapi_spec: dict[str, object] = json.loads(s=spec_text)
199
+ openapi_spec = _prepare_openapi_spec(spec=openapi_spec)
200
+ with respx.mock(
201
+ base_url=_BASE_URL,
202
+ assert_all_called=False,
203
+ ) as mock_router:
204
+ add_openapi_to_respx(
205
+ mock_obj=mock_router,
206
+ spec=openapi_spec,
207
+ base_url=_BASE_URL,
208
+ )
209
+ yield mock_router
@@ -0,0 +1 @@
1
+ SCIM user and group PATCH methods now parse the documented message acknowledgement instead of treating the response as a full resource.
@@ -0,0 +1 @@
1
+ SCIM group PATCH methods now parse the documented message acknowledgement instead of treating the response as a full group.
@@ -0,0 +1 @@
1
+ SCIM pagination preserves an explicit ``startIndex`` of ``0`` instead of rewriting it to ``1``.
@@ -0,0 +1 @@
1
+ ``generate_codestubs`` now requires a request body, matching the API contract.
@@ -0,0 +1 @@
1
+ Custom transports are only replaced by the default when ``transport`` is ``None``, so falsy callables are preserved.
@@ -0,0 +1 @@
1
+ Unexpected HTTP 3xx redirect responses now raise ``RedirectError`` instead of being treated as successful API responses.
@@ -0,0 +1 @@
1
+ Trailing slashes on custom ``base_url`` and ``scim_base_url`` values are stripped so request paths are not double-slashed.