hackerrank-cli 2026.10.1.1__tar.gz → 2026.10.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_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/CHANGELOG.rst +11 -0
  2. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/MANIFEST.in +3 -1
  3. {hackerrank_cli-2026.10.1.1/src/hackerrank_cli.egg-info → hackerrank_cli-2026.10.7}/PKG-INFO +32 -5
  4. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/README.rst +31 -4
  5. hackerrank_cli-2026.10.7/VERSION +1 -0
  6. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/development.rst +2 -3
  7. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/migration.rst +23 -2
  8. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/pyproject.toml +7 -3
  9. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli/__init__.py +84 -29
  10. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli/_version.py +3 -3
  11. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7/src/hackerrank_cli.egg-info}/PKG-INFO +32 -5
  12. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli.egg-info/SOURCES.txt +2 -0
  13. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli.egg-info/scm_file_list.json +2 -0
  14. hackerrank_cli-2026.10.7/src/hackerrank_cli.egg-info/scm_version.json +8 -0
  15. hackerrank_cli-2026.10.7/tests/test_archive.py +145 -0
  16. hackerrank_cli-2026.10.7/tests/test_help/test_help_archive_.txt +11 -0
  17. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/tests/test_help/test_help_questions_.txt +2 -1
  18. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/tests/test_help.py +7 -2
  19. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/uv.lock +53 -37
  20. hackerrank_cli-2026.10.1.1/VERSION +0 -1
  21. hackerrank_cli-2026.10.1.1/src/hackerrank_cli.egg-info/scm_version.json +0 -8
  22. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.dockerignore +0 -0
  23. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.gitattributes +0 -0
  24. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/dependabot.yml +0 -0
  25. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/autofix.yml +0 -0
  26. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/binaries.yml +0 -0
  27. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/ci.yml +0 -0
  28. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/dependabot-merge.yml +0 -0
  29. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/docs.yml +0 -0
  30. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/lint.yml +0 -0
  31. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/nix.yml +0 -0
  32. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/publish-site.yml +0 -0
  33. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.github/workflows/release.yml +0 -0
  34. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.gitignore +0 -0
  35. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/.vale.ini +0 -0
  36. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/Dockerfile +0 -0
  37. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/LICENSE +0 -0
  38. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/bin/__init__.py +0 -0
  39. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/bin/hackerrank_wrapper.py +0 -0
  40. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/__init__.py +0 -0
  41. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/__init__.py +0 -0
  42. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/changelog.rst +0 -0
  43. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/cli.rst +0 -0
  44. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/conf.py +0 -0
  45. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/index.rst +0 -0
  46. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/macos-releases.rst +0 -0
  47. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/docs/source/usage.rst +0 -0
  48. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/flake.lock +0 -0
  49. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/flake.nix +0 -0
  50. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/newsfragments/.gitkeep +0 -0
  51. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/prek.toml +0 -0
  52. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/setup.cfg +0 -0
  53. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/spelling_private_dict.txt +0 -0
  54. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli/__main__.py +0 -0
  55. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli/_sources.py +0 -0
  56. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli/py.typed +0 -0
  57. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli.egg-info/dependency_links.txt +0 -0
  58. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli.egg-info/entry_points.txt +0 -0
  59. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli.egg-info/not-zip-safe +0 -0
  60. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli.egg-info/requires.txt +0 -0
  61. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/src/hackerrank_cli.egg-info/top_level.txt +0 -0
  62. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/styles/snapper/LongProseLine.yml +0 -0
  63. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/styles/snapper/SemanticLineBreaks.yml +0 -0
  64. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/tests/__init__.py +0 -0
  65. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/tests/test_cli.py +0 -0
  66. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/tests/test_help/test_help_root_.txt +0 -0
  67. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/tests/test_help/test_help_upload_.txt +0 -0
  68. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/tests/test_http.py +0 -0
  69. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/tests/test_sources.py +0 -0
  70. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.7}/zizmor.yml +0 -0
@@ -5,6 +5,17 @@ Releases are assembled from ``newsfragments/`` using Towncrier.
5
5
 
6
6
  .. towncrier release notes start
7
7
 
