hackerrank-cli 2026.10.1.1__tar.gz → 2026.10.8__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 (73) hide show
  1. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/ci.yml +4 -0
  2. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/release.yml +10 -4
  3. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/MANIFEST.in +4 -2
  4. {hackerrank_cli-2026.10.1.1/src/hackerrank_cli.egg-info → hackerrank_cli-2026.10.8}/PKG-INFO +32 -5
  5. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/README.rst +31 -4
  6. hackerrank_cli-2026.10.8/VERSION +1 -0
  7. hackerrank_cli-2026.10.1.1/CHANGELOG.rst → hackerrank_cli-2026.10.8/docs/source/changelog/2026.10.1.1.md +2 -11
  8. hackerrank_cli-2026.10.8/docs/source/changelog/2026.10.7.md +7 -0
  9. hackerrank_cli-2026.10.8/docs/source/changelog/2026.10.8.md +6 -0
  10. hackerrank_cli-2026.10.8/docs/source/changelog.md +11 -0
  11. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/source/conf.py +1 -1
  12. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/source/development.rst +14 -6
  13. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/source/migration.rst +23 -2
  14. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/prek.toml +2 -2
  15. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/pyproject.toml +12 -7
  16. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli/__init__.py +84 -29
  17. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli/_version.py +3 -3
  18. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8/src/hackerrank_cli.egg-info}/PKG-INFO +32 -5
  19. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli.egg-info/SOURCES.txt +6 -2
  20. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli.egg-info/scm_file_list.json +6 -2
  21. hackerrank_cli-2026.10.8/src/hackerrank_cli.egg-info/scm_version.json +8 -0
  22. hackerrank_cli-2026.10.8/tests/test_archive.py +145 -0
  23. hackerrank_cli-2026.10.8/tests/test_help/test_help_archive_.txt +11 -0
  24. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/tests/test_help/test_help_questions_.txt +2 -1
  25. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/tests/test_help.py +7 -2
  26. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/uv.lock +55 -37
  27. hackerrank_cli-2026.10.1.1/VERSION +0 -1
  28. hackerrank_cli-2026.10.1.1/docs/source/changelog.rst +0 -1
  29. hackerrank_cli-2026.10.1.1/src/hackerrank_cli.egg-info/scm_version.json +0 -8
  30. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.dockerignore +0 -0
  31. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.gitattributes +0 -0
  32. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/dependabot.yml +0 -0
  33. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/autofix.yml +0 -0
  34. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/binaries.yml +0 -0
  35. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/dependabot-merge.yml +0 -0
  36. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/docs.yml +0 -0
  37. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/lint.yml +0 -0
  38. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/nix.yml +0 -0
  39. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.github/workflows/publish-site.yml +0 -0
  40. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.gitignore +0 -0
  41. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/.vale.ini +0 -0
  42. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/Dockerfile +0 -0
  43. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/LICENSE +0 -0
  44. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/bin/__init__.py +0 -0
  45. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/bin/hackerrank_wrapper.py +0 -0
  46. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/__init__.py +0 -0
  47. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/source/__init__.py +0 -0
  48. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/source/cli.rst +0 -0
  49. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/source/index.rst +0 -0
  50. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/source/macos-releases.rst +0 -0
  51. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/docs/source/usage.rst +0 -0
  52. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/flake.lock +0 -0
  53. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/flake.nix +0 -0
  54. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/newsfragments/.gitkeep +0 -0
  55. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/setup.cfg +0 -0
  56. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/spelling_private_dict.txt +0 -0
  57. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli/__main__.py +0 -0
  58. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli/_sources.py +0 -0
  59. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli/py.typed +0 -0
  60. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli.egg-info/dependency_links.txt +0 -0
  61. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli.egg-info/entry_points.txt +0 -0
  62. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli.egg-info/not-zip-safe +0 -0
  63. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli.egg-info/requires.txt +0 -0
  64. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/src/hackerrank_cli.egg-info/top_level.txt +0 -0
  65. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/styles/snapper/LongProseLine.yml +0 -0
  66. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/styles/snapper/SemanticLineBreaks.yml +0 -0
  67. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/tests/__init__.py +0 -0
  68. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/tests/test_cli.py +0 -0
  69. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/tests/test_help/test_help_root_.txt +0 -0
  70. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/tests/test_help/test_help_upload_.txt +0 -0
  71. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/tests/test_http.py +0 -0
  72. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/tests/test_sources.py +0 -0
  73. {hackerrank_cli-2026.10.1.1 → hackerrank_cli-2026.10.8}/zizmor.yml +0 -0
