hackerrank-cli 2026.10.1.1__py3-none-any.whl

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.
@@ -0,0 +1,224 @@
1
+ """Upload project starter code using the released HackerRank SDK."""
2
+
3
+ import os
4
+ from importlib.metadata import version
5
+ from io import BytesIO
6
+ from pathlib import Path
7
+ from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
8
+
9
+ import click
10
+ import httpx
11
+ import httpx2
12
+ from hackerrank.client import HackerRank
13
+ from hackerrank.exceptions import HackerRankError
14
+ from hackerrank.transports import Transport
15
+
16
+ from hackerrank_cli._sources import PreparedSource, prepare_source
17
+
18
+
19
+ def create_cli(*, transport: Transport | None = None) -> click.Group:
20
+ """Create the CLI, optionally using a supported SDK transport."""
21
+
22
+ @click.group(name="hackerrank")
23
+ @click.version_option(
24
+ version=version(distribution_name="hackerrank-cli"),
25
+ prog_name="hackerrank",
26
+ )
27
+ def cli() -> None:
28
+ """Upload starter files to existing HackerRank project
29
+ questions.
30
+ """
31
+
32
+ @cli.group()
33
+ def questions() -> None:
34
+ """Manage project-question starter files."""
35
+
36
+ @click.argument("question_id")
37
+ @click.option(
38
+ "--directory",
39
+ type=click.Path(path_type=Path, readable=False),
40
+ help="Project directory.",
41
+ )
42
+ @click.option(
43
+ "--file",
44
+ "source_file",
45
+ type=click.Path(path_type=Path, readable=False),
46
+ help="One starter file, uploaded as a single-file project archive.",
47
+ )
48
+ @click.option(
49
+ "--exclude",
50
+ multiple=True,
51
+ help="Additional Gitignore pattern at the upload root. Repeatable.",
52
+ )
53
+ @click.option(
54
+ "--dry-run",
55
+ is_flag=True,
56
+ help="Validate and list files without credentials or network access.",
57
+ )
58
+ @click.option(
59
+ "--retries",
60
+ type=click.IntRange(min=0),
61
+ default=0,
62
+ show_default=True,
63
+ help="Retry count delegated to the SDK for this repeatable upload.",
64
+ )
65
+ def upload( # noqa: PLR0913 - Click command options.
66
+ question_id: str,
67
+ directory: Path | None,
68
+ source_file: Path | None,
69
+ exclude: tuple[str, ...],
70
+ retries: int,
71
+ *,
72
+ dry_run: bool,
73
+ ) -> None:
74
+ """Replace project starter files while preserving question
75
+ metadata.
76
+ """
77
+ _upload(
78
+ question_id=question_id,
79
+ directory=directory,
80
+ source_file=source_file,
81
+ exclude=exclude,
82
+ retries=retries,
83
+ dry_run=dry_run,
84
+ transport=transport,
85
+ )
86
+
87
+ _ = questions.command()(upload)
88
+ return cli
89
+
90
+
91
+ def _validate_upload(
92
+ question_id: str,
93
+ directory: Path | None,
94
+ source_file: Path | None,
95
+ exclude: tuple[str, ...],
96
+ ) -> None:
97
+ """Validate command combinations and the question identifier."""
98
+ if (directory is None) == (source_file is None):
99
+ msg = "Provide exactly one of --directory or --file."
100
+ 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
+ if len(exclude) > 0 and directory is None:
109
+ msg = "--exclude requires --directory."
110
+ raise click.UsageError(message=msg)
111
+
112
+
113
+ def _api_key() -> str:
114
+ """Read a nonempty API token without printing its value."""
115
+ api_key = os.environ.get(key="HACKERRANK_API_TOKEN")
116
+ if api_key is None or api_key.strip() == "":
117
+ msg = (
118
+ "HACKERRANK_API_TOKEN is missing or empty. "
119
+ "Set it in the environment."
120
+ )
121
+ raise click.ClickException(message=msg)
122
+ return api_key
123
+
124
+
125
+ def _project_zip(source: PreparedSource) -> bytes:
126
+ """Package prepared starter files for the released SDK's ZIP API."""
127
+ with BytesIO() as buffer:
128
+ with ZipFile(
129
+ file=buffer, mode="w", compression=ZIP_DEFLATED
130
+ ) as archive:
131
+ if source.directory is None:
132
+ info = ZipInfo(filename=Path(source.files[0]).name)
133
+ info.external_attr = source.file_mode << 16
134
+ archive.writestr(
135
+ zinfo_or_arcname=info,
136
+ data=source.contents,
137
+ compress_type=ZIP_DEFLATED,
138
+ )
139
+ else:
140
+ for name in source.files:
141
+ archive.write(
142
+ filename=source.directory / name, arcname=name
143
+ )
144
+ return buffer.getvalue()
145
+
146
+
147
+ def _send_upload(
148
+ question_id: str,
149
+ archive: bytes,
150
+ retries: int,
151
+ transport: Transport | None,
152
+ ) -> None:
153
+ """Delegate communication and retries to the SDK; report safe
154
+ errors.
155
+ """
156
+ api_key = _api_key()
157
+ try:
158
+ with HackerRank(
159
+ api_key=api_key, transport=transport, retries=retries
160
+ ) as client:
161
+ _ = client.questions.upload_project_zip(
162
+ question_id=question_id, file=archive
163
+ )
164
+ except HackerRankError as error:
165
+ # Response bodies can contain secrets; only report the status.
166
+ msg = f"HackerRank rejected the upload (HTTP {error.status_code})."
167
+ raise click.ClickException(message=msg) from None
168
+ except (httpx.TransportError, httpx2.TransportError):
169
+ msg = "Could not reach HackerRank. Check your network connection."
170
+ raise click.ClickException(message=msg) from None
171
+ except (ValueError, TypeError):
172
+ msg = (
173
+ "HackerRank returned an invalid response. The upload may have "
174
+ "succeeded; check the question before trying again."
175
+ )
176
+ raise click.ClickException(message=msg) from None
177
+
178
+
179
+ def _upload( # noqa: PLR0913 - Click options plus the SDK transport boundary.
180
+ question_id: str,
181
+ directory: Path | None,
182
+ source_file: Path | None,
183
+ exclude: tuple[str, ...],
184
+ retries: int,
185
+ *,
186
+ dry_run: bool,
187
+ transport: Transport | None,
188
+ ) -> None:
189
+ """Prepare every byte before credentials or a network mutation."""
190
+ _validate_upload(
191
+ question_id=question_id,
192
+ directory=directory,
193
+ source_file=source_file,
194
+ exclude=exclude,
195
+ )
196
+ try:
197
+ with prepare_source(
198
+ 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}")
214
+ except UnicodeError:
215
+ msg = ".gitignore files must be valid UTF-8."
216
+ raise click.ClickException(message=msg) from None
217
+ except OSError as error:
218
+ msg = f"Could not prepare upload files ({type(error).__name__})."
219
+ raise click.ClickException(message=msg) from None
220
+ except ValueError as error:
221
+ raise click.ClickException(message=str(object=error)) from None
222
+
223
+
224
+ main: click.Group = create_cli()
@@ -0,0 +1,5 @@
1
+ """Run the CLI with ``python -m hackerrank_cli``."""
2
+
3
+ from hackerrank_cli import main
4
+
5
+ main()
@@ -0,0 +1,182 @@
1
+ """Prepare local sources before handing them to the SDK."""
2
+
3
+ import stat
4
+ from collections.abc import Generator, Iterator, Sequence
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from shutil import copyfile, copymode
9
+ from tempfile import TemporaryDirectory
10
+
11
+ from pathspec import GitIgnoreSpec
12
+
13
+
14
+ def validate_path(path: Path) -> None:
15
+ """Reject symlinks in the path and its ancestors before resolving
16
+ it.
17
+ """
18
+ for component in (path, *path.parents):
19
+ if component.name.casefold() == ".git":
20
+ msg = f"Git metadata cannot be uploaded: {component}"
21
+ raise ValueError(msg)
22
+ if component.is_symlink():
23
+ msg = f"Symbolic links are not supported: {component}"
24
+ raise ValueError(msg)
25
+
26
+
27
+ def _rules(directory: Path) -> tuple[tuple[Path, GitIgnoreSpec], ...]:
28
+ """Read one directory's ignore rules without following symlinks."""
29
+ ignore_file = directory / ".gitignore"
30
+ if ignore_file.is_symlink():
31
+ validate_path(path=ignore_file)
32
+ if ignore_file.is_file():
33
+ return (
34
+ (
35
+ directory,
36
+ GitIgnoreSpec.from_lines(
37
+ lines=ignore_file.read_text(encoding="utf-8").splitlines(),
38
+ ),
39
+ ),
40
+ )
41
+ return ()
42
+
43
+
44
+ def _inherited_rules(
45
+ directory: Path,
46
+ ) -> tuple[tuple[Path, GitIgnoreSpec], ...] | None:
47
+ """Inherit rules from the nearest Git root, or start at the source."""
48
+ for candidate in (directory, *directory.parents):
49
+ if (candidate / ".git").exists():
50
+ bases = [directory]
51
+ while bases[-1] != candidate:
52
+ bases.append(bases[-1].parent)
53
+ rules: list[tuple[Path, GitIgnoreSpec]] = []
54
+ for base in reversed(bases):
55
+ if _is_ignored(path=base, rules=rules, suffix="/"):
56
+ return None
57
+ rules.extend(_rules(directory=base))
58
+ return tuple(rules)
59
+ return _rules(directory=directory)
60
+
61
+
62
+ def _is_ignored(
63
+ path: Path,
64
+ rules: Sequence[tuple[Path, GitIgnoreSpec]],
65
+ *,
66
+ suffix: str,
67
+ ) -> bool:
68
+ """Apply the last matching rule from the deepest matching ignore
69
+ file.
70
+ """
71
+ ignored = False
72
+ for base, spec in rules:
73
+ match = spec.check_file(
74
+ file=path.relative_to(base).as_posix() + suffix
75
+ )
76
+ if match.include is not None:
77
+ ignored = match.include
78
+ return ignored
79
+
80
+
81
+ def require_regular_file(path: Path) -> None:
82
+ """Reject devices, pipes, and other special files before reading."""
83
+ if not stat.S_ISREG(path.stat().st_mode):
84
+ msg = f"Only regular files are supported: {path}"
85
+ raise ValueError(msg)
86
+
87
+
88
+ def selected_files(
89
+ directory: Path, excludes: tuple[str, ...]
90
+ ) -> tuple[Path, ...]:
91
+ """Select regular files using layered Git ignores and final exclusions."""
92
+ exclusion_spec = GitIgnoreSpec.from_lines(lines=excludes)
93
+
94
+ def walk(
95
+ parent: Path, rules: tuple[tuple[Path, GitIgnoreSpec], ...]
96
+ ) -> Iterator[Path]:
97
+ """Traverse selected directories without following links."""
98
+ for path in sorted(parent.iterdir()):
99
+ if path.name.casefold() == ".git":
100
+ continue
101
+ is_directory = path.is_dir()
102
+ suffix = "/" if is_directory else ""
103
+ relative = path.relative_to(directory).as_posix() + suffix
104
+ if exclusion_spec.match_file(file=relative):
105
+ continue
106
+ if _is_ignored(path=path, rules=rules, suffix=suffix):
107
+ continue
108
+ validate_path(path=path)
109
+ if is_directory:
110
+ yield from walk(
111
+ parent=path, rules=(*rules, *_rules(directory=path))
112
+ )
113
+ else:
114
+ require_regular_file(path=path)
115
+ yield path.relative_to(directory)
116
+
117
+ inherited = _inherited_rules(directory=directory)
118
+ if inherited is None:
119
+ return ()
120
+ return tuple(walk(parent=directory, rules=inherited))
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class PreparedSource:
125
+ """Validated bytes or an isolated directory for ZIP preparation."""
126
+
127
+ contents: bytes
128
+ directory: Path | None
129
+ files: tuple[str, ...]
130
+ file_mode: int
131
+
132
+
133
+ @contextmanager
134
+ def prepare_source(
135
+ *, directory: Path | None, file: Path | None, excludes: tuple[str, ...]
136
+ ) -> Generator[PreparedSource]:
137
+ """Read or stage everything before allowing a network mutation."""
138
+ if file is not None:
139
+ validate_path(path=file.absolute())
140
+ if not file.is_file():
141
+ msg = f"Not a regular file: {file}"
142
+ raise ValueError(msg)
143
+ # Preserve source bytes, including non-UTF-8 files.
144
+ contents = file.read_bytes()
145
+ yield PreparedSource(
146
+ contents=contents,
147
+ directory=None,
148
+ files=(str(object=file),),
149
+ file_mode=file.stat().st_mode,
150
+ )
151
+ return
152
+ if directory is None:
153
+ msg = "Provide exactly one of --directory or --file."
154
+ raise ValueError(msg)
155
+ validate_path(path=directory.absolute())
156
+ directory = directory.resolve(strict=True)
157
+ if not directory.is_dir():
158
+ msg = f"Not a directory: {directory}"
159
+ raise ValueError(msg)
160
+ files = selected_files(directory=directory, excludes=excludes)
161
+ if len(files) == 0:
162
+ msg = "No files selected for upload."
163
+ raise ValueError(msg)
164
+ with TemporaryDirectory(prefix="hackerrank-cli-") as temporary:
165
+ staged = Path(temporary).resolve() / "project"
166
+ staged.mkdir()
167
+ for relative in files:
168
+ source = directory / relative
169
+ validate_path(path=source)
170
+ target = staged / relative
171
+ target.parent.mkdir(parents=True, exist_ok=True)
172
+ # Never dereference a symlink introduced after selection.
173
+ # Validate the copied path before packaging its bytes.
174
+ _ = copyfile(src=source, dst=target, follow_symlinks=False)
175
+ validate_path(path=target)
176
+ copymode(src=source, dst=target, follow_symlinks=False)
177
+ yield PreparedSource(
178
+ contents=b"",
179
+ directory=staged,
180
+ files=tuple(path.as_posix() for path in files),
181
+ file_mode=0,
182
+ )
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '2026.10.1.1'
22
+ __version_tuple__ = version_tuple = (2026, 10, 1, 1)
23
+
24
+ __commit_id__ = commit_id = None
File without changes
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: hackerrank-cli
3
+ Version: 2026.10.1.1
4
+ Summary: Upload starter files to existing HackerRank project questions.
5
+ Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://adamtheturtle.github.io/hackerrank-cli/
8
+ Project-URL: Issues, https://github.com/adamtheturtle/hackerrank-cli/issues
9
+ Project-URL: Source, https://github.com/adamtheturtle/hackerrank-cli
10
+ Keywords: cli,hackerrank,starter-code
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.13
18
+ Description-Content-Type: text/x-rst
19
+ License-File: LICENSE
20
+ Requires-Dist: click<8.6,>=8.3
21
+ Requires-Dist: hackerrank>=2026.9.16
22
+ Requires-Dist: httpx>=0.28
23
+ Requires-Dist: httpx2>=2.12
24
+ Requires-Dist: pathspec>=1
25
+ Dynamic: license-file
26
+
27
+ hackerrank-cli
28
+ ==============
29
+
30
+ Upload starter files to an existing HackerRank for Work project question using
31
+ the ``hackerrank`` Python SDK.
32
+ This independent CLI replaces only the project archive, preserving the
33
+ question's name, problem statement, environment, scoring settings, and other
34
+ metadata.
35
+
36
+ The initial scope is project questions such as fullstack, frontend, and backend
37
+ projects.
38
+ Language-specific coding-question stubs are outside this CLI's initial scope.
39
+
40
+ Requires Python 3.13 or later.
41
+ Install from PyPI:
42
+
43
+ .. code-block:: console
44
+
45
+ uv tool install hackerrank-cli
46
+ hackerrank --help
47
+ hackerrank --version
48
+
49
+ You can also install with ``pip install hackerrank-cli``.
50
+
51
+ Usage
52
+ -----
53
+
54
+ Use synthetic question ID ``123456`` below in place of your own question ID.
55
+ Exactly one content source is required:
56
+
57
+ .. code-block:: console
58
+
59
+ hackerrank questions upload 123456 --directory ./starter --dry-run
60
+ hackerrank questions upload 123456 --directory ./starter --exclude '*.zip'
61
+ hackerrank questions upload 123456 --file ./starter.py
62
+
63
+ Real uploads read ``HACKERRANK_API_TOKEN`` from the environment.
64
+ Get a key from the HackerRank for Work API tokens page.
65
+ Missing, empty, and whitespace-only keys fail clearly.
66
+ Dry runs need no key and make no network requests.
67
+
68
+ Exactly one of ``--directory`` or ``--file`` is required.
69
+ Both replace the entire project archive.
70
+ ``--file`` creates a one-file project using the source's basename; it does not
71
+ update a language-specific code stub or upload a prebuilt ZIP as the project
72
+ archive.
73
+
74
+ All file bytes are preserved exactly, including binary files, CRLF, blank
75
+ lines, trailing whitespace, UTF-8 BOMs, and documentation markers.
76
+ Empty single files are valid.
77
+ Neither paths nor IDs are inferred from repository configuration.
78
+ Inputs are fully read, staged, and archived before credentials are read or a
79
+ mutation is issued.
80
+ POSIX executable bits are preserved.
81
+ Errors exit nonzero.
82
+ API response bodies and credentials are never printed.
83
+ The default retry count is zero, matching the released SDK.
84
+ Set ``--retries 3`` to delegate retries and backoff to the SDK for this
85
+ repeatable upload.
86
+ The CLI adds no separate retry loop.
87
+ Each attempt uses the same prepared archive bytes.
88
+
89
+ Directory selection
90
+ -------------------
91
+
92
+ * Both Git and non-Git directories work.
93
+ Paths are relative to the current working directory.
94
+ A Git worktree's ``.git`` file also identifies its root.
95
+ * In a Git repository, rules are inherited from the nearest Git root down to
96
+ the upload directory.
97
+ Outside Git, rules start at the upload directory; unrelated parent
98
+ ``.gitignore`` files are not used.
99
+ * Each traversed directory adds its own ``.gitignore`` rules.
100
+ Later matching rules override earlier ones, and deeper files override
101
+ ancestor rules.
102
+ Patterns are relative to their owning directory.
103
+ Directory-only rules and negations follow Gitignore semantics.
104
+ Excluded directories are pruned, so a child cannot re-include itself unless
105
+ its parent is re-included first.
106
+ This also applies to ancestors of an explicitly selected nested upload root.
107
+ * Ignore rules apply to tracked files too.
108
+ Global Git excludes and ``.git/info/exclude`` are not read.
109
+ Hidden files, including ``.gitignore``, are uploaded unless excluded.
110
+ * ``.git`` files and directories are always excluded at every depth.
111
+ Explicit source paths inside Git metadata are rejected, including an upload
112
+ rooted at a ``.git`` directory.
113
+ Repeatable ``--exclude PATTERN`` options form a final Gitignore rule layer,
114
+ relative to the upload root.
115
+ This layer can exclude files re-included by ``.gitignore``; its own later
116
+ negations can undo its earlier patterns.
117
+ It cannot re-include a file excluded by ``.gitignore`` or Git metadata.
118
+ * ZIP files are included by default.
119
+ Use ``--exclude '*.zip'`` to omit them.
120
+ Add other exclusions explicitly, such as ``--exclude node_modules/``.
121
+ * Selected symlinks (including directory, broken, and external links) and
122
+ symlinks in the source path are rejected rather than dereferenced.
123
+ Ignored links are skipped.
124
+ Symlinked ``.gitignore`` files are rejected when their rules would be read.
125
+ Special files are rejected.
126
+ Empty selections fail.
127
+ Selected files are copied to a temporary directory without dereferencing
128
+ links.
129
+ The CLI packages these prepared files with Python's standard ZIP library.
130
+ The released SDK handles multipart serialization, authentication, network
131
+ requests, response decoding, and retries.
132
+
133
+ Development and distribution
134
+ ----------------------------
135
+
136
+ Development checks, release setup, and build instructions for Docker, Nix, and
137
+ standalone binaries are in ``docs/source/development.rst``.
138
+ 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>`_.
141
+
142
+ See ``docs/source/migration.rst`` for migrating an existing shell uploader and
143
+ keeping repository-specific snippet preparation outside this CLI.
@@ -0,0 +1,11 @@
1
+ hackerrank_cli/__init__.py,sha256=-RqNWmLXJEibbEX794M_7Q-gzDRskf0kRZc1y9EQdTA,7293
2
+ hackerrank_cli/__main__.py,sha256=OCJH11M4QDshkTS5Rci0dUfLZr0YFnzK2aI9yrmWARM,94
3
+ hackerrank_cli/_sources.py,sha256=--ATApfmrcLjxNV4CEZjZrVWeolBm7MDoXjB-3-lJcw,6314
4
+ hackerrank_cli/_version.py,sha256=K6nZoD8HmqUbtHmEmxFXtR8SK0DcDTd6MDQMVfC4jZ8,533
5
+ hackerrank_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ hackerrank_cli-2026.10.1.1.dist-info/licenses/LICENSE,sha256=cnfgEzv8YMudbgPysNfgAdqeOo4zf0y2C3Ctw4RgEE4,1069
7
+ hackerrank_cli-2026.10.1.1.dist-info/METADATA,sha256=TZwDOMuKuOQqYiO1cWGwoRBtnbC4P8590rsxh--8sxM,6016
8
+ hackerrank_cli-2026.10.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ hackerrank_cli-2026.10.1.1.dist-info/entry_points.txt,sha256=3nj4emp_aSgrj_m-OUnLyAVsvW3_VPQpZ4gYe8_oMMg,51
10
+ hackerrank_cli-2026.10.1.1.dist-info/top_level.txt,sha256=SE7-lgO007qGO9eJpU0uWdu6Sy7k5kvojWMB1Im9lX0,15
11
+ hackerrank_cli-2026.10.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hackerrank = hackerrank_cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Dangoor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ hackerrank_cli