8
+ 2026.10.7 (2026-10-07)
9
+ ----------------------
10
+
11
+ Features
12
+ ~~~~~~~~
13
+
14
+ - Export project ZIP archives offline with ``hackerrank questions archive``,
15
+ using the same file selection and packaging as uploads.
16
+ (#6)
17
+
18
+
8
19
  2026.10.1.1 (2026-10-01)
9
20
  ------------------------
10
21
 
@@ -1,4 +1,4 @@
1
- include LICENSE README.rst CHANGELOG.rst VERSION
1
+ include LICENSE README.rst CHANGELOG.rst
2
2
  recursive-include src *.py *.typed
3
3
  recursive-include docs *.rst *.py
4
4
  recursive-include newsfragments *
@@ -11,3 +11,5 @@ recursive-include bin *.py
11
11
  recursive-include tests *.py *.txt
12
12
  include .dockerignore
13
13
  include .gitattributes
14
+
15
+ include VERSION
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hackerrank-cli
3
- Version: 2026.10.1.1
3
+ Version: 2026.10.7
4
4
  Summary: Upload starter files to existing HackerRank project questions.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -44,9 +44,16 @@ Install from PyPI:
44
44
 
45
45
  uv tool install hackerrank-cli
46
46
  hackerrank --help
47
- hackerrank --version
48
47
 
49
- You can also install with ``pip install hackerrank-cli``.
48
+ Or install the checkout for development:
49
+
50
+ .. code-block:: console
51
+
52
+ uv sync --locked
53
+ uv run hackerrank --help
54
+ uv run python -m hackerrank_cli --version
55
+
56
+ ``pip install hackerrank-cli`` is also supported.
50
57
 
51
58
  Usage
52
59
  -----
@@ -86,6 +93,27 @@ repeatable upload.
86
93
  The CLI adds no separate retry loop.
87
94
  Each attempt uses the same prepared archive bytes.
88
95
 
96
+ Offline archives
97
+ ----------------
98
+
99
+ Export the same project ZIP locally without a question ID, credentials, or
100
+ network access:
101
+
102
+ .. code-block:: console
103
+
104
+ hackerrank questions archive --directory ./starter --exclude '*.zip' --output ./starter.zip
105
+ hackerrank questions archive --file ./starter.py --output - > ./starter.zip
106
+
107
+ ``--output`` is required.
108
+ A file destination must not already exist; ``-`` writes only ZIP bytes to
109
+ standard output.
110
+ Shell redirection controls whether its destination is overwritten.
111
+ Source validation and packaging finish before the output is opened.
112
+ The source options, ignore rules, file contents, and executable permissions
113
+ match ``questions upload``.
114
+ Consumers can extract this archive for local checks, then send its bytes with
115
+ the SDK's ``questions.upload_project_zip`` method.
116
+
89
117
  Directory selection
90
118
  -------------------
91
119
 
@@ -136,8 +164,7 @@ Development and distribution
136
164
  Development checks, release setup, and build instructions for Docker, Nix, and
137
165
  standalone binaries are in ``docs/source/development.rst``.
138
166
  The generated CLI reference is in ``docs/source/cli.rst``.
139
- Prebuilt binaries are available from
140
- `GitHub Releases <https://github.com/adamtheturtle/hackerrank-cli/releases>`_.
167
+ There are no prebuilt artifacts yet.
141
168
 
142
169
  See ``docs/source/migration.rst`` for migrating an existing shell uploader and
143
170
  keeping repository-specific snippet preparation outside this CLI.
@@ -18,9 +18,16 @@ Install from PyPI:
18
18
 
19
19
  uv tool install hackerrank-cli
20
20
  hackerrank --help
21
- hackerrank --version
22
21
 
23
- You can also install with ``pip install hackerrank-cli``.
22
+ Or install the checkout for development:
23
+
24
+ .. code-block:: console
25
+
26
+ uv sync --locked
27
+ uv run hackerrank --help
28
+ uv run python -m hackerrank_cli --version
29
+
30
+ ``pip install hackerrank-cli`` is also supported.
24
31
 
25
32
  Usage
26
33
  -----
@@ -60,6 +67,27 @@ repeatable upload.
60
67
  The CLI adds no separate retry loop.
61
68
  Each attempt uses the same prepared archive bytes.
62
69
 
70
+ Offline archives
71
+ ----------------
72
+
73
+ Export the same project ZIP locally without a question ID, credentials, or
74
+ network access:
75
+
76
+ .. code-block:: console
77
+
78
+ hackerrank questions archive --directory ./starter --exclude '*.zip' --output ./starter.zip
79
+ hackerrank questions archive --file ./starter.py --output - > ./starter.zip
80
+
81
+ ``--output`` is required.
82
+ A file destination must not already exist; ``-`` writes only ZIP bytes to
83
+ standard output.
84
+ Shell redirection controls whether its destination is overwritten.
85
+ Source validation and packaging finish before the output is opened.
86
+ The source options, ignore rules, file contents, and executable permissions
87
+ match ``questions upload``.
88
+ Consumers can extract this archive for local checks, then send its bytes with
89
+ the SDK's ``questions.upload_project_zip`` method.
90
+
63
91
  Directory selection
64
92
  -------------------
65
93
 
@@ -110,8 +138,7 @@ Development and distribution
110
138
  Development checks, release setup, and build instructions for Docker, Nix, and
111
139
  standalone binaries are in ``docs/source/development.rst``.
112
140
  The generated CLI reference is in ``docs/source/cli.rst``.
113
- Prebuilt binaries are available from
114
- `GitHub Releases <https://github.com/adamtheturtle/hackerrank-cli/releases>`_.
141
+ There are no prebuilt artifacts yet.
115
142
 
116
143
  See ``docs/source/migration.rst`` for migrating an existing shell uploader and
117
144
  keeping repository-specific snippet preparation outside this CLI.
@@ -0,0 +1 @@
1
+ 2026.10.7
@@ -43,9 +43,8 @@ No separate mock-spec repository is needed for this scope.
43
43
  Add a Towncrier feature or bugfix fragment for each user-visible change.
44
44
 
45
45
  Versions derive from Git tags using setuptools-scm.
46
- Untagged initial builds have a development version.
47
- Use date-based tags such as ``2026.10.02`` for releases; no release tag is
48
- created as part of repository initialization.
46
+ Untagged builds have a development version.
47
+ Use date-based tags such as ``2026.10.7`` for releases.
49
48
  Before tagging a release, assemble notes with
50
49
  ``uv run towncrier build --yes --version VERSION``, commit the changelog, then
51
50
  write the same version to ``VERSION``, commit it, then tag the commit.
@@ -7,8 +7,7 @@ repository that owns the questions.
7
7
  This CLI uploads starter files to an existing project question.
8
8
  It never creates questions or updates metadata.
9
9
 
10
- After the first PyPI release, pin a released ``hackerrank-cli`` version in that
11
- repository.
10
+ Pin a released ``hackerrank-cli`` version in that repository.
12
11
  Replace each project archive upload with:
13
12
 
14
13
  .. code-block:: shell
@@ -27,3 +26,25 @@ The default is zero, matching the released SDK.
27
26
  Uploads use immutable archive bytes so retries send the same content.
28
27
  Do not enable live uploads until the file list and project behavior have been
29
28
  reviewed in the owning repository.
29
+
30
+ Local validation
31
+ ----------------
32
+
33
+ Use ``hackerrank questions archive`` when local checks need the exact project
34
+ layout candidates receive:
35
+
36
+ .. code-block:: shell
37
+
38
+ hackerrank questions archive --directory ./starter --exclude '*.zip' --output ./starter.zip
39
+
40
+ Extract the archive in a temporary directory to run project checks.
41
+ An uploader that already uses the SDK can send the exported bytes with
42
+ ``questions.upload_project_zip`` instead of maintaining another ZIP builder.
43
+ The export command shares source selection and packaging with CLI uploads.
44
+ It makes no network requests and requires no API token.
45
+
46
+ Gitignore patterns match directories as well as files.
47
+ When translating a file-only exclusion such as ``app``, add ``!app/`` after it
48
+ if source directories of that name must remain included.
49
+ Inherited ``.gitignore`` rules still apply, so compare the exported file list
50
+ and contents before replacing an existing packager.
@@ -37,7 +37,8 @@ dev = [
37
37
  "doccmd==2026.9.16.1",
38
38
  "furo==2025.12.19",
39
39
  "interrogate==1.7.0",
40
- "mypy==2.3.1",
40
+ "mypy==2.4.0",
41
+ "mypy-pytest-parametrize==2026.10.5",
41
42
  "mypy-strict-kwargs==2026.8.25.1",
42
43
  "no-defaults==2026.9.1",
43
44
  "prek==0.5.4",
@@ -198,7 +199,8 @@ exclude_gitignore = true
198
199
  follow_untyped_imports = true
199
200
  python_version = "3.13"
200
201
  strict = true
201
- plugins = [ "mypy_strict_kwargs" ]
202
+ plugins = [ "mypy_pytest_parametrize", "mypy_strict_kwargs" ]
203
+ num_workers = "auto"
202
204
 
203
205
  [tool.pyrefly]
204
206
  project-excludes = [ "src/hackerrank_cli/_version.py" ]
@@ -224,15 +226,17 @@ analysis.strict-generic-narrowing = true
224
226
 
225
227
  [tool.pytest]
226
228
  ini_options.testpaths = [ "tests" ]
227
- ini_options.addopts = "-n auto --cov=hackerrank_cli --cov-branch --cov-fail-under=100"
229
+ ini_options.addopts = "-n auto --cov --cov-config=pyproject.toml"
228
230
  ini_options.xfail_strict = true
229
231
  ini_options.beartype_packages = [ "hackerrank_cli" ]
230
232
  ini_options.beartype_test_fixtures = true
231
233
  ini_options.beartype_tests = true
232
234
 
233
235
  [tool.coverage]
236
+ run.source = [ "hackerrank_cli" ]
234
237
  run.omit = [ "src/hackerrank_cli/__main__.py", "src/hackerrank_cli/_version.py" ]
235
238
  run.branch = true
239
+ report.fail_under = 100
236
240
  report.show_missing = true
237
241
 
238
242
  [tool.towncrier]
@@ -1,6 +1,8 @@
1
1
  """Upload project starter code using the released HackerRank SDK."""
2
2
 
3
3
  import os
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
4
6
  from importlib.metadata import version
5
7
  from io import BytesIO
6
8
  from pathlib import Path
@@ -84,27 +86,65 @@ def create_cli(*, transport: Transport | None = None) -> click.Group:
84
86
  transport=transport,
85
87
  )
86
88
 
89
+ @click.option(
90
+ "--directory",
91
+ type=click.Path(path_type=Path, readable=False),
92
+ help="Project directory.",
93
+ )
94
+ @click.option(
95
+ "--file",
96
+ "source_file",
97
+ type=click.Path(path_type=Path, readable=False),
98
+ help="One starter file, packaged as a single-file project archive.",
99
+ )
100
+ @click.option(
101
+ "--exclude",
102
+ multiple=True,
103
+ help="Additional Gitignore pattern at the upload root. Repeatable.",
104
+ )
105
+ @click.option(
106
+ "--output",
107
+ type=click.Path(dir_okay=False, allow_dash=True),
108
+ required=True,
109
+ help=(
110
+ "New ZIP file, or - for binary standard output. "
111
+ "Never overwrites files."
112
+ ),
113
+ )
114
+ def archive(
115
+ directory: Path | None,
116
+ source_file: Path | None,
117
+ exclude: tuple[str, ...],
118
+ output: str,
119
+ ) -> None:
120
+ """Export a project ZIP without credentials or network access."""
121
+ _validate_source(
122
+ directory=directory, source_file=source_file, exclude=exclude
123
+ )
124
+ with (
125
+ _preparation_errors(),
126
+ prepare_source(
127
+ directory=directory, file=source_file, excludes=exclude
128
+ ) as source,
129
+ ):
130
+ contents = _project_zip(source=source)
131
+ with click.open_file(filename=output, mode="xb") as destination:
132
+ _ = destination.write(contents)
133
+
134
+ _ = questions.command()(archive)
87
135
  _ = questions.command()(upload)
88
136
  return cli
89
137
 
90
138
 
91
- def _validate_upload(
92
- question_id: str,
139
+ def _validate_source(
93
140
  directory: Path | None,
94
141
  source_file: Path | None,
95
142
  exclude: tuple[str, ...],
96
143
  ) -> None:
97
- """Validate command combinations and the question identifier."""
144
+ """Validate the shared archive and upload source options."""
98
145
  if (directory is None) == (source_file is None):
99
146
  msg = "Provide exactly one of --directory or --file."
100
147
  raise click.UsageError(message=msg)
101
- if (
102
- not question_id.isascii()
103
- or not question_id.isdecimal()
104
- or int(question_id) < 1
105
- ):
106
- msg = "QUESTION_ID must be a positive decimal integer."
107
- raise click.BadParameter(message=msg, param_hint="QUESTION_ID")
108
148
  if len(exclude) > 0 and directory is None:
109
149
  msg = "--exclude requires --directory."
110
150
  raise click.UsageError(message=msg)
@@ -187,30 +227,45 @@ def _upload( # noqa: PLR0913 - Click options plus the SDK transport boundary.
187
227
  transport: Transport | None,
188
228
  ) -> None:
189
229
  """Prepare every byte before credentials or a network mutation."""
190
- _validate_upload(
191
- question_id=question_id,
230
+ _validate_source(
192
231
  directory=directory,
193
232
  source_file=source_file,
194
233
  exclude=exclude,
195
234
  )
196
- try:
197
- with prepare_source(
235
+ if (
236
+ not question_id.isascii()
237
+ or not question_id.isdecimal()
238
+ or int(question_id) < 1
239
+ ):
240
+ msg = "QUESTION_ID must be a positive decimal integer."
241
+ raise click.BadParameter(message=msg, param_hint="QUESTION_ID")
242
+ with (
243
+ _preparation_errors(),
244
+ prepare_source(
198
245
  directory=directory, file=source_file, excludes=exclude
199
- ) as source:
200
- archive = _project_zip(source=source)
201
- target = f"HackerRank project question {question_id}"
202
- if dry_run:
203
- click.echo(message=f"Would update {target}")
204
- for path in source.files:
205
- click.echo(message=f" {path}")
206
- return
207
- _send_upload(
208
- question_id=question_id,
209
- archive=archive,
210
- retries=retries,
211
- transport=transport,
212
- )
213
- click.echo(message=f"Updated {target}")
246
+ ) as source,
247
+ ):
248
+ archive = _project_zip(source=source)
249
+ target = f"HackerRank project question {question_id}"
250
+ if dry_run:
251
+ click.echo(message=f"Would update {target}")
252
+ for path in source.files:
253
+ click.echo(message=f" {path}")
254
+ return
255
+ _send_upload(
256
+ question_id=question_id,
257
+ archive=archive,
258
+ retries=retries,
259
+ transport=transport,
260
+ )
261
+ click.echo(message=f"Updated {target}")
262
+
263
+
264
+ @contextmanager
265
+ def _preparation_errors() -> Generator[None]:
266
+ """Report source and archive errors consistently for both commands."""
267
+ try:
268
+ yield
214
269
  except UnicodeError:
215
270
  msg = ".gitignore files must be valid UTF-8."
216
271
  raise click.ClickException(message=msg) from None
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2026.10.1.1'
22
- __version_tuple__ = version_tuple = (2026, 10, 1, 1)
21
+ __version__ = version = '2026.10.7'
22
+ __version_tuple__ = version_tuple = (2026, 10, 7)
23
23
 
24
- __commit_id__ = commit_id = 'gb7845e7c1'
24
+ __commit_id__ = commit_id = 'g3aa79e260'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hackerrank-cli
3
- Version: 2026.10.1.1
3
+ Version: 2026.10.7
4
4
  Summary: Upload starter files to existing HackerRank project questions.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -44,9 +44,16 @@ Install from PyPI:
44
44
 
45
45
  uv tool install hackerrank-cli
46
46
  hackerrank --help
47
- hackerrank --version
48
47
 
49
- You can also install with ``pip install hackerrank-cli``.
48
+ Or install the checkout for development:
49
+
50
+ .. code-block:: console
51
+
52
+ uv sync --locked
53
+ uv run hackerrank --help
54
+ uv run python -m hackerrank_cli --version
55
+
56
+ ``pip install hackerrank-cli`` is also supported.
50
57
 
51
58
  Usage
52
59
  -----
@@ -86,6 +93,27 @@ repeatable upload.
86
93
  The CLI adds no separate retry loop.
87
94
  Each attempt uses the same prepared archive bytes.
88
95
 
96
+ Offline archives
97
+ ----------------
98
+
99
+ Export the same project ZIP locally without a question ID, credentials, or
100
+ network access:
101
+
102
+ .. code-block:: console
103
+
104
+ hackerrank questions archive --directory ./starter --exclude '*.zip' --output ./starter.zip
105
+ hackerrank questions archive --file ./starter.py --output - > ./starter.zip
106
+
107
+ ``--output`` is required.
108
+ A file destination must not already exist; ``-`` writes only ZIP bytes to
109
+ standard output.
110
+ Shell redirection controls whether its destination is overwritten.
111
+ Source validation and packaging finish before the output is opened.
112
+ The source options, ignore rules, file contents, and executable permissions
113
+ match ``questions upload``.
114
+ Consumers can extract this archive for local checks, then send its bytes with
115
+ the SDK's ``questions.upload_project_zip`` method.
116
+
89
117
  Directory selection
90
118
  -------------------
91
119
 
@@ -136,8 +164,7 @@ Development and distribution
136
164
  Development checks, release setup, and build instructions for Docker, Nix, and
137
165
  standalone binaries are in ``docs/source/development.rst``.
138
166
  The generated CLI reference is in ``docs/source/cli.rst``.
139
- Prebuilt binaries are available from
140
- `GitHub Releases <https://github.com/adamtheturtle/hackerrank-cli/releases>`_.
167
+ There are no prebuilt artifacts yet.
141
168
 
142
169
  See ``docs/source/migration.rst`` for migrating an existing shell uploader and
143
170
  keeping repository-specific snippet preparation outside this CLI.
@@ -55,10 +55,12 @@ src/hackerrank_cli.egg-info/top_level.txt
55
55
  styles/snapper/LongProseLine.yml
56
56
  styles/snapper/SemanticLineBreaks.yml
57
57
  tests/__init__.py
58
+ tests/test_archive.py
58
59
  tests/test_cli.py
59
60
  tests/test_help.py
60
61
  tests/test_http.py
61
62
  tests/test_sources.py
63
+ tests/test_help/test_help_archive_.txt
62
64
  tests/test_help/test_help_questions_.txt
63
65
  tests/test_help/test_help_root_.txt
64
66
  tests/test_help/test_help_upload_.txt
@@ -45,8 +45,10 @@
45
45
  "styles/snapper/LongProseLine.yml",
46
46
  "styles/snapper/SemanticLineBreaks.yml",
47
47
  "tests/__init__.py",
48
+ "tests/test_archive.py",
48
49
  "tests/test_cli.py",
49
50
  "tests/test_help.py",
51
+ "tests/test_help/test_help_archive_.txt",
50
52
  "tests/test_help/test_help_questions_.txt",
51
53
  "tests/test_help/test_help_root_.txt",
52
54
  "tests/test_help/test_help_upload_.txt",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2026.10.7",
3
+ "distance": 0,
4
+ "node": "g3aa79e260810c5ec602dd26903495b5f856d4022",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-07"
8
+ }
@@ -0,0 +1,145 @@
1
+ """Validate offline archives against the bytes sent through the SDK."""
2
+
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+ from zipfile import ZipFile
6
+
7
+ import pytest
8
+ from click.testing import CliRunner
9
+
10
+ from hackerrank_cli import create_cli
11
+ from tests.test_cli import USAGE_ERROR, recording_transport
12
+
13
+
14
+ def _entries(contents: bytes) -> dict[str, tuple[bytes, int]]:
15
+ """Read file contents and stored permissions from a project ZIP."""
16
+ with ZipFile(file=BytesIO(initial_bytes=contents)) as archive:
17
+ return {
18
+ info.filename: (archive.read(name=info), info.external_attr >> 16)
19
+ for info in archive.infolist()
20
+ }
21
+
22
+
23
+ @pytest.mark.parametrize(argnames="single_file", argvalues=[False, True])
24
+ @pytest.mark.parametrize(argnames="stdout", argvalues=[False, True])
25
+ def test_export_matches_upload(
26
+ tmp_path: Path, *, single_file: bool, stdout: bool
27
+ ) -> None:
28
+ """Export the same bytes and permissions as an upload, without a
29
+ key.
30
+ """
31
+ source = tmp_path / "starter"
32
+ source.mkdir()
33
+ script = source / "run.sh"
34
+ contents = b"#!/bin/sh\r\n# input-begin\r\n\x00\xff \r\n"
35
+ _ = script.write_bytes(data=contents)
36
+ script.chmod(mode=0o755)
37
+ if single_file:
38
+ options = ["--file", str(object=script)]
39
+ else:
40
+ _ = (source / ".gitignore").write_text(data="ignored.txt\n")
41
+ _ = (source / "ignored.txt").write_text(data="ignored")
42
+ _ = (source / "metadata.yml").write_text(data="excluded")
43
+ options = [
44
+ "--directory",
45
+ str(object=source),
46
+ "--exclude",
47
+ "metadata.yml",
48
+ "--exclude",
49
+ ".gitignore",
50
+ ]
51
+ destination = tmp_path / "starter.zip"
52
+ output = "-" if stdout else str(object=destination)
53
+ transport = recording_transport(status=200, failure=None)
54
+ cli = create_cli(transport=transport)
55
+ result = CliRunner().invoke(
56
+ cli=cli,
57
+ args=["questions", "archive", *options, "--output", output],
58
+ env={"HACKERRANK_API_TOKEN": None},
59
+ )
60
+ assert result.exit_code == 0, result.stderr
61
+ assert result.stderr == ""
62
+ assert transport.requests == []
63
+ exported = result.stdout_bytes if stdout else destination.read_bytes()
64
+ assert _entries(contents=exported) == {
65
+ "run.sh": (contents, script.stat().st_mode)
66
+ }
67
+ uploaded = CliRunner().invoke(
68
+ cli=cli,
69
+ args=["questions", "upload", "123456", *options],
70
+ env={"HACKERRANK_API_TOKEN": "synthetic-secret"},
71
+ )
72
+ assert uploaded.exit_code == 0, uploaded.output
73
+ [(_, _, _, files)] = transport.requests
74
+ assert files is not None
75
+ assert _entries(contents=files["file"][1]) == _entries(contents=exported)
76
+
77
+
78
+ @pytest.mark.parametrize(
79
+ argnames="options",
80
+ argvalues=[
81
+ [],
82
+ ["--directory", ".", "--file", "example.py"],
83
+ ["--file", "example.py", "--exclude", "*.zip"],
84
+ ],
85
+ )
86
+ def test_invalid_source_options(tmp_path: Path, options: list[str]) -> None:
87
+ """Reject ambiguous sources before creating an output file."""
88
+ output = tmp_path / "archive.zip"
89
+ result = CliRunner().invoke(
90
+ cli=create_cli(),
91
+ args=[
92
+ "questions",
93
+ "archive",
94
+ *options,
95
+ "--output",
96
+ str(object=output),
97
+ ],
98
+ )
99
+ assert result.exit_code == USAGE_ERROR
100
+ assert output.exists() is False
101
+
102
+
103
+ @pytest.mark.parametrize(argnames="existing", argvalues=[False, True])
104
+ def test_output_errors(tmp_path: Path, *, existing: bool) -> None:
105
+ """Do not overwrite an existing file, and report missing parents."""
106
+ source = tmp_path / "starter.py"
107
+ _ = source.write_bytes(data=b"pass\n")
108
+ output = source if existing else tmp_path / "missing" / "archive.zip"
109
+ result = CliRunner().invoke(
110
+ cli=create_cli(),
111
+ args=[
112
+ "questions",
113
+ "archive",
114
+ "--file",
115
+ str(object=source),
116
+ "--output",
117
+ str(object=output),
118
+ ],
119
+ env={"HACKERRANK_API_TOKEN": None},
120
+ )
121
+ assert result.exit_code == 1
122
+ assert source.read_bytes() == b"pass\n"
123
+ assert "Error:" in result.stderr
124
+
125
+
126
+ def test_preparation_failure(tmp_path: Path) -> None:
127
+ """An empty selection leaves no archive behind."""
128
+ source = tmp_path / "empty"
129
+ source.mkdir()
130
+ output = tmp_path / "archive.zip"
131
+ result = CliRunner().invoke(
132
+ cli=create_cli(),
133
+ args=[
134
+ "questions",
135
+ "archive",
136
+ "--directory",
137
+ str(object=source),
138
+ "--output",
139
+ str(object=output),
140
+ ],
141
+ env={"HACKERRANK_API_TOKEN": None},
142
+ )
143
+ assert result.exit_code == 1
144
+ assert result.stderr == "Error: No files selected for upload.\n"
145
+ assert output.exists() is False
@@ -0,0 +1,11 @@
1
+ Usage: hackerrank questions archive [OPTIONS]
2
+
3
+ Export a project ZIP without credentials or network access.
4
+
5
+ Options:
6
+ --directory PATH Project directory.
7
+ --file PATH One starter file, packaged as a single-file project archive.
8
+ --exclude TEXT Additional Gitignore pattern at the upload root. Repeatable.
9
+ --output FILE New ZIP file, or - for binary standard output. Never
10
+ overwrites files. [required]
11
+ --help Show this message and exit.
@@ -6,4 +6,5 @@ Options:
6
6
  --help Show this message and exit.
7
7
 
8
8
  Commands:
9
- upload Replace project starter files while preserving question metadata.
9
+ archive Export a project ZIP without credentials or network access.
10
+ upload Replace project starter files while preserving question metadata.
@@ -12,8 +12,13 @@ from hackerrank_cli import main
12
12
 
13
13
  @pytest.mark.parametrize(
14
14
  argnames="arguments",
15
- argvalues=[[], ["questions"], ["questions", "upload"]],
16
- ids=["root", "questions", "upload"],
15
+ argvalues=[
16
+ [],
17
+ ["questions"],
18
+ ["questions", "upload"],
19
+ ["questions", "archive"],
20
+ ],
21
+ ids=["root", "questions", "upload", "archive"],
17
22
  )
18
23
  def test_help(
19
24
  arguments: list[str], file_regression: FileRegressionFixture
@@ -773,6 +773,7 @@ dev = [
773
773
  { name = "furo" },
774
774
  { name = "interrogate" },
775
775
  { name = "mypy" },
776
+ { name = "mypy-pytest-parametrize" },
776
777
  { name = "mypy-strict-kwargs" },
777
778
  { name = "no-defaults" },
778
779
  { name = "prek" },
@@ -830,7 +831,8 @@ dev = [
830
831
  { name = "doccmd", specifier = "==2026.9.16.1" },
831
832
  { name = "furo", specifier = "==2025.12.19" },
832
833
  { name = "interrogate", specifier = "==1.7.0" },
833
- { name = "mypy", specifier = "==2.3.1" },
834
+ { name = "mypy", specifier = "==2.4.0" },
835
+ { name = "mypy-pytest-parametrize", specifier = "==2026.10.5" },
834
836
  { name = "mypy-strict-kwargs", specifier = "==2026.8.25.1" },
835
837
  { name = "no-defaults", specifier = "==2026.9.1" },
836
838
  { name = "prek", specifier = "==0.5.4" },
@@ -1325,7 +1327,7 @@ wheels = [
1325
1327
 
1326
1328
  [[package]]
1327
1329
  name = "mypy"
1328
- version = "2.3.1"
1330
+ version = "2.4.0"
1329
1331
  source = { registry = "https://pypi.org/simple" }
1330
1332
  dependencies = [
1331
1333
  { name = "ast-serialize" },
@@ -1334,41 +1336,41 @@ dependencies = [
1334
1336
  { name = "pathspec" },
1335
1337
  { name = "typing-extensions" },
1336
1338
  ]
1337
- sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" }
1338
- wheels = [
1339
- { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" },
1340
- { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" },
1341
- { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" },
1342
- { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" },
1343
- { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" },
1344
- { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" },
1345
- { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" },
1346
- { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" },
1347
- { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" },
1348
- { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" },
1349
- { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" },
1350
- { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" },
1351
- { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" },
1352
- { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" },
1353
- { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" },
1354
- { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" },
1355
- { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" },
1356
- { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" },
1357
- { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" },
1358
- { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" },
1359
- { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" },
1360
- { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" },
1361
- { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" },
1362
- { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" },
1363
- { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" },
1364
- { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" },
1365
- { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" },
1366
- { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" },
1367
- { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" },
1368
- { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" },
1369
- { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" },
1370
- { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" },
1371
- { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" },
1339
+ sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" }
1340
+ wheels = [
1341
+ { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" },
1342
+ { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" },
1343
+ { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" },
1344
+ { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" },
1345
+ { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" },
1346
+ { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" },
1347
+ { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" },
1348
+ { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" },
1349
+ { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" },
1350
+ { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" },
1351
+ { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" },
1352
+ { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" },
1353
+ { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" },
1354
+ { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" },
1355
+ { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" },
1356
+ { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" },
1357
+ { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" },
1358
+ { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" },
1359
+ { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" },
1360
+ { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" },
1361
+ { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" },
1362
+ { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" },
1363
+ { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" },
1364
+ { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" },
1365
+ { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" },
1366
+ { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" },
1367
+ { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" },
1368
+ { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" },
1369
+ { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" },
1370
+ { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" },
1371
+ { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" },
1372
+ { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" },
1373
+ { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" },
1372
1374
  ]
1373
1375
 
1374
1376
  [[package]]
@@ -1380,6 +1382,20 @@ wheels = [
1380
1382
  { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
1381
1383
  ]
1382
1384
 
1385
+ [[package]]
1386
+ name = "mypy-pytest-parametrize"
1387
+ version = "2026.10.5"
1388
+ source = { registry = "https://pypi.org/simple" }
1389
+ dependencies = [
1390
+ { name = "mypy" },
1391
+ { name = "pytest" },
1392
+ { name = "typing-extensions" },
1393
+ ]
1394
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/14/5adf231330daad6ec6e5f1c3433ec181ac554d75b3e0d4bc51a54576a371/mypy_pytest_parametrize-2026.10.5.tar.gz", hash = "sha256:a4776e5b13816a40b7677b6e9d777d8fa75f49bc08e656731235b4e66071bb74", size = 27576, upload-time = "2026-10-05T11:59:42.493Z" }
1395
+ wheels = [
1396
+ { url = "https://files.pythonhosted.org/packages/47/3e/78fc9b9351a6b0dcf398bab37467b043928927caf78481d9d0bcbe1c9700/mypy_pytest_parametrize-2026.10.5-py3-none-any.whl", hash = "sha256:b4ac6fbae8ee9b5b4d3200d45393d322ede45bdda677030612d64bff0dfb05c0", size = 8654, upload-time = "2026-10-05T11:59:41.282Z" },
1397
+ ]
1398
+
1383
1399
  [[package]]
1384
1400
  name = "mypy-strict-kwargs"
1385
1401
  version = "2026.8.25.1"
@@ -1 +0,0 @@
1
- 2026.10.1.1
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2026.10.1.1",
3
- "distance": 0,
4
- "node": "gb7845e7c18148320bd0922d62c89ef9a77fe502b",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-01"
8
- }