diff --git a/.github/workflows/ruff.yml b/.github/workflows/prek.yml similarity index 51% rename from .github/workflows/ruff.yml rename to .github/workflows/prek.yml index e27a370b0..1f3d52b4a 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/prek.yml @@ -1,8 +1,7 @@ -name: Code Quality Checks +name: Prek on: pull_request: - branches: [ main ] push: branches: [ main ] @@ -28,17 +27,10 @@ jobs: run: | uv sync --all-extras - - name: Run code quality checks + - name: Run prek hooks (lint, format, typecheck, uv.lock, tests) + run: | + uv run prek run --all-files + + - name: Run unit tests (via prek) run: | - ./scripts/run_checks.sh --verbose-test-failure || { - echo "" - echo "โŒ Code quality checks failed!" - echo "" - echo "To fix these issues locally, run:" - echo " ./scripts/run_checks.sh --fix" - echo "" - echo "Then commit and push the changes." - echo "" - echo "For more details, see CONTRIBUTING.md" - exit 1 - } \ No newline at end of file + uv run prek run pytest \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..b5be17686 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.1 + hooks: + - id: ruff + - id: ruff-format + + - repo: local + hooks: + - id: pyright + name: Pyright type checking + entry: uv run pyright src tests + language: system + pass_filenames: false + + - id: uv-lock-check + name: uv.lock sync check + entry: uv lock --check + language: system + pass_filenames: false + + - id: pytest + name: Unit tests (manual) + entry: uv run pytest --nbval --current-env --tb=short tests/unit + language: system + pass_filenames: false + stages: [manual] diff --git a/AGENT.md b/AGENT.md index e16357d3d..c98e47341 100644 --- a/AGENT.md +++ b/AGENT.md @@ -8,7 +8,7 @@ This project uses the `uv` package manager. ## Testing -- Always run tests before committing. The test command is `./scripts/run_checks.sh`. +- Always run tests before committing. The test command is `uv run prek run --all-files`. ## Releases diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f2671ec9..cb8ec7367 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,28 +10,32 @@ cd ART Install the dependencies: ```bash -uv sync +uv sync --group dev ``` -### Code Formatting and Linting +### Code Quality Checks (prek) -This project uses [ruff](https://github.com/astral-sh/ruff) for both code formatting and linting. Before submitting a pull request, please ensure your code passes all quality checks: +This project uses [prek](https://github.com/j178/prek) to run local checks (ruff, pyright, uv.lock sync, and unit tests). Before submitting a pull request, please ensure your code passes all quality checks: ```bash -# Run all code quality checks (formatting, linting, and dependency sync) -./scripts/run_checks.sh +# Install git hooks (optional but recommended) +uv run prek install -# Automatically fix any issues that can be fixed -./scripts/run_checks.sh --fix +# Run all checks against all files (formatting, linting, typecheck, uv.lock, tests) +uv run prek run --all-files ``` -The `run_checks.sh` script will: +You can also run individual hooks: -1. Check code formatting with ruff -2. Check for linting issues with ruff -3. Verify that `uv.lock` is in sync with `pyproject.toml` +```bash +uv run prek run ruff +uv run prek run ruff-format +uv run prek run pyright +uv run prek run uv-lock-check +uv run prek run pytest +``` -These checks are automatically run in CI for all pull requests. If your PR fails these checks, simply run `./scripts/run_checks.sh --fix` locally and commit the changes. +These checks are automatically run in CI for all pull requests. If your PR fails these checks, re-run the corresponding `prek` hook locally and commit any fixes. ### Release Process diff --git a/pyproject.toml b/pyproject.toml index 7f1236c18..664e1b4dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ dev = [ "pytest-asyncio>=1.1.0", "duckdb>=1.0.0", "pyarrow>=15.0.0", + "prek>=0.2.29", ] [tool.uv.sources] diff --git a/scripts/run_checks.sh b/scripts/run_checks.sh deleted file mode 100755 index 3fb2c28c1..000000000 --- a/scripts/run_checks.sh +++ /dev/null @@ -1,317 +0,0 @@ -#!/bin/bash - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Parse command line arguments -FIX_FLAG="" -VERBOSE_TEST_FAILURE="" -for arg in "$@"; do - case $arg in - --fix) - FIX_FLAG="--fix" - ;; - --verbose-test-failure) - VERBOSE_TEST_FAILURE="true" - ;; - esac -done - -echo "๐Ÿ” Running code quality checks..." -echo - -# Check if uv is installed -if ! command -v uv &> /dev/null; then - echo -e "${RED}โŒ uv is not installed${NC}" - echo "Please install uv with: curl -LsSf https://astral.sh/uv/install.sh | sh" - exit 1 -fi - -# Track if any checks fail -CHECKS_PASSED=true -TYPECHECK_FAILED=false -TESTS_FAILED=false - -# Run format check -echo "๐Ÿ“ Checking code formatting..." -if [[ -n "$FIX_FLAG" ]]; then - echo " Running: uv run ruff format ." - if uv run ruff format .; then - echo -e "${GREEN}โœ… Code formatted successfully${NC}" - else - echo -e "${RED}โŒ Format fixing failed${NC}" - CHECKS_PASSED=false - fi -else - echo " Running: uv run ruff format --check ." - if uv run ruff format --check .; then - echo -e "${GREEN}โœ… Code formatting looks good${NC}" - else - echo -e "${YELLOW}โš ๏ธ Code formatting issues found${NC}" - echo " Run './scripts/run_checks.sh --fix' to auto-fix" - CHECKS_PASSED=false - fi -fi -echo - -# Run linting check -echo "๐Ÿ”Ž Checking code linting..." -if [[ -n "$FIX_FLAG" ]]; then - echo " Running: uv run ruff check --fix ." - if uv run ruff check --fix .; then - echo -e "${GREEN}โœ… Linting issues fixed${NC}" - else - echo -e "${YELLOW}โš ๏ธ Some linting issues could not be auto-fixed${NC}" - CHECKS_PASSED=false - fi -else - echo " Running: uv run ruff check ." - if uv run ruff check .; then - echo -e "${GREEN}โœ… No linting issues found${NC}" - else - echo -e "${YELLOW}โš ๏ธ Linting issues found${NC}" - echo " Run './scripts/run_checks.sh --fix' to auto-fix some issues" - CHECKS_PASSED=false - fi -fi -echo - -# Run type checking (Pyright) - only on Linux -if [[ "$(uname)" == "Linux" ]]; then - echo "๐Ÿง  Running type checking..." - TMP_PYRIGHT_JSON=$(mktemp) - echo " Running: uv run pyright --outputjson src tests" - # Capture JSON output quietly regardless of success/failure - if uv run pyright --outputjson src tests > "$TMP_PYRIGHT_JSON" 2>/dev/null; then - : # success, continue - else - : # non-zero exit means errors may be present; we'll parse JSON next - fi - - # Parse counts from JSON (errors, warnings, information) - PYRIGHT_COUNTS=$(uv run python - "$TMP_PYRIGHT_JSON" <<'PY' -import json, sys -path = sys.argv[1] -try: - with open(path, 'r') as f: - data = json.load(f) -except Exception: - print("PARSE_ERROR") - sys.exit(0) - -counts = {"error": 0, "warning": 0, "information": 0} -for d in data.get("generalDiagnostics", []): - sev = d.get("severity") - if sev in counts: - counts[sev] += 1 - -print(f"{counts['error']} {counts['warning']} {counts['information']}") -PY -) - - if [[ "$PYRIGHT_COUNTS" == "PARSE_ERROR" ]]; then - echo -e "${RED}โŒ Type checking failed (unable to parse results)${NC}" - CHECKS_PASSED=false - TYPECHECK_FAILED=true - else - ERR_COUNT=$(echo "$PYRIGHT_COUNTS" | awk '{print $1}') - WARN_COUNT=$(echo "$PYRIGHT_COUNTS" | awk '{print $2}') - INFO_COUNT=$(echo "$PYRIGHT_COUNTS" | awk '{print $3}') - if [[ "$ERR_COUNT" -gt 0 ]]; then - echo -e "${RED}โŒ Type checking failed${NC}" - echo " Errors: $ERR_COUNT, Warnings: $WARN_COUNT, Info: $INFO_COUNT" - CHECKS_PASSED=false - TYPECHECK_FAILED=true - else - echo -e "${GREEN}โœ… Type checking passed${NC}" - echo " Errors: $ERR_COUNT, Warnings: $WARN_COUNT, Info: $INFO_COUNT" - fi - fi - rm -f "$TMP_PYRIGHT_JSON" -else - echo "๐Ÿง  Skipping type checking (Linux only)" -fi -echo - -# Run tests - only on Linux -if [[ "$(uname)" == "Linux" ]]; then - echo "๐Ÿงช Running unit tests..." - echo " Running: uv run pytest --nbval --current-env tests/unit" - - # Capture pytest output quietly to parse the summary - PYTEST_OUTPUT=$(mktemp) - if uv run pytest --nbval --current-env --tb=short tests/unit > "$PYTEST_OUTPUT" 2>&1; then - TEST_EXIT_CODE=0 - else - TEST_EXIT_CODE=$? - fi - - # Extract the test summary line (e.g., "===== 5 passed, 2 failed, 1 skipped in 3.45s =====") - # This regex captures various pytest summary formats - TEST_SUMMARY=$(grep -E "^=+ .*(passed|failed|error|skipped|xfailed|xpassed|warning).*=+$" "$PYTEST_OUTPUT" | tail -1) - - if [[ -n "$TEST_SUMMARY" ]]; then - # Parse the summary to extract counts - PASSED=$(echo "$TEST_SUMMARY" | grep -oE "[0-9]+ passed" | grep -oE "[0-9]+" || echo "0") - FAILED=$(echo "$TEST_SUMMARY" | grep -oE "[0-9]+ failed" | grep -oE "[0-9]+" || echo "0") - ERRORS=$(echo "$TEST_SUMMARY" | grep -oE "[0-9]+ error" | grep -oE "[0-9]+" || echo "0") - SKIPPED=$(echo "$TEST_SUMMARY" | grep -oE "[0-9]+ skipped" | grep -oE "[0-9]+" || echo "0") - WARNINGS=$(echo "$TEST_SUMMARY" | grep -oE "[0-9]+ warning" | grep -oE "[0-9]+" || echo "0") - XFAILED=$(echo "$TEST_SUMMARY" | grep -oE "[0-9]+ xfailed" | grep -oE "[0-9]+" || echo "0") - XPASSED=$(echo "$TEST_SUMMARY" | grep -oE "[0-9]+ xpassed" | grep -oE "[0-9]+" || echo "0") - - # Build detailed summary - DETAILS="" - [[ "$PASSED" != "0" ]] && DETAILS="${DETAILS}Passed: $PASSED" - [[ "$FAILED" != "0" ]] && DETAILS="${DETAILS:+$DETAILS, }Failed: $FAILED" - [[ "$ERRORS" != "0" ]] && DETAILS="${DETAILS:+$DETAILS, }Errors: $ERRORS" - [[ "$SKIPPED" != "0" ]] && DETAILS="${DETAILS:+$DETAILS, }Skipped: $SKIPPED" - [[ "$XFAILED" != "0" ]] && DETAILS="${DETAILS:+$DETAILS, }XFailed: $XFAILED" - [[ "$XPASSED" != "0" ]] && DETAILS="${DETAILS:+$DETAILS, }XPassed: $XPASSED" - [[ "$WARNINGS" != "0" ]] && DETAILS="${DETAILS:+$DETAILS, }Warnings: $WARNINGS" - - # Check if there were any failures or errors - if [[ "$FAILED" == "0" && "$ERRORS" == "0" && $TEST_EXIT_CODE -eq 0 ]]; then - echo -e "${GREEN}โœ… All tests passed${NC}" - [[ -n "$DETAILS" ]] && echo " $DETAILS" - else - echo -e "${RED}โŒ Tests failed${NC}" - [[ -n "$DETAILS" ]] && echo " $DETAILS" - CHECKS_PASSED=false - TESTS_FAILED=true - - # If verbose test failure flag is set, dump full test output - if [[ -n "$VERBOSE_TEST_FAILURE" ]]; then - echo - echo "๐Ÿ“‹ Full test output:" - echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" - cat "$PYTEST_OUTPUT" - echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" - fi - fi - else - # Fallback if we can't parse the summary - if [[ $TEST_EXIT_CODE -eq 0 ]]; then - echo -e "${GREEN}โœ… All unit tests passed${NC}" - else - echo -e "${RED}โŒ Some unit tests failed${NC}" - CHECKS_PASSED=false - TESTS_FAILED=true - - # If verbose test failure flag is set, dump full test output - if [[ -n "$VERBOSE_TEST_FAILURE" ]]; then - echo - echo "๐Ÿ“‹ Full test output:" - echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" - cat "$PYTEST_OUTPUT" - echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" - fi - fi - fi - - rm -f "$PYTEST_OUTPUT" -else - echo "๐Ÿงช Skipping unit tests (Linux only)" -fi -echo - -# Check if uv.lock is in sync with pyproject.toml -echo "๐Ÿ”’ Checking if uv.lock is up to date..." -PRIMARY_EXTRAS=(--all-extras) -FALLBACK_EXTRAS=(--extra plotting --extra skypilot) -if [[ -n "$FIX_FLAG" ]]; then - echo " Attempting: uv sync --all-extras" - if uv sync "${PRIMARY_EXTRAS[@]}"; then - # Check if uv.lock was modified - if git diff --quiet uv.lock 2>/dev/null; then - echo -e "${GREEN}โœ… Dependencies are in sync${NC}" - else - echo -e "${GREEN}โœ… Updated uv.lock to match pyproject.toml${NC}" - echo -e "${YELLOW} Don't forget to commit the updated uv.lock file${NC}" - fi - else - echo " Primary sync failed; falling back: uv sync --extra plotting --extra skypilot" - if uv sync "${FALLBACK_EXTRAS[@]}"; then - if git diff --quiet uv.lock 2>/dev/null; then - echo -e "${GREEN}โœ… Dependencies are in sync (fallback extras)${NC}" - else - echo -e "${GREEN}โœ… Updated uv.lock to match pyproject.toml (fallback extras)${NC}" - echo -e "${YELLOW} Don't forget to commit the updated uv.lock file${NC}" - fi - else - echo -e "${RED}โŒ Failed to sync dependencies (both primary and fallback)${NC}" - CHECKS_PASSED=false - fi - fi -else - echo " Checking if uv sync would modify uv.lock..." - # Create a temporary copy of uv.lock - cp uv.lock uv.lock.backup 2>/dev/null || touch uv.lock.backup - - # Try primary extras quietly - if uv sync --quiet "${PRIMARY_EXTRAS[@]}" 2>/dev/null; then - # Check if uv.lock was modified - if diff -q uv.lock uv.lock.backup >/dev/null 2>&1; then - echo -e "${GREEN}โœ… uv.lock is up to date${NC}" - else - echo -e "${YELLOW}โš ๏ธ uv.lock is out of sync with pyproject.toml${NC}" - echo " Run 'uv sync --all-extras' and commit the changes" - CHECKS_PASSED=false - # Restore the original uv.lock - mv uv.lock.backup uv.lock - fi - else - echo " Primary check failed; trying fallback extras quietly..." - if uv sync --quiet "${FALLBACK_EXTRAS[@]}" 2>/dev/null; then - if diff -q uv.lock uv.lock.backup >/dev/null 2>&1; then - echo -e "${GREEN}โœ… uv.lock is up to date (checked with fallback extras)${NC}" - else - echo -e "${YELLOW}โš ๏ธ uv.lock is out of sync with pyproject.toml (fallback extras)${NC}" - echo " Run 'uv sync --extra plotting --extra skypilot' and commit the changes" - CHECKS_PASSED=false - mv uv.lock.backup uv.lock - fi - else - echo -e "${RED}โŒ Failed to check dependencies (both primary and fallback)${NC}" - CHECKS_PASSED=false - # Restore the original uv.lock if it exists - [ -f uv.lock.backup ] && mv uv.lock.backup uv.lock - fi - fi - - # Clean up backup file - rm -f uv.lock.backup -fi -echo - -# Summary -if $CHECKS_PASSED; then - echo -e "${GREEN}๐ŸŽ‰ All checks passed!${NC}" - exit 0 -else - echo -e "${RED}โŒ Some checks failed${NC}" - if [[ -z "$FIX_FLAG" ]]; then - # Show tips for each type of failure - if $TYPECHECK_FAILED; then - echo -e "๐Ÿ’ก Tip: Type errors can't be auto-fixed by --fix. Re-run ${YELLOW}uv run pyright src${NC} to see full diagnostics." - fi - if $TESTS_FAILED; then - if [[ -z "$VERBOSE_TEST_FAILURE" ]]; then - echo -e "๐Ÿ’ก Tip: Test failures can't be auto-fixed by --fix. Re-run ${YELLOW}uv run pytest --nbval --current-env tests/unit${NC} to see full test output." - echo -e " Or use ${YELLOW}./scripts/run_checks.sh --verbose-test-failure${NC} to see full output on failure." - else - echo -e "๐Ÿ’ก Tip: Test failures can't be auto-fixed by --fix. Re-run ${YELLOW}uv run pytest --nbval --current-env tests/unit${NC} to debug." - fi - fi - # Show general fix tip if there are failures but not type/test specific ones - if ! $TYPECHECK_FAILED && ! $TESTS_FAILED; then - echo -e "๐Ÿ’ก Tip: Run ${YELLOW}./scripts/run_checks.sh --fix${NC} to automatically fix some issues" - fi - fi - exit 1 -fi \ No newline at end of file diff --git a/uv.lock b/uv.lock index fec158e41..505c7f8f7 100644 --- a/uv.lock +++ b/uv.lock @@ -5144,6 +5144,7 @@ dev = [ { name = "ipykernel" }, { name = "ipywidgets" }, { name = "nbval" }, + { name = "prek" }, { name = "pyarrow" }, { name = "pyright", extra = ["nodejs"] }, { name = "pytest" }, @@ -5203,6 +5204,7 @@ dev = [ { name = "ipykernel", specifier = ">=6.29.5" }, { name = "ipywidgets", specifier = ">=8.1.5" }, { name = "nbval", specifier = ">=0.11.0" }, + { name = "prek", specifier = ">=0.2.29" }, { name = "pyarrow", specifier = ">=15.0.0" }, { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.403" }, { name = "pytest", specifier = ">=8.4.1" }, @@ -5753,6 +5755,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/f6/d1efedc0f9506e47699616e896d8efe39e8f0b6a7d1d590c3e97455ecf4a/polyfile_weave-0.5.7-py3-none-any.whl", hash = "sha256:880454788bc383408bf19eefd6d1c49a18b965d90c99bccb58f4da65870c82dd", size = 1655397, upload-time = "2025-09-22T19:21:09.142Z" }, ] +[[package]] +name = "prek" +version = "0.2.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/95/d89e32fc02bbb5d20a562062165bfe309d382798dd2e4e76edcfbcd0434a/prek-0.2.29.tar.gz", hash = "sha256:9788d0503a6e13ed84f864beaf12e87eee6140d799e6a379c77c06c801656e75", size = 288357, upload-time = "2026-01-16T11:39:30.905Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/0f/5bcc388fb779c30dcf3b3c512b07520b39c27c31ce9616ea5fc0c34b76aa/prek-0.2.29-py3-none-linux_armv6l.whl", hash = "sha256:ec0c7b67f3fdbfab447ff3cb37284bc5ec26816f19641393a522a107be6a428a", size = 5227889, upload-time = "2026-01-16T11:39:35.244Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d0/c10e88e39dfb914981291c4e6929ddb1de6033e8df5b7f7949bf3864eff4/prek-0.2.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:642ddee15c18d91f79095fd9a57ede8f522997a2ac131dadbe7eb8a770909f62", size = 5645793, upload-time = "2026-01-16T11:39:26.39Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/d0783455cd28905d63326e33ec91527d2df5d65c66c2f10092ee56fac49a/prek-0.2.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0a1e9673d939811d7cf692df4fa313f6d821ca3512e601c0b7ec037f46877ccf", size = 5419376, upload-time = "2026-01-16T11:39:22.087Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/ee193357f149aec65fb597ff85f67465ea8c36ebf450b996186603b8b78c/prek-0.2.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8e47c5beda1f4916f3f8607bb232de93dba8bf61b76aadff3e842a95a26046c0", size = 5497984, upload-time = "2026-01-16T11:39:38.173Z" }, + { url = "https://files.pythonhosted.org/packages/1b/68/2a3dd25749387d925632811abab62b97d4894b58652c14eb6c5a70a4b3ff/prek-0.2.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a60c29bec6c75702e24b85b55819a3dce50c19980995429e79b4c2fc78d38eb", size = 5176032, upload-time = "2026-01-16T11:39:42.436Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/a827b85677971ccc37842de5e5dd9452721ad73cf4e6e2d95c2be4326b00/prek-0.2.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6fd36d2f507d1a5e466cd1bd2cdafbffab244e2667482aba5a25476d077e435", size = 6188687, upload-time = "2026-01-16T11:39:33.995Z" }, + { url = "https://files.pythonhosted.org/packages/db/b1/6e6d79bb77523b0b9331524f9edb454f4c60c70a40543074337946e0c1ba/prek-0.2.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a874c875f41735648e07da88a9ced3ef16b08b5626ad386d8f21c071d39b0a0", size = 5805684, upload-time = "2026-01-16T11:39:39.876Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d7/891129cde04c88e58b1443f9f1aff620d7d4e8ccb22cbd728a209e59895a/prek-0.2.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4724a9e6e15b2791d2bcc312d6702bf2548e7864c8e956451310c1ec81afb47d", size = 5849611, upload-time = "2026-01-16T11:39:32.368Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d8/28fd18a063e9c980b3a496e27d0dd0823849549912d797d797f83bddd30f/prek-0.2.29-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a71ede67340208759a52ca0c144476e4e2220f60a08cc55035d0b50d80fee79c", size = 5547601, upload-time = "2026-01-16T11:39:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/51/2a/683b32b3f0bce0fd8a91b3bb6e9a5a89709d6132e2cbbe48113a05da2384/prek-0.2.29-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8cb4cf35ac26bf7a28f3d8ccda9c4a95da0904aa636ca38c3ae6ae94d0215bea", size = 5552655, upload-time = "2026-01-16T11:39:19.362Z" }, + { url = "https://files.pythonhosted.org/packages/82/60/c8b90dc109a9a0c0127550ece2c02a8e5c61f7e5e2889cc7d271fedf3dc7/prek-0.2.29-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:c11e680fa6c3b3e0e33533729b7bf11a20313c1a860dafd415bc371b7ae4bcdd", size = 5143709, upload-time = "2026-01-16T11:39:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5f/00d81f9ecd1abf0085557e553f7c41eae01714f4c02859ba36ebcf3f7c03/prek-0.2.29-py3-none-musllinux_1_1_i686.whl", hash = "sha256:12cd792070eb47b01b9bd8a2632a74425e8dae016a6396b83683728e1a9ba0aa", size = 5817372, upload-time = "2026-01-16T11:39:25.091Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/6434356f1e1162b4c4130c18ea14657e264947ae76d0b9794012d927a032/prek-0.2.29-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e6643bb734a4d7f49555eaceed71e4afde022c33e16134acf45dbbd93dbde690", size = 5958617, upload-time = "2026-01-16T11:39:41.109Z" }, + { url = "https://files.pythonhosted.org/packages/54/3c/443fdb087d045ae27ed053ef65f4d40bb624c9ac65299ae90f680b36dbd1/prek-0.2.29-py3-none-win32.whl", hash = "sha256:4f5c7dc6452c0342adf07711561a2912d20050cf378f4df6a6bc825b647f9e4b", size = 5063876, upload-time = "2026-01-16T11:39:36.58Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bb/d6698aaab0f04d6743435eeecdf026bda2ddfc96845a3c6f9a044d5d5005/prek-0.2.29-py3-none-win_amd64.whl", hash = "sha256:4297090a24685fc0998699a89b3ab7eb6447ff8d4eca0e836a697b51657930a8", size = 5783120, upload-time = "2026-01-16T11:39:23.359Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d4/60a5356219640df96343039e58723a63ca67915f933bffa1c4779a596523/prek-0.2.29-py3-none-win_arm64.whl", hash = "sha256:95844eeae5bdeb4e3ba91f4afb46115853eaf0d7a6654238320ecbdfb7f33e67", size = 5491654, upload-time = "2026-01-16T11:39:28.132Z" }, +] + [[package]] name = "prettytable" version = "3.17.0"