@@ -44,6 +44,10 @@ jobs:
44
44
  - run: uv build
45
45
  - run: uv run --locked twine check dist/*
46
46
  - run: uv run --locked check-wheel-contents dist/*.whl
47
+ - name: Check release notes exist
48
+ run: |
49
+ RELEASE_VERSION="$(cat VERSION)"
50
+ test -s "docs/source/changelog/$RELEASE_VERSION.md"
47
51
  - name: Smoke installed wheel
48
52
  run: |
49
53
  uv venv /tmp/hackerrank-wheel
@@ -27,10 +27,18 @@ jobs:
27
27
  - run: uv build
28
28
  - run: uv run --locked twine check dist/*
29
29
  - run: uv run --locked check-wheel-contents dist/*.whl
30
+ - name: Select the tagged release notes
31
+ env:
32
+ RELEASE_VERSION: ${{ github.ref_name }}
33
+ run: cp "docs/source/changelog/$RELEASE_VERSION.md" release-notes.md
30
34
  - uses: actions/upload-artifact@v7
31
35
  with:
32
36
  name: distributions
33
37
  path: dist/*
38
+ - uses: actions/upload-artifact@v7
39
+ with:
40
+ name: release-notes
41
+ path: release-notes.md
34
42
  binaries:
35
43
  # actionlint 1.7.12 accepts ./ for local workflows and rejects $/.
36
44
  uses: ./.github/workflows/binaries.yml # zizmor: ignore[self-repository]
@@ -60,18 +68,16 @@ jobs:
60
68
  permissions:
61
69
  contents: write
62
70
  steps:
63
- - uses: actions/checkout@v7
64
- with:
65
- persist-credentials: false
66
71
  - uses: actions/download-artifact@v8
67
72
  with:
68
73
  path: artifacts
69
74
  - name: Attach distributions and binaries
70
75
  env:
76
+ GH_REPO: ${{ github.repository }}
71
77
  GH_TOKEN: ${{ github.token }}
72
78
  TAG: ${{ github.ref_name }}
73
79
  run: gh release create "$TAG" artifacts/distributions/* artifacts/hackerrank-*/*
74
- --title "$TAG" --notes-file CHANGELOG.rst --verify-tag
80
+ --title "$TAG" --notes-file artifacts/release-notes/release-notes.md --verify-tag
75
81
  docker:
76
82
  needs: publish
77
83
  runs-on: ubuntu-latest
@@ -1,6 +1,6 @@
1
- include LICENSE README.rst CHANGELOG.rst VERSION
1
+ include LICENSE README.rst
2
2
  recursive-include src *.py *.typed
3
- recursive-include docs *.rst *.py
3
+ recursive-include docs *.rst *.md *.py
4
4
  recursive-include newsfragments *
5
5
  include .vale.ini prek.toml spelling_private_dict.txt zizmor.yml
6
6
  recursive-include styles/snapper *.yml
@@ -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.8
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.8
@@ -1,15 +1,6 @@
1
- Changelog
2
- =========
1
+ # 2026.10.1.1 (2026-10-01)
3
2
 
4
- Releases are assembled from ``newsfragments/`` using Towncrier.
5
-
6
- .. towncrier release notes start
7
-
8
- 2026.10.1.1 (2026-10-01)
9
- ------------------------
10
-
11
- Features
12
- ~~~~~~~~
3
+ ## Features
13
4
 
14
5
  - Upload existing HackerRank project-question starter files from a directory or
15
6
  a single file, with nested ignore rules, configurable exclusions, offline dry
