hashcheck 0.1.3__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.
@@ -0,0 +1,45 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v5
19
+ with:
20
+ enable-cache: true
21
+
22
+ - name: Set up Python 3.13
23
+ shell: bash
24
+ run: uv python install 3.13
25
+
26
+ - run: uv sync --frozen
27
+ - run: uv run pytest
28
+
29
+ publish:
30
+ if: startsWith(github.ref, 'refs/tags/v')
31
+ needs: test
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ id-token: write # required for trusted publishing
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+
39
+ - uses: astral-sh/setup-uv@v5
40
+ with:
41
+ enable-cache: true
42
+
43
+ - run: uv build
44
+
45
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,22 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `hash-checker` is a Python 3.13 CLI tool (currently in early development). Entry point is `main.py:main()`.
8
+
9
+ ## Commands
10
+
11
+ This project uses `uv` for dependency and environment management.
12
+
13
+ ```bash
14
+ # Run the project
15
+ uv run python main.py
16
+
17
+ # Add a dependency
18
+ uv add <package>
19
+
20
+ # Install dependencies
21
+ uv sync
22
+ ```
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: hashcheck
3
+ Version: 0.1.3
4
+ Summary: Simple file hash checker
5
+ Requires-Python: >=3.13
6
+ Requires-Dist: cloup>=3.0.8
7
+ Description-Content-Type: text/markdown
8
+
9
+ # hash-checker
10
+
11
+ A CLI tool for computing and comparing MD5 hashchecks.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ uv tool install .
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Get the MD5 hash of a file
22
+
23
+ ```bash
24
+ hashcheck <filepath>
25
+ ```
26
+
27
+ ```
28
+ $ hashcheck photo.jpg
29
+ d8e8fca2dc0f896fd7cb4cb0031ba249 photo.jpg
30
+ ```
31
+
32
+ ### Compare files
33
+
34
+ Pass two or more files — hashes are always printed, and the exit code indicates whether they match.
35
+
36
+ ```bash
37
+ hashcheck <file1> <file2> [file3 ...]
38
+ ```
39
+
40
+ ```
41
+ $ hashcheck file_a.zip file_b.zip
42
+ d8e8fca2dc0f896fd7cb4cb0031ba249 file_a.zip
43
+ d8e8fca2dc0f896fd7cb4cb0031ba249 file_b.zip
44
+ All files match.
45
+
46
+ $ hashcheck original.zip modified.zip
47
+ d8e8fca2dc0f896fd7cb4cb0031ba249 original.zip
48
+ aabbcc112233445566778899aabbcc11 modified.zip
49
+ Files do not match.
50
+ ```
51
+
52
+ ## Releasing
53
+
54
+ Releases are published to PyPI automatically by the CI workflow when a version tag is pushed.
55
+
56
+ **1. Bump the version in `pyproject.toml`**
57
+
58
+ ```toml
59
+ [project]
60
+ version = "0.2.0"
61
+ ```
62
+
63
+ **2. Commit the version bump**
64
+
65
+ ```bash
66
+ git add pyproject.toml
67
+ git commit -m "chore: bump version to 0.2.0"
68
+ ```
69
+
70
+ **3. Tag and push**
71
+
72
+ ```bash
73
+ git tag v0.2.0
74
+ git push origin main --tags
75
+ ```
76
+
77
+ The `publish` job in CI will build the package and upload it to PyPI once all tests pass.
78
+
79
+ > **First-time setup:** PyPI Trusted Publishing must be configured before the first release.
80
+ > Go to your PyPI project → *Manage* → *Publishing* and add a trusted publisher:
81
+ > - Publisher: GitHub
82
+ > - Repository: `<owner>/hash-checker`
83
+ > - Workflow: `ci.yml`
84
+ > - Environment: `pypi`
@@ -0,0 +1,76 @@
1
+ # hash-checker
2
+
3
+ A CLI tool for computing and comparing MD5 hashchecks.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv tool install .
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Get the MD5 hash of a file
14
+
15
+ ```bash
16
+ hashcheck <filepath>
17
+ ```
18
+
19
+ ```
20
+ $ hashcheck photo.jpg
21
+ d8e8fca2dc0f896fd7cb4cb0031ba249 photo.jpg
22
+ ```
23
+
24
+ ### Compare files
25
+
26
+ Pass two or more files — hashes are always printed, and the exit code indicates whether they match.
27
+
28
+ ```bash
29
+ hashcheck <file1> <file2> [file3 ...]
30
+ ```
31
+
32
+ ```
33
+ $ hashcheck file_a.zip file_b.zip
34
+ d8e8fca2dc0f896fd7cb4cb0031ba249 file_a.zip
35
+ d8e8fca2dc0f896fd7cb4cb0031ba249 file_b.zip
36
+ All files match.
37
+
38
+ $ hashcheck original.zip modified.zip
39
+ d8e8fca2dc0f896fd7cb4cb0031ba249 original.zip
40
+ aabbcc112233445566778899aabbcc11 modified.zip
41
+ Files do not match.
42
+ ```
43
+
44
+ ## Releasing
45
+
46
+ Releases are published to PyPI automatically by the CI workflow when a version tag is pushed.
47
+
48
+ **1. Bump the version in `pyproject.toml`**
49
+
50
+ ```toml
51
+ [project]
52
+ version = "0.2.0"
53
+ ```
54
+
55
+ **2. Commit the version bump**
56
+
57
+ ```bash
58
+ git add pyproject.toml
59
+ git commit -m "chore: bump version to 0.2.0"
60
+ ```
61
+
62
+ **3. Tag and push**
63
+
64
+ ```bash
65
+ git tag v0.2.0
66
+ git push origin main --tags
67
+ ```
68
+
69
+ The `publish` job in CI will build the package and upload it to PyPI once all tests pass.
70
+
71
+ > **First-time setup:** PyPI Trusted Publishing must be configured before the first release.
72
+ > Go to your PyPI project → *Manage* → *Publishing* and add a trusted publisher:
73
+ > - Publisher: GitHub
74
+ > - Repository: `<owner>/hash-checker`
75
+ > - Workflow: `ci.yml`
76
+ > - Environment: `pypi`
File without changes
@@ -0,0 +1,38 @@
1
+ import hashlib
2
+ import sys
3
+
4
+ import cloup
5
+
6
+
7
+ def md5_hash(filepath: str) -> str:
8
+ md5 = hashlib.md5()
9
+ with open(filepath, "rb") as f:
10
+ for chunk in iter(lambda: f.read(8192), b""):
11
+ md5.update(chunk)
12
+ return md5.hexdigest()
13
+
14
+
15
+ @cloup.command()
16
+ @cloup.argument("files", nargs=-1, required=True)
17
+ def main(files: tuple[str, ...]) -> None:
18
+ """Get the MD5 hash of one or more files. Compares hashes when multiple files are given."""
19
+ hashes: dict[str, str] = {}
20
+ for filepath in files:
21
+ try:
22
+ digest = md5_hash(filepath)
23
+ hashes[filepath] = digest
24
+ print(f"{digest} {filepath}")
25
+ except FileNotFoundError:
26
+ print(f"Error: file not found: {filepath}", file=sys.stderr)
27
+ sys.exit(1)
28
+ except PermissionError:
29
+ print(f"Error: permission denied: {filepath}", file=sys.stderr)
30
+ sys.exit(1)
31
+
32
+ if len(files) > 1:
33
+ unique_hashes = set(hashes.values())
34
+ if len(unique_hashes) == 1:
35
+ print("All files match.")
36
+ else:
37
+ print("Files do not match.", file=sys.stderr)
38
+ sys.exit(1)
@@ -0,0 +1,4 @@
1
+ from hash_checker import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "hashcheck"
3
+ version = "0.1.3"
4
+ description = "Simple file hash checker"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "cloup>=3.0.8",
9
+ ]
10
+
11
+ [project.scripts]
12
+ hashcheck = "hashcheck.main:main"
13
+
14
+ [dependency-groups]
15
+ dev = [
16
+ "pytest>=9.0.2",
17
+ ]
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
@@ -0,0 +1,113 @@
1
+ import hashlib
2
+
3
+ import pytest
4
+ from click.testing import CliRunner
5
+
6
+ from hashcheck.main import main, md5_hash
7
+
8
+
9
+ @pytest.fixture
10
+ def runner():
11
+ return CliRunner()
12
+
13
+
14
+ # --- md5_hash ---
15
+
16
+ def test_md5_hash_returns_correct_digest(tmp_path):
17
+ f = tmp_path / "file.txt"
18
+ f.write_bytes(b"hello world")
19
+ expected = hashlib.md5(b"hello world").hexdigest()
20
+ assert md5_hash(str(f)) == expected
21
+
22
+
23
+ def test_md5_hash_empty_file(tmp_path):
24
+ f = tmp_path / "empty.txt"
25
+ f.write_bytes(b"")
26
+ expected = hashlib.md5(b"").hexdigest()
27
+ assert md5_hash(str(f)) == expected
28
+
29
+
30
+ def test_md5_hash_raises_on_missing_file(tmp_path):
31
+ with pytest.raises(FileNotFoundError):
32
+ md5_hash(str(tmp_path / "nonexistent.txt"))
33
+
34
+
35
+ # --- hashcheck <filepath> ---
36
+
37
+ def test_main_prints_hash(runner, tmp_path):
38
+ f = tmp_path / "file.txt"
39
+ f.write_bytes(b"hello world")
40
+ expected = hashlib.md5(b"hello world").hexdigest()
41
+
42
+ result = runner.invoke(main, [str(f)])
43
+
44
+ assert result.exit_code == 0
45
+ assert f"{expected} {str(f)}" in result.output
46
+
47
+
48
+ def test_main_no_args_shows_error(runner):
49
+ result = runner.invoke(main, [])
50
+ assert result.exit_code != 0
51
+
52
+
53
+ def test_main_file_not_found(runner, tmp_path):
54
+ result = runner.invoke(main, [str(tmp_path / "missing.txt")])
55
+ assert result.exit_code == 1
56
+ assert "file not found" in result.output
57
+
58
+
59
+ # --- hashcheck <file1> <file2> ... ---
60
+
61
+ def test_compare_matching_files(runner, tmp_path):
62
+ f1 = tmp_path / "a.txt"
63
+ f2 = tmp_path / "b.txt"
64
+ f1.write_bytes(b"same content")
65
+ f2.write_bytes(b"same content")
66
+
67
+ result = runner.invoke(main, [str(f1), str(f2)])
68
+
69
+ assert result.exit_code == 0
70
+ assert "All files match." in result.output
71
+ digest = hashlib.md5(b"same content").hexdigest()
72
+ assert f"{digest} {str(f1)}" in result.output
73
+ assert f"{digest} {str(f2)}" in result.output
74
+
75
+
76
+ def test_compare_different_files(runner, tmp_path):
77
+ f1 = tmp_path / "a.txt"
78
+ f2 = tmp_path / "b.txt"
79
+ f1.write_bytes(b"content a")
80
+ f2.write_bytes(b"content b")
81
+
82
+ result = runner.invoke(main, [str(f1), str(f2)])
83
+
84
+ assert result.exit_code == 1
85
+ assert "do not match" in result.output
86
+ assert hashlib.md5(b"content a").hexdigest() in result.output
87
+ assert hashlib.md5(b"content b").hexdigest() in result.output
88
+
89
+
90
+ def test_compare_multiple_files_all_match(runner, tmp_path):
91
+ files = []
92
+ for name in ["a.txt", "b.txt", "c.txt"]:
93
+ f = tmp_path / name
94
+ f.write_bytes(b"identical")
95
+ files.append(str(f))
96
+
97
+ result = runner.invoke(main, files)
98
+
99
+ assert result.exit_code == 0
100
+ assert "All files match." in result.output
101
+ digest = hashlib.md5(b"identical").hexdigest()
102
+ for filepath in files:
103
+ assert f"{digest} {filepath}" in result.output
104
+
105
+
106
+ def test_compare_file_not_found(runner, tmp_path):
107
+ f = tmp_path / "exists.txt"
108
+ f.write_bytes(b"data")
109
+
110
+ result = runner.invoke(main, [str(f), str(tmp_path / "missing.txt")])
111
+
112
+ assert result.exit_code == 1
113
+ assert "file not found" in result.output
@@ -0,0 +1,107 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "click"
7
+ version = "8.3.1"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "colorama", marker = "sys_platform == 'win32'" },
11
+ ]
12
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
13
+ wheels = [
14
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
15
+ ]
16
+
17
+ [[package]]
18
+ name = "cloup"
19
+ version = "3.0.8"
20
+ source = { registry = "https://pypi.org/simple" }
21
+ dependencies = [
22
+ { name = "click" },
23
+ ]
24
+ sdist = { url = "https://files.pythonhosted.org/packages/46/cf/09a31f0f51b5c8ef2343baf37c35a5feb4f6dfdcbd0592a014baf837f2e4/cloup-3.0.8.tar.gz", hash = "sha256:f91c080a725196ddf74feabd6250266f466e97fc16dfe21a762cf6bc6beb3ecb", size = 229657, upload-time = "2025-08-05T02:25:02.83Z" }
25
+ wheels = [
26
+ { url = "https://files.pythonhosted.org/packages/45/0a/494a923f90cd97cdf4fb989cfd06ac0c6745f6dfb8adcef1b7f99d3c7834/cloup-3.0.8-py2.py3-none-any.whl", hash = "sha256:6fe9474dc44fa06f8870e9c797f005de1e3ef891ddc1a9612d9b58598a038323", size = 54647, upload-time = "2025-08-05T02:25:01.536Z" },
27
+ ]
28
+
29
+ [[package]]
30
+ name = "colorama"
31
+ version = "0.4.6"
32
+ source = { registry = "https://pypi.org/simple" }
33
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
34
+ wheels = [
35
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
36
+ ]
37
+
38
+ [[package]]
39
+ name = "hashcheck"
40
+ version = "0.1.3"
41
+ source = { editable = "." }
42
+ dependencies = [
43
+ { name = "cloup" },
44
+ ]
45
+
46
+ [package.dev-dependencies]
47
+ dev = [
48
+ { name = "pytest" },
49
+ ]
50
+
51
+ [package.metadata]
52
+ requires-dist = [{ name = "cloup", specifier = ">=3.0.8" }]
53
+
54
+ [package.metadata.requires-dev]
55
+ dev = [{ name = "pytest", specifier = ">=9.0.2" }]
56
+
57
+ [[package]]
58
+ name = "iniconfig"
59
+ version = "2.3.0"
60
+ source = { registry = "https://pypi.org/simple" }
61
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
62
+ wheels = [
63
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
64
+ ]
65
+
66
+ [[package]]
67
+ name = "packaging"
68
+ version = "26.0"
69
+ source = { registry = "https://pypi.org/simple" }
70
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
71
+ wheels = [
72
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
73
+ ]
74
+
75
+ [[package]]
76
+ name = "pluggy"
77
+ version = "1.6.0"
78
+ source = { registry = "https://pypi.org/simple" }
79
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
80
+ wheels = [
81
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
82
+ ]
83
+
84
+ [[package]]
85
+ name = "pygments"
86
+ version = "2.19.2"
87
+ source = { registry = "https://pypi.org/simple" }
88
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
89
+ wheels = [
90
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
91
+ ]
92
+
93
+ [[package]]
94
+ name = "pytest"
95
+ version = "9.0.2"
96
+ source = { registry = "https://pypi.org/simple" }
97
+ dependencies = [
98
+ { name = "colorama", marker = "sys_platform == 'win32'" },
99
+ { name = "iniconfig" },
100
+ { name = "packaging" },
101
+ { name = "pluggy" },
102
+ { name = "pygments" },
103
+ ]
104
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
105
+ wheels = [
106
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
107
+ ]