diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index b5e8cfd..25f4ad1 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -41,4 +41,3 @@ jobs: prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d300267..9471a05 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -47,4 +47,3 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options # claude_args: '--allowed-tools Bash(gh pr:*)' - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..897684e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,60 @@ +minimum_prek_version: "0.4.14" +default_install_hook_types: [pre-commit] +default_stages: [pre-commit] + +repos: + - repo: builtin + hooks: + - id: trailing-whitespace + stages: [pre-commit] + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + stages: [pre-commit] + - id: fix-byte-order-marker + stages: [pre-commit] + - id: mixed-line-ending + stages: [pre-commit] + - id: check-added-large-files + stages: [pre-commit] + - id: check-case-conflict + stages: [pre-commit] + - id: check-illegal-windows-names + stages: [pre-commit] + - id: check-json + stages: [pre-commit] + exclude: ^template/ + - id: check-toml + stages: [pre-commit] + exclude: ^template/ + - id: check-yaml + stages: [pre-commit] + exclude: ^template/ + - id: check-merge-conflict + stages: [pre-commit] + - id: check-symlinks + stages: [pre-commit] + - id: destroyed-symlinks + stages: [pre-commit] + - id: detect-private-key + stages: [pre-commit] + - id: check-executables-have-shebangs + stages: [pre-commit] + - id: check-shebang-scripts-are-executable + stages: [pre-commit] + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.12.0 + hooks: + - id: uv-lock + stages: [pre-commit] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.3 + hooks: + - id: ruff-check + stages: [pre-commit] + args: [--fix] + exclude: ^template/ + - id: ruff-format + stages: [pre-commit] + exclude: ^template/ diff --git a/.prekignore b/.prekignore new file mode 100644 index 0000000..dd72e64 --- /dev/null +++ b/.prekignore @@ -0,0 +1,2 @@ +# The nested config belongs to generated projects, not this prek workspace. +template/ diff --git a/AGENTS.md b/AGENTS.md index 2e5de05..a6c1228 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,9 @@ This is a copier template for Python projects. It generates new Python projects ## Development +- Run `make bootstrap` once to sync dependencies and install the commit-only prek hook - Run `make test` to verify the template generates correctly +- Run `make prek` to execute all prek checks against tracked files - The test creates a temporary project using copier with default values and checks basic structure ## How the template works @@ -25,4 +27,4 @@ This is a copier template for Python projects. It generates new Python projects 1. Edit files in `template/` to change what gets generated 2. Edit `copier.yml` to add/modify template questions -3. Run `make test` to verify changes work +3. Run `make test` and `make prek` to verify changes work diff --git a/Makefile b/Makefile index 458b382..4e5cda7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,11 @@ -.PHONY: test +.PHONY: bootstrap test prek + +bootstrap: + uv sync --dev + uv run prek install --prepare-hooks test: uv run pytest tests/ -v + +prek: + uv run prek run --all-files diff --git a/README.md b/README.md index 840097a..b5384b3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ A minimal [copier](https://copier.readthedocs.io/) template for Python projects - [uv](https://docs.astral.sh/uv/) for dependency management - [pytest](https://docs.pytest.org/) for testing - [ruff](https://docs.astral.sh/ruff/) for linting and formatting -- [pyright](https://github.com/microsoft/pyright) for type checking +- [ty](https://docs.astral.sh/ty/) for type checking +- [prek](https://prek.j178.dev/) for commit-time checks - Optional [Typer](https://typer.tiangolo.com/) CLI - GitHub Actions CI/CD workflows - MIT license @@ -41,8 +42,20 @@ copier copy gh:jooh/copier-minimal-python my-project ## Development +Set up the development environment and install the commit-only Git hook once: + +```bash +make bootstrap +``` + +Run the template tests and, when needed, every prek check against all tracked files: + ```bash make test +make prek ``` +The installed hook runs automatically on `git commit`. Safe formatting fixes must be reviewed and +staged before retrying the commit; no pre-push or other Git hooks are installed. + See [AGENTS.md](AGENTS.md) for more details on the template structure. diff --git a/pyproject.toml b/pyproject.toml index 457816e..14e5757 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,5 +8,6 @@ requires-python = ">=3.12" dev = [ "copier>=9.0.0", "jinja2-time>=0.2.0", + "prek>=0.4.14", "pytest>=8.0.0", ] diff --git a/template/.github/workflows/cd_version.py b/template/.github/workflows/cd_version.py index 4916f89..22cb813 100644 --- a/template/.github/workflows/cd_version.py +++ b/template/.github/workflows/cd_version.py @@ -4,9 +4,9 @@ import pathlib import re import tomllib -import tomli_w from dataclasses import dataclass +import tomli_w # Strict semantic versioning pattern: MAJOR.MINOR.PATCH # - MAJOR is either 0 (pre-1.0 semantics) or a non-zero integer without leading zeros. @@ -24,7 +24,7 @@ class Version: patch: int @classmethod - def parse(cls, value: str) -> "Version | None": + def parse(cls, value: str) -> Version | None: match = VERSION_PATTERN.match(value) if not match: return None @@ -34,7 +34,7 @@ def parse(cls, value: str) -> "Version | None": patch=int(match.group("patch")), ) - def bump_patch(self) -> "Version": + def bump_patch(self) -> Version: return Version(self.major, self.minor, self.patch + 1) def __str__(self) -> str: @@ -54,7 +54,7 @@ def write_version(pyproject_path: pathlib.Path, version: Version) -> None: data = tomllib.loads(pyproject_path.read_text()) project = data.get("project") if not isinstance(project, dict): - raise ValueError(f"Missing [project] table in {pyproject_path}") + raise TypeError(f"Missing [project] table in {pyproject_path}") if "version" not in project: raise ValueError(f"Missing project.version in {pyproject_path}") project["version"] = str(version) diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml new file mode 100644 index 0000000..2ca6d0b --- /dev/null +++ b/template/.pre-commit-config.yaml @@ -0,0 +1,55 @@ +minimum_prek_version: "0.4.14" +default_install_hook_types: [pre-commit] +default_stages: [pre-commit] + +repos: + - repo: builtin + hooks: + - id: trailing-whitespace + stages: [pre-commit] + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + stages: [pre-commit] + - id: fix-byte-order-marker + stages: [pre-commit] + - id: mixed-line-ending + stages: [pre-commit] + - id: check-added-large-files + stages: [pre-commit] + - id: check-case-conflict + stages: [pre-commit] + - id: check-illegal-windows-names + stages: [pre-commit] + - id: check-json + stages: [pre-commit] + - id: check-toml + stages: [pre-commit] + - id: check-yaml + stages: [pre-commit] + - id: check-merge-conflict + stages: [pre-commit] + - id: check-symlinks + stages: [pre-commit] + - id: destroyed-symlinks + stages: [pre-commit] + - id: detect-private-key + stages: [pre-commit] + - id: check-executables-have-shebangs + stages: [pre-commit] + - id: check-shebang-scripts-are-executable + stages: [pre-commit] + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.12.0 + hooks: + - id: uv-lock + stages: [pre-commit] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.3 + hooks: + - id: ruff-check + stages: [pre-commit] + args: [--fix] + - id: ruff-format + stages: [pre-commit] diff --git a/template/AGENTS.md b/template/AGENTS.md index 9ccf861..b9c0d2e 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -9,16 +9,19 @@ - Requires Python {{ min_python_version }}+ (see `pyproject.toml`). - Environment management is with uv. - Run Python and related CLI tools via `uv run` so they use the uv virtualenv. +- Run `make bootstrap` once after cloning or initializing Git to sync dependencies and install the commit-only prek hook. ## Common commands - Run tests: `make test` - Run type checks: `make typecheck` - Run ruff checks: `make ruff` +- Run all prek hooks against tracked files: `make prek` - Run all checks: `make all-tests` ## Style and conventions - TDD for all code development - write test, then run to verify it fails, then develop, then verify the test passes. - All tasks should end by running `make all-tests` and verifying it passes. +- Run `make prek` before committing; safe automatic fixes must be reviewed and staged. - Prefer updating or adding pytest tests in `tests/` for behavior changes. {%- if has_cli %} - For CLI changes, update both `src/{{ package_name }}/cli.py` and any relevant tests. diff --git a/template/CLAUDE.md b/template/CLAUDE.md index 9ccf861..b9c0d2e 100644 --- a/template/CLAUDE.md +++ b/template/CLAUDE.md @@ -9,16 +9,19 @@ - Requires Python {{ min_python_version }}+ (see `pyproject.toml`). - Environment management is with uv. - Run Python and related CLI tools via `uv run` so they use the uv virtualenv. +- Run `make bootstrap` once after cloning or initializing Git to sync dependencies and install the commit-only prek hook. ## Common commands - Run tests: `make test` - Run type checks: `make typecheck` - Run ruff checks: `make ruff` +- Run all prek hooks against tracked files: `make prek` - Run all checks: `make all-tests` ## Style and conventions - TDD for all code development - write test, then run to verify it fails, then develop, then verify the test passes. - All tasks should end by running `make all-tests` and verifying it passes. +- Run `make prek` before committing; safe automatic fixes must be reviewed and staged. - Prefer updating or adding pytest tests in `tests/` for behavior changes. {%- if has_cli %} - For CLI changes, update both `src/{{ package_name }}/cli.py` and any relevant tests. diff --git a/template/Makefile b/template/Makefile index db44f3c..fbde38b 100644 --- a/template/Makefile +++ b/template/Makefile @@ -1,4 +1,8 @@ -.PHONY: test typecheck ruff all-tests +.PHONY: bootstrap test typecheck ruff all-tests prek + +bootstrap: + uv sync --dev + uv run prek install --prepare-hooks test: uv run pytest --cov={{ package_name }} --cov-report=term-missing --cov-report=xml @@ -10,3 +14,6 @@ ruff: uv run ruff check . all-tests: test typecheck ruff + +prek: + uv run prek run --all-files diff --git a/template/README.md b/template/README.md index 8a26ac6..ab0054c 100644 --- a/template/README.md +++ b/template/README.md @@ -35,10 +35,20 @@ import {{ package_name }} ## Development +After cloning or initializing the Git repository, set up the environment and commit hook: + +```bash +make bootstrap +``` + +The hook runs automatically only on `git commit`. Safe fixes must be reviewed and staged before +retrying the commit. Run every configured hook against all tracked files manually with `make prek`. + ```bash make test make typecheck make ruff +make prek ``` ## Release process diff --git a/template/pyproject.toml b/template/pyproject.toml index ec297ab..877fd28 100644 --- a/template/pyproject.toml +++ b/template/pyproject.toml @@ -54,9 +54,10 @@ exclude = ["uv.lock"] [dependency-groups] dev = [ + "prek>=0.4.14", "pytest>=9.0.2", "pytest-cov>=6.0.0", - "ruff>=0.9.10", + "ruff>=0.16.3", "tomli-w>=1.0.0", "ty>=0.0.14", ] diff --git a/template/tests/test_cd_version.py b/template/tests/test_cd_version.py index 1a1ec1d..6d3b361 100644 --- a/template/tests/test_cd_version.py +++ b/template/tests/test_cd_version.py @@ -2,8 +2,8 @@ import importlib.util import sys -from typing import Protocol, cast from pathlib import Path +from typing import Protocol, cast class CdVersionModule(Protocol): diff --git a/template/tests/test_{{ package_name }}.py b/template/tests/test_{{ package_name }}.py index 39b762b..2326da5 100644 --- a/template/tests/test_{{ package_name }}.py +++ b/template/tests/test_{{ package_name }}.py @@ -1,9 +1,11 @@ """Placeholder tests for {{ package_name }}.""" -import {{ package_name }} -{% if has_cli %} +{% if has_cli -%} from typer.testing import CliRunner +{% endif -%} +import {{ package_name }} +{% if has_cli -%} from {{ package_name }}.cli import app runner = CliRunner() @@ -33,4 +35,4 @@ def test_cli_hello_with_name() -> None: result = runner.invoke(app, ["--name", "World"]) assert result.exit_code == 0 assert "Hello, World!" in result.stdout -{% endif %} +{% endif -%} diff --git a/tests/test_template.py b/tests/test_template.py index c9b9915..d49ad49 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,41 +1,219 @@ """Integration tests for the copier template.""" +import ast +import os import subprocess -import tempfile +import tomllib from pathlib import Path +import pytest -def test_template_generates_with_defaults(): + +def run_command( + command: list[str], cwd: Path, *, check: bool = True +) -> subprocess.CompletedProcess[str]: + """Run a command and include its output in assertion failures.""" + result = subprocess.run( + command, + cwd=cwd, + capture_output=True, + text=True, + timeout=300, + check=False, + ) + if check: + assert result.returncode == 0, ( + f"{' '.join(command)} failed\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + return result + + +def copy_template( + template_dir: Path, + dest: Path, + *, + project_name: str = "test-project", + min_python_version: str | None = None, + has_cli: bool | None = None, +) -> None: + """Render the template, optionally overriding its Python and CLI defaults.""" + command = [ + "uv", + "run", + "copier", + "copy", + "--defaults", + "--trust", + "--data", + f"project_name={project_name}", + "--data", + "description=A test project", + "--data", + "author_name=Test Author", + "--data", + "repo_owner=test-owner", + ] + if min_python_version is not None: + command.extend(["--data", f"min_python_version={min_python_version}"]) + if has_cli is not None: + command.extend(["--data", f"has_cli={str(has_cli).lower()}"]) + command.extend([str(template_dir), str(dest)]) + + run_command(command, cwd=template_dir) + + +def test_template_generates_with_defaults( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: """Test that the template generates successfully with default values.""" + monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull) + monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1") + template_dir = Path(__file__).parent.parent + dest = tmp_path / "test-project" + + copy_template(template_dir, dest) + + assert dest.exists(), "Destination directory was not created" + assert (dest / "pyproject.toml").exists(), "pyproject.toml was not generated" + assert (dest / "src" / "test_project").exists(), "Package directory was not created" + + config = (dest / ".pre-commit-config.yaml").read_text() + assert 'minimum_prek_version: "0.4.14"' in config + assert "default_install_hook_types: [pre-commit]" in config + assert "default_stages: [pre-commit]" in config + assert "repo: builtin" in config + assert "id: uv-lock" in config + assert "id: ruff-check" in config + assert "id: ruff-format" in config + + pyproject = (dest / "pyproject.toml").read_text() + assert '"prek>=0.4.14"' in pyproject + assert '"ruff>=0.16.3"' in pyproject + + makefile = (dest / "Makefile").read_text() + assert "bootstrap:" in makefile + assert "uv run prek install --prepare-hooks" in makefile + assert "prek:" in makefile + assert "uv run prek run --all-files" in makefile + + readme = (dest / "README.md").read_text() + assert "make bootstrap" in readme + assert "automatically only on `git commit`" in readme + assert "make prek" in readme + + agent_guide = (dest / "AGENTS.md").read_text() + assert "commit-only prek hook" in agent_guide + assert "Run all prek hooks against tracked files: `make prek`" in agent_guide - with tempfile.TemporaryDirectory() as tmpdir: - dest = Path(tmpdir) / "test-project" - - result = subprocess.run( - [ - "uv", - "run", - "copier", - "copy", - "--defaults", - "--trust", - "--data", - "project_name=test-project", - "--data", - "description=A test project", - "--data", - "author_name=Test Author", - "--data", - "repo_owner=test-owner", - str(template_dir), - str(dest), - ], - capture_output=True, - text=True, + run_command(["git", "init", "--initial-branch=main"], cwd=dest) + run_command(["git", "config", "user.name", "Template Test"], cwd=dest) + run_command(["git", "config", "user.email", "template-test@example.com"], cwd=dest) + + hooks_dir = dest / ".git" / "hooks" + sentinel = "#!/bin/sh\ntouch legacy-hook-ran\n" + pre_commit_hook = hooks_dir / "pre-commit" + pre_commit_hook.write_text(sentinel) + pre_commit_hook.chmod(0o755) + + run_command(["make", "bootstrap"], cwd=dest) + legacy_hook = hooks_dir / "pre-commit.legacy" + assert legacy_hook.read_text() == sentinel + run_command([str(pre_commit_hook)], cwd=dest) + legacy_marker = dest / "legacy-hook-ran" + assert legacy_marker.exists() + + legacy_marker.unlink() + pre_commit_hook.unlink() + legacy_hook.unlink() + run_command(["make", "bootstrap"], cwd=dest) + + installed_hooks = { + path.name for path in hooks_dir.iterdir() if not path.name.endswith(".sample") + } + assert installed_hooks == {"pre-commit"} + + for hook_stage in ( + "commit-msg", + "manual", + "post-checkout", + "post-commit", + "post-merge", + "post-rewrite", + "pre-merge-commit", + "pre-push", + "pre-rebase", + "prepare-commit-msg", + ): + listed_hooks = run_command( + ["uv", "run", "prek", "list", "--hook-stage", hook_stage], + cwd=dest, ) + assert listed_hooks.stdout == "" + + run_command( + ["uv", "run", "prek", "validate-config", ".pre-commit-config.yaml"], + cwd=dest, + ) + run_command(["git", "add", "--all"], cwd=dest) + run_command(["make", "prek"], cwd=dest) + run_command(["git", "commit", "-m", "Initial commit"], cwd=dest) + + violation = dest / "trailing-whitespace.txt" + violation.write_text("trailing whitespace \n") + run_command(["git", "add", violation.name], cwd=dest) + failed_commit = run_command( + ["git", "commit", "-m", "Add trailing whitespace"], + cwd=dest, + check=False, + ) + + assert failed_commit.returncode != 0 + assert violation.read_text() == "trailing whitespace\n" + commit_count = run_command( + ["git", "rev-list", "--count", "HEAD"], cwd=dest + ).stdout.strip() + assert commit_count == "1" + + run_command(["git", "add", violation.name], cwd=dest) + run_command(["git", "commit", "-m", "Add normalized text"], cwd=dest) + + +@pytest.mark.parametrize("min_python_version", ["3.12", "3.13", "3.14"]) +@pytest.mark.parametrize("has_cli", [False, True]) +def test_template_variants_render( + tmp_path: Path, min_python_version: str, *, has_cli: bool +) -> None: + """Render every supported Python and optional-CLI combination.""" + template_dir = Path(__file__).parent.parent + variant = "cli" if has_cli else "library" + project_name = f"python-{min_python_version.replace('.', '')}-{variant}" + package_name = project_name.replace("-", "_") + dest = tmp_path / project_name + + copy_template( + template_dir, + dest, + project_name=project_name, + min_python_version=min_python_version, + has_cli=has_cli, + ) + + pyproject = tomllib.loads((dest / "pyproject.toml").read_text()) + assert pyproject["project"]["requires-python"] == f">={min_python_version}" + assert ("scripts" in pyproject["project"]) is has_cli + assert (dest / "src" / package_name / "cli.py").exists() is has_cli + + for python_file in dest.rglob("*.py"): + ast.parse(python_file.read_text(), filename=str(python_file)) - assert result.returncode == 0, f"copier failed: {result.stderr}" - assert dest.exists(), "Destination directory was not created" - assert (dest / "pyproject.toml").exists(), "pyproject.toml was not generated" - assert (dest / "src" / "test_project").exists(), "Package directory was not created" + run_command( + [ + "uv", + "run", + "prek", + "validate-config", + str(dest / ".pre-commit-config.yaml"), + ], + cwd=template_dir, + ) diff --git a/uv.lock b/uv.lock index a9332b0..078fa88 100644 --- a/uv.lock +++ b/uv.lock @@ -66,6 +66,7 @@ source = { virtual = "." } dev = [ { name = "copier" }, { name = "jinja2-time" }, + { name = "prek" }, { name = "pytest" }, ] @@ -75,6 +76,7 @@ dev = [ dev = [ { name = "copier", specifier = ">=9.0.0" }, { name = "jinja2-time", specifier = ">=0.2.0" }, + { name = "prek", specifier = ">=0.4.14" }, { name = "pytest", specifier = ">=8.0.0" }, ] @@ -257,6 +259,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl", hash = "sha256:9583d737ac901c474d99d030e4d5eec4c4e6d2d7417b1cf49728cf3be34f6dc8", size = 127383, upload-time = "2025-10-31T05:02:47.002Z" }, ] +[[package]] +name = "prek" +version = "0.4.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/51/135dc6ba2c021ce32b40700c8c337db72d802893e15291f4b3056076582f/prek-0.4.14.tar.gz", hash = "sha256:f6d0952e31ffd6e508660749dd51b8d8de96e955ed12c40e411f3224f502fed2", size = 537668, upload-time = "2026-08-17T04:27:55.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/75/727724174d419cab6e11e0c22c3fbcdd083312b0a0202dfa6befdb7f1bcb/prek-0.4.14-py3-none-linux_armv6l.whl", hash = "sha256:cf7fe2e07c99948ca3326fbd4254054cf4caa71a69cf9032be09c3938544e1e3", size = 5878406, upload-time = "2026-08-17T04:27:30.135Z" }, + { url = "https://files.pythonhosted.org/packages/34/b6/82dfb41347342b4c53c1ddd27cfa81d3e1cf4ad0d5d76493c51e8da58dc9/prek-0.4.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6c8043aa5555c2ada561f4c69429e5117f370ad92f7d708340613b0965e725bd", size = 6216669, upload-time = "2026-08-17T04:27:31.795Z" }, + { url = "https://files.pythonhosted.org/packages/5b/68/ade0ba8b8c0044a3f7ca1a5cd7c97bc10656398c67466b04235f0f0b7418/prek-0.4.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:355570f0d8366a56817e55f44edf736ffc9ff2323894e21cd739ab519d343541", size = 5728085, upload-time = "2026-08-17T04:27:33.322Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/8ddb67fb000d1ad4c95288e6c7e27989d3c172cd88893e3a6f9cdd7199fb/prek-0.4.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:334f48a2b19e63c5ab741e601cb6088caaa11d1b75146900dc298b055452d551", size = 6043650, upload-time = "2026-08-17T04:27:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/5b/49/2780489798147fa1e81fa017af384a40229802cf36680cf72c74478d143c/prek-0.4.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5fbb17270df2dcb3c1aa6edfaa68f850d7968862413e58b1572e41c981f644f9", size = 5786094, upload-time = "2026-08-17T04:27:36.282Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/27a4cdfa767b46663eb32a2b5f78ebaaeefc6e5caa87a21587216a504e31/prek-0.4.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ec46be0b1b45943a0746fdf69feeede274b0230bad32b11e2489bb600846081", size = 6239874, upload-time = "2026-08-17T04:27:37.984Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/5ae58d95bc9dbad75ea13aea83fe0acfaca37d027c6bd7f9f1d1d97f3666/prek-0.4.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a21eea6ee996dbba351c1af8c6bd1959a61c37949757c0b97aef3edde82c126a", size = 6968009, upload-time = "2026-08-17T04:27:39.48Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1c/c6a7800406f987559fc8e7c2cfd257a5a68096e806e953f143b488e00e4f/prek-0.4.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34169cb1c8dfbe4b9cb7af164f5c1c3dff68929c61ca1e1de28d378b32720836", size = 6443370, upload-time = "2026-08-17T04:27:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/92/38/4bf84223216ce2d31b500691d1644a9f35d6e468cb8ecb9a7dfbc66bc82c/prek-0.4.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e39772ebf579f957fdbbc66ef6a7f7433bf603128cd60d5c804c70caac5f69c0", size = 6056449, upload-time = "2026-08-17T04:27:42.722Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/f1980039ab7bf8342c4aa4da2a4cafe40e6a840f456d5bbf86ba024fcdca/prek-0.4.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a79e5940354a789e1311172a960c5daa75e3f643a47ee2be85b7d2ca1d7d980a", size = 5839941, upload-time = "2026-08-17T04:27:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e2/67f067dcb912e157bc7f4cd2aeb422a0de65e7744c675d7f439676f6c832/prek-0.4.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:60b6539c1b28804807849173a6b49d467f6f36ca98bbb4536f5e34114d79942a", size = 5762723, upload-time = "2026-08-17T04:27:45.737Z" }, + { url = "https://files.pythonhosted.org/packages/84/39/6669fff5c3336a2b79cf853c86b95547cf9be720a2ee7e4ae5eaa6cb46a1/prek-0.4.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:24cfabd9e5b8c5546ecaef562841a41a255a9dec1b7ae7761ce5a7a024ab9dd9", size = 6090040, upload-time = "2026-08-17T04:27:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/2a/23/c23210be4c89795a854e76146c2465aaffc3d85ea83df61c1d4d5bb41bca/prek-0.4.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1f08d368da45439f949885bb0b637bc7dfe6910140d5285fa8bae058697ddd06", size = 6570273, upload-time = "2026-08-17T04:27:48.673Z" }, + { url = "https://files.pythonhosted.org/packages/00/9c/4f10728ed36295347e84f01bb3e4c9078c9e337edc66215f5e2066b30551/prek-0.4.14-py3-none-win32.whl", hash = "sha256:5bfb30808ce2099c67d2a2d4cd68dc031e3fec1eeb61d73ef51a8f7c04d021cb", size = 5586715, upload-time = "2026-08-17T04:27:50.471Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/d080a157d4e92927f7618d62d6a23979ef64a10c76a040cb8a8a194c50d7/prek-0.4.14-py3-none-win_amd64.whl", hash = "sha256:29364012d5704475d1092eb8a96ea30b163279096ad5e0c80a620fffa79bc639", size = 5969946, upload-time = "2026-08-17T04:27:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/37/e1/6fc64bb82e7270f61707e00b6d3154a4592ae0b2d3bd308173aa7aabe0a1/prek-0.4.14-py3-none-win_arm64.whl", hash = "sha256:ff588c02e10c8d05150763607671a22d5585c0ff7036c884d3489c2726eb215c", size = 5729906, upload-time = "2026-08-17T04:27:53.52Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52"