@@ -0,0 +1,7 @@
1
+ # 2026.10.7 (2026-10-07)
2
+
3
+ ## Features
4
+
5
+ - Export project ZIP archives offline with `hackerrank questions archive`,
6
+ using the same file selection and packaging as uploads.
7
+ ([#6](https://github.com/adamtheturtle/hackerrank-cli/issues/6))
@@ -0,0 +1,6 @@
1
+ # 2026.10.8 (2026-10-08)
2
+
3
+ ## Bug fixes
4
+
5
+ - Render GitHub release notes correctly and include only that version's
6
+ changes.
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ Releases are assembled from `newsfragments/` using Towncrier.
4
+
5
+ ```{toctree}
6
+ :maxdepth: 1
7
+ :glob:
8
+ :reversed:
9
+
10
+ changelog/*
11
+ ```
@@ -5,7 +5,7 @@ import importlib.metadata
5
5
  project = "hackerrank-cli"
6
6
  author = "Adam Dangoor"
7
7
  release = importlib.metadata.version(distribution_name="hackerrank-cli")
8
- extensions = ["sphinx_click.ext", "sphinxcontrib.spelling"]
8
+ extensions = ["myst_parser", "sphinx_click.ext", "sphinxcontrib.spelling"]
9
9
  html_theme = "furo"
10
10
  nitpicky = True
11
11
  html_theme_options = {
@@ -40,19 +40,27 @@ The upload tests so assert the released SDK's actual wire behavior instead of
40
40
  inventing an OpenAPI schema.
41
41
  No separate mock-spec repository is needed for this scope.
42
42
 
43
- Add a Towncrier feature or bugfix fragment for each user-visible change.
43
+ Add a Towncrier feature or bugfix fragment in Markdown for each user-visible
44
+ change, such as ``newsfragments/123.bugfix.md``.
44
45
 
45
46
  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.
47
+ Untagged builds have a development version.
48
+ Use date-based tags such as ``2026.10.7`` for releases.
49
49
  Before tagging a release, assemble notes with
50
- ``uv run towncrier build --yes --version VERSION``, commit the changelog, then
51
- write the same version to ``VERSION``, commit it, then tag the commit.
50
+ ``uv run towncrier build --yes --version VERSION``, commit the generated notes
51
+ in ``docs/source/changelog/VERSION.md``, then write the same version to
52
+ ``VERSION``, commit it, then tag the commit.
52
53
  Pushing a tag is the explicit publication trigger.
53
54
  The release workflow builds and checks packages, builds three standalone
54
55
  binaries, and then publishes to PyPI, GitHub Releases, and GHCR.
55
56
 
57
+ Towncrier writes a separate Markdown file for each release.
58
+ Sphinx reads those files through MyST and lists them in the changelog.
59
+ The release workflow uploads the tagged version's file directly as its GitHub
60
+ release notes.
61
+ Pull requests check that the notes for ``VERSION`` exist.
62
+ Review the generated Markdown before pushing a release tag.
63
+
56
64
  Local builds
57
65
  ------------
58
66
 
@@ -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.
@@ -597,7 +597,7 @@ hooks = [
597
597
  name = "linkcheck",
598
598
  entry = "uv run --locked --group=dev sphinx-build -M linkcheck docs/source docs/build -W --keep-going -d docs/build/.doctrees-linkcheck",
599
599
  language = "system",
600
- types_or = ["rst"],
600
+ types_or = ["rst", "markdown"],
601
601
  stages = ["manual"],
602
602
  groups = ["docs"],
603
603
  priority = 610,
@@ -609,7 +609,7 @@ hooks = [
609
609
  name = "spelling",
610
610
  entry = "uv run --locked --group=dev sphinx-build -M spelling docs/source docs/build -W -d docs/build/.doctrees-spelling",
611
611
  language = "system",
612
- types_or = ["rst"],
612
+ types_or = ["rst", "markdown"],
613
613
  stages = ["manual"],
614
614
  groups = ["docs"],
615
615
  priority = 610,
@@ -37,8 +37,10 @@ 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",
43
+ "myst-parser==5.1.0",
42
44
  "no-defaults==2026.9.1",
43
45
  "prek==0.5.4",
44
46
  "pydocstringformatter==1.0.0",
@@ -198,7 +200,8 @@ exclude_gitignore = true
198
200
  follow_untyped_imports = true
199
201
  python_version = "3.13"
200
202
  strict = true
201
- plugins = [ "mypy_strict_kwargs" ]
203
+ plugins = [ "mypy_pytest_parametrize", "mypy_strict_kwargs" ]
204
+ num_workers = "auto"
202
205
 
203
206
  [tool.pyrefly]
204
207
  project-excludes = [ "src/hackerrank_cli/_version.py" ]
@@ -224,23 +227,25 @@ analysis.strict-generic-narrowing = true
224
227
 
225
228
  [tool.pytest]
226
229
  ini_options.testpaths = [ "tests" ]
227
- ini_options.addopts = "-n auto --cov=hackerrank_cli --cov-branch --cov-fail-under=100"
230
+ ini_options.addopts = "-n auto --cov --cov-config=pyproject.toml"
228
231
  ini_options.xfail_strict = true
229
232
  ini_options.beartype_packages = [ "hackerrank_cli" ]
230
233
  ini_options.beartype_test_fixtures = true
231
234
  ini_options.beartype_tests = true
232
235
 
233
236
  [tool.coverage]
237
+ run.source = [ "hackerrank_cli" ]
234
238
  run.omit = [ "src/hackerrank_cli/__main__.py", "src/hackerrank_cli/_version.py" ]
235
239
  run.branch = true
240
+ report.fail_under = 100
236
241
  report.show_missing = true
237
242
 
238
243
  [tool.towncrier]
239
244
  directory = "newsfragments"
240
- filename = "CHANGELOG.rst"
241
- title_format = "{version} ({project_date})"
242
- issue_format = "#{issue}"
243
- underlines = [ "-", "~", "^" ]
245
+ filename = "docs/source/changelog/{version}.md"
246
+ title_format = "# {version} ({project_date})"
247
+ issue_format = "[#{issue}](https://github.com/adamtheturtle/hackerrank-cli/issues/{issue})"
248
+ single_file = false
244
249
  ignore = [ ".gitkeep" ]
245
250
  type = [
246
251
  { directory = "feature", name = "Features", showcontent = true },
@@ -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.8'
22
+ __version_tuple__ = version_tuple = (2026, 10, 8)
23
23
 
24
- __commit_id__ = commit_id = 'gb7845e7c1'
24
+ __commit_id__ = commit_id = 'gfb4c14cb0'
@@ -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.8
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.
@@ -2,7 +2,6 @@
2
2
  .gitattributes
3
3
  .gitignore
4
4
  .vale.ini
5
- CHANGELOG.rst
6
5
  Dockerfile
7
6
  LICENSE
8
7
  MANIFEST.in
@@ -29,7 +28,7 @@ bin/__init__.py
29
28
  bin/hackerrank_wrapper.py
30
29
  docs/__init__.py
31
30
  docs/source/__init__.py
32
- docs/source/changelog.rst
31
+ docs/source/changelog.md
33
32
  docs/source/cli.rst
34
33
  docs/source/conf.py
35
34
  docs/source/development.rst
@@ -37,6 +36,9 @@ docs/source/index.rst
37
36
  docs/source/macos-releases.rst
38
37
  docs/source/migration.rst
39
38
  docs/source/usage.rst
39
+ docs/source/changelog/2026.10.1.1.md
40
+ docs/source/changelog/2026.10.7.md
41
+ docs/source/changelog/2026.10.8.md
40
42
  newsfragments/.gitkeep
41
43
  src/hackerrank_cli/__init__.py
42
44
  src/hackerrank_cli/__main__.py
@@ -55,10 +57,12 @@ src/hackerrank_cli.egg-info/top_level.txt
55
57
  styles/snapper/LongProseLine.yml
56
58
  styles/snapper/SemanticLineBreaks.yml
57
59
  tests/__init__.py
60
+ tests/test_archive.py
58
61
  tests/test_cli.py
59
62
  tests/test_help.py
60
63
  tests/test_http.py
61
64
  tests/test_sources.py
65
+ tests/test_help/test_help_archive_.txt
62
66
  tests/test_help/test_help_questions_.txt
63
67
  tests/test_help/test_help_root_.txt
64
68
  tests/test_help/test_help_upload_.txt