diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml new file mode 100644 index 0000000..48c46c2 --- /dev/null +++ b/.github/workflows/python-test.yml @@ -0,0 +1,69 @@ +name: Python Test +permissions: {} +on: + workflow_dispatch: + push: + branches: ["main"] + paths-ignore: + - "doc/**" + - "**/*.md" + - ".github/workflows/static-checks.yml" + - ".pre-commit-config.yaml" + pull_request: + paths-ignore: + - "doc/**" + - "**/*.md" + - ".github/workflows/static-checks.yml" + - ".pre-commit-config.yaml" + schedule: + - cron: "20 14 * * *" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + tests: + name: ${{ format('Test Python {0} {1}', matrix.python_version, ((matrix.dependencies == 'min' || matrix.dependencies == 'max') && format('{0} dependencies', matrix.dependencies)) || (matrix.os || 'ubuntu-latest')) }} + runs-on: ${{ matrix.os || 'ubuntu-latest' }} + env: + PYTHONIOENCODING: utf-8 + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: ${{ matrix.uv_version || 'latest' }} + enable-cache: true + python-version: ${{ matrix.python_version }} + + - name: Setup dependencies + run: | + uv sync $CI_UV_UPGRADE_ARG + env: + CI_UV_UPGRADE_ARG: ${{ matrix.dependencies == 'max' && '--upgrade' || '' }} + UV_RESOLUTION: ${{ matrix.dependencies == 'min' && 'lowest-direct' || 'highest' }} + + - name: Run pytest + run: uv run --frozen pytest -v + + strategy: + matrix: + python_version: ["3.10", "3.14"] + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + dependencies: ["lockfile"] + + include: + - python_version: "3.11" + os: "ubuntu-latest" + - python_version: "3.12" + os: "ubuntu-latest" + - python_version: "3.13" + os: "ubuntu-latest" + - dependencies: "min" + python_version: "3.10" + uv_version: "0.9.9" + - dependencies: "max" + python_version: "3.14" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e3a48b3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Release to PyPI +permissions: {} +on: + push: + tags: + - "v*" +jobs: + build: + name: Build Distributions + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: "latest" + enable-cache: false + + - name: Build distributions + run: uv build + + - name: Upload distributions + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dists + path: dist/ + if-no-files-found: error + + publish: + name: Publish + runs-on: ubuntu-latest + permissions: + id-token: write # for Trusted Publishing + needs: build + + environment: release + + steps: + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: "latest" + enable-cache: false + ignore-empty-workdir: true + + - name: Download distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dists + path: dist/ + + - name: Publish + run: | + uv publish --trusted-publishing always dist/* diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml new file mode 100644 index 0000000..0e28765 --- /dev/null +++ b/.github/workflows/rust-test.yml @@ -0,0 +1,48 @@ +name: Rust Test +permissions: {} +on: + workflow_dispatch: + push: + branches: ["main"] + paths-ignore: + - "doc/**" + - "**/*.md" + - ".github/workflows/static-checks.yml" + - ".pre-commit-config.yaml" + pull_request: + paths-ignore: + - "doc/**" + - "**/*.md" + - ".github/workflows/static-checks.yml" + - ".pre-commit-config.yaml" + schedule: + - cron: "20 14 * * *" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + cargo-test: + name: Cargo Test (${{ matrix.rust }}) + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.rust == 'beta' }} + strategy: + matrix: + rust: [stable, beta] + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.10" + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master + with: + toolchain: ${{ matrix.rust }} + + - name: Run cargo test + run: cargo test --no-default-features diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml new file mode 100644 index 0000000..82d4927 --- /dev/null +++ b/.github/workflows/static-checks.yml @@ -0,0 +1,57 @@ +name: Static Checks +permissions: {} +on: + workflow_dispatch: + push: + branches: ["main"] + pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + analyze-python: + name: Analyze Python + runs-on: "ubuntu-latest" + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: "latest" + enable-cache: true + + - name: Run checks + run: | + uv run --frozen prek -a + env: + PREK_UV_SOURCE: github + + analyze-rust: + name: Analyze Rust + runs-on: "ubuntu-latest" + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.10" + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master + with: + toolchain: stable + components: clippy, rustfmt + + - name: Check formatting + run: cargo fmt --check + + - name: Run clippy + run: cargo clippy --no-default-features -- -D warnings diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..fb542f9 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,29 @@ +name: zizmor +on: + push: + branches: ["main"] + paths-ignore: + - "docs/**" + - "**/*.md" + - ".pre-commit-config.yaml" + pull_request: + branches: ["**"] + paths-ignore: + - "docs/**" + - "**/*.md" + - ".pre-commit-config.yaml" +permissions: {} +jobs: + zizmor: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write # needed to upload results + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 diff --git a/.gitignore b/.gitignore index b7faf40..02a6ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,32 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ + +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# rustc will dump stack traces when hitting an internal compiler error to PWD +rustc-ice-*.txt + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Agent Skills +.agents/skills/ +skills-lock.json diff --git a/.importlinter b/.importlinter new file mode 100644 index 0000000..55d641b --- /dev/null +++ b/.importlinter @@ -0,0 +1,18 @@ +[importlinter] +root_packages = + yamltrip + +[importlinter:contract:0] +name = yamltrip +type = layers +layers = + editor + document + errors +containers = + yamltrip +exhaustive = True +exhaustive_ignores = + _core +ignore_imports = + yamltrip.document -> yamltrip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..965fe27 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +repos: + - repo: https://github.com/tsvikas/sync-with-uv + rev: v0.5.0 + hooks: + - id: sync-with-uv + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.21.2 + hooks: + - id: pyproject-fmt + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.12 + hooks: + - id: ruff-check + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.12 + hooks: + - id: ruff-format + - repo: local + hooks: + - id: ty + name: ty + always_run: true + entry: uv run --frozen --offline ty check + language: system + pass_filenames: false + - repo: local + hooks: + - id: deptry + name: deptry + always_run: true + entry: uv run --frozen --offline deptry src + language: system + pass_filenames: false + - repo: local + hooks: + - id: import-linter + name: import-linter + always_run: true + entry: uv run --frozen --offline lint-imports + language: system + pass_filenames: false + - repo: https://github.com/codespell-project/codespell + rev: v2.4.2 + hooks: + - id: codespell + additional_dependencies: + - tomli diff --git a/Cargo.lock b/Cargo.lock index 578e7b8..8684af9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,67 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + [[package]] name = "indoc" version = "2.0.7" @@ -23,12 +72,34 @@ dependencies = [ "rustversion", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "libc" version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "line-index" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e27e0ed5a392a7f5ba0b3808a2afccff16c64933312c84b57618b49d1209bd2" +dependencies = [ + "nohash-hasher", + "text-size", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + [[package]] name = "memoffset" version = "0.9.1" @@ -38,6 +109,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "once_cell" version = "1.21.3" @@ -129,12 +206,133 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "subfeature" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce89b340c4df706a70c1f25834c10a768fc11afeb69c832e4aa182d3158fac1" +dependencies = [ + "memchr", + "regex", + "serde", +] + [[package]] name = "syn" version = "2.0.111" @@ -152,6 +350,71 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tree-sitter" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-iter" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f90e647c44106848cae33ec24309a2cea64de58066dc279731812be5aa6faf" +dependencies = [ + "tree-sitter", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-yaml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c223db85f05e34794f065454843b0668ebc15d240ada63e2b5939f43ce7c97" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -164,9 +427,56 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "yamlpatch" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5afc92d2beb7a811cbda1fc0db34b6afaa3f4218c3da792ffc5dc633b26b723a" +dependencies = [ + "indexmap", + "line-index", + "serde", + "serde_json", + "serde_yaml", + "subfeature", + "thiserror", + "yamlpath", +] + +[[package]] +name = "yamlpath" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dfc10af3a0b762c85fe94c3d9e00343078bef7048793a652430393faa50e47e" +dependencies = [ + "line-index", + "self_cell", + "serde", + "thiserror", + "tree-sitter", + "tree-sitter-iter", + "tree-sitter-yaml", +] + [[package]] name = "yamltrip" version = "0.1.0" dependencies = [ + "indexmap", "pyo3", + "serde_yaml", + "yamlpatch", + "yamlpath", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 5cab4ce..96d3df7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,20 @@ edition = "2024" [lib] name = "_core" # "cdylib" is necessary to produce a shared library for Python to import from. -crate-type = ["cdylib"] +# "lib" enables `cargo test` for Rust-level unit tests. +crate-type = ["cdylib", "lib"] [dependencies] -# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) # "abi3-py310" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.10 -pyo3 = { version = "0.27.1", features = ["extension-module", "abi3-py310"] } +# "extension-module" is behind a feature flag so `cargo test` can link against libpython +pyo3 = { version = "0.27.1", features = ["abi3-py310"] } +yamlpath = "1" +yamlpatch = "1" +# NOTE: serde_yaml 0.9 is archived/unmaintained (dtolnay). No drop-in +# replacement exists yet. Monitor for a successor or security advisories. +serde_yaml = "0.9" +indexmap = "2" + +[features] +default = ["extension-module"] +extension-module = ["pyo3/extension-module"] diff --git a/README.md b/README.md index e69de29..50bf021 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,139 @@ +# yamltrip + +[![usethis](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/usethis-python/usethis-python/main/assets/badge/v1.json)](https://github.com/usethis-python/yamltrip) [![PyPI Version](https://img.shields.io/pypi/v/yamltrip.svg)](https://pypi.python.org/pypi/yamltrip) [![PyPI License](https://img.shields.io/pypi/l/yamltrip.svg)](https://github.com/usethis-python/yamltrip?tab=MIT-1-ov-file) [![PyPI Supported Versions](https://img.shields.io/pypi/pyversions/yamltrip.svg)](https://pypi.python.org/pypi/yamltrip) + +Edit YAML files from Python, while respecting format and comments during the round-trip. + +Built on [tree-sitter-yaml](https://github.com/tree-sitter-grammars/tree-sitter-yaml) via the [yamlpath](https://crates.io/crates/yamlpath) and [yamlpatch](https://crates.io/crates/yamlpatch) Rust crates. + +## Installation + +```console +# With uv +$ uv add yamltrip + +# With pip +$ pip install yamltrip +``` + +## Quick Start + +```python +import yamltrip + +# Load and read +doc = yamltrip.loads("name: Alice\nage: 30") +print(doc["name"]) # "Alice" +print("name" in doc) # True + +# Immutable mutations: each call returns a new Document +doc2 = doc.replace("age", value=31) +doc3 = doc2.add(key="city", value="Portland") +print(doc3.dumps()) + +# File-based editing with a context manager +with yamltrip.edit("config.yml") as editor: + editor.replace("version", value="2.0") + editor.upsert("settings", "debug", value=True) + # writes back on successful exit; discards on exception +``` + +## API Overview + +### Top-level function + +| Function | Description | +| -------------------------- | ---------------------------------------------- | +| `yamltrip.loads(source)` | Parse a YAML string into a `Document` | +| `yamltrip.load(path)` | Read a YAML file into a `Document` | +| `yamltrip.edit(path)` | Open a YAML file for editing (context manager) | + +### Document (immutable) + +Every mutation method returns a **new** `Document`. The original is never +modified. + +```python +doc = yamltrip.loads("items:\n - a\n - b") + +doc["items"] # ["a", "b"] +doc["items", 0] # "a" +("items", 0) in doc # True + +doc.replace("items", 0, value="x") +doc.add("items", key="c", value=3) +doc.upsert("new", "nested", value=True) +doc.remove("items", 0) +doc.prune_remove("a", "b", "c") # remove + prune empty parents +doc.append("items", value="c") +doc.extend_list("items", values=["d", "e"]) +doc.remove_from_list("items", values=["a"]) + +doc.query("items") # Feature with location info +doc.query_pretty("items") # Feature with surrounding context +doc.extract(feature) # raw YAML text for a Feature +doc.has_anchors() # True if anchors/aliases present +doc.dumps() # full YAML source +doc.dump("output.yml") # write to file +``` + +### Editor (mutable context manager) + +Wraps `Document` with the same mutation methods, but applies changes in place +and writes back to disk when the context exits cleanly: + +```python +with yamltrip.edit("config.yml") as ed: + ed.replace("version", value="2.0") + ed.upsert("new_key", value="new_value") + ed.remove("old_key") + print(ed["version"]) # "2.0" + print(ed.original["version"]) # original value before edits +``` + +### Error Hierarchy + +All yamltrip errors inherit from `YAMLTripError`: + +- **`ParseError`**: YAML input cannot be parsed. +- **`QueryError`**: path not found during lookup. +- **`PatchError`**: mutation operation failed. + - **`KeyExistsError`**: `add()` target already exists. + - **`KeyMissingError`**: `replace()` target does not exist. + +## Limitations + +- **Multi-document YAML streams** (`---` separated) are not supported. +- **YAML tags** (`!!omap`, `!!set`, `!!merge`, custom tags) are not + interpreted. +- **Anchors and aliases** (`&anchor` / `*alias`) are detected + (`doc.has_anchors()`) but not resolved during value extraction. +- **Large integers may lose precision.** YAML integers outside the signed + 64-bit range (i64) may become `float` during deserialization. +- **Editor write-back is not atomic.** `Editor` detects external file changes + between enter and exit, but the check-then-write is racy. Do not use it + with concurrent writers. +- **Line endings preserved as-is.** No CRLF/LF normalization. Mixed line + endings pass through unchanged. + +## Design Decisions + +- **No custom Python class serialization.** Values convert to/from + `str`, `int`, `float`, `bool`, `None`, `list`, and `dict` only. +- **UTF-8 only.** Other encodings raise `ParseError`. +- **Non-finite floats rejected.** `float("inf")`, `float("-inf")`, and + `float("nan")` cannot be serialized. +- **Integer keys cannot create structures.** `upsert()` with integer path + components can update existing sequence entries but cannot create new + intermediate mappings. Only string keys create new mappings. +- **No negative sequence indices.** Python-style negative indexing is not + supported. + +## Acknowledgements + +yamltrip depends entirely on the [yamlpath](https://github.com/zizmorcore/zizmor/tree/main/crates/yamlpath) and [yamlpatch](https://github.com/zizmorcore/zizmor/tree/main/crates/yamlpatch) Rust crates for format-preserving YAML parsing and patching. These libraries use tree-sitter to query and modify YAML source text without discarding comments, whitespace, or key ordering. All of the core logic in yamltrip +passes through them. Thanks to [William Woodruff](https://github.com/woodruffw) for creating and maintaining both crates as part of [zizmor](https://github.com/zizmorcore/zizmor). + +## License + +MIT diff --git a/doc/specs/2026-05-13-yamltrip-design.md b/doc/specs/2026-05-13-yamltrip-design.md new file mode 100644 index 0000000..91ead74 --- /dev/null +++ b/doc/specs/2026-05-13-yamltrip-design.md @@ -0,0 +1,349 @@ +# yamltrip Design Spec + +**Date:** 2026-05-13 +**Status:** Draft + +## Overview + +yamltrip is a round-tripping YAML library for Python, backed by Rust. It wraps +the `yamlpath` and `yamlpatch` crates (from the zizmor project) via PyO3 to +provide comment-, quote-, and indentation-preserving YAML editing. + +### Goals + +- General-purpose YAML library for Python with first-class round-trip support. +- Replace ruamel.yaml for config-editing use cases with better preservation + guarantees and a simpler API. +- Rust-powered performance with a Pythonic interface. + +### Non-goals (v0.1) + +- Multi-document YAML streams (`---` delimiters). +- Full YAML data model (tagged types, `!!omap`, `!!set`, `!!merge`). +- Custom Python class serialization/deserialization. +- YAML emitter controls (flow style, colon alignment, etc.) — not needed since + the original formatting is preserved. + +## Architecture + +Two layers: + +### Layer 1: `yamltrip._core` (Rust / PyO3) + +Direct wrappings of yamlpath + yamlpatch types: + +| Rust Type | Python `_core` Class | Purpose | +|---|---|---| +| `yamlpath::Document` | `_core.Document` | Parse YAML, hold tree-sitter state | +| `yamlpath::Route` | `_core.Route` | Path into a YAML document | +| `yamlpath::Component` | `_core.Component` | Single route segment (Key or Index) | +| `yamlpath::Feature` | `_core.Feature` | Query result with location/span info | +| `yamlpath::Location` | `_core.Location` | Byte offset span (start, end) | +| `yamlpath::FeatureKind` | `_core.FeatureKind` | Enum: BlockMapping, FlowMapping, BlockSequence, FlowSequence, Scalar | +| `yamlpatch::Patch` | `_core.Patch` | Route + Op pair | +| `yamlpatch::Op` | `_core.Op` | Enum with static constructors | +| `yamlpatch::apply_yaml_patches` | `_core.apply_patches()` | Apply patches to YAML string | + +### Layer 2: `yamltrip` (pure Python) + +High-level Pythonic API built on `_core`: + +- `Document` — immutable YAML document value object. +- `Editor` — mutable context manager for file editing. +- `loads()` / `load()` / `edit()` — module-level constructors. +- `Feature`, `Location`, `FeatureKind` — re-exported from `_core`. +- Error types. + +## API + +### Construction + +```python +import yamltrip + +doc = yamltrip.loads("name: foo\nitems:\n - a\n - b") +doc = yamltrip.load("config.yml") +``` + +### Document (immutable) + +Each mutation method returns a new `Document`. The original is never modified. + +```python +class Document: + source: str # current YAML text +``` + +#### Querying + +Returns Python primitives (str, int, float, bool, None, list, dict). + +```python +doc["name"] # → "foo" +doc["items", 0] # → "a" +("name",) in doc # → True +("missing",) in doc # → False +``` + +`__getitem__` accepts a single key or a tuple of keys. Each key is `str` (mapping +key) or `int` (sequence index). Raises `QueryError` if the path doesn't exist. +Python's `__getitem__` naturally handles both: `doc["name"]` receives `"name"`, +`doc["a", "b"]` receives `("a", "b")`. The implementation normalizes single keys +to 1-tuples. + +`__contains__` accepts a tuple of keys. Returns `False` for missing paths (never +raises). + +#### Patch Operations + +All return a new `Document`. + +| Method | Behavior | +|---|---| +| `replace(*keys, value=...)` | Replace value at existing path. Raises `KeyMissingError` if path doesn't exist. | +| `add(*keys, key=..., value=...)` | Add a new key-value pair to the mapping at path. Raises `KeyExistsError` if key already exists. | +| `upsert(*keys, value=...)` | Replace if path exists, create (including intermediate mappings) if not. Never raises for missing/existing keys. | +| `remove(*keys, prune=False)` | Remove key or index at path. If `prune=True`, also removes parent mappings/sequences that become empty, bottom-up. | +| `prune_remove(*keys)` | Convenience for `remove(*keys, prune=True)`. | +| `append(*keys, value=...)` | Append a single item to the sequence at path. | +| `extend_list(*keys, values=[...])` | Append multiple items to the sequence at path. | +| `remove_from_list(*keys, values=[...])` | Remove all occurrences of the given values from the sequence at path. | + +##### `upsert()` behavior + +`upsert(*keys, value=...)` handles three cases internally: + +1. **Full path exists:** delegates to `replace()`. +2. **Partial path exists:** walks down to the deepest existing key, then uses + yamlpatch `MergeInto` to create the remaining nested structure in a single + operation. +3. **No path exists:** uses yamlpatch `Add` with a nested value to create the + entire path in a single operation. + +This is implemented in the Python layer using existing yamlpatch operations. + +##### `upsert()` with no keys (root-level) + +`doc.upsert(value={"a": 1})` with no path keys replaces the entire root document +content. The value must be a mapping. + +##### `remove_from_list()` implementation + +`remove_from_list(*keys, values=[...])` is implemented in the Python layer: + +1. Query the sequence at `*keys` to get its current contents. +2. Find the indices of all elements matching any value in `values`. +3. Remove those indices in reverse order (highest first) using yamlpatch + `Op::Remove`, each with a `Route` targeting the specific index. + +This ensures index stability during removal. + +##### `prune_remove()` / `remove(prune=True)` behavior + +After removing the targeted key: + +1. Walk back up the key path from deepest to shallowest. +2. At each level, check if the parent mapping/sequence is now empty. +3. If empty, remove it too. +4. Stop at the first non-empty parent. + +Implemented in the Python layer as a loop of `remove()` + `__contains__` checks. + +#### Inspection + +```python +feature = doc.query(*keys) # → Feature with location info +text = doc.extract(feature) # → raw text from source at feature's span +``` + +#### Output + +```python +doc.dumps() -> str # return YAML string +doc.dump("config.yml") # write to file +``` + +### Editor (mutable context manager) + +```python +with yamltrip.edit("config.yml") as editor: + # Attributes + editor.original # → Document snapshot from file load (never changes) + editor.document # → current patched Document (updates after each op) + + # All Document patch methods are available directly: + editor.replace("name", value="bar") + editor.add("settings", key="debug", value=True) + editor.upsert("timeout", value=30) + editor.remove("old_key") + editor.prune_remove("section", "subsection", "key") + editor.remove("other", prune=True) + editor.append("items", value="new") + editor.extend_list("items", values=["c", "d"]) + editor.remove_from_list("items", values=["a"]) + + # __getitem__ queries current patched state + editor["name"] # → "bar" + ("settings", "debug") in editor # → True + + # __setitem__ calls upsert + editor["name"] = "baz" + editor["nested", "key"] = "val" + + # Can also query original + editor.original["name"] # → "foo" (original state) + +# File is written on successful __exit__ +# NOT written if an exception occurs +``` + +#### Editor semantics + +- Each patch method mutates `editor.document` in place (internally replacing it + with the new `Document` returned by the operation). +- `editor.original` is set once on `__enter__` and never modified. +- `__getitem__` delegates to `editor.document.__getitem__`. +- `__contains__` delegates to `editor.document.__contains__`. +- `__setitem__` delegates to `upsert()`. Python's `__setitem__` passes a single + key or tuple; the Editor normalizes to a tuple before calling `upsert()`. +- `__exit__` writes `editor.document.dumps()` to the file path only if no + exception occurred. The file is written as UTF-8. + +### Feature / Location / FeatureKind + +```python +class Feature: + location: Location + context: Location | None + kind: FeatureKind + is_multiline: bool + def parent(self) -> Feature | None + +class Location: + start: int # byte offset + end: int # byte offset + +class FeatureKind(Enum): + BLOCK_MAPPING = ... + FLOW_MAPPING = ... + BLOCK_SEQUENCE = ... + FLOW_SEQUENCE = ... + SCALAR = ... +``` + +### Errors + +```python +class YAMLTripError(Exception): ... +class ParseError(YAMLTripError): ... # invalid YAML input +class QueryError(YAMLTripError): ... # path not found during query +class PatchError(YAMLTripError): ... # patch operation failed +class KeyExistsError(PatchError): ... # add() when key already exists +class KeyMissingError(PatchError): ... # replace() when key doesn't exist +``` + +## Mapping to Upstream Crate Operations + +| yamltrip method | yamlpatch `Op` | Notes | +|---|---|---| +| `replace()` | `Op::Replace(value)` | | +| `add()` | `Op::Add { key, value }` | | +| `upsert()` | `Replace`, `Add`, or `MergeInto` | Smart dispatch in Python layer | +| `remove()` | `Op::Remove` | | +| `prune_remove()` | Multiple `Op::Remove` | Python loop | +| `append()` | `Op::Append { value }` | | +| `extend_list()` | Multiple `Op::Append` | One per value | +| `remove_from_list()` | Query + multiple `Op::Remove` | Find matching indices, remove each | + +## Rust `_core` Bindings Detail + +### `_core.Document` + +Wraps `yamlpath::Document`. Constructed from a YAML string. + +```python +doc = _core.Document("name: foo") +doc.source() # → "name: foo" +doc.query_exact(route) # → Feature | None +doc.query_pretty(route) # → Feature +doc.query_key_only(route) # → Feature +doc.query_exists(route) # → bool +doc.extract(feature) # → str +doc.extract_with_leading_whitespace(feature) # → str +doc.feature_comments(feature) # → list[Feature] +doc.has_anchors() # → bool +doc.top_feature() # → Feature +``` + +### `_core.Route` + +Wraps `yamlpath::Route`. Constructed from a list of components. + +```python +route = _core.Route(["name"]) # single key +route = _core.Route(["items", 0]) # key + index +route = _core.Route(["a", "b", "c"]) # nested keys +``` + +### `_core.Op` + +Wraps `yamlpatch::Op`. Static constructors for each variant. + +```python +_core.Op.replace(value) +_core.Op.add(key, value) +_core.Op.remove() +_core.Op.append(value) +_core.Op.merge_into(key, updates) +_core.Op.replace_comment(new) +_core.Op.emplace_comment(new) +_core.Op.rewrite_fragment(from_, to) +``` + +### `_core.apply_patches` + +Wraps `yamlpatch::apply_yaml_patches`. + +```python +new_source = _core.apply_patches(source_str, patches) +# patches: list[_core.Patch] +# each Patch has: route (_core.Route) + operation (_core.Op) +``` + +## File Handling + +- All file I/O uses UTF-8 encoding (YAML spec default). +- `load(path)` raises `FileNotFoundError` if the file doesn't exist. +- `edit(path)` raises `FileNotFoundError` if the file doesn't exist. It is not + a file-creation tool — use `loads("")` + `dump(path)` to create new files. +- `dump(path)` creates the file if it doesn't exist, overwrites if it does. + +## Scope Constraints (v0.1) + +- Python >= 3.10 (PyO3 abi3-py310). +- Single-document YAML only. +- File I/O included (load/dump/edit). +- MIT license (matching upstream crates). + +## Dependencies + +### Rust (Cargo.toml) + +- `pyo3` (existing) +- `yamlpath` (crates.io) +- `yamlpatch` (crates.io) +- `serde_yaml` (for value conversion at the boundary) + +### Python + +- No runtime Python dependencies beyond the compiled `_core` extension. + +## Testing Strategy + +- Unit tests for each `_core` binding (Rust types exposed correctly). +- Unit tests for each `Document` method (Python layer). +- Unit tests for `Editor` context manager (file write semantics, error handling). +- Round-trip property tests: load YAML → apply operations → verify comments, + quotes, indentation preserved. +- Integration tests comparing yamltrip output with expected YAML for real-world + config files (GitHub Actions, pre-commit, pyproject.toml-adjacent YAML). diff --git a/pyproject.toml b/pyproject.toml index bd861f3..c2fe19e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,75 @@ +[build-system] +build-backend = "maturin" +requires = [ "maturin>=1.0,<2.0" ] + [project] name = "yamltrip" version = "0.1.0" description = "A round-tripping YAML library for Python" readme = "README.md" -authors = [ - { name = "Nathan McDougall", email = "nathan.j.mcdougall@gmail.com" } -] +authors = [ { name = "Nathan McDougall", email = "nathan.j.mcdougall@gmail.com" } ] requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] dependencies = [] +[dependency-groups] +dev = [ + "codespell>=2.4.2", + "deptry>=0.25.1", + "import-linter>=2.11", + "prek>=0.3.13", + "pyproject-fmt>=2.21.2", + "pytest>=9.0.3", + "ruff>=0.15.12", + "tomli>=2.4.1", + "ty>=0.0.35", +] +test = [ + "coverage[toml]>=7.14.0", + "mypy>=1.16.0", +] + [tool.maturin] module-name = "yamltrip._core" -python-packages = ["yamltrip"] +python-packages = [ "yamltrip" ] python-source = "src" [tool.uv] -cache-keys = [{ file = "pyproject.toml" }, { file = "src/**/*.rs" }, { file = "Cargo.toml" }, { file = "Cargo.lock" }] +default-groups = [ "test", "dev" ] +link-mode = "symlink" +cache-keys = [ { file = "pyproject.toml" }, { file = "src/**/*.rs" }, { file = "Cargo.toml" }, { file = "Cargo.lock" } ] -[build-system] -requires = ["maturin>=1.0,<2.0"] -build-backend = "maturin" +[tool.codespell] +ignore-regex = [ "[A-Za-z0-9+/]{100,}" ] +ignore-words-list = [ "..." ] +skip = [ "target" ] + +[tool.deptry] +ignore_notebooks = false + +[tool.pyproject-fmt] +keep_full_version = true + +[tool.ty] +src.include = [ "src", "tests" ] + +[tool.coverage] +run.relative_files = true +run.source = [ "src" ] +report.exclude_also = [ + "@(abc\\.)?abstractmethod", + "assert_never(.*)", + "class .*\\bProtocol\\):", + "if TYPE_CHECKING:", + "msg = [\"']", + "raise AssertionError", + "raise NotImplementedError", +] +report.omit = [ "*/pytest-of-*/*" ] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..308c5e7 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,40 @@ +line-length = 88 + +format.docstring-code-format = true + +lint.select = [ + "A", + "ARG", + "B", + "C4", + "D", + "E4", + "E7", + "E9", + "EM", + "ERA", + "F", + "FIX002", + "FLY", + "FURB", + "I", + "INP", + "PLC0415", + "PLE", + "PLR", + "PT", + "RUF", + "S", + "SIM", + "TC", + "TID", + "UP", +] +lint.ignore = [ "PLR2004", "S101", "SIM108" ] +lint.per-file-ignores."!tests/**/*.py" = [ "ARG002", "PT" ] +lint.per-file-ignores."tests/**" = [ "D", "INP", "S603", "TC" ] +lint.flake8-builtins.strict-checking = true +lint.flake8-type-checking.quote-annotations = true +lint.flake8-type-checking.strict = true +lint.pydocstyle.convention = "google" +lint.future-annotations = true diff --git a/src/convert.rs b/src/convert.rs new file mode 100644 index 0000000..7aa504b --- /dev/null +++ b/src/convert.rs @@ -0,0 +1,301 @@ +use pyo3::prelude::*; +use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyNone, PyString, PyTuple}; +use serde_yaml::Value; + +/// Convert a Python object to a serde_yaml::Value. +/// +/// Supported types: None, bool, int, float, str, list, dict. +/// Raises TypeError for unsupported types. +/// +/// # Recursion depth +/// This function recurses into nested lists/dicts without a depth limit. +/// In practice, Python's own recursion limit (~1000 by default) prevents +/// callers from constructing structures deep enough to overflow the Rust +/// stack. A dedicated depth guard is not warranted unless Python's limit +/// is bypassed (e.g. via `sys.setrecursionlimit`). +pub fn py_to_yaml_value(obj: &Bound<'_, PyAny>) -> PyResult { + if obj.is_instance_of::() { + Ok(Value::Null) + } else if obj.is_instance_of::() { + // Must check bool before int, since bool is a subclass of int in Python + Ok(Value::Bool(obj.extract::()?)) + } else if obj.is_instance_of::() { + if let Ok(i) = obj.extract::() { + Ok(Value::Number(i.into())) + } else if let Ok(u) = obj.extract::() { + Ok(Value::Number(u.into())) + } else { + Err(PyErr::new::( + "Integer too large for YAML number (must fit in i64 or u64)", + )) + } + } else if obj.is_instance_of::() { + let f: f64 = obj.extract()?; + if f.is_finite() { + Ok(Value::Number(serde_yaml::Number::from(f))) + } else { + Err(PyErr::new::(format!( + "Cannot convert float value {f} to YAML number" + ))) + } + } else if obj.is_instance_of::() { + Ok(Value::String(obj.extract::()?)) + } else if obj.is_instance_of::() { + let list = obj.cast::()?; + let items: PyResult> = list.iter().map(|item| py_to_yaml_value(&item)).collect(); + Ok(Value::Sequence(items?)) + } else if obj.is_instance_of::() { + let tuple = obj.cast::()?; + let items: PyResult> = + tuple.iter().map(|item| py_to_yaml_value(&item)).collect(); + Ok(Value::Sequence(items?)) + } else if obj.is_instance_of::() { + let dict = obj.cast::()?; + let mut mapping = serde_yaml::Mapping::new(); + for (k, v) in dict.iter() { + let key = py_to_yaml_value(&k)?; + let val = py_to_yaml_value(&v)?; + mapping.insert(key, val); + } + Ok(Value::Mapping(mapping)) + } else { + Err(PyErr::new::(format!( + "Cannot convert {} to YAML value", + obj.get_type().name()? + ))) + } +} + +/// Convert a serde_yaml::Value to a Python object. +/// +/// # Recursion depth +/// This function recurses without a depth limit. The input comes from +/// `serde_yaml::from_str`, which enforces its own recursion limit (128 +/// by default), so the nesting depth is bounded in practice. +pub fn yaml_value_to_py(py: Python<'_>, value: &Value) -> PyResult> { + match value { + Value::Null => Ok(py.None()), + Value::Bool(b) => Ok(b.into_pyobject(py)?.as_any().to_owned().unbind()), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(i.into_pyobject(py)?.into_any().unbind()) + } else if let Some(u) = n.as_u64() { + Ok(u.into_pyobject(py)?.into_any().unbind()) + } else if let Some(f) = n.as_f64() { + Ok(f.into_pyobject(py)?.into_any().unbind()) + } else { + Err(PyErr::new::( + "Cannot convert YAML number to Python", + )) + } + } + Value::String(s) => Ok(s.into_pyobject(py)?.into_any().unbind()), + Value::Sequence(seq) => { + let list = pyo3::types::PyList::empty(py); + for item in seq { + list.append(yaml_value_to_py(py, item)?)?; + } + Ok(list.into_any().unbind()) + } + Value::Mapping(map) => { + let dict = pyo3::types::PyDict::new(py); + for (k, v) in map { + dict.set_item(yaml_value_to_py(py, k)?, yaml_value_to_py(py, v)?)?; + } + Ok(dict.into_any().unbind()) + } + Value::Tagged(tagged) => { + // For tagged values, just convert the inner value + yaml_value_to_py(py, &tagged.value) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_py_to_yaml_none() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let none = py.None().into_bound(py); + let val = py_to_yaml_value(&none).unwrap(); + assert_eq!(val, Value::Null); + }); + } + + #[test] + fn test_py_to_yaml_bool() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let t = true.into_pyobject(py).unwrap(); + assert_eq!(py_to_yaml_value(t.as_any()).unwrap(), Value::Bool(true)); + let f = false.into_pyobject(py).unwrap(); + assert_eq!(py_to_yaml_value(f.as_any()).unwrap(), Value::Bool(false)); + }); + } + + #[test] + fn test_py_to_yaml_int() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let i = 42i64.into_pyobject(py).unwrap().into_any(); + let val = py_to_yaml_value(&i).unwrap(); + assert_eq!(val, Value::Number(42.into())); + }); + } + + #[test] + fn test_py_to_yaml_large_unsigned_int() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + // i64::MAX + 1 is a valid u64 but overflows i64 + let large = (i64::MAX as u64 + 1).into_pyobject(py).unwrap().into_any(); + let val = py_to_yaml_value(&large).unwrap(); + assert_eq!( + val, + Value::Number(serde_yaml::Number::from(i64::MAX as u64 + 1)) + ); + }); + } + + #[test] + fn test_py_to_yaml_float_finite() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let f = 3.14f64.into_pyobject(py).unwrap().into_any(); + let val = py_to_yaml_value(&f).unwrap(); + match val { + Value::Number(n) => assert_eq!(n.as_f64().unwrap(), 3.14), + _ => panic!("expected Number"), + } + }); + } + + #[test] + fn test_py_to_yaml_float_nan_rejected() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let nan = f64::NAN.into_pyobject(py).unwrap().into_any(); + assert!(py_to_yaml_value(&nan).is_err()); + }); + } + + #[test] + fn test_py_to_yaml_float_inf_rejected() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let inf = f64::INFINITY.into_pyobject(py).unwrap().into_any(); + assert!(py_to_yaml_value(&inf).is_err()); + }); + } + + #[test] + fn test_py_to_yaml_string() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let s = "hello".into_pyobject(py).unwrap().into_any(); + assert_eq!(py_to_yaml_value(&s).unwrap(), Value::String("hello".into())); + }); + } + + #[test] + fn test_py_to_yaml_list() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let list = PyList::new(py, &[1i64, 2, 3]).unwrap(); + let val = py_to_yaml_value(list.as_any()).unwrap(); + assert_eq!( + val, + Value::Sequence(vec![ + Value::Number(1.into()), + Value::Number(2.into()), + Value::Number(3.into()), + ]) + ); + }); + } + + #[test] + fn test_py_to_yaml_dict() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let dict = PyDict::new(py); + dict.set_item("key", "value").unwrap(); + let val = py_to_yaml_value(dict.as_any()).unwrap(); + let mut expected = serde_yaml::Mapping::new(); + expected.insert(Value::String("key".into()), Value::String("value".into())); + assert_eq!(val, Value::Mapping(expected)); + }); + } + + #[test] + fn test_py_to_yaml_unsupported_type_rejected() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let set = py.eval(pyo3::ffi::c_str!("set()"), None, None).unwrap(); + assert!(py_to_yaml_value(&set).is_err()); + }); + } + + #[test] + fn test_yaml_to_py_null() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let obj = yaml_value_to_py(py, &Value::Null).unwrap(); + assert!(obj.bind(py).is_none()); + }); + } + + #[test] + fn test_yaml_to_py_bool() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let obj = yaml_value_to_py(py, &Value::Bool(true)).unwrap(); + assert!(obj.extract::(py).unwrap()); + }); + } + + #[test] + fn test_yaml_to_py_number_i64() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let obj = yaml_value_to_py(py, &Value::Number(42.into())).unwrap(); + assert_eq!(obj.extract::(py).unwrap(), 42); + }); + } + + #[test] + fn test_yaml_to_py_string() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let obj = yaml_value_to_py(py, &Value::String("hello".into())).unwrap(); + assert_eq!(obj.extract::(py).unwrap(), "hello"); + }); + } + + #[test] + fn test_yaml_to_py_sequence() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let seq = Value::Sequence(vec![Value::Number(1.into()), Value::Number(2.into())]); + let obj = yaml_value_to_py(py, &seq).unwrap(); + let list = obj.extract::>(py).unwrap(); + assert_eq!(list, vec![1, 2]); + }); + } + + #[test] + fn test_yaml_to_py_tagged_strips_tag() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let tagged = Value::Tagged(Box::new(serde_yaml::value::TaggedValue { + tag: serde_yaml::value::Tag::new("!custom"), + value: Value::String("inner".into()), + })); + let obj = yaml_value_to_py(py, &tagged).unwrap(); + assert_eq!(obj.extract::(py).unwrap(), "inner"); + }); + } +} diff --git a/src/document.rs b/src/document.rs new file mode 100644 index 0000000..614cc52 --- /dev/null +++ b/src/document.rs @@ -0,0 +1,171 @@ +use pyo3::prelude::*; + +use crate::ops::PyPatch; +use crate::types::{PyFeature, PyFeatureKind, PyLocation, PyRoute}; + +/// A parsed YAML document. +#[pyclass(name = "Document", module = "yamltrip._core")] +pub struct PyDocument { + inner: yamlpath::Document, +} + +#[pymethods] +impl PyDocument { + #[new] + fn new(source: &str) -> PyResult { + let doc = yamlpath::Document::new(source).map_err(|e| { + PyErr::new::(format!("Failed to parse YAML: {e}")) + })?; + Ok(Self { inner: doc }) + } + + fn source(&self) -> &str { + self.inner.source() + } + + fn query_exists(&self, route: &PyRoute) -> bool { + let r = route.to_yamlpath_route(); + self.inner.query_exists(&r) + } + + fn query_exact(&self, route: &PyRoute) -> PyResult> { + let r = route.to_yamlpath_route(); + match self.inner.query_exact(&r) { + Ok(Some(feature)) => Ok(Some(convert_feature(&feature))), + Ok(None) => Ok(None), + Err(e) => Err(PyErr::new::(format!( + "Query failed: {e}" + ))), + } + } + + fn query_pretty(&self, route: &PyRoute) -> PyResult { + let r = route.to_yamlpath_route(); + match self.inner.query_pretty(&r) { + Ok(feature) => Ok(convert_feature(&feature)), + Err(e) => Err(PyErr::new::(format!( + "Query failed: {e}" + ))), + } + } + + fn extract(&self, feature: &PyFeature) -> PyResult { + let source = self.inner.source(); + let start = feature.location.start; + let end = feature.location.end; + if end > source.len() || start > end { + return Err(PyErr::new::( + "Feature location is out of bounds", + )); + } + source + .get(start..end) + .map(|s| s.to_string()) + .ok_or_else(|| { + PyErr::new::( + "Feature location is not aligned to UTF-8 character boundaries", + ) + }) + } + + fn has_anchors(&self) -> bool { + self.inner.has_anchors() + } + + /// Parse the YAML value at a route and return it as a Python object. + fn parse_value(&self, py: Python<'_>, route: &PyRoute) -> PyResult> { + let source = self.inner.source(); + let r = route.to_yamlpath_route(); + + if !self.inner.query_exists(&r) { + return Err(PyErr::new::( + "Path not found", + )); + } + + // For root-level, parse entire document. + // Note: tree-sitter gives us the AST structure, but not parsed scalar + // values, so we extract the raw YAML substring and re-parse it with + // serde_yaml. The dedenting is needed because serde_yaml expects + // root-level indentation. + let yaml_str = if route.components.is_empty() { + source.to_string() + } else { + match self.inner.query_exact(&r) { + Ok(Some(feature)) => { + let span = feature.location.byte_span; + let raw = &source[span.0..span.1]; + // Calculate the column offset (in bytes) of the value + // start relative to the beginning of its line, so we can + // dedent continuation lines. + let line_start = source[..span.0].rfind('\n').map(|nl| nl + 1).unwrap_or(0); + let col = span.0 - line_start; + if col == 0 { + raw.to_string() + } else { + raw.split('\n') + .enumerate() + .map(|(i, line)| { + if i == 0 { + line.to_string() + } else if line.len() >= col + && line.as_bytes()[..col].iter().all(|&b| b == b' ') + { + line[col..].to_string() + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") + } + } + Ok(None) => return Ok(py.None()), + Err(e) => { + return Err(PyErr::new::(format!( + "Query error: {e}" + ))); + } + } + }; + + let value: serde_yaml::Value = serde_yaml::from_str(&yaml_str).map_err(|e| { + PyErr::new::(format!("YAML parse error: {e}")) + })?; + + crate::convert::yaml_value_to_py(py, &value) + } + + /// Apply patches to this document and return a new document. + /// NOTE: Similar patch-application logic exists in ops::apply_patches (returns String). + fn apply_patches(&self, patches: Vec) -> PyResult { + let yaml_patches: Vec> = patches + .iter() + .map(|p| yamlpatch::Patch { + route: p.route.to_yamlpath_route(), + operation: p.operation.inner.clone(), + }) + .collect(); + + let result = yamlpatch::apply_yaml_patches(&self.inner, &yaml_patches).map_err(|e| { + PyErr::new::(format!("Patch failed: {e}")) + })?; + + Ok(Self { inner: result }) + } +} + +fn convert_feature(feature: &yamlpath::Feature<'_>) -> PyFeature { + PyFeature { + location: PyLocation { + start: feature.location.byte_span.0, + end: feature.location.byte_span.1, + }, + context: feature.context.as_ref().map(|c| PyLocation { + start: c.byte_span.0, + end: c.byte_span.1, + }), + kind: PyFeatureKind::from(feature.kind()), + is_multiline: feature.is_multiline(), + } +} diff --git a/src/lib.rs b/src/lib.rs index 7b38a4d..da382f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,37 @@ +mod convert; +mod document; +mod ops; +mod types; + use pyo3::prelude::*; -/// A Python module implemented in Rust. The name of this module must match -/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to -/// import the module. +use document::PyDocument; +use ops::{PyOp, PyPatch}; +use types::{PyComponent, PyFeature, PyFeatureKind, PyLocation, PyRoute}; + #[pymodule] mod _core { - use pyo3::prelude::*; + use super::*; + + #[pymodule_export] + use super::PyComponent; + #[pymodule_export] + use super::PyDocument; + #[pymodule_export] + use super::PyFeature; + #[pymodule_export] + use super::PyFeatureKind; + #[pymodule_export] + use super::PyLocation; + #[pymodule_export] + use super::PyOp; + #[pymodule_export] + use super::PyPatch; + #[pymodule_export] + use super::PyRoute; #[pyfunction] - fn hello_from_bin() -> String { - "Hello from yamltrip!".to_string() + fn apply_patches(source: &str, patches: Vec) -> PyResult { + ops::apply_patches(source, patches) } } diff --git a/src/ops.rs b/src/ops.rs new file mode 100644 index 0000000..eeef2ad --- /dev/null +++ b/src/ops.rs @@ -0,0 +1,214 @@ +use pyo3::prelude::*; + +use crate::convert::py_to_yaml_value; +use crate::types::PyRoute; + +/// A YAML patch operation. +#[pyclass(name = "Op", module = "yamltrip._core")] +#[derive(Clone, Debug)] +pub struct PyOp { + pub inner: yamlpatch::Op<'static>, +} + +#[pymethods] +impl PyOp { + #[staticmethod] + fn replace(value: &Bound<'_, PyAny>) -> PyResult { + let val = py_to_yaml_value(value)?; + Ok(Self { + inner: yamlpatch::Op::Replace(val), + }) + } + + #[staticmethod] + fn add(key: &str, value: &Bound<'_, PyAny>) -> PyResult { + let val = py_to_yaml_value(value)?; + Ok(Self { + inner: yamlpatch::Op::Add { + key: key.to_string(), + value: val, + }, + }) + } + + #[staticmethod] + fn remove() -> Self { + Self { + inner: yamlpatch::Op::Remove, + } + } + + #[staticmethod] + fn append(value: &Bound<'_, PyAny>) -> PyResult { + let val = py_to_yaml_value(value)?; + Ok(Self { + inner: yamlpatch::Op::Append { value: val }, + }) + } + + /// Merge key-value pairs into an existing mapping. + #[staticmethod] + fn merge_into(key: &str, updates: &Bound<'_, PyAny>) -> PyResult { + let dict = updates.cast::().map_err(|_| { + let type_name = updates + .get_type() + .name() + .map_or_else(|_| "".to_string(), |n| n.to_string()); + PyErr::new::(format!( + "updates must be a dict, got {type_name}" + )) + })?; + let mut map = indexmap::IndexMap::new(); + for (k, v) in dict.iter() { + let key_str: String = k.extract()?; + let val = py_to_yaml_value(&v)?; + map.insert(key_str, val); + } + Ok(Self { + inner: yamlpatch::Op::MergeInto { + key: key.to_string(), + updates: map, + }, + }) + } + + fn __repr__(&self) -> String { + match &self.inner { + yamlpatch::Op::Replace(val) => { + format!("Op.replace({})", yaml_value_repr(val)) + } + yamlpatch::Op::Add { key, value } => { + format!("Op.add({}, {})", yaml_str_repr(key), yaml_value_repr(value)) + } + yamlpatch::Op::Remove => "Op.remove()".to_string(), + yamlpatch::Op::Append { value } => { + format!("Op.append({})", yaml_value_repr(value)) + } + yamlpatch::Op::MergeInto { key, .. } => { + format!("Op.merge_into({}, ...)", yaml_str_repr(key)) + } + yamlpatch::Op::RewriteFragment { from, to } => { + format!( + "Op.rewrite_fragment({}, {})", + yaml_str_repr(&format!("{from:?}")), + yaml_str_repr(to) + ) + } + yamlpatch::Op::ReplaceComment { new } => { + format!("Op.replace_comment({})", yaml_str_repr(new)) + } + yamlpatch::Op::EmplaceComment { new } => { + format!("Op.emplace_comment({})", yaml_str_repr(new)) + } + } + } + + /// The kind of operation: "replace", "add", "remove", "append", or "merge_into". + #[getter] + fn kind(&self) -> &str { + match &self.inner { + yamlpatch::Op::Replace(_) => "replace", + yamlpatch::Op::Add { .. } => "add", + yamlpatch::Op::Remove => "remove", + yamlpatch::Op::Append { .. } => "append", + yamlpatch::Op::MergeInto { .. } => "merge_into", + yamlpatch::Op::RewriteFragment { .. } => "rewrite_fragment", + yamlpatch::Op::ReplaceComment { .. } => "replace_comment", + yamlpatch::Op::EmplaceComment { .. } => "emplace_comment", + } + } +} + +/// Format a string as a Python-style repr. +fn yaml_str_repr(s: &str) -> String { + let escaped = s.replace('\\', "\\\\").replace('\'', "\\'"); + format!("'{escaped}'") +} + +/// Format a serde_yaml::Value as a Python-style repr. +fn yaml_value_repr(val: &serde_yaml::Value) -> String { + match val { + serde_yaml::Value::Null => "None".to_string(), + serde_yaml::Value::Bool(b) => if *b { "True" } else { "False" }.to_string(), + serde_yaml::Value::Number(n) => format!("{n}"), + serde_yaml::Value::String(s) => yaml_str_repr(s), + serde_yaml::Value::Sequence(_) => "[...]".to_string(), + serde_yaml::Value::Mapping(_) => "{...}".to_string(), + serde_yaml::Value::Tagged(t) => yaml_value_repr(&t.value), + } +} + +/// A patch: a route + an operation. +#[pyclass(name = "Patch", module = "yamltrip._core")] +#[derive(Clone, Debug)] +pub struct PyPatch { + #[pyo3(get)] + pub route: PyRoute, + #[pyo3(get)] + pub operation: PyOp, +} + +#[pymethods] +impl PyPatch { + #[new] + fn new(route: PyRoute, operation: PyOp) -> Self { + Self { route, operation } + } +} + +/// Apply a list of patches to a YAML source string. +/// NOTE: Similar patch-application logic exists in PyDocument::apply_patches (returns Document). +#[pyfunction] +pub fn apply_patches(source: &str, patches: Vec) -> PyResult { + let document = yamlpath::Document::new(source).map_err(|e| { + PyErr::new::(format!("Invalid YAML: {e}")) + })?; + + let yaml_patches: Vec> = patches + .iter() + .map(|p| yamlpatch::Patch { + route: p.route.to_yamlpath_route(), + operation: p.operation.inner.clone(), + }) + .collect(); + + let result = yamlpatch::apply_yaml_patches(&document, &yaml_patches).map_err(|e| { + PyErr::new::(format!("Patch failed: {e}")) + })?; + + Ok(result.source().to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_yaml_value_repr_string_plain() { + let val = serde_yaml::Value::String("hello".to_string()); + assert_eq!(yaml_value_repr(&val), "'hello'"); + } + + #[test] + fn test_yaml_value_repr_string_with_single_quote() { + let val = serde_yaml::Value::String("it's".to_string()); + assert_eq!(yaml_value_repr(&val), r"'it\'s'"); + } + + #[test] + fn test_yaml_value_repr_string_with_backslash() { + let val = serde_yaml::Value::String(r"a\b".to_string()); + assert_eq!(yaml_value_repr(&val), r"'a\\b'"); + } + + #[test] + fn test_yaml_value_repr_null() { + assert_eq!(yaml_value_repr(&serde_yaml::Value::Null), "None"); + } + + #[test] + fn test_yaml_value_repr_bool() { + assert_eq!(yaml_value_repr(&serde_yaml::Value::Bool(true)), "True"); + assert_eq!(yaml_value_repr(&serde_yaml::Value::Bool(false)), "False"); + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..35401a7 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,286 @@ +use pyo3::prelude::*; + +/// Byte offset span in the YAML source. +#[pyclass(name = "Location", module = "yamltrip._core", frozen, eq, hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PyLocation { + #[pyo3(get)] + pub start: usize, + #[pyo3(get)] + pub end: usize, +} + +#[pymethods] +impl PyLocation { + #[new] + fn new(start: usize, end: usize) -> PyResult { + if start > end { + return Err(PyErr::new::(format!( + "Location start ({start}) must not exceed end ({end})" + ))); + } + Ok(Self { start, end }) + } + + fn __repr__(&self) -> String { + format!("Location(start={}, end={})", self.start, self.end) + } +} + +impl From for PyLocation { + fn from(loc: yamlpath::Location) -> Self { + Self { + start: loc.byte_span.0, + end: loc.byte_span.1, + } + } +} + +/// The kind of a YAML feature. +#[pyclass( + name = "FeatureKind", + module = "yamltrip._core", + frozen, + eq, + eq_int, + hash +)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PyFeatureKind { + Scalar, + BlockMapping, + FlowMapping, + BlockSequence, + FlowSequence, +} + +impl From for PyFeatureKind { + fn from(kind: yamlpath::FeatureKind) -> Self { + match kind { + yamlpath::FeatureKind::Scalar => Self::Scalar, + yamlpath::FeatureKind::BlockMapping => Self::BlockMapping, + yamlpath::FeatureKind::FlowMapping => Self::FlowMapping, + yamlpath::FeatureKind::BlockSequence => Self::BlockSequence, + yamlpath::FeatureKind::FlowSequence => Self::FlowSequence, + } + } +} + +/// A single route component — either a mapping key or a sequence index. +#[pyclass(name = "Component", module = "yamltrip._core", frozen, eq, hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PyComponent { + Key { name: String }, + Index { index: usize }, +} + +#[pymethods] +impl PyComponent { + #[staticmethod] + fn key(name: &str) -> Self { + Self::Key { + name: name.to_string(), + } + } + + #[staticmethod] + fn index(index: usize) -> Self { + Self::Index { index } + } + + fn __repr__(&self) -> String { + match self { + Self::Key { name } => format!("Component.key('{name}')"), + Self::Index { index } => format!("Component.index({index})"), + } + } +} + +/// A path into a YAML document. +#[pyclass(name = "Route", module = "yamltrip._core", frozen, eq, hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PyRoute { + pub components: Vec, +} + +#[pymethods] +impl PyRoute { + #[new] + fn new(parts: Vec>) -> PyResult { + let mut components = Vec::new(); + for part in parts { + if let Ok(s) = part.extract::() { + components.push(PyComponent::Key { name: s }); + } else if let Ok(i) = part.extract::() { + components.push(PyComponent::Index { index: i }); + } else if part.extract::().is_ok() { + return Err(PyErr::new::( + "Route indices must be non-negative integers", + )); + } else { + return Err(PyErr::new::(format!( + "Route components must be str or int, got {}", + part.get_type().name()? + ))); + } + } + Ok(Self { components }) + } + + fn __len__(&self) -> usize { + self.components.len() + } + + fn __repr__(&self) -> String { + let parts: Vec = self.components.iter().map(|c| c.__repr__()).collect(); + format!("Route([{}])", parts.join(", ")) + } +} + +impl PyRoute { + /// Convert to a yamlpath::Route. + pub fn to_yamlpath_route(&self) -> yamlpath::Route<'_> { + let components: Vec> = self + .components + .iter() + .map(|c| match c { + PyComponent::Key { name } => yamlpath::Component::Key(name.as_str().into()), + PyComponent::Index { index } => yamlpath::Component::Index(*index), + }) + .collect(); + yamlpath::Route::from(components) + } +} + +/// The result of a YAML path query. +#[pyclass(name = "Feature", module = "yamltrip._core", frozen, eq, hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PyFeature { + #[pyo3(get)] + pub location: PyLocation, + #[pyo3(get)] + pub context: Option, + #[pyo3(get)] + pub kind: PyFeatureKind, + #[pyo3(get)] + pub is_multiline: bool, +} + +#[pymethods] +impl PyFeature { + fn __repr__(&self) -> String { + format!( + "Feature(location={}, kind={:?})", + self.location.__repr__(), + self.kind + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_location_equality() { + let a = PyLocation { start: 0, end: 5 }; + let b = PyLocation { start: 0, end: 5 }; + let c = PyLocation { start: 0, end: 6 }; + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn test_location_from_yamlpath() { + let loc = yamlpath::Location { + byte_span: (10, 20), + point_span: ((0, 10), (0, 20)), + }; + let py_loc = PyLocation::from(loc); + assert_eq!(py_loc.start, 10); + assert_eq!(py_loc.end, 20); + } + + #[test] + fn test_feature_kind_from_yamlpath() { + assert_eq!( + PyFeatureKind::from(yamlpath::FeatureKind::Scalar), + PyFeatureKind::Scalar + ); + assert_eq!( + PyFeatureKind::from(yamlpath::FeatureKind::BlockMapping), + PyFeatureKind::BlockMapping + ); + assert_eq!( + PyFeatureKind::from(yamlpath::FeatureKind::FlowMapping), + PyFeatureKind::FlowMapping + ); + assert_eq!( + PyFeatureKind::from(yamlpath::FeatureKind::BlockSequence), + PyFeatureKind::BlockSequence + ); + assert_eq!( + PyFeatureKind::from(yamlpath::FeatureKind::FlowSequence), + PyFeatureKind::FlowSequence + ); + } + + #[test] + fn test_component_equality() { + assert_eq!( + PyComponent::Key { + name: "a".to_string() + }, + PyComponent::Key { + name: "a".to_string() + } + ); + assert_ne!( + PyComponent::Key { + name: "a".to_string() + }, + PyComponent::Index { index: 0 } + ); + } + + #[test] + fn test_route_to_yamlpath_conversion() { + let route = PyRoute { + components: vec![ + PyComponent::Key { + name: "a".to_string(), + }, + PyComponent::Index { index: 0 }, + PyComponent::Key { + name: "b".to_string(), + }, + ], + }; + // Just verify it doesn't panic + let _yamlpath_route = route.to_yamlpath_route(); + } + + #[test] + fn test_empty_route_conversion() { + let route = PyRoute { components: vec![] }; + let _yamlpath_route = route.to_yamlpath_route(); + } + + #[test] + fn test_route_negative_int_error_message() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let neg = (-1i64).into_pyobject(py).unwrap().into_any(); + let parts = vec![neg]; + let list = pyo3::types::PyList::new(py, &parts).unwrap(); + let bound_list: Vec> = list.iter().collect(); + let result = PyRoute::new(bound_list); + let err = result.unwrap_err(); + let msg = err.to_string(); + assert!( + !msg.contains("must be str or int"), + "Error should not say 'must be str or int' for a negative int, got: {msg}" + ); + }); + } +} diff --git a/src/yamltrip/__init__.py b/src/yamltrip/__init__.py index 8e032fb..eb9431c 100644 --- a/src/yamltrip/__init__.py +++ b/src/yamltrip/__init__.py @@ -1,5 +1,57 @@ -from yamltrip._core import hello_from_bin +"""yamltrip — round-tripping YAML library for Python.""" +from __future__ import annotations -def hello() -> str: - return hello_from_bin() +from pathlib import Path + +from yamltrip._core import Component, Feature, FeatureKind, Location, Route +from yamltrip.document import Document +from yamltrip.editor import Editor +from yamltrip.errors import ( + KeyExistsError, + KeyMissingError, + ParseError, + PatchError, + QueryError, + YAMLTripError, +) + + +def loads(source: str) -> Document: + """Parse a YAML string into a Document.""" + return Document(source) + + +def load(path: str | Path) -> Document: + """Read a YAML file into a Document.""" + try: + source = Path(path).read_text(encoding="utf-8") + except UnicodeDecodeError: + msg = f"File is not valid UTF-8: {path}" + raise ParseError(msg) from None + return Document(source) + + +def edit(path: str | Path) -> Editor: + """Open a YAML file for editing (context manager).""" + return Editor(path) + + +__all__ = [ + "Component", + "Document", + "Editor", + "Feature", + "FeatureKind", + "KeyExistsError", + "KeyMissingError", + "Location", + "ParseError", + "PatchError", + "QueryError", + "Route", + "YAMLTripError", + "edit", + "load", + "loads", +] diff --git a/src/yamltrip/_core.pyi b/src/yamltrip/_core.pyi index d52129e..9680e1b 100644 --- a/src/yamltrip/_core.pyi +++ b/src/yamltrip/_core.pyi @@ -1 +1,121 @@ -def hello_from_bin() -> str: ... +"""Type stubs for the yamltrip._core native module.""" + +from typing import Any, ClassVar, final + +__all__ = [ + "Component", + "Document", + "Feature", + "FeatureKind", + "Location", + "Op", + "Patch", + "Route", + "apply_patches", +] + +@final +class Location: + def __new__(cls, start: int, end: int) -> Location: ... + @property + def start(self) -> int: ... + @property + def end(self) -> int: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + def __repr__(self) -> str: ... + +@final +class FeatureKind: + Scalar: ClassVar[FeatureKind] + BlockMapping: ClassVar[FeatureKind] + FlowMapping: ClassVar[FeatureKind] + BlockSequence: ClassVar[FeatureKind] + FlowSequence: ClassVar[FeatureKind] + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + def __int__(self) -> int: ... + def __repr__(self) -> str: ... + +class Component: + @final + class Key(Component): + __match_args__: ClassVar[tuple[str, ...]] + def __new__(cls, name: str) -> Component.Key: ... + @property + def name(self) -> str: ... + + @final + class Index(Component): + __match_args__: ClassVar[tuple[str, ...]] + def __new__(cls, index: int) -> Component.Index: ... + @property + def index(self) -> int: ... # type: ignore[override] + + @staticmethod + def key(name: str) -> Component: ... + @staticmethod + def index(index: int) -> Component: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + def __repr__(self) -> str: ... + +@final +class Route: + def __new__(cls, parts: list[str | int]) -> Route: ... + def __len__(self) -> int: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + def __repr__(self) -> str: ... + +@final +class Feature: + @property + def location(self) -> Location: ... + @property + def context(self) -> Location | None: ... + @property + def kind(self) -> FeatureKind: ... + @property + def is_multiline(self) -> bool: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + def __repr__(self) -> str: ... + +@final +class Document: + def __new__(cls, source: str) -> Document: ... + def source(self) -> str: ... + def query_exists(self, route: Route) -> bool: ... + def query_exact(self, route: Route) -> Feature | None: ... + def query_pretty(self, route: Route) -> Feature: ... + def extract(self, feature: Feature) -> str: ... + def parse_value(self, route: Route) -> Any: ... + def has_anchors(self) -> bool: ... + def apply_patches(self, patches: list[Patch]) -> Document: ... + +@final +class Op: + @staticmethod + def replace(value: Any) -> Op: ... + @staticmethod + def add(key: str, value: Any) -> Op: ... + @staticmethod + def remove() -> Op: ... + @staticmethod + def append(value: Any) -> Op: ... + @staticmethod + def merge_into(key: str, updates: dict[str, Any]) -> Op: ... + @property + def kind(self) -> str: ... + def __repr__(self) -> str: ... + +@final +class Patch: + def __new__(cls, route: Route, operation: Op) -> Patch: ... + @property + def route(self) -> Route: ... + @property + def operation(self) -> Op: ... + +def apply_patches(source: str, patches: list[Patch]) -> str: ... diff --git a/src/yamltrip/document.py b/src/yamltrip/document.py new file mode 100644 index 0000000..ed60c6c --- /dev/null +++ b/src/yamltrip/document.py @@ -0,0 +1,300 @@ +"""Immutable YAML Document class.""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast + +from yamltrip import _core +from yamltrip.errors import ( + KeyExistsError, + KeyMissingError, + ParseError, + PatchError, + QueryError, +) + +if TYPE_CHECKING: + from collections.abc import Sequence + +KeyPart = str | int + + +def _normalize_keys(keys: object) -> tuple[KeyPart, ...]: + """Normalize __getitem__ input to a tuple of keys.""" + if isinstance(keys, (str, int)): + return (keys,) + if isinstance(keys, tuple): + for k in keys: + if not isinstance(k, (str, int)): + msg = f"Key elements must be str or int, got {type(k).__name__}" + raise TypeError(msg) + return cast("tuple[KeyPart, ...]", tuple(keys)) + msg = f"Keys must be str, int, or tuple, got {type(keys).__name__}" + raise TypeError(msg) + + +def _make_route(keys: Sequence[KeyPart]) -> _core.Route: + """Build a _core.Route from a sequence of keys.""" + return _core.Route(list(keys)) + + +def _check_no_int_keys_for_creation(keys: Sequence[KeyPart]) -> None: + """Raise PatchError if any key is an int (cannot create sequences via upsert).""" + for k in keys: + if isinstance(k, int): + msg = ( + f"Cannot create intermediate structure with integer key {k}; " + "only string keys can create new mappings" + ) + raise PatchError(msg) + + +class Document: + """An immutable YAML document. + + Each mutation method returns a new Document — the original is never modified. + + Equality and hashing are based on the raw source text, not semantic content. + Two documents with equivalent YAML but different formatting (e.g. extra + whitespace) are considered unequal. This is intentional for a round-tripping + library where formatting is significant. + + Each instance holds a native tree-sitter parse tree plus a copy of the + source text. Memory is freed when the Python object is garbage-collected. + """ + + def __init__(self, source: str) -> None: + """Parse a YAML string into an immutable document.""" + try: + self._core_doc = _core.Document(source) + except (ValueError, RuntimeError) as e: + raise ParseError(str(e)) from None + self._source = source + + @classmethod + def _from_core(cls, core_doc: _core.Document) -> Document: + """Construct a Document from an already-parsed _core.Document.""" + obj = object.__new__(cls) + obj._core_doc = core_doc + obj._source = core_doc.source() + return obj + + def _apply_patches(self, patches: list[_core.Patch]) -> Document: + """Apply patches to this document and return a new Document.""" + try: + core_doc = self._core_doc.apply_patches(patches) + except RuntimeError as e: + raise PatchError(str(e)) from None + return Document._from_core(core_doc) + + @property + def source(self) -> str: + """The current YAML source text.""" + return self._source + + def __eq__(self, other: object) -> bool: + """Compare documents by their source text.""" + if not isinstance(other, Document): + return NotImplemented + return self._source == other._source + + def __hash__(self) -> int: + """Hash based on source text.""" + return hash(self._source) + + def __repr__(self) -> str: + """Return a developer-friendly representation.""" + return f"Document(<{len(self._source)} bytes>)" + + def __getitem__(self, keys: object) -> Any: + """Retrieve the parsed value at the given path. + + An empty tuple ``()`` retrieves the entire document as a Python object. + """ + normalized = _normalize_keys(keys) + route = _make_route(normalized) + try: + return self._core_doc.parse_value(route) + except (ValueError, KeyError) as e: + raise QueryError(str(e)) from None + + def __contains__(self, keys: object) -> bool: + """Check whether a path exists in the document. + + An empty tuple ``()`` checks that the document root exists (always True + for a successfully parsed document). + """ + normalized = _normalize_keys(keys) + route = _make_route(normalized) + return self._core_doc.query_exists(route) + + def query(self, *keys: KeyPart) -> _core.Feature: + """Return the Feature at the given path.""" + route = _make_route(keys) + try: + feature = self._core_doc.query_exact(route) + except KeyError as e: + raise QueryError(str(e)) from None + if feature is None: + msg = f"Path has no value: {keys}" + raise QueryError(msg) + return feature + + def query_pretty(self, *keys: KeyPart) -> _core.Feature: + """Return a Feature with context (surrounding structure) at the path.""" + route = _make_route(keys) + try: + return self._core_doc.query_pretty(route) + except KeyError as e: + raise QueryError(str(e)) from None + + def has_anchors(self) -> bool: + """Check whether the document contains YAML anchors (&anchor/*alias).""" + return self._core_doc.has_anchors() + + def extract(self, feature: _core.Feature) -> str: + """Extract the raw YAML text for a feature.""" + return self._core_doc.extract(feature) + + def dumps(self) -> str: + """Return the YAML source text.""" + return self._source + + def dump(self, path: str | Path) -> None: + """Write the YAML source text to a file.""" + Path(path).write_text(self._source, encoding="utf-8") + + def replace(self, *keys: KeyPart, value: Any) -> Document: + """Replace value at an existing path. Raises KeyMissingError if missing.""" + route = _make_route(keys) + if not self._core_doc.query_exists(route): + msg = f"Path not found: {keys}" + raise KeyMissingError(msg) + + op = _core.Op.replace(value) + patch = _core.Patch(route=route, operation=op) + return self._apply_patches([patch]) + + def add(self, *keys: KeyPart, key: str, value: Any) -> Document: + """Add a new key to the mapping at path. Raises KeyExistsError if exists.""" + full_path = (*keys, key) + check_route = _make_route(full_path) + if self._core_doc.query_exists(check_route): + msg = f"Key already exists: {full_path}" + raise KeyExistsError(msg) + + route = _make_route(keys) + op = _core.Op.add(key, value) + patch = _core.Patch(route=route, operation=op) + return self._apply_patches([patch]) + + def _create_at( + self, + parent_keys: tuple[KeyPart, ...], + child_keys: tuple[KeyPart, ...], + value: Any, + ) -> Document: + """Create a nested value under parent_keys using child_keys.""" + _check_no_int_keys_for_creation(child_keys) + first_key = child_keys[0] + if not isinstance(first_key, str): + msg = f"Expected string key, got {type(first_key).__name__}" + raise TypeError(msg) + nested_value = value + for k in reversed(child_keys[1:]): + nested_value = {k: nested_value} + route = _make_route(parent_keys) + if isinstance(nested_value, dict): + op = _core.Op.merge_into(first_key, nested_value) + else: + op = _core.Op.add(first_key, nested_value) + patch = _core.Patch(route=route, operation=op) + return self._apply_patches([patch]) + + def upsert(self, *keys: KeyPart, value: Any) -> Document: + """Replace if exists, create (with intermediate mappings) if not.""" + if not keys: + route = _make_route(()) + op = _core.Op.replace(value) + patch = _core.Patch(route=route, operation=op) + return self._apply_patches([patch]) + + full_route = _make_route(keys) + if self._core_doc.query_exists(full_route): + return self.replace(*keys, value=value) + + # Find deepest existing ancestor + for depth in range(len(keys) - 1, 0, -1): + ancestor_keys = keys[:depth] + ancestor_route = _make_route(ancestor_keys) + if self._core_doc.query_exists(ancestor_route): + return self._create_at(ancestor_keys, keys[depth:], value) + + # No path exists — add at root + return self._create_at((), keys, value) + + def remove(self, *keys: KeyPart, prune: bool = False) -> Document: + """Remove the key/index at path.""" + route = _make_route(keys) + op = _core.Op.remove() + patch = _core.Patch(route=route, operation=op) + doc = self._apply_patches([patch]) + + if prune and len(keys) > 1: + for depth in range(len(keys) - 1, 0, -1): + parent_keys = keys[:depth] + if parent_keys in doc: + parent_val = doc[parent_keys] + if parent_val is None or parent_val in ({}, []): + doc = doc.remove(*parent_keys) + else: + break + else: + break + return doc + + def prune_remove(self, *keys: KeyPart) -> Document: + """Remove key and prune empty parents.""" + return self.remove(*keys, prune=True) + + def append(self, *keys: KeyPart, value: Any) -> Document: + """Append a single item to the sequence at path.""" + route = _make_route(keys) + op = _core.Op.append(value) + patch = _core.Patch(route=route, operation=op) + return self._apply_patches([patch]) + + def extend_list(self, *keys: KeyPart, values: Sequence[Any]) -> Document: + """Append multiple items to the sequence at path.""" + if not values: + return self + route = _make_route(keys) + patches = [ + _core.Patch(route=route, operation=_core.Op.append(v)) for v in values + ] + return self._apply_patches(patches) + + def remove_from_list(self, *keys: KeyPart, values: Sequence[Any]) -> Document: + """Remove all occurrences of given values from the sequence at path.""" + current_list = self[keys] + if not isinstance(current_list, list): + msg = f"Value at {keys} is not a list" + raise PatchError(msg) + + values_list = list(values) + indices_to_remove = sorted( + (i for i, item in enumerate(current_list) if item in values_list), + reverse=True, + ) + + if not indices_to_remove: + return self + patches = [ + _core.Patch( + route=_make_route((*keys, idx)), + operation=_core.Op.remove(), + ) + for idx in indices_to_remove + ] + return self._apply_patches(patches) diff --git a/src/yamltrip/editor.py b/src/yamltrip/editor.py new file mode 100644 index 0000000..01f35b6 --- /dev/null +++ b/src/yamltrip/editor.py @@ -0,0 +1,137 @@ +"""Mutable YAML Editor context manager.""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from yamltrip.document import Document, _normalize_keys + +if TYPE_CHECKING: + from collections.abc import Sequence + from types import TracebackType + + from yamltrip._core import Feature + from yamltrip.document import KeyPart + + +class Editor: + """A mutable context manager for editing YAML files. + + On successful exit, writes the modified document back to the file. + On exception, the file is left unchanged. + """ + + def __init__(self, path: str | Path) -> None: + """Create an editor for the given YAML file path.""" + self._path = Path(path) + self._original: Document | None = None + self._document: Document | None = None + self._original_source: str | None = None + + def __repr__(self) -> str: + """Return a developer-friendly representation.""" + return f"Editor('{self._path}')" + + def __enter__(self) -> Editor: + """Read the file and enter the editing context.""" + if not self._path.exists(): + msg = f"File not found: {self._path}" + raise FileNotFoundError(msg) + source = self._path.read_text(encoding="utf-8") + doc = Document(source) + self._original_source = source + self._original = doc + self._document = doc + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Write changes on success, discard on exception. + + The external-modification check is best-effort: it detects changes + made between ``__enter__`` and ``__exit__`` but is not atomic and + cannot guard against concurrent writes during the write itself. + """ + if exc_type is None and self._document is not None: + current_source = self._path.read_text(encoding="utf-8") + if current_source != self._original_source: + msg = f"File was modified externally: {self._path}" + raise RuntimeError(msg) + self._path.write_text(self._document.dumps(), encoding="utf-8") + self._original = None + self._document = None + self._original_source = None + + @property + def original(self) -> Document: + """The document as it was when the editor was entered.""" + if self._original is None: + msg = "Editor must be used as a context manager" + raise RuntimeError(msg) + return self._original + + @property + def document(self) -> Document: + """The current in-progress document.""" + if self._document is None: + msg = "Editor must be used as a context manager" + raise RuntimeError(msg) + return self._document + + def __getitem__(self, keys: object) -> Any: + """Retrieve the parsed value at the given path.""" + return self.document[keys] + + def __contains__(self, keys: object) -> bool: + """Check whether a path exists in the document.""" + return keys in self.document + + def __setitem__(self, keys: object, value: Any) -> None: + """Upsert a value at the given path.""" + normalized = _normalize_keys(keys) + self._document = self.document.upsert(*normalized, value=value) + + def replace(self, *keys: KeyPart, value: Any) -> None: + """Replace the value at an existing path.""" + self._document = self.document.replace(*keys, value=value) + + def add(self, *keys: KeyPart, key: str, value: Any) -> None: + """Add a new key to the mapping at path.""" + self._document = self.document.add(*keys, key=key, value=value) + + def upsert(self, *keys: KeyPart, value: Any) -> None: + """Replace if exists, create if not.""" + self._document = self.document.upsert(*keys, value=value) + + def remove(self, *keys: KeyPart, prune: bool = False) -> None: + """Remove the key or index at path.""" + self._document = self.document.remove(*keys, prune=prune) + + def prune_remove(self, *keys: KeyPart) -> None: + """Remove key and prune empty parents.""" + self._document = self.document.prune_remove(*keys) + + def append(self, *keys: KeyPart, value: Any) -> None: + """Append an item to the sequence at path.""" + self._document = self.document.append(*keys, value=value) + + def extend_list(self, *keys: KeyPart, values: Sequence[Any]) -> None: + """Append multiple items to the sequence at path.""" + self._document = self.document.extend_list(*keys, values=values) + + def remove_from_list(self, *keys: KeyPart, values: Sequence[Any]) -> None: + """Remove all occurrences of given values from the sequence at path.""" + self._document = self.document.remove_from_list(*keys, values=values) + + def query(self, *keys: KeyPart) -> Feature: + """Return the Feature at the given path.""" + return self.document.query(*keys) + + def extract(self, feature: Feature) -> str: + """Extract the raw YAML text for a feature.""" + return self.document.extract(feature) diff --git a/src/yamltrip/errors.py b/src/yamltrip/errors.py new file mode 100644 index 0000000..db49e65 --- /dev/null +++ b/src/yamltrip/errors.py @@ -0,0 +1,25 @@ +"""yamltrip error types.""" + + +class YAMLTripError(Exception): + """Base exception for all yamltrip errors.""" + + +class ParseError(YAMLTripError): + """Raised when YAML input cannot be parsed.""" + + +class QueryError(YAMLTripError): + """Raised when a path query fails (path not found).""" + + +class PatchError(YAMLTripError): + """Raised when a patch operation fails.""" + + +class KeyExistsError(PatchError): + """Raised by add() when the key already exists.""" + + +class KeyMissingError(PatchError): + """Raised by replace() when the key doesn't exist.""" diff --git a/tests/test_core_document.py b/tests/test_core_document.py new file mode 100644 index 0000000..6b430bd --- /dev/null +++ b/tests/test_core_document.py @@ -0,0 +1,119 @@ +import pytest + +from yamltrip._core import Document, FeatureKind, Route + + +class TestDocumentParsing: + def test_parse_simple(self): + doc = Document("name: foo") + assert doc.source() == "name: foo" + + def test_parse_invalid_raises(self): + with pytest.raises(ValueError, match="Failed to parse YAML"): + Document("{") + + +class TestDocumentQuery: + def test_query_exists_true(self): + doc = Document("name: foo") + route = Route(["name"]) + assert doc.query_exists(route) is True + + def test_query_exists_false(self): + doc = Document("name: foo") + route = Route(["missing"]) + assert doc.query_exists(route) is False + + def test_query_exact(self): + doc = Document("name: foo") + route = Route(["name"]) + feature = doc.query_exact(route) + assert feature is not None + assert feature.location.start >= 0 + assert feature.location.end > feature.location.start + + def test_query_exact_missing(self): + doc = Document("name: foo") + route = Route(["missing"]) + with pytest.raises(KeyError): + doc.query_exact(route) + + def test_extract(self): + doc = Document("name: foo") + route = Route(["name"]) + feature = doc.query_exact(route) + assert feature is not None + assert doc.extract(feature) == "foo" + + def test_nested_query(self): + doc = Document("a:\n b: 42") + route = Route(["a", "b"]) + feature = doc.query_exact(route) + assert feature is not None + assert doc.extract(feature) == "42" + + def test_sequence_query(self): + doc = Document("items:\n - a\n - b") + route = Route(["items", 0]) + feature = doc.query_exact(route) + assert feature is not None + assert doc.extract(feature) == "a" + + +class TestExtractCrossDocumentUTF8: + """Using a Feature from one document on another can produce byte offsets + that land mid-UTF-8 codepoint. This must raise ValueError, not crash.""" + + def test_extract_mid_utf8_raises_not_panics(self): + # "x: y" — scalar "y" is at byte offset 3..4 + doc_a = Document("x: y") + feature = doc_a.query_exact(Route(["x"])) + assert feature is not None + assert feature.location.start == 3 + assert feature.location.end == 4 + + # "🎉: z" — 🎉 is 4 UTF-8 bytes (F0 9F 8E 89), so byte 3 is a + # continuation byte, not a char boundary. + doc_b = Document("\U0001f389: z") + + # Should raise a clean ValueError, not a Rust panic / PanicException. + with pytest.raises(ValueError, match="UTF-8"): + doc_b.extract(feature) + + +class TestParseValueUTF8: + """parse_value must not panic on documents with multi-byte UTF-8 chars.""" + + def test_parse_value_multibyte_key(self): + doc = Document("🔑: hello") + assert doc.parse_value(Route(["🔑"])) == "hello" + + def test_parse_value_multibyte_value(self): + doc = Document("key: 🎉") + assert doc.parse_value(Route(["key"])) == "🎉" + + def test_parse_value_multibyte_nested(self): + doc = Document("日本:\n 名前: こんにちは") + assert doc.parse_value(Route(["日本", "名前"])) == "こんにちは" + + def test_parse_value_multibyte_multiline_dedent(self): + """Exercises the dedenting path with multi-byte characters.""" + doc = Document("parent:\n 子: |\n 日本語テキスト\n 二行目") + result = doc.parse_value(Route(["parent", "子"])) + assert "日本語テキスト" in result + + +class TestDocumentFeatureKind: + def test_scalar_kind(self): + doc = Document("name: foo") + route = Route(["name"]) + feature = doc.query_exact(route) + assert feature is not None + assert feature.kind == FeatureKind.Scalar + + def test_mapping_kind(self): + doc = Document("a:\n b: 1") + route = Route(["a"]) + feature = doc.query_exact(route) + assert feature is not None + assert feature.kind == FeatureKind.BlockMapping diff --git a/tests/test_core_ops.py b/tests/test_core_ops.py new file mode 100644 index 0000000..9776d45 --- /dev/null +++ b/tests/test_core_ops.py @@ -0,0 +1,65 @@ +from yamltrip._core import Op, Patch, Route, apply_patches + + +class TestOpConstructors: + def test_replace(self): + op = Op.replace("bar") + assert op is not None + + def test_add(self): + op = Op.add("new_key", "new_value") + assert op is not None + + def test_remove(self): + op = Op.remove() + assert op is not None + + def test_append(self): + op = Op.append("item") + assert op is not None + + +class TestApplyPatches: + def test_replace_value(self): + source = "name: foo" + patches = [Patch(route=Route(["name"]), operation=Op.replace("bar"))] + result = apply_patches(source, patches) + assert "bar" in result + assert "foo" not in result + + def test_add_key(self): + source = "name: foo" + patches = [Patch(route=Route([]), operation=Op.add("age", 30))] + result = apply_patches(source, patches) + assert "age" in result + assert "30" in result + assert "name: foo" in result # original preserved + + def test_remove_key(self): + source = "name: foo\nage: 30" + patches = [Patch(route=Route(["age"]), operation=Op.remove())] + result = apply_patches(source, patches) + assert "name: foo" in result + assert "age" not in result + + def test_append_to_sequence(self): + source = "items:\n - a\n - b" + patches = [Patch(route=Route(["items"]), operation=Op.append("c"))] + result = apply_patches(source, patches) + assert "- c" in result + assert "- a" in result + + def test_preserves_comments(self): + source = "# top comment\nname: foo # inline" + patches = [Patch(route=Route(["name"]), operation=Op.replace("bar"))] + result = apply_patches(source, patches) + assert "# top comment" in result + assert "# inline" in result + assert "bar" in result + + def test_preserves_indentation(self): + source = "a:\n b: 1\n c: 2" + patches = [Patch(route=Route(["a", "b"]), operation=Op.replace(99))] + result = apply_patches(source, patches) + assert " b: 99" in result + assert " c: 2" in result diff --git a/tests/test_core_types.py b/tests/test_core_types.py new file mode 100644 index 0000000..6a4c819 --- /dev/null +++ b/tests/test_core_types.py @@ -0,0 +1,77 @@ +from yamltrip._core import Component, FeatureKind, Location, Route + + +class TestLocation: + def test_start_end(self): + loc = Location(start=0, end=5) + assert loc.start == 0 + assert loc.end == 5 + + def test_repr(self): + loc = Location(start=0, end=5) + assert "0" in repr(loc) + assert "5" in repr(loc) + + def test_eq(self): + assert Location(0, 5) == Location(0, 5) + assert Location(0, 5) != Location(0, 6) + + def test_hash(self): + s = {Location(0, 5), Location(0, 5), Location(0, 6)} + assert len(s) == 2 + + +class TestFeatureKind: + def test_variants_exist(self): + # FeatureKind uses PascalCase in PyO3 enums + assert FeatureKind.Scalar is not None + assert FeatureKind.BlockMapping is not None + assert FeatureKind.FlowMapping is not None + assert FeatureKind.BlockSequence is not None + assert FeatureKind.FlowSequence is not None + + def test_hash(self): + s = {FeatureKind.Scalar, FeatureKind.BlockMapping, FeatureKind.Scalar} + assert len(s) == 2 + + +class TestComponent: + def test_key(self): + c = Component.key("name") + assert repr(c) == "Component.key('name')" + + def test_index(self): + c = Component.index(0) + assert repr(c) == "Component.index(0)" + + def test_eq(self): + assert Component.key("a") == Component.key("a") + assert Component.key("a") != Component.key("b") + assert Component.index(0) == Component.index(0) + assert Component.key("0") != Component.index(0) + + def test_hash(self): + s = {Component.key("a"), Component.key("a"), Component.key("b")} + assert len(s) == 2 + + +class TestRoute: + def test_from_keys(self): + route = Route(["a", "b"]) + assert len(route) == 2 + + def test_from_mixed(self): + route = Route(["items", 0]) + assert len(route) == 2 + + def test_empty(self): + route = Route([]) + assert len(route) == 0 + + def test_eq(self): + assert Route(["a", "b"]) == Route(["a", "b"]) + assert Route(["a"]) != Route(["a", "b"]) + + def test_hash(self): + s = {Route(["a", "b"]), Route(["a", "b"]), Route(["a"])} + assert len(s) == 2 diff --git a/tests/test_document.py b/tests/test_document.py new file mode 100644 index 0000000..8456636 --- /dev/null +++ b/tests/test_document.py @@ -0,0 +1,261 @@ +import pytest + +from yamltrip._core import FeatureKind +from yamltrip.document import Document +from yamltrip.errors import ( + KeyExistsError, + KeyMissingError, + QueryError, +) + + +class TestDocumentConstruction: + def test_from_string(self): + doc = Document("name: foo") + assert doc.source == "name: foo" + + def test_empty_string(self): + doc = Document("") + assert doc.source == "" + + def test_equality(self): + assert Document("name: foo") == Document("name: foo") + + def test_inequality(self): + assert Document("name: foo") != Document("name: bar") + + def test_equality_not_implemented_for_other_types(self): + assert Document("name: foo") != "name: foo" + + def test_hashable(self): + doc1 = Document("name: foo") + doc2 = Document("name: foo") + assert hash(doc1) == hash(doc2) + assert {doc1, doc2} == {doc1} + + +class TestDocumentGetitem: + def test_single_key(self): + doc = Document("name: foo") + assert doc["name"] == "foo" + + def test_nested_keys(self): + doc = Document("a:\n b: 42") + assert doc["a", "b"] == 42 + + def test_sequence_index(self): + doc = Document("items:\n - a\n - b") + assert doc["items", 0] == "a" + assert doc["items", 1] == "b" + + def test_missing_key_raises(self): + doc = Document("name: foo") + with pytest.raises(QueryError): + doc["missing"] + + def test_integer_value(self): + doc = Document("count: 42") + assert doc["count"] == 42 + + def test_boolean_value(self): + doc = Document("flag: true") + assert doc["flag"] is True + + def test_null_value(self): + doc = Document("nothing: null") + assert doc["nothing"] is None + + def test_list_value(self): + doc = Document("items:\n - a\n - b") + result = doc["items"] + assert result == ["a", "b"] + + def test_dict_value(self): + doc = Document("a:\n b: 1\n c: 2") + result = doc["a"] + assert result == {"b": 1, "c": 2} + + +class TestParseValueDedent: + """Tests for parse_value's dedent logic on multiline/nested YAML values.""" + + def test_block_literal_scalar(self): + source = "desc: |\n line one\n line two\n" + doc = Document(source) + assert doc["desc"] == "line one\nline two\n" + + def test_block_folded_scalar(self): + source = "desc: >\n line one\n line two\n" + doc = Document(source) + assert doc["desc"] == "line one line two\n" + + def test_block_literal_strip(self): + source = "desc: |-\n line one\n line two\n" + doc = Document(source) + assert doc["desc"] == "line one\nline two" + + def test_block_literal_keep(self): + source = "desc: |+\n line one\n line two\n\n" + doc = Document(source) + assert doc["desc"] == "line one\nline two\n\n" + + def test_block_scalar_with_blank_lines(self): + source = "desc: |\n first\n\n second\n" + doc = Document(source) + assert doc["desc"] == "first\n\nsecond\n" + + def test_nested_block_scalar(self): + source = "outer:\n inner: |\n hello\n world\n" + doc = Document(source) + assert doc["outer", "inner"] == "hello\nworld\n" + + def test_deeply_nested_value(self): + source = "a:\n b:\n c:\n d: deep\n" + doc = Document(source) + assert doc["a", "b", "c", "d"] == "deep" + + def test_nested_mapping_value(self): + source = "root:\n x: 1\n y: 2\n" + doc = Document(source) + assert doc["root"] == {"x": 1, "y": 2} + + def test_nested_sequence_value(self): + source = "root:\n items:\n - a\n - b\n - c\n" + doc = Document(source) + assert doc["root", "items"] == ["a", "b", "c"] + + +class TestDocumentContains: + def test_key_exists(self): + doc = Document("name: foo") + assert "name" in doc + + def test_key_missing(self): + doc = Document("name: foo") + assert "missing" not in doc + + def test_nested_key_exists(self): + doc = Document("a:\n b: 1") + assert ("a", "b") in doc + + def test_nested_key_missing(self): + doc = Document("a:\n b: 1") + assert ("a", "c") not in doc + + +class TestDocumentInspection: + def test_query_returns_feature(self): + doc = Document("name: foo") + feature = doc.query("name") + assert feature.kind == FeatureKind.Scalar + + def test_extract(self): + doc = Document("name: foo") + feature = doc.query("name") + assert doc.extract(feature) == "foo" + + +class TestDocumentOutput: + def test_dumps(self): + source = "name: foo\n" + doc = Document(source) + assert doc.dumps() == source + + def test_dump(self, tmp_path): + source = "name: foo\n" + doc = Document(source) + p = tmp_path / "out.yml" + doc.dump(p) + assert p.read_text(encoding="utf-8") == source + + +class TestDocumentReplace: + def test_replace_scalar(self): + doc = Document("name: foo") + doc2 = doc.replace("name", value="bar") + assert doc2["name"] == "bar" + assert doc["name"] == "foo" # original unchanged + + def test_replace_preserves_comments(self): + doc = Document("# header\nname: foo # inline") + doc2 = doc.replace("name", value="bar") + assert "# header" in doc2.source + assert "# inline" in doc2.source + + def test_replace_missing_raises(self): + doc = Document("name: foo") + with pytest.raises(KeyMissingError): + doc.replace("missing", value="bar") + + +class TestDocumentAdd: + def test_add_key(self): + doc = Document("name: foo") + doc2 = doc.add(key="age", value=30) + assert doc2["age"] == 30 + assert doc2["name"] == "foo" + + def test_add_existing_raises(self): + doc = Document("name: foo") + with pytest.raises(KeyExistsError): + doc.add(key="name", value="bar") + + def test_add_to_nested(self): + doc = Document("a:\n b: 1") + doc2 = doc.add("a", key="c", value=2) + assert doc2["a", "c"] == 2 + + +class TestDocumentUpsert: + def test_upsert_existing(self): + doc = Document("name: foo") + doc2 = doc.upsert("name", value="bar") + assert doc2["name"] == "bar" + + def test_upsert_missing(self): + doc = Document("name: foo") + doc2 = doc.upsert("age", value=30) + assert doc2["age"] == 30 + + +class TestDocumentRemove: + def test_remove_key(self): + doc = Document("name: foo\nage: 30") + doc2 = doc.remove("age") + assert "age" not in doc2 + assert doc2["name"] == "foo" + + +class TestDocumentPruneRemove: + def test_prune_remove(self): + doc = Document("a:\n b:\n c: 1") + doc2 = doc.prune_remove("a", "b", "c") + assert "a" not in doc2 + + def test_remove_with_prune_flag(self): + doc = Document("a:\n b:\n c: 1") + doc2 = doc.remove("a", "b", "c", prune=True) + assert "a" not in doc2 + + +class TestDocumentAppend: + def test_append(self): + doc = Document("items:\n - a\n - b") + doc2 = doc.append("items", value="c") + result = doc2["items"] + assert "c" in result + + def test_extend_list(self): + doc = Document("items:\n - a") + doc2 = doc.extend_list("items", values=["b", "c"]) + result = doc2["items"] + assert "b" in result + assert "c" in result + + def test_remove_from_list(self): + doc = Document("items:\n - a\n - b\n - c") + doc2 = doc.remove_from_list("items", values=["b"]) + result = doc2["items"] + assert "b" not in result + assert "a" in result + assert "c" in result diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py new file mode 100644 index 0000000..fc64a79 --- /dev/null +++ b/tests/test_edge_cases.py @@ -0,0 +1,111 @@ +"""Tests for critical edge cases.""" + +import pytest + +from yamltrip.document import Document +from yamltrip.editor import Editor +from yamltrip.errors import PatchError, QueryError + + +class TestExtendListEmpty: + def test_extend_list_empty_returns_same(self): + doc = Document("items:\n - a") + doc2 = doc.extend_list("items", values=[]) + assert doc2 is doc + + def test_extend_list_empty_source_unchanged(self): + doc = Document("items:\n - a") + doc2 = doc.extend_list("items", values=[]) + assert doc2.source == doc.source + + +class TestUpsertIntKeyCreation: + def test_upsert_int_key_at_root_raises(self): + doc = Document("name: foo") + with pytest.raises(PatchError, match="integer key"): + doc.upsert(0, value="bar") + + def test_upsert_int_key_intermediate_raises(self): + doc = Document("name: foo") + with pytest.raises(PatchError, match="integer key"): + doc.upsert("items", 0, "sub", value=True) + + def test_upsert_int_key_existing_path_works(self): + doc = Document("items:\n - a\n - b") + doc2 = doc.upsert("items", 0, value="x") + assert doc2["items", 0] == "x" + + +class TestUpsertRootReplace: + def test_upsert_empty_keys_replaces_root(self): + doc = Document("name: foo") + doc2 = doc.upsert(value="replaced") + assert "replaced" in doc2.source + + +class TestRemoveFromListDuplicates: + def test_removes_all_occurrences(self): + doc = Document("items:\n - a\n - b\n - a\n - c") + doc2 = doc.remove_from_list("items", values=["a"]) + result = doc2["items"] + assert "a" not in result + assert "b" in result + assert "c" in result + + +class TestContainsTypeError: + def test_float_key_raises_type_error(self): + doc = Document("name: foo") + with pytest.raises(TypeError): + 3.14 in doc # noqa: B015 + + def test_object_key_raises_type_error(self): + doc = Document("name: foo") + with pytest.raises(TypeError): + object() in doc # noqa: B015 + + +class TestHasAnchors: + def test_document_without_anchors(self): + doc = Document("name: foo") + assert doc.has_anchors() is False + + def test_document_with_anchors(self): + doc = Document("defaults: &defaults\n a: 1\noverrides:\n <<: *defaults") + assert doc.has_anchors() is True + + +class TestEditorExternalModification: + def test_external_modification_raises(self, tmp_path): + p = tmp_path / "test.yml" + p.write_text("name: foo\n", encoding="utf-8") + + editor = Editor(p) + editor.__enter__() + editor.replace("name", value="bar") + # Simulate external modification by changing the file content + p.write_text("name: baz\n", encoding="utf-8") + with pytest.raises(RuntimeError, match="modified externally"): + editor.__exit__(None, None, None) + + +class TestDocumentRepr: + def test_repr_shows_byte_count(self): + doc = Document("name: foo") + assert repr(doc) == "Document(<9 bytes>)" + + def test_repr_empty(self): + doc = Document("") + assert repr(doc) == "Document(<0 bytes>)" + + +class TestQueryMissingPath: + def test_query_missing_raises(self): + doc = Document("name: foo") + with pytest.raises(QueryError): + doc.query("missing") + + def test_getitem_missing_raises(self): + doc = Document("name: foo") + with pytest.raises(QueryError): + doc["missing"] diff --git a/tests/test_editor.py b/tests/test_editor.py new file mode 100644 index 0000000..5bd9e86 --- /dev/null +++ b/tests/test_editor.py @@ -0,0 +1,139 @@ +import pytest + +from yamltrip.editor import Editor + + +@pytest.fixture +def yaml_file(tmp_path): + p = tmp_path / "test.yml" + p.write_text("name: foo\nage: 30\nitems:\n - a\n - b\n", encoding="utf-8") + return p + + +class TestEditorContextManager: + def test_read_on_enter(self, yaml_file): + with Editor(yaml_file) as editor: + assert editor["name"] == "foo" + + def test_write_on_exit(self, yaml_file): + with Editor(yaml_file) as editor: + editor.replace("name", value="bar") + content = yaml_file.read_text(encoding="utf-8") + assert "bar" in content + + def test_no_write_on_exception(self, yaml_file): + with pytest.raises(RuntimeError, match="boom"), Editor(yaml_file) as editor: # noqa: PT012 + editor.replace("name", value="bar") + msg = "boom" + raise RuntimeError(msg) + content = yaml_file.read_text(encoding="utf-8") + assert "foo" in content + assert "bar" not in content + + def test_file_not_found(self, tmp_path): + with pytest.raises(FileNotFoundError), Editor(tmp_path / "missing.yml"): + pass + + +class TestEditorOriginal: + def test_original_unchanged(self, yaml_file): + with Editor(yaml_file) as editor: + editor.replace("name", value="bar") + assert editor.original["name"] == "foo" + assert editor["name"] == "bar" + + +class TestEditorOperations: + def test_replace(self, yaml_file): + with Editor(yaml_file) as editor: + editor.replace("name", value="bar") + assert editor["name"] == "bar" + + def test_upsert(self, yaml_file): + with Editor(yaml_file) as editor: + editor.upsert("new_key", value="new_val") + assert editor["new_key"] == "new_val" + + def test_remove(self, yaml_file): + with Editor(yaml_file) as editor: + editor.remove("age") + assert "age" not in editor + + def test_prune_remove(self, yaml_file): + p = yaml_file.parent / "nested.yml" + p.write_text("a:\n b:\n c: 1\n", encoding="utf-8") + with Editor(p) as editor: + editor.prune_remove("a", "b", "c") + assert "a" not in editor + + def test_append(self, yaml_file): + with Editor(yaml_file) as editor: + editor.append("items", value="c") + result = editor["items"] + assert "c" in result + + def test_setitem(self, yaml_file): + with Editor(yaml_file) as editor: + editor["name"] = "baz" + assert editor["name"] == "baz" + + def test_setitem_nested(self, yaml_file): + with Editor(yaml_file) as editor: + editor["new", "nested"] = "val" + assert editor["new", "nested"] == "val" + + def test_contains(self, yaml_file): + with Editor(yaml_file) as editor: + assert "name" in editor + assert "missing" not in editor + + def test_document_attribute(self, yaml_file): + with Editor(yaml_file) as editor: + doc = editor.document + assert doc["name"] == "foo" + + def test_add(self, yaml_file): + with Editor(yaml_file) as editor: + editor.add(key="color", value="red") + assert editor["color"] == "red" + + def test_extend_list(self, yaml_file): + with Editor(yaml_file) as editor: + editor.extend_list("items", values=["c", "d"]) + result = editor["items"] + assert "c" in result + assert "d" in result + + def test_remove_from_list(self, yaml_file): + with Editor(yaml_file) as editor: + editor.remove_from_list("items", values=["a"]) + result = editor["items"] + assert "a" not in result + assert "b" in result + + def test_query(self, yaml_file): + with Editor(yaml_file) as editor: + feature = editor.query("name") + assert feature is not None + + def test_extract(self, yaml_file): + with Editor(yaml_file) as editor: + feature = editor.query("name") + text = editor.extract(feature) + assert "foo" in text + + +class TestEditorGuards: + def test_original_outside_context(self, yaml_file): + editor = Editor(yaml_file) + with pytest.raises(RuntimeError, match="context manager"): + _ = editor.original + + def test_document_outside_context(self, yaml_file): + editor = Editor(yaml_file) + with pytest.raises(RuntimeError, match="context manager"): + _ = editor.document + + def test_repr(self, yaml_file): + editor = Editor(yaml_file) + assert "Editor(" in repr(editor) diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..1288221 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,35 @@ +import pytest + +from yamltrip.errors import ( + KeyExistsError, + KeyMissingError, + ParseError, + PatchError, + QueryError, + YAMLTripError, +) + + +class TestErrorHierarchy: + def test_base_error(self): + assert issubclass(YAMLTripError, Exception) + + def test_parse_error(self): + assert issubclass(ParseError, YAMLTripError) + + def test_query_error(self): + assert issubclass(QueryError, YAMLTripError) + + def test_patch_error(self): + assert issubclass(PatchError, YAMLTripError) + + def test_key_exists_error(self): + assert issubclass(KeyExistsError, PatchError) + + def test_key_missing_error(self): + assert issubclass(KeyMissingError, PatchError) + + def test_raise_and_catch_base(self): + msg = "key already exists" + with pytest.raises(YAMLTripError, match=msg): + raise KeyExistsError(msg) diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..088f034 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,83 @@ +"""End-to-end integration tests exercising file I/O round-trips.""" + +import yamltrip + + +class TestLoadMutateDumpLoad: + def test_load_replace_dump_reload(self, tmp_path): + p = tmp_path / "config.yml" + p.write_text("name: Alice\nage: 30\n", encoding="utf-8") + + doc = yamltrip.load(p) + doc2 = doc.replace("age", value=31) + doc2.dump(p) + + reloaded = yamltrip.load(p) + assert reloaded["name"] == "Alice" + assert reloaded["age"] == 31 + + def test_load_add_dump_reload(self, tmp_path): + p = tmp_path / "config.yml" + p.write_text("name: Alice\n", encoding="utf-8") + + doc = yamltrip.load(p) + doc2 = doc.add(key="city", value="Portland") + doc2.dump(p) + + reloaded = yamltrip.load(p) + assert reloaded["name"] == "Alice" + assert reloaded["city"] == "Portland" + + def test_multi_mutation_round_trip(self, tmp_path): + p = tmp_path / "config.yml" + p.write_text("version: 1\nitems:\n - a\n - b\n", encoding="utf-8") + + doc = yamltrip.load(p) + doc = doc.replace("version", value=2) + doc = doc.append("items", value="c") + doc.dump(p) + + reloaded = yamltrip.load(p) + assert reloaded["version"] == 2 + assert reloaded["items"] == ["a", "b", "c"] + + +class TestEditorEndToEnd: + def test_editor_writes_on_success(self, tmp_path): + p = tmp_path / "config.yml" + p.write_text("version: 1\n", encoding="utf-8") + + with yamltrip.edit(p) as editor: + editor.replace("version", value=2) + + reloaded = yamltrip.load(p) + assert reloaded["version"] == 2 + + def test_editor_discards_on_exception(self, tmp_path): + p = tmp_path / "config.yml" + p.write_text("version: 1\n", encoding="utf-8") + + try: + with yamltrip.edit(p) as editor: + editor.replace("version", value=2) + msg = "abort" + raise RuntimeError(msg) + except RuntimeError: + pass + + reloaded = yamltrip.load(p) + assert reloaded["version"] == 1 + + def test_editor_multi_operation(self, tmp_path): + p = tmp_path / "config.yml" + p.write_text("name: Alice\nage: 30\nitems:\n - x\n", encoding="utf-8") + + with yamltrip.edit(p) as editor: + editor.replace("name", value="Bob") + editor.replace("age", value=25) + editor.append("items", value="y") + + reloaded = yamltrip.load(p) + assert reloaded["name"] == "Bob" + assert reloaded["age"] == 25 + assert reloaded["items"] == ["x", "y"] diff --git a/tests/test_public_api.py b/tests/test_public_api.py new file mode 100644 index 0000000..035e610 --- /dev/null +++ b/tests/test_public_api.py @@ -0,0 +1,48 @@ +"""Tests for the public API surface.""" + +import yamltrip + + +class TestTopLevelFunctions: + def test_loads(self): + doc = yamltrip.loads("name: foo") + assert doc["name"] == "foo" + + def test_load(self, tmp_path): + p = tmp_path / "test.yml" + p.write_text("name: bar\n", encoding="utf-8") + doc = yamltrip.load(p) + assert doc["name"] == "bar" + + def test_edit(self, tmp_path): + p = tmp_path / "test.yml" + p.write_text("name: foo\n", encoding="utf-8") + with yamltrip.edit(p) as editor: + editor["name"] = "bar" + assert "bar" in p.read_text(encoding="utf-8") + + +class TestExports: + def test_all_exports_accessible(self): + for name in yamltrip.__all__: + assert hasattr(yamltrip, name), f"{name} missing from yamltrip" + + def test_document_class(self): + assert yamltrip.Document is not None + + def test_editor_class(self): + assert yamltrip.Editor is not None + + def test_error_classes(self): + assert issubclass(yamltrip.ParseError, yamltrip.YAMLTripError) + assert issubclass(yamltrip.QueryError, yamltrip.YAMLTripError) + assert issubclass(yamltrip.PatchError, yamltrip.YAMLTripError) + assert issubclass(yamltrip.KeyExistsError, yamltrip.PatchError) + assert issubclass(yamltrip.KeyMissingError, yamltrip.PatchError) + + def test_core_types(self): + assert yamltrip.Location is not None + assert yamltrip.Route is not None + assert yamltrip.Component is not None + assert yamltrip.Feature is not None + assert yamltrip.FeatureKind is not None diff --git a/tests/test_roundtrip.py b/tests/test_roundtrip.py new file mode 100644 index 0000000..f759959 --- /dev/null +++ b/tests/test_roundtrip.py @@ -0,0 +1,114 @@ +"""Round-trip preservation tests.""" + +from yamltrip import Document + + +class TestCommentPreservation: + def test_inline_comment(self): + source = "name: foo # important\nage: 30\n" + doc = Document(source).replace("name", value="bar") + assert "# important" in doc.source + + def test_header_comment(self): + source = "# File header\nname: foo\n" + doc = Document(source).replace("name", value="bar") + assert "# File header" in doc.source + + def test_comment_between_keys(self): + source = "a: 1\n# middle\nb: 2\n" + doc = Document(source).replace("a", value=10) + assert "# middle" in doc.source + + +class TestQuotePreservation: + def test_single_quotes_replaced(self): + source = "name: 'foo'\n" + doc = Document(source).replace("name", value="bar") + # The value is replaced; we mainly check it round-trips without errors + assert "bar" in doc.source + + def test_double_quotes(self): + source = 'name: "foo"\n' + doc = Document(source).replace("name", value="bar") + assert "bar" in doc.source + + +class TestBlankLinePreservation: + def test_blank_lines_between_sections(self): + source = "a: 1\n\nb: 2\n" + doc = Document(source).replace("a", value=10) + assert "\n\n" in doc.source + + def test_trailing_newline(self): + source = "name: foo\n" + doc = Document(source).replace("name", value="bar") + assert doc.source.endswith("\n") + + +class TestIndentationPreservation: + def test_nested_indent_preserved(self): + source = "parent:\n child: foo\n other: bar\n" + doc = Document(source).replace("parent", "child", value="baz") + # Check indentation is preserved (4-space indent) + lines = doc.source.split("\n") + child_line = next(line for line in lines if "child" in line) + assert child_line.startswith(" ") + + def test_two_space_indent(self): + source = "parent:\n child: foo\n" + doc = Document(source).replace("parent", "child", value="baz") + lines = doc.source.split("\n") + child_line = next(line for line in lines if "child" in line) + assert child_line.startswith(" ") + + +class TestSequencePreservation: + def test_append_preserves_sequence_style(self): + source = "items:\n - a\n - b\n" + doc = Document(source).append("items", value="c") + assert "- a" in doc.source + assert "- b" in doc.source + assert "- c" in doc.source + + def test_remove_preserves_others(self): + source = "items:\n - a\n - b\n - c\n" + doc = Document(source).remove_from_list("items", values=["b"]) + assert "- a" in doc.source + assert "- c" in doc.source + assert "- b" not in doc.source + + +class TestIdempotent: + def test_no_op_roundtrip(self): + source = "# header\nname: foo # inline\nitems:\n - a\n - b\n" + doc = Document(source) + assert doc.source == source + + def test_replace_same_value(self): + source = "name: foo\n" + doc = Document(source).replace("name", value="foo") + # Value should still be there + assert doc["name"] == "foo" + + +class TestComplexDocument: + def test_multi_operation_preserves_structure(self): + source = ( + "# Config file\n" + "server:\n" + " host: localhost # default\n" + " port: 8080\n" + "\n" + "database:\n" + " url: postgres://localhost\n" + " pool: 5\n" + ) + doc = Document(source) + doc = doc.replace("server", "port", value=9090) + doc = doc.replace("database", "pool", value=10) + + assert "# Config file" in doc.source + assert "# default" in doc.source + assert doc["server", "host"] == "localhost" + assert doc["server", "port"] == 9090 + assert doc["database", "pool"] == 10 diff --git a/tests/test_stubs.py b/tests/test_stubs.py new file mode 100644 index 0000000..3180b6e --- /dev/null +++ b/tests/test_stubs.py @@ -0,0 +1,41 @@ +"""Validate that _core.pyi stubs match the actual native module.""" + +import subprocess +import sys + +import pytest + + +def _stubtest_supports_ignore_disjoint_bases() -> bool: + """Check if the installed mypy stubtest supports --ignore-disjoint-bases.""" + result = subprocess.run( + [sys.executable, "-m", "mypy.stubtest", "--help"], + capture_output=True, + text=True, + ) + return "--ignore-disjoint-bases" in result.stdout + + +def test_stubtest_core(): + """Run mypy stubtest against yamltrip._core to catch stub drift.""" + cmd = [ + sys.executable, + "-m", + "mypy.stubtest", + "yamltrip._core", + ] + if _stubtest_supports_ignore_disjoint_bases(): + cmd.append("--ignore-disjoint-bases") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + ) + except FileNotFoundError: + pytest.skip("mypy not installed") + + assert result.returncode == 0, ( + f"stubtest found stub mismatches:\n{result.stdout}\n{result.stderr}" + ) diff --git a/uv.lock b/uv.lock index 97ad1a2..3114b60 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,856 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/9d/912fefab0e30aee6a3af8a62bbea4a81b29afa4ba2c973d31170620a26de/ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b", size = 60689, upload-time = "2026-04-30T23:24:48.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/57/a54d4de491d6cdd7a4e4b0952cc3ca9f60dcefa7b5fb48d6d492debe1649/ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14", size = 1182966, upload-time = "2026-04-30T23:23:57.376Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/a5db014bb0f91b209236b57c429389e31290c0093532b8436d577699b2fa/ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22", size = 1171316, upload-time = "2026-04-30T23:23:59.63Z" }, + { url = "https://files.pythonhosted.org/packages/15/59/fd55133e478c4326f60a11df02573bf7ccb2ac685810b50f1803d0f68053/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f", size = 1232234, upload-time = "2026-04-30T23:24:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/0ca1d26357ecb4a697d74d00b73ef3137f24c140424125393a0de820eb09/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66", size = 1233437, upload-time = "2026-04-30T23:24:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/7078ec94dd6e124b8e028ac77016a4f13c83fa1c145790f2e68f3816998b/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8", size = 1440188, upload-time = "2026-04-30T23:24:04.717Z" }, + { url = "https://files.pythonhosted.org/packages/21/16/cca7195ef55a012f8013c3442afa91d287a0a36dcf88b480b262475135b3/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9", size = 1254211, upload-time = "2026-04-30T23:24:06.18Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/f3d4dfae67dee6580534361a6343367d34217e7d25cff858bd1d8f03b8ed/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e", size = 1255973, upload-time = "2026-04-30T23:24:07.772Z" }, + { url = "https://files.pythonhosted.org/packages/14/41/55fbfe02c42f40fbe3e74eda167d977d555ff720ce1abfa08515236efd88/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de", size = 1298629, upload-time = "2026-04-30T23:24:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/28/36/7d2501cacc7989fb8504aa9da2a2022a174200a59d4e6639de4367a57fdd/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df", size = 1408435, upload-time = "2026-04-30T23:24:11.013Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/54e3b469c3fa0bf9cd532fa643d1d33b73303f8d70beac3e366b68dd64b7/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728", size = 1508174, upload-time = "2026-04-30T23:24:12.635Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/9b9621865b02c60539e26d9b114a312b4fa46aa703e33e79317174bfea21/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d", size = 1502354, upload-time = "2026-04-30T23:24:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/34/dd/f138bc5c43b0c414fdd12eefe15677839323078b6e75301ad7f96cd26d45/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d", size = 1450504, upload-time = "2026-04-30T23:24:16.076Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/97ef9e1c315601db74365955c8edd3292e3055500d6317602815dbdf08ae/ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9", size = 1058662, upload-time = "2026-04-30T23:24:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d6/e2c3483c31580fdb623f92ad38d2f856cde4b9205a3e6bd84760f3de7d82/ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1", size = 1100349, upload-time = "2026-04-30T23:24:18.992Z" }, + { url = "https://files.pythonhosted.org/packages/ab/89/29abcb1fe18a429cda60c6e0bbd1d6e90499339842a2f548d7567542357e/ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0", size = 1072895, upload-time = "2026-04-30T23:24:20.706Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/72abad83966ed6235647c9f956417dc1e17e997696388521910e3d1fa3f4/ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9", size = 1190024, upload-time = "2026-04-30T23:24:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/eb88584b2f0234e581762011208ca203252bf6c98e59b4769daa571f3576/ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc", size = 1178633, upload-time = "2026-04-30T23:24:24.35Z" }, + { url = "https://files.pythonhosted.org/packages/56/51/cf1ec1ff3e616373d0dcbd5fad502e0029dc541f13ab642259762a7d127f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4", size = 1241351, upload-time = "2026-04-30T23:24:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/68fcf50478cf1093f2d423f034ae06453122c8b415d8e21a44668eca485d/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7", size = 1239582, upload-time = "2026-04-30T23:24:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c1/a6c9fa284eceb5fc6f21347e968445a051d7ca2c4d34e6a04314646dbcee/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94", size = 1448853, upload-time = "2026-04-30T23:24:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/8ad3829a09e4e8c5328a53ce7d4711d660944e3e164c5f6abcc2c8f27167/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61", size = 1262204, upload-time = "2026-04-30T23:24:31.482Z" }, + { url = "https://files.pythonhosted.org/packages/25/13/44aa28d97f10e25247e8576b5f6b2795d4fa1a80acc88acc942c508d06f7/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0", size = 1266458, upload-time = "2026-04-30T23:24:33.088Z" }, + { url = "https://files.pythonhosted.org/packages/d8/58/b3a8be3777cd3744324fd5cec0d80d37cd96fc7cbb0fb010e03dff1e870f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f", size = 1308700, upload-time = "2026-04-30T23:24:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/13/03/f8312d6b57f5471a9dc7946f22b8798a1fc296d38c25766223aacadec42c/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a", size = 1416724, upload-time = "2026-04-30T23:24:36.562Z" }, + { url = "https://files.pythonhosted.org/packages/50/5d/13fc3789a7abac00559da2e2e9f386db4612aa1f84fc53d09bf714c37545/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b", size = 1515441, upload-time = "2026-04-30T23:24:38.018Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/7ab43fc7a23b1f970281093228f5f79bed6edeed7a3e672bde6d7a832a58/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9", size = 1510522, upload-time = "2026-04-30T23:24:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/56/ec/d75fc2b788d319f1fad77c14156896f31afdfc68af85b505e5bdebcb9592/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57", size = 1460917, upload-time = "2026-04-30T23:24:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/95/74/f99c81193a2725911e1911ae567ed27c2f2419332c7f3537366f9d238cac/ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2", size = 1067804, upload-time = "2026-04-30T23:24:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/76af00c47daa151e89f98ae21fbbcb2840aaa9f5766579c4da76a3c57188/ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549", size = 1105561, upload-time = "2026-04-30T23:24:44.578Z" }, + { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "codespell" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "deptry" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "packaging" }, + { name = "requirements-parser" }, + { name = "tomli", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/b2/50ccc99362ae7757342978b7ecb3b98e47fade721fd617d74db1948ec3a1/deptry-0.25.1.tar.gz", hash = "sha256:45c8cd982c85cd4faae573ddff6920de7eec735336db6973f26a765ae7950f7d", size = 509748, upload-time = "2026-03-18T23:22:18.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/1d/b538dc635e873b25360d761cfe1fa0ccd7d6c69b698047e552f33401e60d/deptry-0.25.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a4dd1148db24a1ddacfa8b840836c6019c2f864fcb7579dd089fd217606338c8", size = 1850319, upload-time = "2026-03-18T23:22:15.65Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a9/511477a8f0ae4f6021d68a80bdca77e7ffb0722008dc24ee5d9ef49f5c88/deptry-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c67c666d916ef12013c0772e40d78be0f21577a495d8d99ec5fcb18c332d393d", size = 1759259, upload-time = "2026-03-18T23:22:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4b/c9f0bdda410912a6df79a789cb118fa29acae02a397794ead3c84adcda5c/deptry-0.25.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d39279828dbf4efc1abb40bf50a71b21499c36759bed5a8d8a3c0e3149b091", size = 1872012, upload-time = "2026-03-18T23:22:19.145Z" }, + { url = "https://files.pythonhosted.org/packages/72/9c/6f6f9125bac74b5d5d2af89536cbdb3fa159b6466aa097b74e7e85e8e030/deptry-0.25.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14bfcc28b4326ed8c6abb30691b19077d4ef8613cfba6c37ef5b1f471775bf6f", size = 1926575, upload-time = "2026-03-18T23:22:11.269Z" }, + { url = "https://files.pythonhosted.org/packages/52/48/2a5e705a7f898295966ade67bd1223e2af96da433e25b39f6b9483ba2c7b/deptry-0.25.1-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:555f5f9a487899ec9bf301eecba1745e14d212c4b354f4d3a5fd691e907366d3", size = 2050816, upload-time = "2026-03-18T23:22:27.439Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/50f189a894e1f3bf21266299112c8a06cb731838976e1b9a9cadd0b4a86e/deptry-0.25.1-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d21b3545ab2bfec53f3f45c6f5f201d55f713323327f8d12674505469ae6b7", size = 2145416, upload-time = "2026-03-18T23:22:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6a/3f82f7a06217778282bc4456af1b4ffb3bc4b2c8e7891d00e8323f9ad0b8/deptry-0.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:b59a560cb7dffb21832a98bb80d33d614cfb5630ea36ce21833eabf4eae3df99", size = 1718489, upload-time = "2026-03-18T23:22:28.589Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7f/cd6b3ac8cf95f2f1c5c7a74ff6452e9098af89a9b56607381f677880641e/deptry-0.25.1-cp310-abi3-win_arm64.whl", hash = "sha256:6efffd8116fb9d2c45a251382ce4ce1c38dbb17179f581ec9231ed5390f7fc12", size = 1647020, upload-time = "2026-03-18T23:22:23.311Z" }, + { url = "https://files.pythonhosted.org/packages/46/e7/b554568a84197c0a4177b51c9880b55e9861de08d9acfd914a08148a1faa/deptry-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:30d64d4df1c08bc69de56cb0b4ec1f4cd9fa2e42582347d5b1eb25fd0e401745", size = 1846779, upload-time = "2026-03-18T23:22:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/d9/63/38cf5ab4b81fcb1c58909ab0fe1ccc62b36f61c5f7d213a7d0474f620925/deptry-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:87bcd90f99a98bb059c7580bc315c3f87d97fe2db725530030bc974176834735", size = 1758420, upload-time = "2026-03-18T23:22:14.315Z" }, + { url = "https://files.pythonhosted.org/packages/a8/fb/234c333d5dfcc810bb3ca5b3b420355bdd759901c75e41f0441a9871a1cd/deptry-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f31eb5c520651b102568dd91f738222b250a3e44c9e95d4941322109b8d40a", size = 1870345, upload-time = "2026-03-18T23:22:29.734Z" }, + { url = "https://files.pythonhosted.org/packages/59/62/cb63e5210d1ba36cf68cdc0e4fdea73e48f80ac3b7680228816f39ff696a/deptry-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df88952a2bab7517ef23cb304b979199b28449e5d9db2e9ba9bc27a286ac852b", size = 1922759, upload-time = "2026-03-18T23:22:17.121Z" }, + { url = "https://files.pythonhosted.org/packages/a3/8c/e079c44ed98464930e83ca54ea5d40fec522d234e8428e06a1be7f6c7a9a/deptry-0.25.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e6f7b8fa72932e51e86799b10dcd29381b2132dc799c790dca3b28ab08dffb28", size = 2049576, upload-time = "2026-03-18T23:22:12.797Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f2/6a89ad9e5e8e9d37def57a28020d6d7fbcf900b2e5f4dfbbace349cdca91/deptry-0.25.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e3fa3321078e11cd1ac3f10ce3ff0547731c53f9253b87c757a8749c76fe8fa9", size = 2144676, upload-time = "2026-03-18T23:22:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/b3358690a1a47381d995c3d3587798ab2cd086baf4b839e35183599aa2e1/deptry-0.25.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:03c032c32492fde434736954fbcaff09c02bf207b0f793b77e9040300e34b344", size = 1715518, upload-time = "2026-03-18T23:22:20.486Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "grimp" +version = "3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/46/79764cfb61a3ac80dadae5d94fb10acdb7800e31fecf4113cf3d345e4952/grimp-3.14.tar.gz", hash = "sha256:645fbd835983901042dae4e1b24fde3a89bf7ac152f9272dd17a97e55cb4f871", size = 830882, upload-time = "2025-12-10T17:55:01.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a0/de4e6fef8cbe60db6e0be253f68d8e8375df7209eeb73b7286d33a67f701/grimp-3.14-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:17364365c27c111514fd9d17844f275ed074ec9feca0d6cf9bd5bf9218db2412", size = 2184283, upload-time = "2025-12-10T17:53:39.922Z" }, + { url = "https://files.pythonhosted.org/packages/ce/45/4d0926cee327855e719ea5efea28adfcb434fedf2e0c60c054784c635d12/grimp-3.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:25273ea53ac1492e7343bd9d9d9b60445f707bc0d162eca85288c7325579ee47", size = 2118617, upload-time = "2025-12-10T17:53:16.911Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9a/e84ac4acfd0cc87116d79a13ac66f30028d35750a823723c9d14b0c143d1/grimp-3.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53b8f69bdf070fddbbc13f60a5cdb42efb102516770b34f076456ec4ce960627", size = 2283636, upload-time = "2025-12-10T17:52:04.371Z" }, + { url = "https://files.pythonhosted.org/packages/66/db/1451492b350a12371a77ed0f32da4a667623747b2a17b4310360704f999d/grimp-3.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1aa397596bb6d616200be1fd6570e87ddc225c192845c649d4f6015175b77bc6", size = 2234938, upload-time = "2025-12-10T17:52:17.702Z" }, + { url = "https://files.pythonhosted.org/packages/1d/42/999148c107f19ccaca34462e2fb1714bec95443b1c296bda8e6f8e556086/grimp-3.14-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2892ca934fc19c6d51d6c0a609d4db7e97c4721cc9a609f2bab8fe8e1ec1821", size = 2389984, upload-time = "2025-12-10T17:52:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/18/35/e7c8fd66cfea469a04a1f360654fba24c0253c5c7544fbe75b8397dac5ef/grimp-3.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e9367b9fa9c97cb8d1974a164d5981852b498977a097ad7335fc012ab96498b", size = 2569059, upload-time = "2025-12-10T17:52:32.162Z" }, + { url = "https://files.pythonhosted.org/packages/88/31/0d717f976e88c435d2b0adb430040ef6658abcef0a10bea4b845f9ff924d/grimp-3.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87f398915c716c13736460a54f8dc5d70494d7d616039f547c0093f252307109", size = 2357843, upload-time = "2025-12-10T17:52:45.72Z" }, + { url = "https://files.pythonhosted.org/packages/c6/62/c76f56a7d4f9dabf705171510e63947d1fbc97e9b7f89d81cdc2d862519b/grimp-3.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5551a825b14e52642428ef7c4a5790819bfaee0fdae94f89ce248cff3d7109bb", size = 2308577, upload-time = "2025-12-10T17:53:08.329Z" }, + { url = "https://files.pythonhosted.org/packages/fc/09/abb8eb3611dcb778c34172be8c91416e1fef3a1df8bdb0b11d8e66cf54cd/grimp-3.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6ee7a2fab52ce0c6ae81fa1f2319bad5bd361110994567477f26be018043d63d", size = 2465683, upload-time = "2025-12-10T17:53:47.815Z" }, + { url = "https://files.pythonhosted.org/packages/97/b8/64d44c6f269421b1fe38df324f082b86cc3ae1c3790340ee89ec4ea3f9c2/grimp-3.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6d1434172a02cd97425126260dec80a8fd0491d9467b822d871498199c296c91", size = 2500554, upload-time = "2025-12-10T17:54:01.969Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3c/be6c50b58f4e97ce3c90ba803769ebf7cc08d8f5d760f64df7b8f622a2a2/grimp-3.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9a85bf0a8c4b58db12184fe53a469a7189b4c63397a2eaca0d9efe410f6f68e7", size = 2502900, upload-time = "2025-12-10T17:54:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/cd/31/90daee1de255465216011b1713d6ae22d08c8a1c3463d8f2d76c59f76f0a/grimp-3.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:53d9ed23fb7da4c886affeb6b8bce7c19d8b09f2e1631a482c9446a20d504bdf", size = 2515218, upload-time = "2025-12-10T17:54:46.001Z" }, + { url = "https://files.pythonhosted.org/packages/f8/26/4c20dbbafaf1108e8640985d6a18da684828319c775e10674f15d21620fa/grimp-3.14-cp310-cp310-win32.whl", hash = "sha256:d05110b9afda361ff8d90740a8344ccfd2d59a5a1977d517b9bce178738ed34f", size = 1874010, upload-time = "2025-12-10T17:55:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/91/00/bcfa3bcca8de826c594404d66726150a861aacab8e22715b6f7b8cfe8292/grimp-3.14-cp310-cp310-win_amd64.whl", hash = "sha256:fad2a819756b5c0441b8841c2e6f541960b13edd09b672e6e199232dcf9bcb7a", size = 2014136, upload-time = "2025-12-10T17:55:02.7Z" }, + { url = "https://files.pythonhosted.org/packages/25/31/d4a86207c38954b6c3d859a1fc740a80b04bbe6e3b8a39f4e66f9633dfa4/grimp-3.14-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f1c91e3fa48c2196bf62e3c71492140d227b2bfcd6d15e735cbc0b3e2d5308e0", size = 2185572, upload-time = "2025-12-10T17:53:41.287Z" }, + { url = "https://files.pythonhosted.org/packages/f5/61/ed4cba5bd75d37fe46e17a602f616619a9e4f74ad8adfcf560ce4b2a1697/grimp-3.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6291c8f1690a9fe21b70923c60b075f4a89676541999e3d33084cbc69ac06a1", size = 2118002, upload-time = "2025-12-10T17:53:18.546Z" }, + { url = "https://files.pythonhosted.org/packages/77/6a/688f6144d0b207d7845bd8ab403820a83630ce3c9420cbbc7c9e9282f9c0/grimp-3.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ec312383935c2d09e4085c8435780ada2e13ebef14e105609c2988a02a5b2ce", size = 2283939, upload-time = "2025-12-10T17:52:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/a5/98/4c540de151bf3fd58d6d7b3fe2269b6a6af6c61c915de1bc991802bfaff8/grimp-3.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f43cbf640e73ee703ad91639591046828d20103a1c363a02516e77a66a4ac07", size = 2233693, upload-time = "2025-12-10T17:52:18.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7b/84b4b52b6c6dd5bf083cb1a72945748f56ea2e61768bbebf87e8d9d0ef75/grimp-3.14-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a93c9fddccb9ff16f5c6b5fca44227f5f86cba7cffc145d2176119603d2d7c7", size = 2389745, upload-time = "2025-12-10T17:53:00.659Z" }, + { url = "https://files.pythonhosted.org/packages/a7/33/31b96907c7dd78953df5e1ce67c558bd6057220fa1203d28d52566315a2e/grimp-3.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5653a2769fdc062cb7598d12200352069c9c6559b6643af6ada3639edb98fcc3", size = 2569055, upload-time = "2025-12-10T17:52:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/b2/24/ce1a8110f3d5b178153b903aafe54b6a9216588b5bff3656e30af43e9c29/grimp-3.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:071c7ddf5e5bb7b2fdf79aefdf6e1c237cd81c095d6d0a19620e777e85bf103c", size = 2358044, upload-time = "2025-12-10T17:52:47.545Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/16d98c02287bc99884843478b9a68b04a2ef13b5cb8b9f36a9ca7daea75b/grimp-3.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e01b7a4419f535b667dfdcb556d3815b52981474f791fb40d72607228389a31", size = 2310304, upload-time = "2025-12-10T17:53:09.679Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/0fde9781b0f6b4f9227d485685f48f6bcc70b95af22e2f85ff7f416cbfc1/grimp-3.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c29682f336151d1d018d0c3aa9eeaa35734b970e4593fa396b901edca7ef5c79", size = 2463682, upload-time = "2025-12-10T17:53:49.185Z" }, + { url = "https://files.pythonhosted.org/packages/51/cb/2baff301c2c2cc2792b6e225ea0784793ca587c81b97572be0bad122cfc8/grimp-3.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a5c4fd71f363ea39e8aab0630010ced77a8de9789f27c0acdd0d7e6269d4a8ef", size = 2500573, upload-time = "2025-12-10T17:54:03.899Z" }, + { url = "https://files.pythonhosted.org/packages/96/69/797e4242f42d6665da5fe22cb250cae3f14ece4cb22ad153e9cd97158179/grimp-3.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766911e3ba0b13d833fdd03ad1f217523a8a2b2527b5507335f71dca1153183d", size = 2503005, upload-time = "2025-12-10T17:54:32.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/45/da1a27a6377807ca427cd56534231f0920e1895e16630204f382a0df14c5/grimp-3.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:154e84a2053e9f858ae48743de23a5ad4eb994007518c29371276f59b8419036", size = 2515776, upload-time = "2025-12-10T17:54:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/b918a29ce98029cd7a9e33a584be43a93288d5283fb7ccef5b6b2ba39ede/grimp-3.14-cp311-cp311-win32.whl", hash = "sha256:3189c86c3e73016a1907ee3ba9f7a6ca037e3601ad09e60ce9bf12b88877f812", size = 1873189, upload-time = "2025-12-10T17:55:11.872Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/2327c203f83a25766fbd62b0df3b24230d422b6e53518ff4d1c5e69793f1/grimp-3.14-cp311-cp311-win_amd64.whl", hash = "sha256:201f46a6a4e5ee9dfba4a2f7d043f7deab080d1d84233f4a1aee812678c25307", size = 2014277, upload-time = "2025-12-10T17:55:04.144Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/a35ff62f35aa5fd148053506eddd7a8f2f6afaed31870dc608dd0eb38e4f/grimp-3.14-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ffabc6940301214753bad89ec0bfe275892fa1f64b999e9a101f6cebfc777133", size = 2178573, upload-time = "2025-12-10T17:53:42.836Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/bd2e80273da4d46110969fc62252e5372e0249feb872bc7fe76fdc7f1818/grimp-3.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:075d9a1c78d607792d0ed8d4d3d7754a621ef04c8a95eaebf634930dc9232bb2", size = 2110452, upload-time = "2025-12-10T17:53:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/c3/7307249c657d34dca9d250d73ba027d6cfe15a98fb3119b6e5210bc388b7/grimp-3.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ff52addeb20955a4d6aa097bee910573ffc9ef0d3c8a860844f267ad958156", size = 2283064, upload-time = "2025-12-10T17:52:07.673Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d2/cae4cf32dc8d4188837cc4ab183300d655f898969b0f169e240f3b7c25be/grimp-3.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d10e0663e961fcbe8d0f54608854af31f911f164c96a44112d5173050132701f", size = 2235893, upload-time = "2025-12-10T17:52:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/3f58bc3064fc305dac107d08003ba65713a5bc89a6d327f1c06b30cce752/grimp-3.14-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ab874d7ddddc7a1291259cf7c31a4e7b5c612e9da2e24c67c0eb1a44a624e67", size = 2393376, upload-time = "2025-12-10T17:53:02.397Z" }, + { url = "https://files.pythonhosted.org/packages/06/b8/f476f30edf114f04cb58e8ae162cb4daf52bda0ab01919f3b5b7edb98430/grimp-3.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54fec672ec83355636a852177f5a470c964bede0f6730f9ba3c7b5c8419c9eab", size = 2571342, upload-time = "2025-12-10T17:52:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/2e44d3c4f591f95f86322a8f4dbb5aac17001d49e079f3a80e07e7caaf09/grimp-3.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9e221b5e8070a916c780e88c877fee2a61c95a76a76a2a076396e459511b0bb", size = 2359022, upload-time = "2025-12-10T17:52:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/42b4d6bc0ea119ce2e91e1788feabf32c5433e9617dbb495c2a3d0dc7f12/grimp-3.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea6b495f9b4a8d82f5ce544921e76d0d12017f5d1ac3a3bd2f5ac88ab055b1c", size = 2309424, upload-time = "2025-12-10T17:53:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/6a731989625c1790f4da7602dcbf9d6525512264e853cda77b3b3602d5e0/grimp-3.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:655e8d3f79cd99bb859e09c9dd633515150e9d850879ca71417d5ac31809b745", size = 2462754, upload-time = "2025-12-10T17:53:50.886Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/3d1571c0a39a59dd68be4835f766da64fe64cbab0d69426210b716a8bdf0/grimp-3.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a14f10b1b71c6c37647a76e6a49c226509648107abc0f48c1e3ecd158ba05531", size = 2501356, upload-time = "2025-12-10T17:54:06.014Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/8950b8229095ebda5c54c8784e4d1f0a6e19423f2847289ef9751f878798/grimp-3.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:81685111ee24d3e25f8ed9e77ed00b92b58b2414e1a1c2937236026900972744", size = 2504631, upload-time = "2025-12-10T17:54:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/23bed3da9206138d36d01890b656c7fb7adfb3a37daac8842d84d8777ade/grimp-3.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce8352a8ea0e27b143136ea086582fc6653419aa8a7c15e28ed08c898c42b185", size = 2514751, upload-time = "2025-12-10T17:54:49.384Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/6f1f55c97ee982f133ec5ccb22fc99bf5335aee70c208f4fb86cd833b8d5/grimp-3.14-cp312-cp312-win32.whl", hash = "sha256:3fc0f98b3c60d88e9ffa08faff3200f36604930972f8b29155f323b76ea25a06", size = 1875041, upload-time = "2025-12-10T17:55:13.326Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/03ba01288e2a41a948bc8526f32c2eeaddd683ed34be1b895e31658d5a4c/grimp-3.14-cp312-cp312-win_amd64.whl", hash = "sha256:6bca77d1d50c8dc402c96af21f4e28e2f1e9938eeabd7417592a22bd83cde3c3", size = 2013868, upload-time = "2025-12-10T17:55:05.907Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bd/d12a9c821b79ba31fc52243e564712b64140fc6d011c2bdbb483d9092a12/grimp-3.14-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af8a625554beea84530b98cc471902155b5fc042b42dc47ec846fa3e32b0c615", size = 2178632, upload-time = "2025-12-10T17:53:44.55Z" }, + { url = "https://files.pythonhosted.org/packages/96/8c/d6620dbc245149d5a5a7a9342733556ba91a672f358259c0ab31d889b56b/grimp-3.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0dd1942ffb419ad342f76b0c3d3d2d7f312b264ddc578179d13ce8d5acec1167", size = 2110288, upload-time = "2025-12-10T17:53:21.662Z" }, + { url = "https://files.pythonhosted.org/packages/60/9d/ea51edc4eb295c99786040051c66466bfa235fd1def9f592057b36e03d0f/grimp-3.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:537f784ce9b4acf8657f0b9714ab69a6c72ffa752eccc38a5a85506103b1a194", size = 2282197, upload-time = "2025-12-10T17:52:09.304Z" }, + { url = "https://files.pythonhosted.org/packages/28/6e/7db27818ced6a797f976ca55d981a3af5c12aec6aeda12d63965847cd028/grimp-3.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78ab18c08770aa005bef67b873bc3946d33f65727e9f3e508155093db5fa57d6", size = 2235720, upload-time = "2025-12-10T17:52:21.806Z" }, + { url = "https://files.pythonhosted.org/packages/37/26/0e3bbae4826bd6eaabf404738400414071e73ddb1e65bf487dcce17858c4/grimp-3.14-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28ca58728c27e7292c99f964e6ece9295c2f9cfdefc37c18dea0679c783ffb6f", size = 2393023, upload-time = "2025-12-10T17:53:04.149Z" }, + { url = "https://files.pythonhosted.org/packages/49/f2/7da91db5703da34c7ef4c7cddcbb1a8fc30cd85fe54756eba942c6fb27d8/grimp-3.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b5577de29c6c5ae6e08d4ca0ac361b45dba323aa145796e6b320a6ea35414b7", size = 2571108, upload-time = "2025-12-10T17:52:36.523Z" }, + { url = "https://files.pythonhosted.org/packages/25/5e/4d6278f18032c7208696edf8be24a4b5f7fad80acc20ffca737344bcecb5/grimp-3.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d7d1f9f42306f455abcec34db877e4887ff15f2777a43491f7ccbd6936c449b", size = 2358531, upload-time = "2025-12-10T17:52:50.521Z" }, + { url = "https://files.pythonhosted.org/packages/24/fb/231c32493161ac82f27af6a56965daefa0ec6030fdaf5b948ddd5d68d000/grimp-3.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39bd5c9b7cef59ee30a05535e9cb4cbf45a3c503f22edce34d0aa79362a311a9", size = 2308831, upload-time = "2025-12-10T17:53:12.587Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/f6db325bf5efbbebc9c85cad0af865e821a12a0ba58ee309e938cbd5fedf/grimp-3.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fec3116b4f780a1bc54176b19e6b9f2e36e2ef3164b8fc840660566af35df88", size = 2462138, upload-time = "2025-12-10T17:53:52.403Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/cc3fe29cf07f70364018086840c228a190539ab8105147e34588db590792/grimp-3.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0233a35a5bbb23688d63e1736b54415fa9994ace8dfeb7de8514ed9dee212968", size = 2501393, upload-time = "2025-12-10T17:54:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/e5/eb/54cada9a726455148da23f64577b5cd164164d23a6449e3fa14551157356/grimp-3.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e46b2fef0f1da7e7e2f8129eb93c7e79db716ff7810140a22ce5504e10ed86df", size = 2504514, upload-time = "2025-12-10T17:54:36.34Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/e6afe4f0652df07e8762f61899d1202b73c22c559c804d0a09e5aab2ff17/grimp-3.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e6d9b50623ee1c3d2a1927ec3f5d408995ea1f92f3e91ed996c908bb40e856f", size = 2514018, upload-time = "2025-12-10T17:54:50.76Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/2b8550acc1f010301f02c4fe9664810929fd9277cd032ab608b8534a96fb/grimp-3.14-cp313-cp313-win32.whl", hash = "sha256:fd57c56f5833c99320ec77e8ba5508d56f6fb48ec8032a942f7931cc6ebb80ce", size = 1874922, upload-time = "2025-12-10T17:55:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/46/c7/bc9db5a54ef22972cd17d15ad80a8fee274a471bd3f02300405702d29ea5/grimp-3.14-cp313-cp313-win_amd64.whl", hash = "sha256:173307cf881a126fe5120b7bbec7d54384002e3c83dcd8c4df6ce7f0fee07c53", size = 2013705, upload-time = "2025-12-10T17:55:07.488Z" }, + { url = "https://files.pythonhosted.org/packages/80/7e/02710bf5e50997168c84ac622b10dd41d35515efd0c67549945ad20996a0/grimp-3.14-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebe29f8f13fbd7c314908ed535183a36e6db71839355b04869b27f23c58fa082", size = 2281868, upload-time = "2025-12-10T17:52:10.589Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/2e440c6762cc78bd50582e1b092357d2255f0852ccc6218d8db25170ab31/grimp-3.14-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073d285b00100153fd86064c7726bb1b6d610df1356d33bb42d3fd8809cb6e72", size = 2230917, upload-time = "2025-12-10T17:52:23.212Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bb/2e7dce129b88f07fc525fe5c97f28cfb7ed7b62c59386d39226b4d08969c/grimp-3.14-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6d6efc37e1728bbfcd881b89467be5f7b046292597b3ebe5f8e44e89ea8b6cb", size = 2571371, upload-time = "2025-12-10T17:52:37.84Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2b/8f1be8294af60c953687db7dec25525d87ed9c2aa26b66dcbe5244abaca2/grimp-3.14-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5337d65d81960b712574c41e85b480d4480bbb5c6f547c94e634f6c60d730889", size = 2356980, upload-time = "2025-12-10T17:52:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/ead91e04b3ddd4774ae74601860ea0f0f21bcf6b970b6769ba9571eb2904/grimp-3.14-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:84a7fea63e352b325daa89b0b7297db411b7f0036f8d710c32f8e5090e1fc3ca", size = 2461540, upload-time = "2025-12-10T17:53:53.749Z" }, + { url = "https://files.pythonhosted.org/packages/94/aa/f8a085ff73c37d6e6a37de9f58799a3fea9e16badf267aaef6f11c9a53a3/grimp-3.14-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d0b19a3726377165fe1f7184a8af317734d80d32b371b6c5578747867ab53c0b", size = 2497925, upload-time = "2025-12-10T17:54:23.842Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a3/db3c2d6df07fe74faf5a28fcf3b44fad2831d323ba4a3c2ff66b77a6520c/grimp-3.14-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9caa4991f530750f88474a3f5ecf6ef9f0d064034889d92db00cfb4ecb78aa24", size = 2501794, upload-time = "2025-12-10T17:54:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/095f4e3765e7b60425a41e9fbd2b167f8b0acb957cc88c387f631778a09d/grimp-3.14-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1876efc119b99332a5cc2b08a6bdaada2f0ad94b596f0372a497e2aa8bda4d94", size = 2515203, upload-time = "2025-12-10T17:54:52.555Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5f/ee02a3a1237282d324f596a50923bf9d2cb1b1230ef2fef49fb4d3563c2c/grimp-3.14-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3ccf03e65864d6bc7bf1c003c319f5330a7627b3677f31143f11691a088464c2", size = 2177150, upload-time = "2025-12-10T17:53:46.145Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/2a92889e5fc78e8ef5c548e6a5c6fed78b817eeb0253aca586c28108393a/grimp-3.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9ecd58fa58a270e7523f8bec9e6452f4fdb9c21e4cd370640829f1e43fa87a69", size = 2109280, upload-time = "2025-12-10T17:53:23.345Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/5d0b9ab54821e7fbdeb02f3919fa2cb8b9f0c3869fa6e4b969a5766f0ffa/grimp-3.14-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d75d1f8f7944978b39b08d870315174f1ffcd5123be6ccff8ce90467ace648a", size = 2283367, upload-time = "2025-12-10T17:52:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/c2/96/a77c40c92faf7500f42ac019ab8de108b04ffe3db8ec8d6f90416d2322ce/grimp-3.14-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f70bbb1dd6055d08d29e39a78a11c4118c1778b39d17cd8271e18e213524ca7", size = 2237125, upload-time = "2025-12-10T17:52:24.606Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/3e1483721c83057bff921cf454dd5ff3e661ae1d2e63150a380382d116c2/grimp-3.14-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f21b7c003626c902669dc26ede83a91220cf0a81b51b27128370998c2f247b4", size = 2391735, upload-time = "2025-12-10T17:53:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/25fad4a174fe672d42f3e5616761a8120a3b03c8e9e2ae3f31159561968a/grimp-3.14-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80d9f056415c936b45561310296374c4319b5df0003da802c84d2830a103792a", size = 2571388, upload-time = "2025-12-10T17:52:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/456df7f6a765ce3f160eb32a0f64ed0c1c3cd39b518555dde02087f9b6e4/grimp-3.14-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0332963cd63a45863775d4237e59dedf95455e0a1ea50c356be23100c5fc1d7c", size = 2359637, upload-time = "2025-12-10T17:52:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/3e5005ef21a4e2243f0da489aba86aaaff0bc11d5240d67113482cba88e0/grimp-3.14-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4144350d074f2058fe7c89230a26b34296b161f085b0471a692cb2fe27036f", size = 2308335, upload-time = "2025-12-10T17:53:13.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/03/4e055f756946d6f71ab7e9d1f8536a9e476777093dd7a050f40412d1a2b1/grimp-3.14-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e148e67975e92f90a8435b1b4c02180b9a3f3d725b7a188ba63793f1b1e445a0", size = 2463680, upload-time = "2025-12-10T17:53:55.507Z" }, + { url = "https://files.pythonhosted.org/packages/26/b9/3c76b7c2e1587e4303a6eff6587c2117c3a7efe1b100cd13d8a4a5613572/grimp-3.14-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1093f7770cb5f3ca6f99fb152f9c949381cc0b078dfdfe598c8ab99abaccda3b", size = 2502808, upload-time = "2025-12-10T17:54:25.383Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/ada10b85ad3125ebedea10256d9c568b6bf28339d2f79d2d196a7b94f633/grimp-3.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a213f45ec69e9c2b28ffd3ba5ab12cc9859da17083ba4dc39317f2083b618111", size = 2504013, upload-time = "2025-12-10T17:54:39.762Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/7c369f749d50b0ceac23cd6874ca4695cc1359a96091c7010301e5c8b619/grimp-3.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f003ac3f226d2437a49af0b6036f26edba57f8a32d329275dbde1b2b2a00a56", size = 2515043, upload-time = "2025-12-10T17:54:54.437Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/85135fe83826ce11ae56a340d32a1391b91eed94d25ce7bc318019f735de/grimp-3.14-cp314-cp314-win32.whl", hash = "sha256:eec81be65a18f4b2af014b1e97296cc9ee20d1115529bf70dd7e06f457eac30b", size = 1877509, upload-time = "2025-12-10T17:55:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/db/61/e4a2234edecb3bb3cff8963bc4ec5cc482a9e3c54f8df0946d7d90003830/grimp-3.14-cp314-cp314-win_amd64.whl", hash = "sha256:cd3bab6164f1d5e313678f0ab4bf45955afe7f5bdb0f2f481014aa9cca7e81ba", size = 2014364, upload-time = "2025-12-10T17:55:08.896Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/3d304443fbf1df4d60c09668846d0c8a605c6c95646226e41d8f5c3254da/grimp-3.14-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1df33de479be4d620f69633d1876858a8e64a79c07907d47cf3aaf896af057", size = 2281385, upload-time = "2025-12-10T17:52:13.668Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/493e2648dbb83b3fc517ee675e464beb0154551d726053c7982a3138c6a8/grimp-3.14-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07096d4402e9d5a2c59c402ea3d601f4b7f99025f5e32f077468846fc8d3821b", size = 2231470, upload-time = "2025-12-10T17:52:26.104Z" }, + { url = "https://files.pythonhosted.org/packages/80/84/e772b302385a6b7ec752c88f84ffe35c33d14076245ae27a635aed9c63a2/grimp-3.14-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:712bc28f46b354316af50c469c77953ba3d6cb4166a62b8fb086436a8b05d301", size = 2571579, upload-time = "2025-12-10T17:52:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/69/92/5b23aa7b89c5f4f2cfa636cbeaf33e784378a6b0a823d77a3448670dfacc/grimp-3.14-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abe2bbef1cf8e27df636c02f60184319f138dee4f3a949405c21a4b491980397", size = 2356545, upload-time = "2025-12-10T17:52:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/bcf2116f4b1c3939ab35f9cdddd9ca59e953e57e9a0ac0c143deaf9f29cc/grimp-3.14-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2f9ae3fabb7a7a8468ddc96acc84ecabd84f168e7ca508ee94d8f32ea9bd5de2", size = 2461022, upload-time = "2025-12-10T17:53:56.923Z" }, + { url = "https://files.pythonhosted.org/packages/81/ce/1a076dce6bc22bca4b9ad5d1bbcd7e1023dcf7bf20ea9404c6462d78f049/grimp-3.14-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:efaf11ea73f7f12d847c54a5d6edcbe919e0369dce2d1aabae6c50792e16f816", size = 2498256, upload-time = "2025-12-10T17:54:27.214Z" }, + { url = "https://files.pythonhosted.org/packages/45/ea/ac735bed202c1c5c019e611b92d3861779e0cfbe2d20fdb0dec94266d248/grimp-3.14-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e089c9ab8aa755ff5af88c55891727783b4eb6b228e7bdf278e17209d954aa1e", size = 2502056, upload-time = "2025-12-10T17:54:41.537Z" }, + { url = "https://files.pythonhosted.org/packages/80/8f/774ce522de6a7e70fbeceeaeb6fbe502f5dfb8365728fb3bb4cb23463da8/grimp-3.14-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ad14d5deb56721ac24ab939747f72ab3d378d42e7d1f038317d33b052b77", size = 2515157, upload-time = "2025-12-10T17:54:55.874Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5d/c3f19b63b79a2dea14f0f8e6ba0c48df44518269e74c7f1d81ba996a4244/grimp-3.14-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1d4f96c0159b33647295ad36683fe7be55fa620de6e54e970c913cb88d0a5a6", size = 2285654, upload-time = "2025-12-10T17:52:15.021Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6d/eff77f352b7e4dc42a4db8a6246229d91eb0bd656b05e414f7ae6a768832/grimp-3.14-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e715f78fda0019b493459f97efc48462912b4c5b5d261215d94c05115511d311", size = 2236470, upload-time = "2025-12-10T17:52:29.369Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/7a4db594ed4d102027457284ded598699e7fed6127915dabb1d541fe4972/grimp-3.14-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d0a885b04edbe908cd6f2f8cb0999dd2a348091d241bd9842f9ea593fabdce5", size = 2571826, upload-time = "2025-12-10T17:52:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/65/c3/42909708a3c85f6c708baa88ac2e65895fac7aeb231d8d68c47ecca19a39/grimp-3.14-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6995b20574313ba66b73d288f431af24b9d23d60c861e8f5cbf0d0e26ad9c49", size = 2359312, upload-time = "2025-12-10T17:52:56.53Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/74e8f72667e2b06c50636af945fb7782e326a6cae63d0fc3a26238da1e3d/grimp-3.14-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d2a170deb9f4790221dcde8c47e60be7fcd52999062241ac944ce556efa1d24d", size = 2467674, upload-time = "2025-12-10T17:53:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/a7877641a1abff217c9ae31528ebde6117873c6a64ab4868de5bf98b79a0/grimp-3.14-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1d4a28e2545a83c853a6357ccf4a5105e3f74419a75312b5ebaf0435085cd938", size = 2502271, upload-time = "2025-12-10T17:54:28.708Z" }, + { url = "https://files.pythonhosted.org/packages/13/88/1893d76889db0b4829d536710a9ee272117df7c45450a61750c5641fadeb/grimp-3.14-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:9aa74d848c083725add12e0e6d42a01ddfd8ee84e9504ad7254204985e3c5c92", size = 2504368, upload-time = "2025-12-10T17:54:42.995Z" }, + { url = "https://files.pythonhosted.org/packages/50/b7/400c93f07cc651ff8ee7cd40faf42b5ac5447d88dd42dac440e53ba020d1/grimp-3.14-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:acf0acedaf105c8d3747abf073c6a2dd1379bafcb5807926fd6d5fe4b0980698", size = 2516841, upload-time = "2025-12-10T17:54:57.322Z" }, + { url = "https://files.pythonhosted.org/packages/65/cc/dbc00210d0324b8fc1242d8e857757c7e0b62ff0fc0c1bc8dcc42342da85/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c8a8aab9b4310a7e69d7d845cac21cf14563aa0520ea322b948eadeae56d303", size = 2284804, upload-time = "2025-12-10T17:52:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/80/89/851d3d345342e9bcec3fe85d3997db29501fa59f958c1566bf3e24d9d7d9/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d781943b27e5875a41c8f9cfc80f8f0a349f864379192b8c3faa0e6a22593313", size = 2235176, upload-time = "2025-12-10T17:52:30.795Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/5f94702a8d5c121cafcdc9664de34c34f19d0d91a1127bf3946a2631f7a3/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9630d4633607aff94d0ac84b9c64fef1382cdb05b00d9acbde47f8745e264871", size = 2391258, upload-time = "2025-12-10T17:53:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a2/df8c79de5c9e227856d048cc1551c4742a5f97660c40304ac278bd48607f/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb00e1bcca583668554a8e9e1e4229a1d11b0620969310aae40148829ff6a32", size = 2571443, upload-time = "2025-12-10T17:52:43.853Z" }, + { url = "https://files.pythonhosted.org/packages/f0/21/747b7ed9572bbdc34a76dfec12ce510e80164b1aa06d3b21b34994e5f567/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3389da4ceaaa7f7de24a668c0afc307a9f95997bd90f81ec359a828a9bd1d270", size = 2357767, upload-time = "2025-12-10T17:52:57.84Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e6/485c5e3b64933e71f72f0cc45b0d7130418a6a5a13cedc2e8411bd76f290/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd7a32970ef97e42d4e7369397c7795287d84a736d788ccb90b6c14f0561d975", size = 2309069, upload-time = "2025-12-10T17:53:15.203Z" }, + { url = "https://files.pythonhosted.org/packages/31/bd/12024a8cba1c77facc1422a7b48cd0d04c252fc9178fd6f99dc05a8af57b/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:fd1278623fa09f62abc0fd8a6500f31b421a1fd479980f44c2926020a0becf02", size = 2466429, upload-time = "2025-12-10T17:54:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7f/0e5977887e1c8f00f84bb4125217534806ffdcef9cf52f3580aa3b151f4b/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:9cfa52c89333d3d8fe9dc782529e888270d060231c3783e036d424044671dde0", size = 2501190, upload-time = "2025-12-10T17:54:30.107Z" }, + { url = "https://files.pythonhosted.org/packages/42/6b/06acb94b6d0d8c7277bb3e33f93224aa3be5b04643f853479d3bf7b23ace/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:48a5be4a12fca6587e6885b4fc13b9e242ab8bf874519292f0f13814aecf52cc", size = 2503440, upload-time = "2025-12-10T17:54:44.444Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4d/2e531370d12e7a564f67f680234710bbc08554238a54991cd244feb61fb6/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3fcc332466783a12a42cd317fd344c30fe734ba4fa2362efff132dc3f8d36da7", size = 2516525, upload-time = "2025-12-10T17:54:58.987Z" }, +] + +[[package]] +name = "import-linter" +version = "2.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "grimp" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/55b697a17bb15c6cb88d97d73716813f5427281527b90f02cc0a600abc6e/import_linter-2.11.tar.gz", hash = "sha256:5abc3394797a54f9bae315e7242dc98715ba485f840ac38c6d3192c370d0085e", size = 1153682, upload-time = "2026-03-06T12:11:38.198Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/aa/2ed2c89543632ded7196e0d93dcc6c7fe87769e88391a648c4a298ea864a/import_linter-2.11-py3-none-any.whl", hash = "sha256:3dc54cae933bae3430358c30989762b721c77aa99d424f56a08265be0eeaa465", size = 637315, upload-time = "2026-03-06T12:11:36.599Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" }, + { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "prek" +version = "0.3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/59/0a279983f96bd5d538b4975f0a23121082aa3b8560b6649fdf61f8011b07/prek-0.3.13.tar.gz", hash = "sha256:c48586ee3708bfbf3df80121f55583e9a7d0fa166b08172c091fe5971e92a0ac", size = 444848, upload-time = "2026-05-05T18:07:09.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/6a/9baa2bda21dccc2927e952416f6cc23a75eb99c9ed18837164ac2e4a5640/prek-0.3.13-py3-none-linux_armv6l.whl", hash = "sha256:b00d38f01235073c35aa5f48df57fefef45a6cec2ae0884d750345a2c7220370", size = 5506622, upload-time = "2026-05-05T18:06:53.091Z" }, + { url = "https://files.pythonhosted.org/packages/56/77/d44b5d9bdca0879b865f8e47bf84cf5dc9e8b358d029e6d9b83d8809c116/prek-0.3.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0d89ac712c60e34d1550a606ad5fdfb8ad71d44ced8afa2fa5cbc106be4abd9e", size = 5878743, upload-time = "2026-05-05T18:07:23.164Z" }, + { url = "https://files.pythonhosted.org/packages/08/cf/19e8525cde8b3aa12858aca434d1fa653ef3b152da5af11eafc857634dc2/prek-0.3.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f9b5265863d18b5be4ea094fdce4fd6ca61a8c89a70ee3d8ee153b3e0ed6b272", size = 5434909, upload-time = "2026-05-05T18:07:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9a/e5f97194782de4dab622ce09dafb3ebdd2ee4d354a83ac4def7ebeee236c/prek-0.3.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:64b59a1550780af2bba37297c704b17f81d8e9df6288af1fab4017938e33b1db", size = 5697536, upload-time = "2026-05-05T18:07:05.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8c/e1f548ffc4b227e4c2b5a9b30f5978a7e0e6dad51305b97a2ba5b2a923e7/prek-0.3.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ce6cd8f114ba9bbdbe97422103fd886101949b1c42e588a7543c4436ead2020", size = 5428160, upload-time = "2026-05-05T18:07:01.489Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/abd919b00905a32d21dca2cec32c707860cf217da2431b62dd52684b310e/prek-0.3.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc03e924a24d8d961f56195853c8b206cb196be6db4ad8312125dae847d718ac", size = 5827275, upload-time = "2026-05-05T18:07:17.437Z" }, + { url = "https://files.pythonhosted.org/packages/af/ed/cafd2b80d58a83faf8371c6543bd1475a2224242a3294da7f8582f6aa551/prek-0.3.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ca8c526a23873177fb3b92013500b08ef5f8bedc7263f9f3a44dd2f49645a26", size = 6710293, upload-time = "2026-05-05T18:07:10.663Z" }, + { url = "https://files.pythonhosted.org/packages/35/09/52a4a27596b764173a34d74db09356b30faaacb4a1075b75adbc036a0008/prek-0.3.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5bbb175478438a871e3281d2c3c3f067288af73ad81707a9bdebfd769766c7d", size = 6096556, upload-time = "2026-05-05T18:07:19.46Z" }, + { url = "https://files.pythonhosted.org/packages/63/60/80f61729ce6498815d46d5580cf76da2c157c9b6494046183682441a0ea3/prek-0.3.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a65327a014d838341af757dfc05a706d10e8e33f039bc32bb3dbe2fa21c440c0", size = 5693267, upload-time = "2026-05-05T18:07:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/36/9d/c7a663fe70676ffab2e0c6c9a71997a3ccd002ed5bc60b7422a937911af0/prek-0.3.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b6a200843a36a5b0c41764ce7639ccb3471d48b097f1c5e3fc8f034219b42626", size = 5532865, upload-time = "2026-05-05T18:07:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/32/68/506ef5a235536030e16f61e7210474554f6e05f845f27df5877d2dbb1a06/prek-0.3.13-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bdacaad8f35f343e063d251211fe34db1de9e5cc591795361ad69a6485202258", size = 5395951, upload-time = "2026-05-05T18:06:55.183Z" }, + { url = "https://files.pythonhosted.org/packages/3e/00/22d7c6db7f43b58f7d015913c12660c9bbc82751cff6cfd8c31993cf30eb/prek-0.3.13-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f00328f1c520d8fefb910ab0d3c6764ee330d227952baa19b7e3de7242bd8b3b", size = 5681195, upload-time = "2026-05-05T18:07:12.804Z" }, + { url = "https://files.pythonhosted.org/packages/10/e3/fdf9882238796914ddaf11381a9083b374980156200a953324f6c795f34d/prek-0.3.13-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e5530a867bcf5b172b7513a64e71b06a337d1d184696227ae953845867376b8d", size = 6212085, upload-time = "2026-05-05T18:07:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/528759931344b5c7103085798f5fa2e86d27d9410b753a6bcbe7726aa8ba/prek-0.3.13-py3-none-win32.whl", hash = "sha256:326fac2bdce00074ce6c5046b861d310638aee2b9de1ed241ba7eb32bdc83898", size = 5199566, upload-time = "2026-05-05T18:07:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d0/8715ee837c73314a02767d20652cc312d1b6ff6733fa00f52de2b648bc3a/prek-0.3.13-py3-none-win_amd64.whl", hash = "sha256:841049f89f5ec9f4035299283d11e566ac5a068e3742ead1055ea04f886831fc", size = 5589599, upload-time = "2026-05-05T18:06:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/ff/cf/0af0b15be0ebd82f7e50adee149b05a73533d78cb1b97cb889f0647ebffe/prek-0.3.13-py3-none-win_arm64.whl", hash = "sha256:a9fd74e0aec550c6b8d41076fdcdd6ff121cd7d94d743c1338bd794784e3c775", size = 5419029, upload-time = "2026-05-05T18:06:59.645Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyproject-fmt" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toml-fmt-common" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/30/c2e3505604c097a591bddd02fafe674d3d7049c9a1a8fdbf109e2bd583ef/pyproject_fmt-2.21.2.tar.gz", hash = "sha256:7fcfdab7eb7da356998ff5ecdff8ebf85c1080523e28c085a4643950d962107f", size = 155067, upload-time = "2026-05-05T00:55:17.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/a6/a237b1ca70d06a48e75dcaca385cdeae82de73cecade3c8274566837c3dc/pyproject_fmt-2.21.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:28e222e4267d338b737bdd454de5d6ac2291a93c839665ba880174860a94dedd", size = 5080595, upload-time = "2026-05-05T00:54:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/be/ab/84192abff5a4e8545f7087f9df9e2fceba8c5820de23f77951af0f59ecf2/pyproject_fmt-2.21.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f2632a23bff5c2b607f466320436352e8c29cd1dfad4b3a452719ba72475d5c8", size = 4880644, upload-time = "2026-05-05T00:54:29.683Z" }, + { url = "https://files.pythonhosted.org/packages/47/7c/ab5f5e361a037931b786f72c01c0288b4a5161d3d95a8a171f95924a8dc6/pyproject_fmt-2.21.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:37a6fa1462aa1413dae14f2a204e232452ea509af72641091dd10ecc6fd58f5e", size = 5043706, upload-time = "2026-05-05T00:54:31.805Z" }, + { url = "https://files.pythonhosted.org/packages/8d/76/e6d98b567164342b5c0e571053becabcb4ae2b067e0b86d9647accb76461/pyproject_fmt-2.21.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:749e9a6715b92d660a38efe13077910ffc75d688096af953d7bee1c4490157df", size = 5397345, upload-time = "2026-05-05T00:54:33.901Z" }, + { url = "https://files.pythonhosted.org/packages/df/9c/b26d84d4f25414450f8e74604abada0f2687312f34305d2a56a75314de15/pyproject_fmt-2.21.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:bbaa5c5a1650a6fb181d43f8e625cf225c991b0457feaa48735df1be29c368c2", size = 5084825, upload-time = "2026-05-05T00:54:35.99Z" }, + { url = "https://files.pythonhosted.org/packages/af/a7/098b5ff64f1497025340bddd68a103380a4b579a77e5fe38d97f161dc43e/pyproject_fmt-2.21.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93fe997b4531542b4f76b187600e5a1ad0f8856be9a81054bddb8e11309f370f", size = 5595994, upload-time = "2026-05-05T00:54:37.678Z" }, + { url = "https://files.pythonhosted.org/packages/36/20/ba56926c5ee296bdc055e403cf5419e2b0b2d4a385609e95558841a62691/pyproject_fmt-2.21.2-cp310-abi3-win_amd64.whl", hash = "sha256:1ef6e5b86b85673f1fc4fd1cb3ccd833ef3293b3b19ed04ae4668604615feef5", size = 5309188, upload-time = "2026-05-05T00:54:39.764Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fd/3660664b632eba6098767067d7c36cd65fb3dcc8040e5de63ceea8138975/pyproject_fmt-2.21.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:e1b6b7d8db36ad0e57c20f789f640b72922f26fa2bf962faa32135be61ffd436", size = 5077560, upload-time = "2026-05-05T00:54:41.472Z" }, + { url = "https://files.pythonhosted.org/packages/53/8c/db7ca56575f775edfad331698502b57ced33affe29a9e01954bfee3b65a6/pyproject_fmt-2.21.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b1281cf2494092ce0015fa9b0471576864ee67ae8f7c465e0346f7ef64ed0f72", size = 4878018, upload-time = "2026-05-05T00:54:43.193Z" }, + { url = "https://files.pythonhosted.org/packages/0f/10/a22b3510f276a8fba48d80b4b8ceaf6699a899d0851e7b6f1d4369b66c0c/pyproject_fmt-2.21.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:9f6f776e829fe94c66c30914d539862649332ab7d7cb0b1d5c434860256edcb3", size = 5037434, upload-time = "2026-05-05T00:54:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/86/21/3b521e79d9183d63cf53f884235a52ab8b3ea9e5c8ea9350cb4b346ded62/pyproject_fmt-2.21.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a7170de7b9f83c21b47efbb79c5a0bf31fee67e94929d68b7af9f3d8633e8292", size = 5387604, upload-time = "2026-05-05T00:54:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/95/18/12f560f322b14a1ce047acbdc8f90c48f296cc76a9a97de4d1e6f23304e9/pyproject_fmt-2.21.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ece67ca5bdae611fa5d35c0becad30663354c972149412bd448834725c9c8a3", size = 5585008, upload-time = "2026-05-05T00:54:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fd/f8a62c2a6f82fea16fb2cb8d6649eed3b413c23ba5ac3afcb471637c6bc7/pyproject_fmt-2.21.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e12661bd2b274603464b797c7e583880b384133dceb1c9f2878a78c44a4272ee", size = 5304214, upload-time = "2026-05-05T00:54:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/55937920df4ef20bd3d7f2e8fa86d8daed6dfa22356f0c9ec02f64786f6a/pyproject_fmt-2.21.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:89c475e8ae7d8787bfade5983c800f32f4dc60a22af75320b24038eaf505fd5f", size = 5078567, upload-time = "2026-05-05T00:54:53.178Z" }, + { url = "https://files.pythonhosted.org/packages/ac/72/a04e2fd34900c539c9f9e0cb539b16e506fb2c864537e91e0d470dfad4ff/pyproject_fmt-2.21.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6888f795677781200694933b5a1e8634b0bf3fa2217f70af2009ce2c399dab33", size = 4878228, upload-time = "2026-05-05T00:54:55.025Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/078547cae9373d4086567f2254367dd624a157461cf0ae8df67a3c068c4f/pyproject_fmt-2.21.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:44df97b6ea0b268016c66d96d40adcdd5174a54c07eda694c923a2bb1d94e1de", size = 5036407, upload-time = "2026-05-05T00:54:56.664Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/0a77bf68872fbd12e12cfdec27e9f9400898b025bb96c8bb3bc53a896e0c/pyproject_fmt-2.21.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:2129e95534828cf3e27f0587dbc010d7801ffa20518e2ef329f654b8752c12fb", size = 5387771, upload-time = "2026-05-05T00:54:58.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/81/49820a5e18db78804b149fed924bcb54912e269875c00beac26525313b3f/pyproject_fmt-2.21.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d8e29b36e2b104b58c6478b8d52398f68faa1014891988e8e45b9d946ad1773d", size = 5584368, upload-time = "2026-05-05T00:54:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/f0b29381f5f6cef6819cadb57e39ef82f6eb0c43411a0f36314a0ac8e2d6/pyproject_fmt-2.21.2-cp314-cp314t-win_amd64.whl", hash = "sha256:737b7a1019ddef2fc78840cf53b5f7a6201acafc4169650079ff07b094a5d439", size = 5304226, upload-time = "2026-05-05T00:55:01.599Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/20649fd503a8338a92624c8d048b789059885ba248344ed80e80ff2339bf/pyproject_fmt-2.21.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:84fbfe66178781a864e9d6bbe82cae2de08620e5c8971d2c0df1e77768b3dbb0", size = 5078497, upload-time = "2026-05-05T00:55:03.42Z" }, + { url = "https://files.pythonhosted.org/packages/82/58/f97bccf32c7ac48129a2be5d849812f8f9c9174e5acb7a0b8af6035fc5cf/pyproject_fmt-2.21.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:041d013e564d64be01b3aca4a44ea90a19022fe9841391d35d0de7ac33fcaecd", size = 4877296, upload-time = "2026-05-05T00:55:05.048Z" }, + { url = "https://files.pythonhosted.org/packages/38/17/a7cfcba7ff57e97d6f120480a28296a1564be07cecf238a30977b36bbf0d/pyproject_fmt-2.21.2-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:f434fe89c6206d0b93531a785bf7edc503656adabcba87c362021482a0fcd3cd", size = 5036335, upload-time = "2026-05-05T00:55:06.663Z" }, + { url = "https://files.pythonhosted.org/packages/25/86/8f4e408ebdcc6ead5c995c5a33145b660d12db127df4131b0f3307a2614c/pyproject_fmt-2.21.2-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:5edc63f6dc478653075735aac7c858e6763f9dba84bb102d599bf48fe6aa7148", size = 5388280, upload-time = "2026-05-05T00:55:08.331Z" }, + { url = "https://files.pythonhosted.org/packages/3d/23/60db9ea8923ed833598716e4699ff2594b0c1453d5ae1af31f99b660b776/pyproject_fmt-2.21.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:143b9b11213d55af03042d27f735a3ffcef865d30584366e4b07b9c0366a1611", size = 5584403, upload-time = "2026-05-05T00:55:10.545Z" }, + { url = "https://files.pythonhosted.org/packages/37/4f/b3df45225448f4e04f3728dc622ba23acf5be1d8f78ee581dc856b04f272/pyproject_fmt-2.21.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fe589df7a04c0081c07ce9aefde1e9a0dd10d9f24866461d4d0156908c3ce954", size = 5079488, upload-time = "2026-05-05T00:55:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c1/9d3e222a8be0423db301c85f015981fbcb91e053be4f9001af6e563a7d69/pyproject_fmt-2.21.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec64674d821fcab61959daf8780ed8dc4fcaa438a47bbffbfc1cf0e8b8664b0", size = 4879506, upload-time = "2026-05-05T00:55:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/4e/42/5071c42f5776bb98ec03d3c35bfe07002709e853912bdd00ee48c46d38d5/pyproject_fmt-2.21.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3581bfa9ed41f5c9423ffb80b16e7320869a7977090df68311e0c87c4467213d", size = 5394213, upload-time = "2026-05-05T00:55:15.938Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "requirements-parser" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/96/fb6dbfebb524d5601d359a47c78fe7ba1eef90fc4096404aa60c9a906fbb/requirements_parser-0.13.0.tar.gz", hash = "sha256:0843119ca2cb2331de4eb31b10d70462e39ace698fd660a915c247d2301a4418", size = 22630, upload-time = "2025-05-21T13:42:05.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/60/50fbb6ffb35f733654466f1a90d162bcbea358adc3b0871339254fbc37b2/requirements_parser-0.13.0-py3-none-any.whl", hash = "sha256:2b3173faecf19ec5501971b7222d38f04cb45bb9d87d0ad629ca71e2e62ded14", size = 14782, upload-time = "2025-05-21T13:42:04.007Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + +[[package]] +name = "toml-fmt-common" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/f3/1a54191399599a229d04868083c30234c62e569cfaaf367f6c792ac53e2d/toml_fmt_common-1.3.2.tar.gz", hash = "sha256:d0da45f0244e8d410787fa8ddf5a5594016abb2a7b97f400ce20ec8b0a6a4804", size = 7583, upload-time = "2026-03-20T17:32:11.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/27/403387a0b8ad8e5d57bec82ed0e5528432859ea8e1475916c8c8ac45197c/toml_fmt_common-1.3.2-py3-none-any.whl", hash = "sha256:f3329fbdd962f629d1777071a7f37813f6cf17b10a33830a5393996fc2064bef", size = 5557, upload-time = "2026-03-20T17:32:10.263Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "ty" +version = "0.0.35" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/53/440e7b1212c4b0abbd4adb7aed93f4971aa1f8dca386ac5515930afa9172/ty-0.0.35.tar.gz", hash = "sha256:8375c240ab38138a19db07996c9808fb7a92047c1492e1ce587c2ef5112ad3a9", size = 5629237, upload-time = "2026-05-10T18:25:17.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/84/19662ee881675815b7fafff940a365be1985730465afd9b75cb2edd5f8b3/ty-0.0.35-py3-none-linux_armv6l.whl", hash = "sha256:85ae1e59b9fb0b40e9d84fe61b29653c5f2f5e78b487ece371a7a38c20c781cf", size = 11198741, upload-time = "2026-05-10T18:24:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/62/df/7e5b6f83d85b4d2e5b72b5dceb388f440acc10679417bd46f829b9200fab/ty-0.0.35-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:709dbb7af4fcadb1196863c00b8791bbbbcc9dacbe15a0ff17f0af82b35d415b", size = 10948304, upload-time = "2026-05-10T18:24:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/59/94/72d7263aca055cde427f0ebcf08d6a74e5a5fee1d1e7fdd553696089cecb/ty-0.0.35-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2cb0877419ab0c8708b6925cb0c2800b263842bd3c425113f200538772f3a0cc", size = 10407413, upload-time = "2026-05-10T18:24:37.422Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/fda6fae8a81ce0cb5f24cdfe63260e110c7af8844e31fa07d1e6e8ef0232/ty-0.0.35-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7afbcfc61904b7e82e7fe1a1db832a40d8f01e69dee1775f6594e552980536c", size = 10932614, upload-time = "2026-05-10T18:24:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/72/3d/b98d8d4aa1a5ed6daaf15864e838f605ca7b1e8b93b7e17b96ed4bc4dfed/ty-0.0.35-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b61498cc3e4178031c079951257fbdb209a891b4feb10ad6c40f615a51846f41", size = 10962982, upload-time = "2026-05-10T18:24:44.88Z" }, + { url = "https://files.pythonhosted.org/packages/18/c4/2881aad71bf6fb2f8df17fc8e4bc89e904e54490a3ee747b5ef73f98ac85/ty-0.0.35-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573b1eacda349fc8dba0d767b41631c3a6f66412363127c5bf2b1b40a1d898d2", size = 11476274, upload-time = "2026-05-10T18:24:42.4Z" }, + { url = "https://files.pythonhosted.org/packages/34/0f/7717650adaeaddd23eea70470e2c26d3f0b9b18fdc7f26ec9552d6001f17/ty-0.0.35-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7209746158d6393c1040aa64b3ca29622e212ea7d8bae22ba50dbcbb4f96f0a", size = 12012027, upload-time = "2026-05-10T18:25:00.752Z" }, + { url = "https://files.pythonhosted.org/packages/22/c9/1a16cb4aab6f4707d8f550772e91abc26d1c8870f19b5e2453ad10bb8209/ty-0.0.35-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4466a1470aa4418d49a9aa45d9da7de42033addd0a2837c5b2b0eb71d3c2bcd3", size = 11648894, upload-time = "2026-05-10T18:25:12.44Z" }, + { url = "https://files.pythonhosted.org/packages/18/a1/a977c0e07e9f88db9c67f90c6342a4dc4422c8091fa07bf26521870687c5/ty-0.0.35-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb44bb742d52c309dcaa6598bcf4d82eb4bf1241b9e4940461e522e30093fe8b", size = 11560482, upload-time = "2026-05-10T18:25:05.172Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c1/a5fb11227d5cc4ac3f29a115d8c8bc817578e8ef6907d1e4c914ddbf45ee/ty-0.0.35-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:34b219250736c989b2670a03782c61315f523f3a2be37f1f90b1207e2212c188", size = 11718495, upload-time = "2026-05-10T18:24:54.12Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cb/e92e4317388b6d1fd821a46941b448a8a1ff0bf13e22147c5167d8fa1b00/ty-0.0.35-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88e2ac497decc0940ef1a07571dee8a746112a93a09cdc7f8bca0099752e2e05", size = 10900815, upload-time = "2026-05-10T18:25:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4f/03bd87388a92567f262f35ac64e10d2be047d258f2dfcf1405f500fa2b90/ty-0.0.35-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:02cae51b53e6ec17d5d827ff1a3a76fd119705b56a92156e04399eda6e911596", size = 10998051, upload-time = "2026-05-10T18:25:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/b4/60/6edbc375ee6073973200096168f644e1081e5e55a7d42596826465b275de/ty-0.0.35-py3-none-musllinux_1_2_i686.whl", hash = "sha256:11871d730c9400d899ac0b9f3d660ed2e7e433377c8725549f8250a36a7f2620", size = 11148910, upload-time = "2026-05-10T18:24:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b1/a845d2066ed521c477450f436d4bd353d107e7c02dd6536a485944aaf892/ty-0.0.35-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ad0a2f0530d0933dcc99ad36ac556c63e384ea72ab9a18d23ad2e2c9fd61c73", size = 11671005, upload-time = "2026-05-10T18:24:56.223Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/1d5912a54fb66b2f95ac828ae61d422ef5afeae1263e4d231e40796c229f/ty-0.0.35-py3-none-win32.whl", hash = "sha256:0e25d63ec4ab116e7f6757e44d16ca9216bca679d19ecc36d119cf80faada61a", size = 10481096, upload-time = "2026-05-10T18:24:39.976Z" }, + { url = "https://files.pythonhosted.org/packages/3b/36/1c7f8632bfec1c321f01581d4c940a3617b24bd3e8b37c8a7363d33fbfc4/ty-0.0.35-py3-none-win_amd64.whl", hash = "sha256:6a0a6d259f6f2f8f2f954c6f013d4e0b5eba68af6b353bf19a47d59ec254a3d5", size = 11555691, upload-time = "2026-05-10T18:25:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fb/59325221bce52f6e833d6865ce8360ef7d5e1e21151b38df6dc77c4327a7/ty-0.0.35-py3-none-win_arm64.whl", hash = "sha256:619c52c0fb2aa21961a848a1995135ad3b6d0a9aa54da0194e60f679cc200e13", size = 10925457, upload-time = "2026-05-10T18:25:10.352Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] [[package]] name = "yamltrip" version = "0.1.0" source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "codespell" }, + { name = "deptry" }, + { name = "import-linter" }, + { name = "prek" }, + { name = "pyproject-fmt" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "tomli" }, + { name = "ty" }, +] +test = [ + { name = "coverage", extra = ["toml"] }, + { name = "mypy" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "codespell", specifier = ">=2.4.2" }, + { name = "deptry", specifier = ">=0.25.1" }, + { name = "import-linter", specifier = ">=2.11" }, + { name = "prek", specifier = ">=0.3.13" }, + { name = "pyproject-fmt", specifier = ">=2.21.2" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "ruff", specifier = ">=0.15.12" }, + { name = "tomli", specifier = ">=2.4.1" }, + { name = "ty", specifier = ">=0.0.35" }, +] +test = [ + { name = "coverage", extras = ["toml"], specifier = ">=7.14.0" }, + { name = "mypy", specifier = ">=1.16.0" }, +]